diff --git a/README.md b/README.md index f8e90c1..c5ddefc 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ 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. +Unbound variables (upper and lower case) that aren't macros will become free variables. 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. @@ -76,7 +76,6 @@ Lamb treats each λ expression as a binary tree. Variable binding and reduction ## Todo (pre-release): - Make command output accessible in prompt - - Prettyprint functions and rename bound variables - Prettier colors - Prevent macro-chaining recursion - step-by-step reduction @@ -86,11 +85,9 @@ Lamb treats each λ expression as a binary tree. Variable binding and reduction ## Todo: - - Optimization: clone only if absolutely necessary - Better class mutation: when is a node no longer valid? - Loop detection - Command-line options (load a file, run a set of commands) - $\alpha$-equivalence check - Unchurch macro: make church numerals human-readable - - Smart alignment in all printouts - Syntax highlighting: parenthesis, bound variables, macros, etc \ No newline at end of file diff --git a/lamb/commands.py b/lamb/commands.py index e165c99..c3bf146 100644 --- a/lamb/commands.py +++ b/lamb/commands.py @@ -62,7 +62,7 @@ def cmd_save(command, runner) -> None: with open(target, "w") as f: f.write("\n".join( - [f"{n} = {e}" for n, e in runner.macro_table.items()] + [f"{n} = {e.export()}" for n, e in runner.macro_table.items()] )) printf( diff --git a/lamb/node.py b/lamb/node.py index bfb0e36..4a01067 100644 --- a/lamb/node.py +++ b/lamb/node.py @@ -1,4 +1,5 @@ import enum +import lamb class Direction(enum.Enum): UP = enum.auto() @@ -207,6 +208,12 @@ class Node: def __str__(self) -> str: return print_node(self) + def export(self) -> str: + """ + Convert this tree to a parsable string. + """ + return print_node(self, export = True) + def bind_variables(self, *, ban_macro_name = None): return bind_variables( self, @@ -214,7 +221,7 @@ class Node: ) class EndNode(Node): - def print_value(self): + def print_value(self, *, export: bool = False) -> str: raise NotImplementedError("EndNodes MUST provide a `print_value` method!") class ExpandableEndNode(EndNode): @@ -228,8 +235,11 @@ class FreeVar(EndNode): def __repr__(self): return f"" - def print_value(self): - return f"{self.name}" + def print_value(self, *, export: bool = False) -> str: + if export: + return f"{self.name}'" + else: + return f"{self.name}'" def copy(self): return FreeVar(self.name) @@ -248,7 +258,7 @@ class Macro(ExpandableEndNode): def __repr__(self): return f"" - def print_value(self): + def print_value(self, *, export: bool = False) -> str: return self.name def expand(self, *, macro_table = {}) -> tuple[ReductionType, Node]: @@ -274,7 +284,7 @@ class Church(ExpandableEndNode): def __repr__(self): return f"" - def print_value(self): + def print_value(self, *, export: bool = False) -> str: return str(self.value) def expand(self, *, macro_table = {}) -> tuple[ReductionType, Node]: @@ -316,7 +326,7 @@ class Bound(EndNode): def __repr__(self): return f"<{self.name} {self.identifier}>" - def print_value(self): + def print_value(self, *, export: bool = False) -> str: return self.name class Func(Node): @@ -378,15 +388,31 @@ class Call(Node): return Call(None, None) # type: ignore -def print_node(node: Node) -> str: +def print_node(node: Node, *, export: bool = False) -> str: if not isinstance(node, Node): raise TypeError(f"I don't know how to print a {type(node)}") out = "" + bound_subs = {} + for s, n in node: if isinstance(n, EndNode): - out += n.print_value() + if isinstance(n, Bound): + if n.identifier not in bound_subs.keys(): + o = n.print_value(export = export) + if o in bound_subs.items(): + i = 1 + while o in bound_subs.items(): + o = lamb.utils.subscript(i := i + 1) + bound_subs[n.identifier] = o + else: + bound_subs[n.identifier] = n.print_value() + + + out += bound_subs[n.identifier] + else: + out += n.print_value(export = export) elif isinstance(n, Func): if s == Direction.UP: @@ -502,7 +528,7 @@ def call_func(fn: Func, arg: Node): raise Exception("Tried to substitute a None bound variable.") n.parent.set_side(n.parent_side, clone(arg)) # type: ignore - return clone(fn.left) + return fn.left # Do a single reduction step diff --git a/lamb/runner.py b/lamb/runner.py index 3325134..82b0336 100644 --- a/lamb/runner.py +++ b/lamb/runner.py @@ -116,7 +116,7 @@ class Runner: # Show reduction count if (i >= self.iter_update) and (i % self.iter_update == 0): - print(f" Reducing... {i}", end = "\r") + print(f" Reducing... {i:,}", end = "\r") try: red_type, node = lamb.node.reduce( diff --git a/lamb/utils.py b/lamb/utils.py index 88049b4..722bd56 100644 --- a/lamb/utils.py +++ b/lamb/utils.py @@ -70,4 +70,35 @@ def show_greeting(): "<_s> A λ calculus engine", "<_p> Type :help for help", "" - ])), style = style) \ No newline at end of file + ])), style = style) + +def subscript(num: int): + sub = { + "0": "₀", + "1": "₁", + "2": "₂", + "3": "₃", + "4": "₄", + "5": "₅", + "6": "₆", + "7": "₇", + "8": "₈", + "9": "₉" + } + + sup = { + "0": "⁰", + "1": "¹", + "2": "²", + "3": "³", + "4": "⁴", + "5": "⁵", + "6": "⁶", + "7": "⁷", + "8": "⁸", + "9": "⁹" + } + + return "".join( + [sup[x] for x in str(num)] + ) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 5a4af68..aeea7d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ description = "A lambda calculus engine" # # Patch release: # Small, compatible fixes. -version = "0.1.0" +version = "0.1.1" dependencies = [ "prompt-toolkit==3.0.31",