From 9f467fa2234e94e2dc9618bb752437da8a75471e Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 7 Nov 2022 20:15:04 -0800 Subject: [PATCH] Cleaned up files --- README.md | 1 - lamb/__init__.py | 3 +- lamb/__main__.py | 2 +- lamb/nodes/__init__.py | 3 + lamb/nodes/functions.py | 229 +++++++++++++++++++++ lamb/nodes/misc.py | 33 ++++ lamb/{node.py => nodes/nodes.py} | 328 ++++--------------------------- lamb/runner/__init__.py | 2 + lamb/{ => runner}/commands.py | 2 +- lamb/runner/misc.py | 42 ++++ lamb/{ => runner}/runner.py | 91 +++------ 11 files changed, 374 insertions(+), 362 deletions(-) create mode 100644 lamb/nodes/__init__.py create mode 100644 lamb/nodes/functions.py create mode 100644 lamb/nodes/misc.py rename lamb/{node.py => nodes/nodes.py} (53%) create mode 100644 lamb/runner/__init__.py rename lamb/{ => runner}/commands.py (99%) create mode 100644 lamb/runner/misc.py rename lamb/{ => runner}/runner.py (69%) diff --git a/README.md b/README.md index 5cad3d3..b35b54f 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,6 @@ Lamb treats each λ expression as a binary tree. Variable binding and reduction - Prevent macro-chaining recursion - Full-reduce option (expand all macros) - step-by-step reduction - - Cleanup files - Update screenshot - Update documentation & "internals" section. - PyPi package diff --git a/lamb/__init__.py b/lamb/__init__.py index 64075ae..38688c1 100644 --- a/lamb/__init__.py +++ b/lamb/__init__.py @@ -1,7 +1,6 @@ from . import utils -from . import node +from . import nodes from . import parser -from . import commands from .runner import Runner from .runner import StopReason \ No newline at end of file diff --git a/lamb/__main__.py b/lamb/__main__.py index cb80c02..2cc9b6b 100755 --- a/lamb/__main__.py +++ b/lamb/__main__.py @@ -52,7 +52,7 @@ while True: ("class:text", "\n") ]), style = lamb.utils.style) continue - except lamb.node.ReductionError as e: + except lamb.nodes.ReductionError as e: printf(FormattedText([ ("class:err", f"{e.msg}\n") ]), style = lamb.utils.style) diff --git a/lamb/nodes/__init__.py b/lamb/nodes/__init__.py new file mode 100644 index 0000000..3f4c702 --- /dev/null +++ b/lamb/nodes/__init__.py @@ -0,0 +1,3 @@ +from .misc import * +from .nodes import * +from .functions import * \ No newline at end of file diff --git a/lamb/nodes/functions.py b/lamb/nodes/functions.py new file mode 100644 index 0000000..79b13a8 --- /dev/null +++ b/lamb/nodes/functions.py @@ -0,0 +1,229 @@ +import lamb +import lamb.nodes as lbn + +def print_node(node: lbn.Node, *, export: bool = False) -> str: + if not isinstance(node, lbn.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, lbn.EndNode): + if isinstance(n, lbn.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, lbn.Func): + if s == lbn.Direction.UP: + if isinstance(n.parent, lbn.Call): + out += "(" + + if isinstance(n.parent, lbn.Func): + out += n.input.name + else: + out += "λ" + n.input.name + if not isinstance(n.left, lbn.Func): + out += "." + elif s == lbn.Direction.LEFT: + if isinstance(n.parent, lbn.Call): + out += ")" + + elif isinstance(n, lbn.Call): + if s == lbn.Direction.UP: + out += "(" + elif s == lbn.Direction.LEFT: + out += " " + elif s == lbn.Direction.RIGHT: + out += ")" + + return out + +def clone(node: lbn.Node): + if not isinstance(node, lbn.Node): + raise TypeError(f"I don't know what to do with a {type(node)}") + + out = node.copy() + out_ptr = out # Stays one step behind ptr, in the new tree. + ptr = node + from_side = lbn.Direction.UP + + if isinstance(node, lbn.EndNode): + return out + + # We're not using a TreeWalker here because + # we need more control over our pointer when cloning. + while True: + if isinstance(ptr, lbn.EndNode): + from_side, ptr = ptr.go_up() + _, out_ptr = out_ptr.go_up() + elif isinstance(ptr, lbn.Func) or isinstance(ptr, lbn.Root): + if from_side == lbn.Direction.UP: + from_side, ptr = ptr.go_left() + out_ptr.set_side(ptr.parent_side, ptr.copy()) + _, out_ptr = out_ptr.go_left() + elif from_side == lbn.Direction.LEFT: + from_side, ptr = ptr.go_up() + _, out_ptr = out_ptr.go_up() + elif isinstance(ptr, lbn.Call): + if from_side == lbn.Direction.UP: + from_side, ptr = ptr.go_left() + out_ptr.set_side(ptr.parent_side, ptr.copy()) + _, out_ptr = out_ptr.go_left() + elif from_side == lbn.Direction.LEFT: + from_side, ptr = ptr.go_right() + out_ptr.set_side(ptr.parent_side, ptr.copy()) + _, out_ptr = out_ptr.go_right() + elif from_side == lbn.Direction.RIGHT: + from_side, ptr = ptr.go_up() + _, out_ptr = out_ptr.go_up() + + if ptr is node.parent: + break + return out + +def prepare(root: lbn.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, lbn.Root): + raise TypeError(f"I don't know what to do with a {type(root)}") + + bound_variables = {} + + output = { + "has_history": False, + "free_variables": set() + } + + it = iter(root) + for s, n in it: + if isinstance(n, lbn.History): + output["has_history"] = True + + # If this expression is part of a macro, + # make sure we don't reference it inside itself. + elif isinstance(n, lbn.Macro): + if (n.name == ban_macro_name) and (ban_macro_name is not None): + raise lbn.ReductionError("Macro cannot reference self") + + # 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, lbn.Func): + if s == lbn.Direction.UP: + # Add this function's input to the table of bound variables. + # If it is already there, raise an error. + if (n.input.name in bound_variables): + raise lbn.ReductionError(f"Bound variable name conflict: \"{n.input.name}\"") + else: + bound_variables[n.input.name] = lbn.Bound(n.input.name) + n.input = bound_variables[n.input.name] + + elif s == lbn.Direction.LEFT: + del bound_variables[n.input.name] + + return output + +# Apply a function. +# Returns the function's output. +def call_func(fn: lbn.Func, arg: lbn.Node): + for s, n in fn: + if isinstance(n, lbn.Bound) and (s == lbn.Direction.UP): + if n == fn.input: + if n.parent is None: + raise Exception("Tried to substitute a None bound variable.") + + n.parent.set_side(n.parent_side, clone(arg)) # type: ignore + return fn.left + +# Do a single reduction step +def reduce(root: lbn.Root) -> tuple[lbn.ReductionType, lbn.Root]: + if not isinstance(root, lbn.Root): + raise TypeError(f"I can't reduce a {type(root)}") + + out = root + for s, n in out: + if isinstance(n, lbn.Call) and (s == lbn.Direction.UP): + if isinstance(n.left, lbn.Func): + n.parent.set_side( + n.parent_side, # type: ignore + call_func(n.left, n.right) + ) + + return lbn.ReductionType.FUNCTION_APPLY, out + + elif isinstance(n.left, lbn.ExpandableEndNode): + r, n.left = n.left.expand() + return r, out + return lbn.ReductionType.NOTHING, out + + +def expand(root: lbn.Root, *, force_all = False) -> tuple[int, lbn.Root]: + """ + Expands expandable nodes in the given tree. + + If force_all is false, this only expands + ExpandableEndnodes that have "always_expand" set to True. + + If force_all is True, this expands ALL + ExpandableEndnodes. + """ + + if not isinstance(root, lbn.Root): + raise TypeError(f"I don't know what to do with a {type(root)}") + + out = root + macro_expansions = 0 + + it = iter(root) + for s, n in it: + if ( + isinstance(n, lbn.ExpandableEndNode) and + (force_all or n.always_expand) + ): + + 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 + return macro_expansions, out \ No newline at end of file diff --git a/lamb/nodes/misc.py b/lamb/nodes/misc.py new file mode 100644 index 0000000..0420c02 --- /dev/null +++ b/lamb/nodes/misc.py @@ -0,0 +1,33 @@ +import enum + +class Direction(enum.Enum): + UP = enum.auto() + LEFT = enum.auto() + RIGHT = enum.auto() + +class ReductionType(enum.Enum): + # Nothing happened. This implies that + # an expression cannot be reduced further. + NOTHING = enum.auto() + + # We replaced a macro with an expression. + MACRO_EXPAND = enum.auto() + + # We expanded a history reference + HIST_EXPAND = enum.auto() + + # We turned a church numeral into an expression + AUTOCHURCH = enum.auto() + + # We applied a function. + # This is the only type of "formal" reduction step. + FUNCTION_APPLY = enum.auto() + +class ReductionError(Exception): + """ + Raised when we encounter an error while reducing. + + These should be caught and elegantly presented to the user. + """ + def __init__(self, msg: str): + self.msg = msg diff --git a/lamb/node.py b/lamb/nodes/nodes.py similarity index 53% rename from lamb/node.py rename to lamb/nodes/nodes.py index 427d439..a1ba50e 100644 --- a/lamb/node.py +++ b/lamb/nodes/nodes.py @@ -1,37 +1,5 @@ -import enum import lamb - -class Direction(enum.Enum): - UP = enum.auto() - LEFT = enum.auto() - RIGHT = enum.auto() - -class ReductionType(enum.Enum): - # Nothing happened. This implies that - # an expression cannot be reduced further. - NOTHING = enum.auto() - - # We replaced a macro with an expression. - MACRO_EXPAND = enum.auto() - - # We expanded a history reference - HIST_EXPAND = enum.auto() - - # We turned a church numeral into an expression - AUTOCHURCH = enum.auto() - - # We applied a function. - # This is the only type of "formal" reduction step. - FUNCTION_APPLY = enum.auto() - -class ReductionError(Exception): - """ - Raised when we encounter an error while reducing. - - These should be caught and elegantly presented to the user. - """ - def __init__(self, msg: str): - self.msg = msg +import lamb.nodes as lbn class TreeWalker: """ @@ -48,7 +16,7 @@ class TreeWalker: self.expr = expr self.ptr = expr self.first_step = True - self.from_side = Direction.UP + self.from_side = lbn.Direction.UP def __iter__(self): return self @@ -64,21 +32,21 @@ class TreeWalker: return self.from_side, self.ptr if isinstance(self.ptr, Root): - if self.from_side == Direction.UP: + if self.from_side == lbn.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: + if self.from_side == lbn.Direction.UP: self.from_side, self.ptr = self.ptr.go_left() - elif self.from_side == Direction.LEFT: + elif self.from_side == lbn.Direction.LEFT: self.from_side, self.ptr = self.ptr.go_up() elif isinstance(self.ptr, Call): - if self.from_side == Direction.UP: + if self.from_side == lbn.Direction.UP: self.from_side, self.ptr = self.ptr.go_left() - elif self.from_side == Direction.LEFT: + elif self.from_side == lbn.Direction.LEFT: self.from_side, self.ptr = self.ptr.go_right() - elif self.from_side == Direction.RIGHT: + elif self.from_side == lbn.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)}") @@ -127,9 +95,9 @@ class Node: """ if (parent is not None) and (side is None): - raise Exception("If a node has a parent, it must have a direction.") + raise Exception("If a node has a parent, it must have a lbn.direction.") if (parent is None) and (side is not None): - raise Exception("If a node has no parent, it cannot have a direction.") + raise Exception("If a node has no parent, it cannot have a lbn.direction.") self.parent = parent self.parent_side = side return self @@ -141,7 +109,7 @@ class Node: @left.setter def left(self, node): if node is not None: - node._set_parent(self, Direction.LEFT) + node._set_parent(self, lbn.Direction.LEFT) self._left = node @property @@ -151,27 +119,27 @@ class Node: @right.setter def right(self, node): if node is not None: - node._set_parent(self, Direction.RIGHT) + node._set_parent(self, lbn.Direction.RIGHT) self._right = node - def set_side(self, side: Direction, node): + def set_side(self, side: lbn.Direction, node): """ A wrapper around Node.left and Node.right that automatically selects a side. """ - if side == Direction.LEFT: + if side == lbn.Direction.LEFT: self.left = node - elif side == Direction.RIGHT: + elif side == lbn.Direction.RIGHT: self.right = node else: raise TypeError("Can only set left or right side.") - def get_side(self, side: Direction): - if side == Direction.LEFT: + def get_side(self, side: lbn.Direction): + if side == lbn.Direction.LEFT: return self.left - elif side == Direction.RIGHT: + elif side == lbn.Direction.RIGHT: return self.right else: raise TypeError("Can only get left or right side.") @@ -188,7 +156,7 @@ class Node: if self._left is None: raise Exception("Can't go left when left is None") - return Direction.UP, self._left + return lbn.Direction.UP, self._left def go_right(self): """ @@ -200,7 +168,7 @@ class Node: """ if self._right is None: raise Exception("Can't go right when right is None") - return Direction.UP, self._right + return lbn.Direction.UP, self._right def go_up(self): """ @@ -221,17 +189,17 @@ class Node: raise NotImplementedError("Nodes MUST provide a `copy` method!") def __str__(self) -> str: - return print_node(self) + return lbn.print_node(self) def export(self) -> str: """ Convert this tree to a parsable string. """ - return print_node(self, export = True) + return lbn.print_node(self, export = True) def set_runner(self, runner): for s, n in self: - if s == Direction.UP: + if s == lbn.Direction.UP: n.runner = runner # type: ignore return self @@ -241,7 +209,7 @@ class EndNode(Node): class ExpandableEndNode(EndNode): always_expand = False - def expand(self) -> tuple[ReductionType, Node]: + def expand(self) -> tuple[lbn.ReductionType, Node]: raise NotImplementedError("ExpandableEndNodes MUST provide an `expand` method!") class FreeVar(EndNode): @@ -280,13 +248,13 @@ class Macro(ExpandableEndNode): def print_value(self, *, export: bool = False) -> str: return self.name - def expand(self) -> tuple[ReductionType, Node]: + def expand(self) -> tuple[lbn.ReductionType, Node]: if self.name in self.runner.macro_table: # 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) + lbn.ReductionType.MACRO_EXPAND, + lbn.clone(self.runner.macro_table[self.name].left) ) else: raise Exception(f"Macro {self.name} is not defined") @@ -315,16 +283,16 @@ class Church(ExpandableEndNode): def print_value(self, *, export: bool = False) -> str: return str(self.value) - def expand(self) -> tuple[ReductionType, Node]: + def expand(self) -> tuple[lbn.ReductionType, Node]: f = Bound("f") a = Bound("a") chain = a for i in range(self.value): - chain = Call(clone(f), clone(chain)) + chain = Call(lbn.clone(f), lbn.clone(chain)) return ( - ReductionType.AUTOCHURCH, + lbn.ReductionType.AUTOCHURCH, Func(f, Func(a, chain)).set_runner(self.runner) ) @@ -350,13 +318,13 @@ class History(ExpandableEndNode): def print_value(self, *, export: bool = False) -> str: return "$" - def expand(self) -> tuple[ReductionType, Node]: + def expand(self) -> tuple[lbn.ReductionType, Node]: if len(self.runner.history) == 0: - raise ReductionError(f"There isn't any history to reference.") + raise lbn.ReductionError(f"There isn't any history to reference.") # .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) + return lbn.ReductionType.HIST_EXPAND, lbn.clone(self.runner.history[-1].left) def copy(self): return History(runner = self.runner) @@ -463,232 +431,4 @@ class Call(Node): return f"" def copy(self): - return Call(None, None, runner = self.runner) # type: ignore - - -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): - 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: - if isinstance(n.parent, Call): - out += "(" - - if isinstance(n.parent, Func): - out += n.input.name - else: - out += "λ" + n.input.name - if not isinstance(n.left, Func): - out += "." - elif s == Direction.LEFT: - if isinstance(n.parent, Call): - out += ")" - - elif isinstance(n, Call): - if s == Direction.UP: - out += "(" - elif s == Direction.LEFT: - out += " " - elif s == Direction.RIGHT: - out += ")" - - return out - -def clone(node: Node): - if not isinstance(node, Node): - raise TypeError(f"I don't know what to do with a {type(node)}") - - out = node.copy() - out_ptr = out # Stays one step behind ptr, in the new tree. - ptr = node - from_side = Direction.UP - - if isinstance(node, EndNode): - return out - - # We're not using a TreeWalker here because - # we need more control over our pointer when cloning. - while True: - if isinstance(ptr, EndNode): - from_side, ptr = ptr.go_up() - _, out_ptr = out_ptr.go_up() - 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()) - _, out_ptr = out_ptr.go_left() - elif from_side == Direction.LEFT: - from_side, ptr = ptr.go_up() - _, out_ptr = out_ptr.go_up() - elif isinstance(ptr, Call): - if from_side == Direction.UP: - from_side, ptr = ptr.go_left() - out_ptr.set_side(ptr.parent_side, ptr.copy()) - _, out_ptr = out_ptr.go_left() - elif from_side == Direction.LEFT: - from_side, ptr = ptr.go_right() - out_ptr.set_side(ptr.parent_side, ptr.copy()) - _, out_ptr = out_ptr.go_right() - elif from_side == Direction.RIGHT: - from_side, ptr = ptr.go_up() - _, out_ptr = out_ptr.go_up() - - if ptr is node.parent: - break - return out - -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 = {} - - output = { - "has_history": False, - "free_variables": set() - } - - it = iter(root) - for s, n in it: - if isinstance(n, History): - output["has_history"] = True - - # If this expression is part of a macro, - # make sure we don't reference it inside itself. - elif isinstance(n, Macro): - if (n.name == ban_macro_name) and (ban_macro_name is not None): - raise ReductionError("Macro cannot reference self") - - # 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. - # If it is already there, raise an error. - if (n.input.name in bound_variables): - raise ReductionError(f"Bound variable name conflict: \"{n.input.name}\"") - else: - bound_variables[n.input.name] = Bound(n.input.name) - n.input = bound_variables[n.input.name] - - elif s == Direction.LEFT: - del bound_variables[n.input.name] - - return output - -# Apply a function. -# Returns the function's output. -def call_func(fn: Func, arg: Node): - for s, n in fn: - if isinstance(n, Bound) and (s == Direction.UP): - if n == fn.input: - if n.parent is None: - raise Exception("Tried to substitute a None bound variable.") - - n.parent.set_side(n.parent_side, clone(arg)) # type: ignore - return fn.left - -# Do a single reduction step -def reduce(root: Root) -> tuple[ReductionType, Root]: - if not isinstance(root, Root): - raise TypeError(f"I can't reduce a {type(root)}") - - out = root - for s, n in out: - if isinstance(n, Call) and (s == Direction.UP): - if isinstance(n.left, Func): - n.parent.set_side( - n.parent_side, # type: ignore - call_func(n.left, n.right) - ) - - return ReductionType.FUNCTION_APPLY, out - - elif isinstance(n.left, ExpandableEndNode): - r, n.left = n.left.expand() - return r, out - return ReductionType.NOTHING, out - - -def expand(root: Root, *, force_all = False) -> tuple[int, Root]: - """ - Expands expandable nodes in the given tree. - - If force_all is false, this only expands - ExpandableEndnodes that have "always_expand" set to True. - - If force_all is True, this expands ALL - ExpandableEndnodes. - """ - - if not isinstance(root, Root): - raise TypeError(f"I don't know what to do with a {type(root)}") - - out = root - macro_expansions = 0 - - it = iter(root) - for s, n in it: - if ( - isinstance(n, ExpandableEndNode) and - (force_all or n.always_expand) - ): - - 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 - return macro_expansions, out \ No newline at end of file + return Call(None, None, runner = self.runner) # type: ignore \ No newline at end of file diff --git a/lamb/runner/__init__.py b/lamb/runner/__init__.py new file mode 100644 index 0000000..bdb12aa --- /dev/null +++ b/lamb/runner/__init__.py @@ -0,0 +1,2 @@ +from .runner import Runner +from .runner import StopReason \ No newline at end of file diff --git a/lamb/commands.py b/lamb/runner/commands.py similarity index 99% rename from lamb/commands.py rename to lamb/runner/commands.py index 3959385..f9bda14 100644 --- a/lamb/commands.py +++ b/lamb/runner/commands.py @@ -123,7 +123,7 @@ def cmd_load(command, runner): ) return - if not isinstance(x, lamb.runner.MacroDef): + if not isinstance(x, lamb.runner.runner.MacroDef): printf( FormattedText([ ("class:warn", f"Skipping line {i+1:02}: "), diff --git a/lamb/runner/misc.py b/lamb/runner/misc.py new file mode 100644 index 0000000..cf19383 --- /dev/null +++ b/lamb/runner/misc.py @@ -0,0 +1,42 @@ +import enum +import lamb + +class StopReason(enum.Enum): + BETA_NORMAL = ("class:text", "β-normal form") + 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 + def from_parse(result): + return MacroDef( + result[0].name, + result[1] + ) + + def __init__(self, label: str, expr: lamb.nodes.Node): + self.label = label + self.expr = expr + + def __repr__(self): + return f"<{self.label} := {self.expr!r}>" + + def __str__(self): + return f"{self.label} := {self.expr}" + + def set_runner(self, runner): + return self.expr.set_runner(runner) + +class Command: + @staticmethod + def from_parse(result): + return Command( + result[0], + result[1:] + ) + + def __init__(self, name, args): + self.name = name + self.args = args diff --git a/lamb/runner.py b/lamb/runner/runner.py similarity index 69% rename from lamb/runner.py rename to lamb/runner/runner.py index 14c5c0a..2e4acda 100644 --- a/lamb/runner.py +++ b/lamb/runner/runner.py @@ -7,46 +7,11 @@ import time import lamb +from lamb.runner.misc import MacroDef +from lamb.runner.misc import Command +from lamb.runner.misc import StopReason +from lamb.runner import commands as cmd -class StopReason(enum.Enum): - BETA_NORMAL = ("class:text", "β-normal form") - 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 - def from_parse(result): - return MacroDef( - result[0].name, - result[1] - ) - - def __init__(self, label: str, expr: lamb.node.Node): - self.label = label - self.expr = expr - - def __repr__(self): - return f"<{self.label} := {self.expr!r}>" - - def __str__(self): - return f"{self.label} := {self.expr}" - - def set_runner(self, runner): - return self.expr.set_runner(runner) - -class Command: - @staticmethod - def from_parse(result): - return Command( - result[0], - result[1:] - ) - - def __init__(self, name, args): - self.name = name - self.args = args class Runner: @@ -59,14 +24,14 @@ class Runner: self.prompt_session = prompt_session self.prompt_message = prompt_message self.parser = lamb.parser.LambdaParser( - action_func = lamb.node.Func.from_parse, - action_bound = lamb.node.Macro.from_parse, - action_macro = lamb.node.Macro.from_parse, - action_call = lamb.node.Call.from_parse, - action_church = lamb.node.Church.from_parse, + action_func = lamb.nodes.Func.from_parse, + action_bound = lamb.nodes.Macro.from_parse, + action_macro = lamb.nodes.Macro.from_parse, + action_call = lamb.nodes.Call.from_parse, + action_church = lamb.nodes.Church.from_parse, action_macro_def = MacroDef.from_parse, action_command = Command.from_parse, - action_history = lamb.node.History.from_parse + action_history = lamb.nodes.History.from_parse ) # Maximum amount of reductions. @@ -84,30 +49,30 @@ class Runner: # so that all digits appear to be changing. self.iter_update = 231 - self.history: list[lamb.node.Root] = [] + self.history: list[lamb.nodes.Root] = [] def prompt(self): return self.prompt_session.prompt( message = self.prompt_message ) - def parse(self, line) -> tuple[lamb.node.Root | MacroDef | Command, dict]: + def parse(self, line) -> tuple[lamb.nodes.Root | MacroDef | Command, dict]: e = self.parser.parse_line(line) o = {} if isinstance(e, MacroDef): - e.expr = lamb.node.Root(e.expr) + e.expr = lamb.nodes.Root(e.expr) e.set_runner(self) - o = lamb.node.prepare(e.expr, ban_macro_name = e.label) - elif isinstance(e, lamb.node.Node): - e = lamb.node.Root(e) + o = lamb.nodes.prepare(e.expr, ban_macro_name = e.label) + elif isinstance(e, lamb.nodes.Node): + e = lamb.nodes.Root(e) e.set_runner(self) - o = lamb.node.prepare(e) + o = lamb.nodes.prepare(e) return e, o - def reduce(self, node: lamb.node.Root, *, status = {}) -> None: + def reduce(self, node: lamb.nodes.Root, *, status = {}) -> None: warning_text = [] @@ -130,12 +95,12 @@ class Runner: ] only_macro = ( - isinstance(node.left, lamb.node.Macro) or - isinstance(node.left, lamb.node.Church) + isinstance(node.left, lamb.nodes.Macro) or + isinstance(node.left, lamb.nodes.Church) ) if only_macro: stop_reason = StopReason.SHOW_MACRO - m, node = lamb.node.expand(node, force_all = only_macro) + m, node = lamb.nodes.expand(node, force_all = only_macro) macro_expansions += m @@ -162,20 +127,20 @@ class Runner: print(f" Reducing... {k:,}", end = "\r") try: - red_type, node = lamb.node.reduce(node) + red_type, node = lamb.nodes.reduce(node) except KeyboardInterrupt: stop_reason = StopReason.INTERRUPT break # If we can't reduce this expression anymore, # it's in beta-normal form. - if red_type == lamb.node.ReductionType.NOTHING: + if red_type == lamb.nodes.ReductionType.NOTHING: stop_reason = StopReason.BETA_NORMAL break # Count reductions k += 1 - if red_type == lamb.node.ReductionType.FUNCTION_APPLY: + if red_type == lamb.nodes.ReductionType.FUNCTION_APPLY: macro_expansions += 1 if k >= self.iter_update: @@ -213,7 +178,7 @@ class Runner: ("class:text", str(node)), # type: ignore ] - self.history.append(lamb.node.expand(node, force_all = True)[1]) + self.history.append(lamb.nodes.expand(node, force_all = True)[1]) printf( @@ -253,7 +218,7 @@ class Runner: # If this line is a command, do the command. elif isinstance(e, Command): - if e.name not in lamb.commands.commands: + if e.name not in cmd.commands: printf( FormattedText([ ("class:warn", f"Unknown command \"{e.name}\"") @@ -261,10 +226,10 @@ class Runner: style = lamb.utils.style ) else: - lamb.commands.commands[e.name](e, self) + cmd.commands[e.name](e, self) # If this line is a plain expression, reduce it. - elif isinstance(e, lamb.node.Node): + elif isinstance(e, lamb.nodes.Node): self.reduce(e, status = o) # We shouldn't ever get here.