diff --git a/lamb/__init__.py b/lamb/__init__.py index e69de29..64075ae 100644 --- a/lamb/__init__.py +++ b/lamb/__init__.py @@ -0,0 +1,7 @@ +from . import utils +from . import node +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 2415211..df3e566 100755 --- a/lamb/__main__.py +++ b/lamb/__main__.py @@ -1,4 +1,3 @@ -from subprocess import call from prompt_toolkit import PromptSession from prompt_toolkit.completion import WordCompleter from prompt_toolkit import print_formatted_text as printf @@ -6,13 +5,9 @@ from prompt_toolkit.formatted_text import FormattedText from prompt_toolkit.formatted_text import to_plain_text from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.lexers import Lexer - from pyparsing import exceptions as ppx -import enum -import lamb.node -import lamb.parser -import lamb.utils as utils +import lamb # Simple lexer for highlighting. @@ -24,7 +19,7 @@ class LambdaLexer(Lexer): return inner -utils.show_greeting() +lamb.utils.show_greeting() # Replace "\" with pretty "λ"s @@ -34,67 +29,19 @@ def _(event): event.current_buffer.insert_text("λ") -""" -r = runner.Runner( +r = lamb.Runner( prompt_session = PromptSession( - style = utils.style, + style = lamb.utils.style, lexer = LambdaLexer(), key_bindings = bindings ), - prompt_message = FormattedText([ ("class:prompt", "~~> ") - ]), -) -""" - -macro_table = {} - -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 bind_variables(self): - return self.expr.bind_variables() - -class Command: - @staticmethod - def from_parse(result): - return Command( - result[0], - result[1:] - ) - - def __init__(self, name, args): - self.name = name - self.args = args - -p = 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_macro_def = MacroDef.from_parse, - action_command = Command.from_parse + ]) ) -for l in [ +r.run_lines([ "T = λab.a", "F = λab.b", "NOT = λa.(a F T)", @@ -110,22 +57,42 @@ for l in [ "MULT = λnmf.n (m f)", "H = λp.((PAIR (p F)) (S (p F)))", "D = λn.n H (PAIR 0 0) T", - "FAC = λyn.(Z n)(1)(MULT n (y (D n)))", - "3 NOT T" -]: - n = p.parse_line(l) - n.bind_variables() + "FAC = λyn.(Z n)(1)(MULT n (y (D n)))" +]) - if isinstance(n, MacroDef): - macro_table[n.label] = n.expr - print(n) - else: - for i in range(100): - r, n = lamb.node.reduce( - n, - macro_table = macro_table - ) - if not r: - break - print(n) \ No newline at end of file +while True: + try: + i = r.prompt() + + # Catch Ctrl-C and Ctrl-D + except KeyboardInterrupt: + printf("\n\nGoodbye.\n") + break + except EOFError: + printf("\n\nGoodbye.\n") + break + + # Skip empty lines + if i.strip() == "": + continue + + # Try to run an input line. + # Catch parse errors and point them out. + try: + x = r.run(i) + except ppx.ParseException as e: + l = len(to_plain_text(r.prompt_session.message)) + printf(FormattedText([ + ("class:err", " "*(e.loc + l) + "^\n"), + ("class:err", f"Syntax error at char {e.loc}."), + ("class:text", "\n") + ]), style = lamb.utils.style) + continue + except lamb.node.ReductionError as e: + printf(FormattedText([ + ("class:err", f"{e.msg}\n") + ]), style = lamb.utils.style) + continue + + printf("") diff --git a/lamb/commands.py b/lamb/commands.py index 321253c..b77df98 100644 --- a/lamb/commands.py +++ b/lamb/commands.py @@ -7,9 +7,7 @@ import os.path from pyparsing import exceptions as ppx -import lamb.tokens -import lamb.utils - +import lamb commands = {} help_texts = {} @@ -114,7 +112,7 @@ def load(command, runner): ) return - if not isinstance(x, lamb.tokens.macro_expression): + if not isinstance(x, lamb.runner.MacroDef): printf( FormattedText([ ("class:warn", f"Skipping line {i+1:02}: "), diff --git a/lamb/node.py b/lamb/node.py index ded33eb..32b470e 100644 --- a/lamb/node.py +++ b/lamb/node.py @@ -124,8 +124,11 @@ class Node: def __str__(self) -> str: return print_node(self) - def bind_variables(self): - return bind_variables(self) + def bind_variables(self, *, ban_macro_name = None): + return bind_variables( + self, + ban_macro_name = ban_macro_name + ) class EndNode(Node): def print_value(self): @@ -367,7 +370,7 @@ def clone(node: Node): break return out -def bind_variables(node: Node) -> None: +def bind_variables(node: Node, *, ban_macro_name = None) -> None: if not isinstance(node, Node): raise TypeError(f"I don't know what to do with a {type(node)}") @@ -375,6 +378,15 @@ def bind_variables(node: Node) -> None: bound_variables = {} for s, n in node: + + # If this expression is part of a macro, + # make sure we don't reference it inside itself. + # + # TODO: A chain of macros could be used to work around this. Fix that! + if isinstance(n, Macro) and ban_macro_name is not None: + if n.name == ban_macro_name: + raise ReductionError("Macro cannot reference self") + if isinstance(n, Func): if s == Direction.UP: # Add this function's input to the table of bound variables. diff --git a/lamb/parser.py b/lamb/parser.py index 2f7b219..158a1cf 100755 --- a/lamb/parser.py +++ b/lamb/parser.py @@ -60,7 +60,7 @@ class LambdaParser: self.pp_all = ( self.pp_expr ^ self.pp_macro_def ^ - #self.pp_command ^ + self.pp_command ^ self.pp_call ) diff --git a/lamb/runner.py b/lamb/runner.py index 2109674..466113a 100644 --- a/lamb/runner.py +++ b/lamb/runner.py @@ -1,14 +1,9 @@ -from tkinter import E 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 class StopReason(enum.Enum): @@ -18,20 +13,59 @@ class StopReason(enum.Enum): INTERRUPT = ("class:warn", "User interrupt") RECURSION = ("class:err", "Python Recursion Error") +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 bind_variables(self, *, ban_macro_name = None): + return self.expr.bind_variables( + ban_macro_name = ban_macro_name + ) + +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: - def __init__(self, prompt_session: PromptSession, prompt_message): + def __init__( + self, + prompt_session: PromptSession, + prompt_message + ): self.macro_table = {} self.prompt_session = prompt_session self.prompt_message = prompt_message - self.parser = LambdaParser( - action_command = tokens.command.from_parse, - action_macro_def = tokens.macro_expression.from_parse, - action_church = tokens.church_num.from_parse, - action_func = tokens.lambda_func.from_parse, - action_bound = tokens.macro.from_parse, - action_macro = tokens.macro.from_parse, - action_apply = tokens.lambda_apply.from_parse + 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_macro_def = MacroDef.from_parse, + action_command = Command.from_parse ) # Maximum amount of reductions. @@ -49,18 +83,15 @@ class Runner: 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) - if isinstance(e, tokens.macro_expression): + + if isinstance(e, MacroDef): e.bind_variables(ban_macro_name = e.label) - else: + elif isinstance(e, lamb.node.Node): e.bind_variables() return e - def reduce_expression(self, expr: tokens.LambdaToken) -> None: - + def reduce(self, node: lamb.node.Node) -> None: # Reduction Counter. # We also count macro (and church) expansions, # and subtract those from the final count. @@ -72,36 +103,39 @@ class Runner: while (self.reduction_limit is None) or (i < self.reduction_limit): try: - r = expr.reduce() + w, r = lamb.node.reduce( + node, + macro_table = self.macro_table + ) except RecursionError: stop_reason = StopReason.RECURSION break - expr = r.output + node = r #print(expr) #self.prompt() # If we can't reduce this expression anymore, # it's in beta-normal form. - if not r.was_reduced: + if not w: stop_reason = StopReason.BETA_NORMAL break # Count reductions #i += 1 - if ( - r.reduction_type == tokens.ReductionType.MACRO_EXPAND or - r.reduction_type == tokens.ReductionType.AUTOCHURCH - ): - macro_expansions += 1 - else: - i += 1 + #if ( + # r.reduction_type == tokens.ReductionType.MACRO_EXPAND or + # r.reduction_type == tokens.ReductionType.AUTOCHURCH + # ): + # macro_expansions += 1 + #else: + i += 1 if ( stop_reason == StopReason.BETA_NORMAL or stop_reason == StopReason.LOOP_DETECTED ): - out_str = str(r.output) # type: ignore + out_str = str(r) # type: ignore printf(FormattedText([ ("class:result_header", f"\nExit reason: "), @@ -116,7 +150,7 @@ class Runner: ("class:result_header", "\n\n => "), ("class:text", out_str), - ]), style = utils.style) + ]), style = lamb.utils.style) else: printf(FormattedText([ ("class:result_header", f"\nExit reason: "), @@ -127,9 +161,14 @@ class Runner: ("class:result_header", f"\nReductions: "), ("class:text", str(i)), - ]), style = utils.style) + ]), style = lamb.utils.style) - def save_macro(self, macro: tokens.macro_expression, *, silent = False) -> None: + def save_macro( + self, + macro: MacroDef, + *, + silent = False + ) -> None: was_rewritten = macro.label in self.macro_table self.macro_table[macro.label] = macro.expr @@ -139,23 +178,28 @@ class Runner: ("class:syn_macro", macro.label), ("class:text", " to "), ("class:text", str(macro.expr)) - ]), style = utils.style) + ]), style = lamb.utils.style) # Apply a list of definitions - def run(self, line: str, *, silent = False) -> None: + 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): + if isinstance(e, MacroDef): self.save_macro(e, silent = silent) # If this line is a command, do the command. - elif isinstance(e, tokens.command): - commands.run(e, self) + elif isinstance(e, Command): + lamb.commands.run(e, self) # If this line is a plain expression, reduce it. - elif isinstance(e, tokens.LambdaToken): - self.reduce_expression(e) + elif isinstance(e, lamb.node.Node): + self.reduce(e) # We shouldn't ever get here. else: