diff --git a/README.md b/README.md index 98e41c6..07493d1 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,6 @@ The lines in a file look exactly the same as regular entries in the prompt, but ## Todo (pre-release, in this order): - Prevent macro-chaining recursion - Full-reduce option (expand all macros) - - step-by-step reduction - Update screenshot - Update documentation - Write "how it works" diff --git a/lamb/nodes/functions.py b/lamb/nodes/functions.py index 1f3236d..723068a 100644 --- a/lamb/nodes/functions.py +++ b/lamb/nodes/functions.py @@ -58,15 +58,24 @@ def print_node(node: lbn.Node, *, export: bool = False) -> str: 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() + macro_map = {} + if isinstance(node, lbn.Func): + c = node.copy() + macro_map[node.input.identifier] = c.input.identifier # type: ignore + else: + c = node.copy() + + out = c 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 @@ -79,7 +88,18 @@ def clone(node: lbn.Node): 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()) + + if isinstance(ptr, lbn.Func): + c = ptr.copy() + macro_map[ptr.input.identifier] = c.input.identifier # type: ignore + elif isinstance(ptr, lbn.Bound): + c = ptr.copy() + if c.identifier in macro_map: + c.identifier = macro_map[c.identifier] + else: + c = ptr.copy() + out_ptr.set_side(ptr.parent_side, c) + _, out_ptr = out_ptr.go_left() elif from_side == lbn.Direction.LEFT: from_side, ptr = ptr.go_up() @@ -87,11 +107,33 @@ def clone(node: lbn.Node): 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()) + + if isinstance(ptr, lbn.Func): + c = ptr.copy() + macro_map[ptr.input.identifier] = c.input.identifier # type: ignore + elif isinstance(ptr, lbn.Bound): + c = ptr.copy() + if c.identifier in macro_map: + c.identifier = macro_map[c.identifier] + else: + c = ptr.copy() + out_ptr.set_side(ptr.parent_side, c) + _, 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()) + + if isinstance(ptr, lbn.Func): + c = ptr.copy() + macro_map[ptr.input.identifier] = c.input.identifier # type: ignore + elif isinstance(ptr, lbn.Bound): + c = ptr.copy() + if c.identifier in macro_map: + c.identifier = macro_map[c.identifier] + else: + c = ptr.copy() + out_ptr.set_side(ptr.parent_side, c) + _, out_ptr = out_ptr.go_right() elif from_side == lbn.Direction.RIGHT: from_side, ptr = ptr.go_up() diff --git a/lamb/nodes/misc.py b/lamb/nodes/misc.py index 0420c02..671a789 100644 --- a/lamb/nodes/misc.py +++ b/lamb/nodes/misc.py @@ -5,6 +5,7 @@ class Direction(enum.Enum): LEFT = enum.auto() RIGHT = enum.auto() + class ReductionType(enum.Enum): # Nothing happened. This implies that # an expression cannot be reduced further. @@ -23,6 +24,16 @@ class ReductionType(enum.Enum): # This is the only type of "formal" reduction step. FUNCTION_APPLY = enum.auto() +# Pretty, short names for each reduction type. +# These should all have the same length. +reduction_text = { + ReductionType.NOTHING: "N", + ReductionType.MACRO_EXPAND: "M", + ReductionType.HIST_EXPAND: "H", + ReductionType.AUTOCHURCH: "C", + ReductionType.FUNCTION_APPLY: "F", +} + class ReductionError(Exception): """ Raised when we encounter an error while reducing. diff --git a/lamb/nodes/nodes.py b/lamb/nodes/nodes.py index e231e20..6a7cc28 100644 --- a/lamb/nodes/nodes.py +++ b/lamb/nodes/nodes.py @@ -350,7 +350,11 @@ class Bound(EndNode): self.identifier = forced_id def copy(self): - return Bound(self.name, forced_id = self.identifier, runner = self.runner) + return Bound( + self.name, + forced_id = self.identifier, + runner = self.runner + ) def __eq__(self, other): if not isinstance(other, Bound): @@ -388,7 +392,14 @@ class Func(Node): return f"" def copy(self): - return Func(self.input, None, runner = self.runner) # type: ignore + return Func( + Bound( + self.input.name, + runner = self.runner + ), + None, # type: ignore + runner = self.runner + ) class Root(Node): """ diff --git a/lamb/runner/commands.py b/lamb/runner/commands.py index f9bda14..905a876 100644 --- a/lamb/runner/commands.py +++ b/lamb/runner/commands.py @@ -2,6 +2,7 @@ from prompt_toolkit.formatted_text import FormattedText from prompt_toolkit.formatted_text import HTML from prompt_toolkit import print_formatted_text as printf from prompt_toolkit.shortcuts import clear as clear_screen +from prompt_toolkit import prompt import os.path from pyparsing import exceptions as ppx @@ -27,6 +28,53 @@ def lamb_command( help_texts[name] = help_text return inner +@lamb_command( + command_name = "step", + help_text = "Toggle step-by-step reduction" +) +def cmd_step(command, runner) -> None: + if len(command.args) > 1: + printf( + HTML( + f"Command :{command.name} takes no more than one argument." + ), + style = lamb.utils.style + ) + return + + target = not runner.step_reduction + if len(command.args) == 1: + if command.args[0].lower() in ("y", "yes"): + target = True + elif command.args[0].lower() in ("n", "no"): + target = False + else: + printf( + HTML( + f"Usage: :step [yes|no]" + ), + style = lamb.utils.style + ) + return + + + if target: + printf( + HTML( + f"Enabled step-by-step reduction." + ), + style = lamb.utils.style + ) + runner.step_reduction = True + else: + printf( + HTML( + f"Disabled step-by-step reduction." + ), + style = lamb.utils.style + ) + runner.step_reduction = False + @lamb_command( command_name = "save", @@ -44,7 +92,7 @@ def cmd_save(command, runner) -> None: target = command.args[0] if os.path.exists(target): - confirm = runner.prompt_session.prompt( + confirm = prompt( message = FormattedText([ ("class:warn", "File exists. Overwrite? "), ("class:text", "[yes/no]: ") @@ -174,7 +222,7 @@ def mdel(command, runner) -> None: help_text = "Delete all macros" ) def clearmacros(command, runner) -> None: - confirm = runner.prompt_session.prompt( + confirm = prompt( message = FormattedText([ ("class:warn", "Are you sure? "), ("class:text", "[yes/no]: ") diff --git a/lamb/runner/runner.py b/lamb/runner/runner.py index 9a7804d..5fa2ce0 100644 --- a/lamb/runner/runner.py +++ b/lamb/runner/runner.py @@ -1,5 +1,7 @@ from prompt_toolkit import PromptSession from prompt_toolkit.formatted_text import FormattedText +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit import prompt from prompt_toolkit import print_formatted_text as printf import enum import math @@ -13,6 +15,13 @@ from lamb.runner.misc import StopReason from lamb.runner import commands as cmd +# Keybindings for step prompt. +# Prevents any text from being input. +step_bindings = KeyBindings() +@step_bindings.add("") +def _(event): + pass + class Runner: def __init__( @@ -51,6 +60,9 @@ class Runner: self.history: list[lamb.nodes.Root] = [] + # If true, reduce step-by-step. + self.step_reduction = False + def prompt(self): return self.prompt_session.prompt( message = self.prompt_message @@ -96,7 +108,19 @@ class Runner: if len(warnings) != 0: printf(FormattedText(warnings), style = lamb.utils.style) + if self.step_reduction: + printf(FormattedText([ + ("class:warn", "Step-by-step reduction is enabled.\n"), + ("class:muted", "Press "), + ("class:cmd_key", "ctrl-c"), + ("class:muted", " to continue automatically.\n"), + ("class:muted", "Press "), + ("class:cmd_key", "enter"), + ("class:muted", " to step.\n"), + ]), style = lamb.utils.style) + + skip_to_end = False while ( ( (self.reduction_limit is None) or @@ -105,7 +129,10 @@ class Runner: ): # Show reduction count - if (k >= self.iter_update) and (k % self.iter_update == 0): + if ( + ( (k >= self.iter_update) and (k % self.iter_update == 0) ) + and not (self.step_reduction and not skip_to_end) + ): print(f" Reducing... {k:,}", end = "\r") try: @@ -125,6 +152,27 @@ class Runner: if red_type == lamb.nodes.ReductionType.FUNCTION_APPLY: macro_expansions += 1 + # Pause after step if necessary + if self.step_reduction and not skip_to_end: + try: + s = prompt( + message = FormattedText([ + ("class:muted", lamb.nodes.reduction_text[red_type]), + ("class:muted", f":{k:03} "), + ("class:text", str(node)), + ]), + style = lamb.utils.style, + key_bindings = step_bindings + ) + except KeyboardInterrupt or EOFError: + skip_to_end = True + printf(FormattedText([ + ("class:warn", "Skipping to end."), + ]), style = lamb.utils.style) + + if self.step_reduction: + print("") + if k >= self.iter_update: # Clear reduction counter if it was printed print(" " * round(14 + math.log10(k)), end = "\r")