diff --git a/lamb/node.py b/lamb/node.py index 4a01067..c78e953 100644 --- a/lamb/node.py +++ b/lamb/node.py @@ -14,6 +14,9 @@ class ReductionType(enum.Enum): # 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() @@ -101,6 +104,10 @@ class Node: self._left: Node | None = None self._right: Node | None = None + # The runner this node is attached to. + # Set by Node.set_runner() + self.runner: lamb.runner.Runner = None # type: ignore + def __iter__(self): return TreeWalker(self) @@ -220,17 +227,25 @@ class Node: ban_macro_name = ban_macro_name ) + def set_runner(self, runner): + for s, n in self: + if s == Direction.UP: + n.runner = runner # type: ignore + return self + class EndNode(Node): def print_value(self, *, export: bool = False) -> str: raise NotImplementedError("EndNodes MUST provide a `print_value` method!") class ExpandableEndNode(EndNode): - def expand(self, *, macro_table = {}) -> tuple[ReductionType, Node]: + always_expand = False + def expand(self) -> tuple[ReductionType, Node]: raise NotImplementedError("ExpandableEndNodes MUST provide an `expand` method!") class FreeVar(EndNode): - def __init__(self, name: str): + def __init__(self, name: str, *, runner = None): self.name = name + self.runner = runner # type: ignore def __repr__(self): return f"" @@ -249,11 +264,12 @@ class Macro(ExpandableEndNode): def from_parse(results): return Macro(results[0]) - def __init__(self, name: str) -> None: + def __init__(self, name: str, *, runner = None) -> None: super().__init__() self.name = name self.left = None self.right = None + self.runner = runner # type: ignore def __repr__(self): return f"" @@ -261,25 +277,26 @@ class Macro(ExpandableEndNode): def print_value(self, *, export: bool = False) -> str: return self.name - def expand(self, *, macro_table = {}) -> tuple[ReductionType, Node]: - if self.name in macro_table: - return ReductionType.MACRO_EXPAND, clone(macro_table[self.name]) + 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]) else: - return ReductionType.MACRO_TO_FREE, FreeVar(self.name) + return ReductionType.MACRO_TO_FREE, FreeVar(self.name, runner = self.runner) def copy(self): - return Macro(self.name) + return Macro(self.name, runner = self.runner) class Church(ExpandableEndNode): @staticmethod def from_parse(results): return Church(int(results[0])) - def __init__(self, value: int) -> None: + def __init__(self, value: int, *, runner = None) -> None: super().__init__() self.value = value self.left = None self.right = None + self.runner = runner # type: ignore def __repr__(self): return f"" @@ -287,7 +304,7 @@ class Church(ExpandableEndNode): def print_value(self, *, export: bool = False) -> str: return str(self.value) - def expand(self, *, macro_table = {}) -> tuple[ReductionType, Node]: + def expand(self) -> tuple[ReductionType, Node]: f = Bound("f") a = Bound("a") chain = a @@ -297,17 +314,46 @@ class Church(ExpandableEndNode): return ( ReductionType.AUTOCHURCH, - Func(f, Func(a, chain)) + Func(f, Func(a, chain)).set_runner(self.runner) ) def copy(self): - return Church(self.value) + return Church(self.value, runner = self.runner) + +class History(ExpandableEndNode): + always_expand = True + + @staticmethod + def from_parse(results): + return History() + + def __init__(self, *, runner = None) -> None: + super().__init__() + self.left = None + self.right = None + self.runner = runner # type: ignore + + def __repr__(self): + return f"<$>" + + def print_value(self, *, export: bool = False) -> str: + return "$" + + 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]) + + def copy(self): + return History(runner = self.runner) + bound_counter = 0 class Bound(EndNode): - def __init__(self, name: str, *, forced_id = None): + def __init__(self, name: str, *, forced_id = None, runner = None): self.name = name global bound_counter + self.runner = runner # type: ignore if forced_id is None: self.identifier = bound_counter @@ -316,7 +362,7 @@ class Bound(EndNode): self.identifier = forced_id def copy(self): - return Bound(self.name, forced_id = self.identifier) + return Bound(self.name, forced_id = self.identifier, runner = self.runner) def __eq__(self, other): if not isinstance(other, Bound): @@ -343,17 +389,18 @@ class Func(Node): Func.from_parse(result) ) - def __init__(self, input: Macro | Bound, output: Node) -> None: + def __init__(self, input: Macro | Bound, output: Node, *, runner = None) -> None: super().__init__() self.input: Macro | Bound = input self.left: Node = output self.right: None = None + self.runner = runner # type: ignore def __repr__(self): return f"" def copy(self): - return Func(self.input, None) # type: ignore + return Func(self.input, None, runner = self.runner) # type: ignore class Call(Node): @staticmethod @@ -376,16 +423,17 @@ class Call(Node): )] + results[2:] ) - def __init__(self, fn: Node, arg: Node) -> None: + def __init__(self, fn: Node, arg: Node, *, runner = None) -> None: super().__init__() self.left: Node = fn self.right: Node = arg + self.runner = runner # type: ignore def __repr__(self): return f"" def copy(self): - return Call(None, None) # type: ignore + return Call(None, None, runner = self.runner) # type: ignore def print_node(node: Node, *, export: bool = False) -> str: @@ -476,21 +524,31 @@ def clone(node: Node): break return out -def bind_variables(node: Node, *, ban_macro_name = None) -> None: +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)}") bound_variables = {} + output = { + "has_history": False, + "free_variables": set() + } + for s, n in node: + 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. - if isinstance(n, Macro) and ban_macro_name is not None: - if n.name == ban_macro_name: + elif isinstance(n, Macro): + if (n.name == ban_macro_name) and (ban_macro_name is not None): raise ReductionError("Macro cannot reference self") - if isinstance(n, Func): + if n.name not in node.runner.macro_table: + output["free_variables"].add(n.name) + + 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. @@ -517,6 +575,7 @@ def bind_variables(node: Node, *, ban_macro_name = None) -> None: 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. # Returns the function's output. @@ -532,7 +591,7 @@ def call_func(fn: Func, arg: Node): # Do a single reduction step -def reduce(node: Node, *, macro_table = {}) -> tuple[ReductionType, Node]: +def reduce(node: Node) -> tuple[ReductionType, Node]: if not isinstance(node, Node): raise TypeError(f"I can't reduce a {type(node)}") @@ -552,16 +611,14 @@ def reduce(node: Node, *, macro_table = {}) -> tuple[ReductionType, Node]: return ReductionType.FUNCTION_APPLY, out elif isinstance(n.left, ExpandableEndNode): - r, n.left = n.left.expand( - macro_table = macro_table - ) + r, n.left = n.left.expand() return r, out return ReductionType.NOTHING, out # Expand all expandable end nodes. -def force_expand_macros(node: Node, *, macro_table = {}) -> tuple[int, Node]: +def finalize_macros(node: Node, *, force = False) -> tuple[int, Node]: if not isinstance(node, Node): raise TypeError(f"I can't reduce a {type(node)}") @@ -572,15 +629,18 @@ def force_expand_macros(node: Node, *, macro_table = {}) -> tuple[int, Node]: macro_expansions = 0 while True: - if isinstance(ptr, ExpandableEndNode): + if ( + isinstance(ptr, ExpandableEndNode) and + (force or ptr.always_expand) + ): if ptr.parent is None: - ptr = ptr.expand(macro_table = macro_table)[1] + ptr = ptr.expand()[1] out = ptr ptr._set_parent(None, None) else: ptr.parent.set_side( ptr.parent_side, # type: ignore - ptr.expand(macro_table = macro_table)[1] + ptr.expand()[1] ) ptr = ptr.parent.get_side( ptr.parent_side # type: ignore diff --git a/lamb/parser.py b/lamb/parser.py index ac5da20..7acd65e 100755 --- a/lamb/parser.py +++ b/lamb/parser.py @@ -16,13 +16,14 @@ class LambdaParser: self.pp_bound = pp.Char(pp.srange("[a-z]")) self.pp_name = self.pp_bound ^ self.pp_macro self.pp_church = pp.Word(pp.nums) + self.pp_history = pp.Char("$") # Function calls. # # # self.pp_call = pp.Forward() - self.pp_call <<= (self.pp_expr | self.pp_bound)[2, ...] + self.pp_call <<= (self.pp_expr | self.pp_bound | self.pp_history)[2, ...] # Function definitions, right associative. # Function args MUST be lowercase. @@ -43,7 +44,7 @@ class LambdaParser: pp.line_start() + self.pp_macro + pp.Suppress("=") + - (self.pp_expr ^ self.pp_call) + (self.pp_expr ^ self.pp_call ^ self.pp_history) ) self.pp_expr <<= ( @@ -51,7 +52,8 @@ class LambdaParser: self.pp_lambda_fun ^ self.pp_name ^ (self.lp + self.pp_expr + self.rp) ^ - (self.lp + self.pp_call + self.rp) + (self.lp + self.pp_call + self.rp) ^ + (self.lp + self.pp_history + self.rp) ) self.pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_") + pp.Word(pp.alphas + pp.nums + "_")[0, ...] @@ -61,7 +63,8 @@ class LambdaParser: self.pp_expr ^ self.pp_macro_def ^ self.pp_command ^ - self.pp_call + self.pp_call ^ + self.pp_history ) def __init__( @@ -73,7 +76,8 @@ class LambdaParser: action_func, action_bound, action_macro, - action_call + action_call, + action_history ): self.make_parser() @@ -85,6 +89,7 @@ class LambdaParser: self.pp_macro.set_parse_action(action_macro) self.pp_bound.set_parse_action(action_bound) self.pp_call.set_parse_action(action_call) + self.pp_history.set_parse_action(action_history) def parse_line(self, line: str): return self.pp_all.parse_string( diff --git a/lamb/runner.py b/lamb/runner.py index 82b0336..30e8f4b 100644 --- a/lamb/runner.py +++ b/lamb/runner.py @@ -37,6 +37,9 @@ class MacroDef: ban_macro_name = ban_macro_name ) + def set_runner(self, runner): + return self.expr.set_runner(runner) + class Command: @staticmethod def from_parse(result): @@ -66,7 +69,8 @@ class Runner: action_call = lamb.node.Call.from_parse, action_church = lamb.node.Church.from_parse, action_macro_def = MacroDef.from_parse, - action_command = Command.from_parse + action_command = Command.from_parse, + action_history = lamb.node.History.from_parse ) # Maximum amount of reductions. @@ -84,22 +88,48 @@ class Runner: # so that all digits appear to be changing. self.iter_update = 231 + self.history = [] + def prompt(self): return self.prompt_session.prompt( message = self.prompt_message ) - def parse(self, line): + def parse(self, line) -> tuple[lamb.node.Node | MacroDef | Command, dict]: e = self.parser.parse_line(line) + o = {} if isinstance(e, MacroDef): - e.bind_variables(ban_macro_name = e.label) + e.set_runner(self) + o = e.bind_variables(ban_macro_name = e.label) elif isinstance(e, lamb.node.Node): - e.bind_variables() - return e + e.set_runner(self) + o = e.bind_variables() + return e, o - def reduce(self, node: lamb.node.Node) -> None: + def reduce(self, node: lamb.node.Node, *, status = {}) -> None: + + # Show warnings + warning_text = [] + + if status["has_history"] and len(self.history) != 0: + warning_text += [ + ("class:code", "$"), + ("class:warn", " will be expanded to "), + ("class:code", str(self.history[-1])), + ("class:warn", "\n") + ] + + for i in status["free_variables"]: + warning_text += [ + ("class:warn", "Macro "), + ("class:code", i), + ("class:warn", " will become a free variable.\n"), + ] + + printf(FormattedText(warning_text), style = lamb.utils.style) + # Reduction Counter. # We also count macro (and church) expansions, # and subtract those from the final count. @@ -119,10 +149,7 @@ class Runner: print(f" Reducing... {i:,}", end = "\r") try: - red_type, node = lamb.node.reduce( - node, - macro_table = self.macro_table - ) + red_type, node = lamb.node.reduce(node) except KeyboardInterrupt: stop_reason = StopReason.INTERRUPT break @@ -138,13 +165,9 @@ class Runner: if red_type == lamb.node.ReductionType.FUNCTION_APPLY: macro_expansions += 1 - # Expand all macros if we need to - if full_reduce: - m, node = lamb.node.force_expand_macros( - node, - macro_table = self.macro_table - ) - macro_expansions += m + # Expand all remaining macros + m, node = lamb.node.finalize_macros(node, force = full_reduce) + macro_expansions += m if i >= self.iter_update: # Clear reduction counter @@ -176,6 +199,9 @@ class Runner: ("class:text", str(node)), # type: ignore ] + self.history.append(lamb.node.finalize_macros(node, force = True)[1]) + + printf( FormattedText(out_text), style = lamb.utils.style @@ -205,7 +231,7 @@ class Runner: *, silent = False ) -> None: - e = self.parse(line) + e, o = self.parse(line) # If this line is a macro definition, save the macro. if isinstance(e, MacroDef): @@ -225,7 +251,7 @@ class Runner: # If this line is a plain expression, reduce it. elif isinstance(e, lamb.node.Node): - self.reduce(e) + self.reduce(e, status = o) # We shouldn't ever get here. else: