diff --git a/README.md b/README.md index 52dcaf7..5299adb 100644 --- a/README.md +++ b/README.md @@ -76,20 +76,21 @@ Lamb treats each λ expression as a binary tree. Variable binding and reduction ## Todo (pre-release): - Prettier colors - - Prevent macro-chaining recursion - - step-by-step reduction - - Full-reduce option (expand all macros) - - PyPi package - Cleanup warnings - - Preprocess method: bind, macros to free, etc - - History queue - Truncate long expressions in warnings + - Prevent macro-chaining recursion + - Full-reduce option (expand all macros) + - step-by-step reduction + - Cleanup files + - PyPi package ## Todo: + - History queue + command indexing + - Show history command - 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 + - Command-line options (load a file, run a set of commands) - Unchurch macro: make church numerals human-readable - Syntax highlighting: parenthesis, bound variables, macros, etc \ No newline at end of file diff --git a/lamb/node.py b/lamb/node.py index 6b74005..dbb4a3f 100644 --- a/lamb/node.py +++ b/lamb/node.py @@ -20,9 +20,6 @@ class ReductionType(enum.Enum): # We turned a church numeral into an expression AUTOCHURCH = enum.auto() - # We replaced a macro with a free variable. - MACRO_TO_FREE = enum.auto() - # We applied a function. # This is the only type of "formal" reduction step. FUNCTION_APPLY = enum.auto() @@ -50,27 +47,32 @@ class TreeWalker: def __init__(self, expr): self.expr = expr self.ptr = expr + self.first_step = True self.from_side = Direction.UP + def __iter__(self): + return self + def __next__(self): # This could be implemented without checking the node type, # but there's no reason to do that. # Maybe later? - if self.ptr is self.expr.parent: - raise StopIteration - out = self.ptr - out_side = self.from_side - if isinstance(self.ptr, EndNode): + if self.first_step: + self.first_step = False + return self.from_side, self.ptr + + if isinstance(self.ptr, Root): + if self.from_side == Direction.UP: + self.from_side, self.ptr = self.ptr.go_left() + elif isinstance(self.ptr, EndNode): self.from_side, self.ptr = self.ptr.go_up() - elif isinstance(self.ptr, Func): if self.from_side == Direction.UP: self.from_side, self.ptr = self.ptr.go_left() elif self.from_side == Direction.LEFT: self.from_side, self.ptr = self.ptr.go_up() - elif isinstance(self.ptr, Call): if self.from_side == Direction.UP: self.from_side, self.ptr = self.ptr.go_left() @@ -78,11 +80,18 @@ class TreeWalker: self.from_side, self.ptr = self.ptr.go_right() elif self.from_side == Direction.RIGHT: self.from_side, self.ptr = self.ptr.go_up() - else: raise TypeError(f"I don't know how to iterate a {type(self.ptr)}") - return out_side, out + # Stop conditions + if isinstance(self.expr, Root): + if self.ptr is self.expr: + raise StopIteration + else: + if self.ptr is self.expr.parent: + raise StopIteration + + return self.from_side, self.ptr class Node: """ @@ -93,11 +102,11 @@ class Node: def __init__(self): # The node this one is connected to. # None if this is the top objects. - self.parent: Node | None = None + self.parent: Node = None # type: ignore # What direction this is relative to the parent. # Left of Right. - self.parent_side: Direction | None = None + self.parent_side: Direction = None # type: ignore # Left and right nodes, None if empty self._left: Node | None = None @@ -220,12 +229,6 @@ class Node: """ return print_node(self, export = True) - def bind_variables(self, *, ban_macro_name = None): - return bind_variables( - self, - ban_macro_name = ban_macro_name - ) - def set_runner(self, runner): for s, n in self: if s == Direction.UP: @@ -279,9 +282,17 @@ class Macro(ExpandableEndNode): def expand(self) -> tuple[ReductionType, Node]: if self.name in self.runner.macro_table: - return ReductionType.MACRO_EXPAND, clone(self.runner.macro_table[self.name]) + # The element in the macro table will be a Root node, + # so we clone its left element. + return ( + ReductionType.MACRO_EXPAND, + clone(self.runner.macro_table[self.name].left) + ) else: - return ReductionType.MACRO_TO_FREE, FreeVar(self.name, runner = self.runner) + raise Exception(f"Macro {self.name} is not defined") + + def to_freevar(self): + return FreeVar(self.name, runner = self.runner) def copy(self): return Macro(self.name, runner = self.runner) @@ -342,7 +353,10 @@ class History(ExpandableEndNode): def expand(self) -> tuple[ReductionType, Node]: if len(self.runner.history) == 0: raise ReductionError(f"There isn't any history to reference.") - return ReductionType.HIST_EXPAND, clone(self.runner.history[-1]) + # .left is VERY important! + # self.runner.history will contain Root nodes, + # and we don't want those *inside* our tree. + return ReductionType.HIST_EXPAND, clone(self.runner.history[-1].left) def copy(self): return History(runner = self.runner) @@ -401,6 +415,23 @@ class Func(Node): def copy(self): return Func(self.input, None, runner = self.runner) # type: ignore +class Root(Node): + """ + Root node. + Used at the top of an expression. + """ + + def __init__(self, left: Node, *, runner = None) -> None: + super().__init__() + self.left: Node = left + self.runner = runner # type: ignore + + def __repr__(self): + return f"" + + def copy(self): + return Root(None, runner = self.runner) # type: ignore + class Call(Node): @staticmethod def from_parse(results): @@ -498,7 +529,7 @@ def clone(node: Node): if isinstance(ptr, EndNode): from_side, ptr = ptr.go_up() _, out_ptr = out_ptr.go_up() - elif isinstance(ptr, Func): + elif isinstance(ptr, Func) or isinstance(ptr, Root): if from_side == Direction.UP: from_side, ptr = ptr.go_left() out_ptr.set_side(ptr.parent_side, ptr.copy()) @@ -523,9 +554,17 @@ def clone(node: Node): break return out -def bind_variables(node: Node, *, ban_macro_name = None) -> dict: - if not isinstance(node, Node): - raise TypeError(f"I don't know what to do with a {type(node)}") +def prepare(root: Root, *, ban_macro_name = None) -> dict: + """ + Prepare an expression for expansion. + This will does the following: + - Binds variables + - Turns unbound macros into free variables + - Generates warnings + """ + + if not isinstance(root, Root): + raise TypeError(f"I don't know what to do with a {type(root)}") bound_variables = {} @@ -534,7 +573,8 @@ def bind_variables(node: Node, *, ban_macro_name = None) -> dict: "free_variables": set() } - for s, n in node: + it = iter(root) + for s, n in it: if isinstance(n, History): output["has_history"] = True @@ -544,9 +584,26 @@ def bind_variables(node: Node, *, ban_macro_name = None) -> dict: if (n.name == ban_macro_name) and (ban_macro_name is not None): raise ReductionError("Macro cannot reference self") - if n.name not in node.runner.macro_table: - output["free_variables"].add(n.name) + # Bind variables + if n.name in bound_variables: + n.parent.set_side( + n.parent_side, + clone(bound_variables[n.name]) + ) + it.ptr = n.parent.get_side(n.parent_side) + # Turn undefined macros into free variables + elif n.name not in root.runner.macro_table: + output["free_variables"].add(n.name) + n.parent.set_side( + n.parent_side, + n.to_freevar() + ) + it.ptr = n.parent.get_side(n.parent_side) + + + # Save bound variables when we enter a function's sub-tree, + # delete them when we exit it. elif isinstance(n, Func): if s == Direction.UP: # Add this function's input to the table of bound variables. @@ -557,23 +614,9 @@ def bind_variables(node: Node, *, ban_macro_name = None) -> dict: bound_variables[n.input.name] = Bound(n.input.name) n.input = bound_variables[n.input.name] - # If output is a macro, swap it with a bound variable. - if isinstance(n.left, Macro): - if n.left.name in bound_variables: - n.left = clone(bound_variables[n.left.name]) - elif s == Direction.LEFT: del bound_variables[n.input.name] - elif isinstance(n, Call): - if s == Direction.UP: - # Bind macros - if isinstance(n.left, Macro): - if n.left.name in bound_variables: - n.left = clone(bound_variables[n.left.name]) - if isinstance(n.right, Macro): - if n.right.name in bound_variables: - n.right = clone(bound_variables[n.right.name]) return output # Apply a function. @@ -589,22 +632,18 @@ def call_func(fn: Func, arg: Node): return fn.left # Do a single reduction step -def reduce(node: Node) -> tuple[ReductionType, Node]: - if not isinstance(node, Node): - raise TypeError(f"I can't reduce a {type(node)}") +def reduce(root: Root) -> tuple[ReductionType, Root]: + if not isinstance(root, Root): + raise TypeError(f"I can't reduce a {type(root)}") - out = node + out = root for s, n in out: if isinstance(n, Call) and (s == Direction.UP): if isinstance(n.left, Func): - if n.parent is None: - out = call_func(n.left, n.right) - out._set_parent(None, None) - else: - n.parent.set_side( - n.parent_side, # type: ignore - call_func(n.left, n.right) - ) + n.parent.set_side( + n.parent_side, # type: ignore + call_func(n.left, n.right) + ) return ReductionType.FUNCTION_APPLY, out @@ -614,7 +653,7 @@ def reduce(node: Node) -> tuple[ReductionType, Node]: return ReductionType.NOTHING, out -def expand(node: Node, *, force_all = False) -> tuple[int, Node]: +def expand(root: Root, *, force_all = False) -> tuple[int, Root]: """ Expands expandable nodes in the given tree. @@ -625,50 +664,25 @@ def expand(node: Node, *, force_all = False) -> tuple[int, Node]: ExpandableEndnodes. """ - if not isinstance(node, Node): - raise TypeError(f"I don't know what to do with a {type(node)}") + if not isinstance(root, Root): + raise TypeError(f"I don't know what to do with a {type(root)}") - out = clone(node) - ptr = out - from_side = Direction.UP + out = root macro_expansions = 0 - while True: + it = iter(root) + for s, n in it: if ( - isinstance(ptr, ExpandableEndNode) and - (force_all or ptr.always_expand) + isinstance(n, ExpandableEndNode) and + (force_all or n.always_expand) ): - if ptr.parent is None: - ptr = ptr.expand()[1] - out = ptr - ptr._set_parent(None, None) - else: - ptr.parent.set_side( - ptr.parent_side, # type: ignore - ptr.expand()[1] - ) - ptr = ptr.parent.get_side( - ptr.parent_side # type: ignore - ) + + n.parent.set_side( + n.parent_side, # type: ignore + n.expand()[1] + ) + it.ptr = n.parent.get_side( + n.parent_side # type: ignore + ) macro_expansions += 1 - - - # Tree walk logic - if isinstance(ptr, EndNode): - from_side, ptr = ptr.go_up() - elif isinstance(ptr, Func): - if from_side == Direction.UP: - from_side, ptr = ptr.go_left() - elif from_side == Direction.LEFT: - from_side, ptr = ptr.go_up() - elif isinstance(ptr, Call): - if from_side == Direction.UP: - from_side, ptr = ptr.go_left() - elif from_side == Direction.LEFT: - from_side, ptr = ptr.go_right() - elif from_side == Direction.RIGHT: - from_side, ptr = ptr.go_up() - if ptr is node.parent: - break - return macro_expansions, out \ No newline at end of file diff --git a/lamb/runner.py b/lamb/runner.py index 4bbdebf..478cd1c 100644 --- a/lamb/runner.py +++ b/lamb/runner.py @@ -13,6 +13,7 @@ class StopReason(enum.Enum): LOOP_DETECTED = ("class:warn", "Loop detected") MAX_EXCEEDED = ("class:err", "Too many reductions") INTERRUPT = ("class:warn", "User interrupt") + SHOW_MACRO = ("class:text", "Displaying macro content") class MacroDef: @staticmethod @@ -32,11 +33,6 @@ class MacroDef: def __str__(self): return f"{self.label} := {self.expr}" - def bind_variables(self, *, ban_macro_name = None): - return self.expr.bind_variables( - ban_macro_name = ban_macro_name - ) - def set_runner(self, runner): return self.expr.set_runner(runner) @@ -88,27 +84,30 @@ class Runner: # so that all digits appear to be changing. self.iter_update = 231 - self.history = [] + self.history: list[lamb.node.Root] = [] def prompt(self): return self.prompt_session.prompt( message = self.prompt_message ) - def parse(self, line) -> tuple[lamb.node.Node | MacroDef | Command, dict]: + def parse(self, line) -> tuple[lamb.node.Root | MacroDef | Command, dict]: e = self.parser.parse_line(line) o = {} if isinstance(e, MacroDef): + e.expr = lamb.node.Root(e.expr) e.set_runner(self) - o = e.bind_variables(ban_macro_name = e.label) + o = lamb.node.prepare(e.expr, ban_macro_name = e.label) elif isinstance(e, lamb.node.Node): + e = lamb.node.Root(e) e.set_runner(self) - o = e.bind_variables() + o = lamb.node.prepare(e) + return e, o - def reduce(self, node: lamb.node.Node, *, status = {}) -> None: + def reduce(self, node: lamb.node.Root, *, status = {}) -> None: warning_text = [] @@ -130,12 +129,9 @@ class Runner: ("class:warn", "\n") ] - only_macro = isinstance(node, lamb.node.ExpandableEndNode) + only_macro = isinstance(node.left, lamb.node.Macro) if only_macro: - warning_text += [ - ("class:warn", "All macros will be expanded"), - ("class:warn", "\n") - ] + stop_reason = StopReason.SHOW_MACRO m, node = lamb.node.expand(node, force_all = only_macro) macro_expansions += m @@ -147,10 +143,16 @@ class Runner: ("class:warn", " is a free variable\n"), ] - printf(FormattedText(warning_text), style = lamb.utils.style) + if len(warning_text) != 0: + printf(FormattedText(warning_text), style = lamb.utils.style) - while (self.reduction_limit is None) or (k < self.reduction_limit): + while ( + ( + (self.reduction_limit is None) or + (k < self.reduction_limit) + ) and not only_macro + ): # Show reduction count if (k >= self.iter_update) and (k % self.iter_update == 0): @@ -177,22 +179,32 @@ class Runner: # Clear reduction counter if it was printed print(" " * round(14 + math.log10(k)), end = "\r") - out_text += [ - ("class:result_header", f"Runtime: "), - ("class:text", f"{time.time() - start_time:.03f} seconds"), + if only_macro: + out_text += [ + ("class:result_header", f"Displaying macro content") + ] - ("class:result_header", f"\nExit reason: "), - stop_reason.value, + else: + out_text += [ + ("class:result_header", f"Runtime: "), + ("class:text", f"{time.time() - start_time:.03f} seconds"), - ("class:result_header", f"\nMacro expansions: "), - ("class:text", f"{macro_expansions:,}"), + ("class:result_header", f"\nExit reason: "), + stop_reason.value, - ("class:result_header", f"\nReductions: "), - ("class:text", f"{k:,}\t"), - ("class:muted", f"(Limit: {self.reduction_limit:,})") - ] + ("class:result_header", f"\nMacro expansions: "), + ("class:text", f"{macro_expansions:,}"), - if (stop_reason == StopReason.BETA_NORMAL or stop_reason == StopReason.LOOP_DETECTED): + ("class:result_header", f"\nReductions: "), + ("class:text", f"{k:,}\t"), + ("class:muted", f"(Limit: {self.reduction_limit:,})") + ] + + if ( + stop_reason == StopReason.BETA_NORMAL or + stop_reason == StopReason.LOOP_DETECTED or + only_macro + ): out_text += [ ("class:result_header", "\n\n => "), ("class:text", str(node)), # type: ignore