diff --git a/lamb/__main__.py b/lamb/__main__.py index d9b98d8..6a1f6d9 100755 --- a/lamb/__main__.py +++ b/lamb/__main__.py @@ -9,7 +9,6 @@ from prompt_toolkit.lexers import Lexer from pyparsing import exceptions as ppx import lamb.runner as runner -import lamb.runstatus as rs import lamb.tokens as tokens import lamb.utils as utils @@ -100,31 +99,4 @@ while True: ]), style = utils.style) continue - # If this line defined a macro, print nothing. - if isinstance(x, rs.MacroStatus): - printf(FormattedText([ - ("class:text", "Set "), - ("class:syn_macro", x.macro_label), - ("class:text", " to "), - ("class:text", str(x.macro_expr)) - ]), style = utils.style) - - - if isinstance(x, rs.CommandStatus): - pass - - # If this line was an expression, print reduction status - elif isinstance(x, rs.ReduceStatus): - printf(FormattedText([ - ("class:result_header", f"\nExit reason: "), - x.stop_reason.value, - - ("class:result_header", f"\nReduction count: "), - ("class:text", str(x.reduction_count)), - - - ("class:result_header", "\n\n => "), - ("class:text", str(x.result)), - ]), style = utils.style) - printf("") diff --git a/lamb/commands.py b/lamb/commands.py index b1f25b9..321253c 100644 --- a/lamb/commands.py +++ b/lamb/commands.py @@ -6,9 +6,9 @@ from prompt_toolkit.shortcuts import clear as clear_screen import os.path from pyparsing import exceptions as ppx -import lamb.runstatus as rs -import lamb.utils as utils +import lamb.tokens +import lamb.utils commands = {} @@ -26,7 +26,7 @@ def run(command, runner) -> None: FormattedText([ ("class:warn", f"Unknown command \"{command.name}\"") ]), - style = utils.style + style = lamb.utils.style ) else: commands[command.name](command, runner) @@ -39,7 +39,7 @@ def save(command, runner) -> None: HTML( "Command :save takes exactly one argument." ), - style = utils.style + style = lamb.utils.style ) return @@ -57,7 +57,7 @@ def save(command, runner) -> None: HTML( "Cancelled." ), - style = utils.style + style = lamb.utils.style ) return @@ -70,7 +70,7 @@ def save(command, runner) -> None: HTML( f"Wrote {len(runner.macro_table)} macros to {target}" ), - style = utils.style + style = lamb.utils.style ) @@ -81,7 +81,7 @@ def load(command, runner): HTML( "Command :load takes exactly one argument." ), - style = utils.style + style = lamb.utils.style ) return @@ -91,7 +91,7 @@ def load(command, runner): HTML( f"File {target} doesn't exist." ), - style = utils.style + style = lamb.utils.style ) return @@ -101,7 +101,7 @@ def load(command, runner): for i in range(len(lines)): l = lines[i] try: - x = runner.run(l, macro_only = True) + x = runner.parse(l) except ppx.ParseException as e: printf( FormattedText([ @@ -110,25 +110,30 @@ def load(command, runner): ("class:err", l[e.loc]), ("class:cmd_code", l[e.loc+1:]) ]), - style = utils.style + style = lamb.utils.style ) - except rs.NotAMacro: + return + + if not isinstance(x, lamb.tokens.macro_expression): printf( FormattedText([ ("class:warn", f"Skipping line {i+1:02}: "), ("class:cmd_code", l), ("class:warn", f" is not a macro definition.") ]), - style = utils.style - ) - else: - printf( - FormattedText([ - ("class:ok", f"Loaded {x.macro_label}: "), - ("class:cmd_code", str(x.macro_expr)) - ]), - style = utils.style + style = lamb.utils.style ) + return + + runner.save_macro(x, silent = True) + + printf( + FormattedText([ + ("class:ok", f"Loaded {x.label}: "), + ("class:cmd_code", str(x.expr)) + ]), + style = lamb.utils.style + ) @@ -139,7 +144,7 @@ def mdel(command, runner) -> None: HTML( "Command :mdel takes exactly one argument." ), - style = utils.style + style = lamb.utils.style ) return @@ -149,7 +154,7 @@ def mdel(command, runner) -> None: HTML( f"Macro \"{target}\" is not defined" ), - style = utils.style + style = lamb.utils.style ) return @@ -166,13 +171,13 @@ def macros(command, runner) -> None: ("class:cmd_text", f"\t{name} \t {exp}\n") for name, exp in runner.macro_table.items() ]), - style = utils.style + style = lamb.utils.style ) @lamb_command(help_text = "Clear the screen") def clear(command, runner) -> None: clear_screen() - utils.show_greeting() + lamb.utils.show_greeting() @lamb_command(help_text = "Print this help") @@ -196,5 +201,5 @@ def help(command, runner) -> None: ]) + "" ), - style = utils.style + style = lamb.utils.style ) \ No newline at end of file diff --git a/lamb/runner.py b/lamb/runner.py index c36d619..8576409 100644 --- a/lamb/runner.py +++ b/lamb/runner.py @@ -1,10 +1,20 @@ from prompt_toolkit import PromptSession +from prompt_toolkit.formatted_text import FormattedText +from prompt_toolkit import print_formatted_text as printf + +import enum import lamb.commands as commands from lamb.parser import LambdaParser import lamb.tokens as tokens import lamb.utils as utils -import lamb.runstatus as rs + + +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") class Runner: @@ -24,6 +34,7 @@ class Runner: # Maximum amount of reductions. # If None, no maximum is enforced. + # Must be at least 1. self.reduction_limit: int | None = 1_000_000 # Ensure bound variables are unique. @@ -34,8 +45,16 @@ class Runner: def prompt(self): return self.prompt_session.prompt(message = self.prompt_message) + def parse(self, line): + e = self.parser.parse_line(line) + # Give the elements of this expression access to the runner. + # Runner must be set BEFORE variables are bound. + e.set_runner(self) + e.bind_variables() + return e - def reduce_expression(self, expr: tokens.LambdaToken) -> rs.ReduceStatus: + + def reduce_expression(self, expr: tokens.LambdaToken) -> None: # Reduction Counter. # We also count macro (and church) expansions, @@ -43,7 +62,7 @@ class Runner: i = 0 macro_expansions = 0 - + stop_reason = StopReason.MAX_EXCEEDED while (self.reduction_limit is None) or (i < self.reduction_limit): r = expr.reduce() expr = r.output @@ -54,11 +73,8 @@ class Runner: # If we can't reduce this expression anymore, # it's in beta-normal form. if not r.was_reduced: - return rs.ReduceStatus( - reduction_count = i - macro_expansions, - stop_reason = rs.StopReason.BETA_NORMAL, - result = r.output - ) + stop_reason = StopReason.BETA_NORMAL + break # Count reductions #i += 1 @@ -70,43 +86,47 @@ class Runner: else: i += 1 - return rs.ReduceStatus( - reduction_count = i, # - macro_expansions, - stop_reason = rs.StopReason.MAX_EXCEEDED, - result = r.output # type: ignore - ) + out_str = str(r.output) # type: ignore + printf(FormattedText([ + ("class:result_header", f"\nExit reason: "), + stop_reason.value, + + ("class:result_header", f"\nReduction count: "), + ("class:text", str(i)), + + + ("class:result_header", "\n\n => "), + ("class:text", out_str), + ]), style = utils.style) + + def save_macro(self, macro: tokens.macro_expression, *, silent = False) -> None: + was_rewritten = macro.label in self.macro_table + self.macro_table[macro.label] = macro.expr + + if not silent: + printf(FormattedText([ + ("class:text", "Set "), + ("class:syn_macro", macro.label), + ("class:text", " to "), + ("class:text", str(macro.expr)) + ]), style = utils.style) # Apply a list of definitions - def run(self, line: str, *, macro_only = False) -> rs.RunStatus: - e = self.parser.parse_line(line) - # Give the elements of this expression access to the runner. - # Runner must be set BEFORE variables are bound. - e.set_runner(self) - e.bind_variables() + def run(self, line: str, *, silent = False) -> None: + e = self.parse(line) # If this line is a macro definition, save the macro. if isinstance(e, tokens.macro_expression): - was_rewritten = e.label in self.macro_table - self.macro_table[e.label] = e.exp - - return rs.MacroStatus( - was_rewritten = was_rewritten, - macro_label = e.label, - macro_expr = e.exp - ) - - elif macro_only: - raise rs.NotAMacro() + self.save_macro(e, silent = silent) # If this line is a command, do the command. elif isinstance(e, tokens.command): commands.run(e, self) - return rs.CommandStatus(cmd = e.name) # If this line is a plain expression, reduce it. elif isinstance(e, tokens.LambdaToken): - return self.reduce_expression(e) + self.reduce_expression(e) # We shouldn't ever get here. else: @@ -115,4 +135,4 @@ class Runner: def run_lines(self, lines: list[str]): for l in lines: - self.run(l) + self.run(l, silent = True) diff --git a/lamb/runstatus.py b/lamb/runstatus.py deleted file mode 100644 index f307e6c..0000000 --- a/lamb/runstatus.py +++ /dev/null @@ -1,84 +0,0 @@ -from prompt_toolkit.formatted_text import FormattedText -from prompt_toolkit.formatted_text import HTML -import enum - -import lamb.tokens as tokens - - -class NotAMacro(Exception): - """ - Raised when we try to run a non-macro line - while enforcing macro_only in Runner.run(). - - This should be caught and elegantly presented to the user. - """ - pass - -class RunStatus: - """ - Base class for run status. - These are returned whenever the runner does something. - """ - pass - -class MacroStatus(RunStatus): - """ - Returned when a macro is defined. - - Values: - `was_rewritten`: If true, an old macro was replaced. - `macro_label`: The name of the macro we just made. - `macro_expr`: The expr of the macro we just made. - """ - - def __init__( - self, - *, - was_rewritten: bool, - macro_label: str, - macro_expr - ): - self.was_rewritten = was_rewritten - self.macro_label = macro_label - self.macro_expr = macro_expr - - -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") - - -class ReduceStatus(RunStatus): - """ - Returned when an expression is reduced. - - Values: - `reduction_count`: How many reductions were made. - `stop_reason`: Why we stopped. See `StopReason`. - """ - - def __init__( - self, - *, - reduction_count: int, - stop_reason: StopReason, - result: tokens.LambdaToken - ): - self.reduction_count = reduction_count - self.stop_reason = stop_reason - self.result = result - - -class CommandStatus(RunStatus): - """ - Returned when a command is executed. - Doesn't do anything interesting. - - Values: - `cmd`: The command that was run, without a colon. - """ - - def __init__(self, *, cmd: str): - self.cmd = cmd \ No newline at end of file diff --git a/lamb/tokens.py b/lamb/tokens.py index a6cd961..f1a9034 100755 --- a/lamb/tokens.py +++ b/lamb/tokens.py @@ -75,13 +75,27 @@ class church_num(LambdaToken): return f"<{self.val}>" def __str__(self): return f"{self.val}" + + def to_church(self): + """ + Return this number as an expanded church numeral. + """ + f = bound_variable("f", runner = self.runner) + a = bound_variable("a", runner = self.runner) + chain = a + + for i in range(self.val): + chain = lambda_apply(f, chain) + + return lambda_func( + f, + lambda_func(a, chain) + ) + def reduce(self, *, force_substitute = False) -> ReductionStatus: if force_substitute: # Only expand macros if we NEED to return ReductionStatus( - output = utils.autochurch( - self.runner, - self.val - ), + output = self.to_church(), was_reduced = True, reduction_type = ReductionType.AUTOCHURCH ) @@ -214,19 +228,19 @@ class macro_expression: ) def set_runner(self, runner): - self.exp.set_runner(runner) + self.expr.set_runner(runner) def bind_variables(self): - self.exp.bind_variables() + self.expr.bind_variables() - def __init__(self, label: str, exp: LambdaToken): + def __init__(self, label: str, expr: LambdaToken): self.label = label - self.exp = exp + self.expr = expr def __repr__(self): - return f"<{self.label} := {self.exp!r}>" + return f"<{self.label} := {self.expr!r}>" def __str__(self): - return f"{self.label} := {self.exp}" + return f"{self.label} := {self.expr}" class bound_variable(LambdaToken): diff --git a/lamb/utils.py b/lamb/utils.py index 6e2895a..276070d 100644 --- a/lamb/utils.py +++ b/lamb/utils.py @@ -3,30 +3,6 @@ from prompt_toolkit.formatted_text import HTML from prompt_toolkit import print_formatted_text as printf from importlib.metadata import version -import lamb.tokens as tokens - - -def autochurch(runner, num): - """ - Makes a church numeral from an integer. - """ - - f = tokens.bound_variable("f", runner = runner) - a = tokens.bound_variable("a", runner = runner) - - chain = a - - for i in range(num): - chain = tokens.lambda_apply(f, chain) - - return tokens.lambda_func( - f, - tokens.lambda_func( - a, - chain - ) - ) - style = Style.from_dict({ # type: ignore # Basic formatting