diff --git a/README.md b/README.md index 15b20e8..5cad3d3 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ ## Usage + Type lambda expressions into the prompt, and Lamb will evaluate them. \ Use your `\` (backslash) key to type a `λ`. \ To define macros, use `=`. For example, @@ -30,22 +31,30 @@ To define macros, use `=`. For example, Note that there are spaces in `λa.a F T`. With no spaces, `aFT` will be parsed as one variable. \ Lambda functions can only take single-letter, lowercase arguments. `λA.A` is not valid syntax. \ -Unbound variables (upper and lower case) that aren't macros will become free variables. Free variables will be shown with a `'`, like `a'`. +Free variables will be shown with a `'`, like `a'`. -Be careful, macros are case-sensitive. If you define a macro `MAC` and accidentally write `mac` in the prompt, `mac` will become a free variable. +Macros are case-sensitive. If you define a macro `MAC` and accidentally write `mac` in the prompt, `mac` will become a free variable. Numbers will automatically be converted to Church numerals. For example, the following line will reduce to `T`. ``` ==> 3 NOT F ``` -If an expression takes too long to evaluate, you may interrupt reduction with `Ctrl-C`. +If an expression takes too long to evaluate, you may interrupt reduction with `Ctrl-C`. \ +Exit the prompt with `Ctrl-C` or `Ctrl-D`. + +There are many useful macros in [macros.lamb](./macros.lamb). Load them with the `:load` command: +``` +==> :load macros.lamb +``` + +Have fun! ------------------------------------------------- ## Commands -Lamb comes with a few commands. Prefix them with a `:` +Lamb understands many commands. Prefix them with a `:` in the prompt. `:help` Prints a help message @@ -59,8 +68,10 @@ Lamb comes with a few commands. Prefix them with a `:` `:clearmacros` Delete all macros -`:save [filename]`\ -`:load [filename]` Save or load the current environment to a file. The lines in a file look exactly the same as regular entries in the prompt, but must only contain macro definitions. +`:save [filename]` \ +`:load [filename]` \ +Save or load macros from a file. +The lines in a file look exactly the same as regular entries in the prompt, but can only contain macro definitions. See [macros.lamb](./macros.lamb) for an example. ------------------------------------------------- diff --git a/lamb/__main__.py b/lamb/__main__.py index 651c6cd..cb80c02 100755 --- a/lamb/__main__.py +++ b/lamb/__main__.py @@ -1,66 +1,29 @@ +if __name__ != "__main__": + raise ImportError("lamb.__main__ should never be imported. Run it directly.") + from prompt_toolkit import PromptSession -from prompt_toolkit.completion import WordCompleter from prompt_toolkit import print_formatted_text as printf from prompt_toolkit.formatted_text import FormattedText from prompt_toolkit.formatted_text import to_plain_text -from prompt_toolkit.key_binding import KeyBindings -from prompt_toolkit.lexers import Lexer from pyparsing import exceptions as ppx import lamb -# Simple lexer for highlighting. -# Improve this later. -class LambdaLexer(Lexer): - def lex_document(self, document): - def inner(line_no): - return [("class:text", str(document.lines[line_no]))] - return inner - - lamb.utils.show_greeting() -# Replace "\" with pretty "λ"s -bindings = KeyBindings() -@bindings.add("\\") -def _(event): - event.current_buffer.insert_text("λ") - - r = lamb.Runner( prompt_session = PromptSession( style = lamb.utils.style, - lexer = LambdaLexer(), - key_bindings = bindings + lexer = lamb.utils.LambdaLexer(), + key_bindings = lamb.utils.bindings ), prompt_message = FormattedText([ ("class:prompt", "==> ") ]) ) - -r.run_lines([ - "T = λab.a", - "F = λab.b", - "NOT = λa.(a F T)", - "AND = λab.(a b F)", - "OR = λab.(a T b)", - "XOR = λab.(a (NOT b) b)", - "M = λx.(x x)", - "W = M M", - "Y = λf.( (λx.(f (x x))) (λx.(f (x x))) )", - "PAIR = λabi.( i a b )", - "S = λnfa.(f (n f a))", - "Z = λn.n (λa.F) T", - "MULT = λnmf.n (m f)", - "H = λp.((PAIR (p F)) (S (p F)))", - "D = λn.n H (PAIR 0 0) T", - "FAC = λyn.(Z n)(1)(MULT n (y (D n)))" -]) - - while True: try: i = r.prompt() diff --git a/lamb/commands.py b/lamb/commands.py index dc4c4a4..3959385 100644 --- a/lamb/commands.py +++ b/lamb/commands.py @@ -101,7 +101,14 @@ def cmd_load(command, runner): lines = [x.strip() for x in f.readlines()] for i in range(len(lines)): - l = lines[i] + l = lines[i].strip() + + # Skip comments and empty lines + if l.startswith("#"): + continue + if l == "": + continue + try: x = runner.parse(l)[0] except ppx.ParseException as e: diff --git a/lamb/parser.py b/lamb/parser.py index 7acd65e..0d32a81 100755 --- a/lamb/parser.py +++ b/lamb/parser.py @@ -56,7 +56,7 @@ class LambdaParser: (self.lp + self.pp_history + self.rp) ) - self.pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_") + pp.Word(pp.alphas + pp.nums + "_")[0, ...] + self.pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_") + pp.Word(pp.alphas + pp.nums + "_.")[0, ...] self.pp_all = ( diff --git a/lamb/utils.py b/lamb/utils.py index f70bbee..cfe151b 100644 --- a/lamb/utils.py +++ b/lamb/utils.py @@ -1,5 +1,7 @@ from prompt_toolkit.styles import Style from prompt_toolkit.formatted_text import HTML +from prompt_toolkit.lexers import Lexer +from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit import print_formatted_text as printf from importlib.metadata import version @@ -14,6 +16,11 @@ style = Style.from_dict({ # type: ignore "code": "#AAAAAA italic", "muted": "#AAAAAA", + # Syntax highlighting colors + "syn_cmd": "#FFFFFF italic", + "syn_lambda": "#AAAAAA", + "syn_paren": "#AAAAAA", + # Command formatting # cmd_h: section titles # cmd_key: keyboard keys, usually one character @@ -28,6 +35,46 @@ style = Style.from_dict({ # type: ignore }) +# Replace "\" with pretty "λ"s +bindings = KeyBindings() +@bindings.add("\\") +def _(event): + event.current_buffer.insert_text("λ") + +# Simple lexer for highlighting. +# Improve this later. +class LambdaLexer(Lexer): + def lex_document(self, document): + def inner(line_no): + out = [] + tmp_str = [] + d = str(document.lines[line_no]) + + if d.startswith(":"): + return [ + ("class:syn_cmd", d) + ] + + for c in d: + if c in "\\λ.": + if len(tmp_str) != 0: + out.append(("class:text", "".join(tmp_str))) + out.append(("class:syn_lambda", c)) + tmp_str = [] + elif c in "()": + if len(tmp_str) != 0: + out.append(("class:text", "".join(tmp_str))) + out.append(("class:syn_paren", c)) + tmp_str = [] + else: + tmp_str.append(c) + + if len(tmp_str) != 0: + out.append(("class:text", "".join(tmp_str))) + return out + return inner + + def show_greeting(): # | _.._ _.|_ # |_(_|| | ||_) diff --git a/macros.lamb b/macros.lamb new file mode 100644 index 0000000..de78894 --- /dev/null +++ b/macros.lamb @@ -0,0 +1,77 @@ +# How to use exported files in lamb: +# +# [Syntax Highlighting] +# Most languages' syntax highlighters will +# highlight this code well. Set it manually +# in your editor. +# +# Don't use a language for which you have a +# linter installed, you'll get lots of errors. +# +# Choose a language you don't have extenstions for, +# and a language that uses # comments. +# +# The following worked well in vscode: +# - Julia +# - Perl +# - Coffeescript +# - R + +# [Writing macros] +# If you don't have a custom keyboard layout that can +# create λs, you may use backslashes instead. +# (As in `T = \ab.b`) +# +# This file must only contain macro definitons. Commands will be ignored. +# Statements CANNOT be split among multiple lines. +# Comments CANNOT be on the same line as macro defintions. +# All leading whitespace is ignored. + + +# Misc Combinators +M = λx.(x x) +W = (M M) +Y = λf.((λx.(f (x x))) (λx.(f (x x)))) + + +# Booleans +T = λab.a +F = λab.b +NOT = λa.(a F T) +AND = λab.(a b F) +OR = λab.(a T b) +XOR = λab.((a (NOT b)) b) + + +# Numbers +# PAIR: prerequisite for H. +# Makes a two-value tuple, indexed with T and F. +# +# H: shift-and-add, prerequisite for D +# +# S: successor (adds 1) +# +# D: predecessor (subtracts 1) +# +# Z: tests if a number is zero +# NZ: equivalent to `NOT Z` +# +# ADD: adds two numbers +# +# MULT: multiply two numbers +# +# FAC: +# Recursive factorial. Call with `Y FAC ` +# Don't call this with numbers bigger than 5 unless you're very patient. +# +# `Y FAC 6` required 867,920 reductions and took 10 minutes to run. + +PAIR = λabi.(i a b) +H = λp.((PAIR (p F)) (S (p F))) +S = λnfa.(f (n f a)) +D = λn.((n H) ((PAIR 0) 0) T) +Z = λn.(n (λa.F) T) +NZ = λn.(n (λa.T) F) +ADD = λmn.(m S n) +MULT = λnmf.(n (m f)) +FAC = λyn.( (Z n)(1)((MULT n) (y (D n))) ) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 08c9749..322ef57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ description = "A lambda calculus engine" # # Patch release: # Small, compatible fixes. -version = "0.1.2" +version = "0.1.3" dependencies = [ "prompt-toolkit==3.0.31",