diff --git a/.vscode/settings.json b/.vscode/settings.json index 6a0417c..af7b709 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "Packrat", "pyparsing", "runstatus", + "srange", "subvar" ], "python.analysis.typeCheckingMode": "basic" diff --git a/README.md b/README.md index 11bfd98..c68e413 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ - Command and macro autocomplete - step-by-step reduction - Maybe a better icon? - - Warn when overwriting macro - Syntax highlighting: parenthesis, bound variables, macros, etc - Pin header to top of screen - PyPi package diff --git a/lamb/__main__.py b/lamb/__main__.py index 7c12200..43cb809 100755 --- a/lamb/__main__.py +++ b/lamb/__main__.py @@ -52,13 +52,11 @@ r.run_lines([ "AND = λab.(a F b)", "OR = λab.(a T b)", "XOR = λab.(a (NOT a b) b)", - "w = λx.(x x)", - "W = w w", + "M = λx.(x x)", + "W = M M", "Y = λf.( (λx.(f (x x))) (λx.(f (x x))) )", "PAIR = λabi.( i a b )", - "inc = λnfa.(f (n f a))", - "zero = λax.x", - "one = λfx.(f x)" + "S = λnfa.(f (n f a))", ]) @@ -89,7 +87,7 @@ while True: ("class:err", " "*(e.loc + l) + "^\n"), ("class:err", f"Syntax error at char {e.loc}."), ("class:text", "\n") - ])) + ]), style = utils.style) continue except tokens.ReductionError as e: printf(FormattedText([ diff --git a/lamb/parser.py b/lamb/parser.py index b0a49d4..1442a1c 100755 --- a/lamb/parser.py +++ b/lamb/parser.py @@ -1,82 +1,96 @@ import pyparsing as pp -# Packrat gives a MAD speed boost. +# Packrat gives a significant speed boost. pp.ParserElement.enablePackrat() -import lamb.tokens as tokens -import lamb.utils as utils +class LambdaParser: + def make_parser(self): + self.lp = pp.Suppress("(") + self.rp = pp.Suppress(")") + self.pp_expr = pp.Forward() -class Parser: - lp = pp.Suppress("(") - rp = pp.Suppress(")") + # Bound variables are ALWAYS lowercase and single-character. + # We still create macro objects from them, they are turned into + # bound variables after the expression is built. + self.pp_macro = pp.Word(pp.alphas + "_") + 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) - # Simple tokens - pp_expr = pp.Forward() - pp_macro = pp.Word(pp.alphas + "_") - pp_macro.set_parse_action(tokens.macro.from_parse) + # Function calls. + # + # + # + self.pp_call = pp.Forward() + self.pp_call <<= (self.pp_expr | self.pp_bound)[2, ...] - pp_church = pp.Word(pp.nums) - pp_church.set_parse_action(utils.autochurch) + # Function definitions, right associative. + # Function args MUST be lowercase. + # + # => + self.pp_lambda_fun = ( + (pp.Suppress("λ") | pp.Suppress("\\")) + + pp.Group(self.pp_bound[1, ...]) + + pp.Suppress(".") + + (self.pp_expr ^ self.pp_call) + ) - # Function calls. - # `tokens.lambda_apply.from_parse` handles chained calls. - # - # - # - pp_call = pp.Forward() - pp_call <<= pp_expr[2, ...] - pp_call.set_parse_action(tokens.lambda_apply.from_parse) + # Assignment. + # Can only be found at the start of a line. + # + # = + self.pp_macro_def = ( + pp.line_start() + + self.pp_macro + + pp.Suppress("=") + + (self.pp_expr ^ self.pp_call) + ) - # Function definitions. - # Right associative. - # - # => - pp_lambda_fun = ( - (pp.Suppress("λ") | pp.Suppress("\\")) + - pp.Group(pp.Char(pp.alphas)[1, ...]) + - pp.Suppress(".") + - (pp_expr ^ pp_call) - ) - pp_lambda_fun.set_parse_action(tokens.lambda_func.from_parse) + self.pp_expr <<= ( + self.pp_church ^ + self.pp_lambda_fun ^ + self.pp_name ^ + (self.lp + self.pp_expr + self.rp) ^ + (self.lp + self.pp_call + self.rp) + ) - # Assignment. - # Can only be found at the start of a line. - # - # = - pp_macro_def = ( - pp.line_start() + - pp_macro + - pp.Suppress("=") + - (pp_expr ^ pp_call) - ) - pp_macro_def.set_parse_action(tokens.macro_expression.from_parse) - - pp_expr <<= ( - pp_church ^ - pp_lambda_fun ^ - pp_macro ^ - (lp + pp_expr + rp) ^ - (lp + pp_call + rp) - ) - - pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_") + pp.Word(pp.alphas + "_")[0, ...] - pp_command.set_parse_action(tokens.command.from_parse) + self.pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_") + pp.Word(pp.alphas + "_")[0, ...] - pp_all = ( - pp_expr ^ - pp_macro_def ^ - pp_command ^ - pp_call - ) + self.pp_all = ( + self.pp_expr ^ + self.pp_macro_def ^ + self.pp_command ^ + self.pp_call + ) - @staticmethod - def parse_line(line): - return Parser.pp_all.parse_string( + def __init__( + self, + *, + action_command, + action_macro_def, + action_church, + action_func, + action_bound, + action_macro, + action_apply + ): + + self.make_parser() + + self.pp_command.set_parse_action(action_command) + self.pp_macro_def.set_parse_action(action_macro_def) + self.pp_church.set_parse_action(action_church) + self.pp_lambda_fun.set_parse_action(action_func) + self.pp_macro.set_parse_action(action_macro) + self.pp_bound.set_parse_action(action_bound) + self.pp_call.set_parse_action(action_apply) + + def parse_line(self, line: str): + return self.pp_all.parse_string( line, parse_all = True )[0] - @staticmethod - def run_tests(lines): - return Parser.pp_all.run_tests(lines) \ No newline at end of file + def run_tests(self, lines: list[str]): + return self.pp_all.run_tests(lines) \ No newline at end of file diff --git a/lamb/runner.py b/lamb/runner.py index 0da75b7..241f6f5 100644 --- a/lamb/runner.py +++ b/lamb/runner.py @@ -1,8 +1,9 @@ from prompt_toolkit import PromptSession import lamb.commands as commands -from lamb.parser import Parser +from lamb.parser import LambdaParser import lamb.tokens as tokens +import lamb.utils as utils import lamb.runstatus as rs @@ -11,11 +12,25 @@ class Runner: 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 = utils.autochurch(self), + 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 + ) # Maximum amount of reductions. # If None, no maximum is enforced. self.reduction_limit: int | None = 300 + # Ensure bound variables are unique. + # This is automatically incremented whenever we make + # a bound variable. + self.bound_variable_counter = 0 + def prompt(self): return self.prompt_session.prompt(message = self.prompt_message) @@ -29,7 +44,8 @@ class Runner: macro_expansions = 0 while (self.reduction_limit is None) or (i < self.reduction_limit): - r = expr.reduce(self.macro_table) + print(repr(expr)) + r = expr.reduce() expr = r.output # If we can't reduce this expression anymore, @@ -55,13 +71,15 @@ class Runner: # Apply a list of definitions def run(self, line: str, *, macro_only = False) -> rs.RunStatus: - e = Parser.parse_line(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() # If this line is a macro definition, save the macro. if isinstance(e, tokens.macro_expression): was_rewritten = e.label in self.macro_table - - e.exp.bind_variables() self.macro_table[e.label] = e.exp return rs.MacroStatus( @@ -80,7 +98,6 @@ class Runner: # If this line is a plain expression, reduce it. elif isinstance(e, tokens.LambdaToken): - e.bind_variables() return self.reduce_expression(e) # We shouldn't ever get here. diff --git a/lamb/tokens.py b/lamb/tokens.py index 05dadcb..617a577 100755 --- a/lamb/tokens.py +++ b/lamb/tokens.py @@ -40,16 +40,18 @@ class ReductionStatus: # this will be false. self.was_reduced = was_reduced - class LambdaToken: """ Base class for all lambda tokens. """ + def set_runner(self, runner): + self.runner = runner + def bind_variables(self) -> None: pass - def reduce(self, macro_table) -> ReductionStatus: + def reduce(self) -> ReductionStatus: return ReductionStatus( was_reduced = False, output = self @@ -121,7 +123,6 @@ class macro(LambdaToken): def reduce( self, - macro_table = {}, *, # To keep output readable, we avoid expanding macros as often as possible. # Macros are irreducible if force_substitute is false. @@ -132,10 +133,10 @@ class macro(LambdaToken): auto_free_vars = True ) -> ReductionStatus: - if (self.name in macro_table) and force_substitute: + if (self.name in self.runner.macro_table) and force_substitute: if force_substitute: # Only expand macros if we NEED to return ReductionStatus( - output = macro_table[self.name], + output = self.runner.macro_table[self.name], reduction_type = ReductionType.MACRO_EXPAND, was_reduced = True ) @@ -173,6 +174,11 @@ class macro_expression: result[1] ) + def set_runner(self, runner): + self.exp.set_runner(runner) + def bind_variables(self): + self.exp.bind_variables() + def __init__(self, label: str, exp: LambdaToken): self.label = label self.exp = exp @@ -184,14 +190,14 @@ class macro_expression: return f"{self.label} := {self.exp}" -bound_variable_counter = 0 class bound_variable(LambdaToken): - def __init__(self, forced_id = None): - global bound_variable_counter + def __init__(self, name: str, *, runner, forced_id = None): + self.original_name = name + self.runner = runner if forced_id is None: - self.identifier = bound_variable_counter - bound_variable_counter += 1 + self.identifier = self.runner.bound_variable_counter + self.runner.bound_variable_counter += 1 else: self.identifier = forced_id @@ -201,7 +207,7 @@ class bound_variable(LambdaToken): return self.identifier == other.identifier def __repr__(self): - return f"" + return f"<{self.original_name} {self.identifier}>" class lambda_func(LambdaToken): """ @@ -219,15 +225,20 @@ class lambda_func(LambdaToken): def from_parse(result): if len(result[0]) == 1: return lambda_func( - macro(result[0][0]), + result[0][0], result[1] ) else: return lambda_func( - macro(result[0].pop(0)), + result[0].pop(0), lambda_func.from_parse(result) ) + def set_runner(self, runner): + self.runner = runner + self.input.set_runner(runner) + self.output.set_runner(runner) + def __init__( self, input_var: macro | bound_variable, @@ -280,10 +291,14 @@ class lambda_func(LambdaToken): # We only need to check for collisions if we're # binding another function's variable. If this # function starts the bind chain, skip that step. - if not ((placeholder is None) and (val is None)): + if placeholder is not None: if not binding_self and isinstance(self.input, macro): if self.input == placeholder: - raise ReductionError(f"Variable name conflict: \"{self.input.name}\"") + raise ReductionError(f"Bound variable name conflict: \"{self.input.name}\"") + + if self.input.name in self.runner.macro_table: + raise ReductionError(f"Bound variable name conflict: \"{self.input.name}\" is a macro") + # If this function's variables haven't been bound yet, # bind them BEFORE binding the outer function's. @@ -292,7 +307,10 @@ class lambda_func(LambdaToken): # functions' variables, we won't be able to detect # name conflicts. if isinstance(self.input, macro) and not binding_self: - new_bound_var = bound_variable() + new_bound_var = bound_variable( + self.input.name, + runner = self.runner + ) self.bind_variables( self.input, new_bound_var, @@ -310,15 +328,15 @@ class lambda_func(LambdaToken): elif isinstance(self.output, lambda_apply): self.output.bind_variables(placeholder, val) - def reduce(self, macro_table = {}) -> ReductionStatus: + def reduce(self) -> ReductionStatus: - r = self.output.reduce(macro_table) + r = self.output.reduce() # If a macro becomes a free variable, # reduce twice. if r.reduction_type == ReductionType.MACRO_TO_FREE: self.output = r.output - return self.reduce(macro_table) + return self.reduce() return ReductionStatus( was_reduced = r.was_reduced, @@ -394,6 +412,11 @@ class lambda_apply(LambdaToken): )] + result[2:] ) + def set_runner(self, runner): + self.runner = runner + self.fn.set_runner(runner) + self.arg.set_runner(runner) + def __init__( self, fn: LambdaToken, @@ -416,9 +439,6 @@ class lambda_apply(LambdaToken): """ Does exactly what lambda_func.bind_variables does, but acts on applications instead. - - There will be little documentation in this method, - see lambda_func.bind_variables. """ if (placeholder is None) and (val != placeholder): @@ -474,7 +494,7 @@ class lambda_apply(LambdaToken): new_arg ) - def reduce(self, macro_table = {}) -> ReductionStatus: + def reduce(self) -> ReductionStatus: # If we can directly apply self.fn, do so. if isinstance(self.fn, lambda_func): @@ -491,17 +511,16 @@ class lambda_apply(LambdaToken): # Macros must be reduced before we apply them as functions. # This is the only place we force substitution. r = self.fn.reduce( - macro_table, force_substitute = True ) else: - r = self.fn.reduce(macro_table) + r = self.fn.reduce() # If a macro becomes a free variable, # reduce twice. if r.reduction_type == ReductionType.MACRO_TO_FREE: self.fn = r.output - return self.reduce(macro_table) + return self.reduce() if r.was_reduced: return ReductionStatus( @@ -514,11 +533,11 @@ class lambda_apply(LambdaToken): ) else: - r = self.arg.reduce(macro_table) + r = self.arg.reduce() if r.reduction_type == ReductionType.MACRO_TO_FREE: self.arg = r.output - return self.reduce(macro_table) + return self.reduce() return ReductionStatus( was_reduced = r.was_reduced, diff --git a/lamb/utils.py b/lamb/utils.py index 23fb1b9..76e31e1 100644 --- a/lamb/utils.py +++ b/lamb/utils.py @@ -6,31 +6,33 @@ from importlib.metadata import version import lamb.tokens as tokens -def autochurch(results): +def autochurch(runner): """ Makes a church numeral from an integer. """ - num = int(results[0]) + def inner(results): + num = int(results[0]) - f = tokens.bound_variable() - a = tokens.bound_variable() + f = tokens.bound_variable("f", runner = runner) + a = tokens.bound_variable("a", runner = runner) - chain = a + chain = a - for i in range(num): - chain = tokens.lambda_apply(f, chain) + for i in range(num): + chain = tokens.lambda_apply(f, chain) - return tokens.lambda_func( - f, - tokens.lambda_func( - a, - chain + return tokens.lambda_func( + f, + tokens.lambda_func( + a, + chain + ) ) - ) + return inner -style = Style.from_dict({ +style = Style.from_dict({ # type: ignore # Basic formatting "text": "#FFFFFF", "warn": "#FFFF00",