From 99217297d21a981efa8c7a241bfc2e215777e7e3 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 20 Oct 2022 11:02:49 -0700 Subject: [PATCH] Added basic lambda parser --- main.py | 104 ++++++++++++++ parser.py | 65 +++++++++ tokens.py | 420 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 589 insertions(+) create mode 100755 main.py create mode 100755 parser.py create mode 100755 tokens.py diff --git a/main.py b/main.py new file mode 100755 index 0000000..0772f44 --- /dev/null +++ b/main.py @@ -0,0 +1,104 @@ +from parser import Parser +import tokens + + +class lambda_runner: + def __init__(self): + self.macro_table = {} + self.expr = None + + # Apply a list of definitions + def run_names(self, lines): + print("Added names:") + for l in lines: + if isinstance(l, str): + e = Parser.parse_assign(l) + else: + e = l + + if e.label in self.macro_table: + raise NameError(f"Label {e.label} exists!") + + e.exp.bind_variables() + self.macro_table[e.label] = e.exp + print(f"\t{e}") + print("\n") + + def set_expr(self, expr: str | None = None): + if expr == None: + self.expr = None + print("Removed expression.\n") + return + + self.expr = Parser.parse_expression(expr) + self.expr.bind_variables() + print(f"Set expression to {self.expr}\n") + + def run(self): + if isinstance(self.expr, tokens.lambda_apply): + self.expr = self.expr.expand(self.macro_table) + elif isinstance(self.expr, tokens.lambda_func): + self.expr = self.expr.expand(self.macro_table) + else: + return None + return self.expr + + +r = lambda_runner() + +r.run_names([ + "T = a -> b -> a", + "F = a -> b -> a", + "NOT = a -> (a F T)", + "AND = a -> b -> (a F b)", + "OR = a -> b -> (a T b)", + "XOR = a -> b -> (a (NOT a b) b)" +]) + +r.run_names([ + "w = x -> (x x)", + "W = (w w)", + "Y = f -> ( (x -> (f (x x))) (x -> (f (x x))) )", + #"l = if_true -> if_false -> which -> ( which if_true if_false )" +]) + +r.run_names([ + "inc = n -> f -> x -> (f (n f x))", + "zero = a -> x -> x", + "one = f -> x -> (f x)", +]) + +print("\n") + +#AND = r.run() +#OR = r.run() +#XOR = r.run() + +r.set_expr( + "(" + + "inc (inc (inc (zero)))" + + ")" +) + +print(repr(r.expr)) +print("") + +outs = [str(r.expr)] +for i in range(300): + x = r.run() + s = str(x) + p = s if len(s) < 100 else s[:97] + "..." + + if s in outs: + print(p) + print("\nLoop detected, exiting.") + break + + if x is None: + print("\nCannot evaluate any further.") + break + + outs.append(s) + print(p) + +print(f"Performed {i} {'operations' if i != 1 else 'operation'}.") \ No newline at end of file diff --git a/parser.py b/parser.py new file mode 100755 index 0000000..2bba6b1 --- /dev/null +++ b/parser.py @@ -0,0 +1,65 @@ +import pyparsing as pp +import tokens + +class Parser: + """ + Macro_def must be on its own line. + macro_def :: var = expr + + var :: word + lambda_fun :: var -> expr + call :: '(' (var | expr) ')' + + expr :: define | var | call | '(' expr ')' + """ + + lp = pp.Suppress("(") + rp = pp.Suppress(")") + func_char = pp.Suppress("->") + macro_char = pp.Suppress("=") + + # Simple tokens + pp_expr = pp.Forward() + pp_name = pp.Word(pp.alphas + "_") + pp_name.set_parse_action(tokens.macro.from_parse) + + # Function definitions. + # Right associative. + # + # => + pp_lambda_fun = pp_name + func_char + pp_expr + pp_lambda_fun.set_parse_action(tokens.lambda_func.from_parse) + + # Assignment. + # Can only be found at the start of a line. + # + # = + pp_macro_def = pp.line_start() + pp_name + macro_char + pp_expr + pp_macro_def.set_parse_action(tokens.macro_expression.from_parse) + + # Function calls. + # `tokens.lambda_func.from_parse` handles chained calls. + # + # () + # ()()() + # ()() + # ()()()() + pp_call = pp.Forward() + pp_call <<= pp_expr[2, ...] + pp_call.set_parse_action(tokens.lambda_apply.from_parse) + + pp_expr <<= pp_lambda_fun ^ (lp + pp_expr + rp) ^ pp_name ^ (lp + pp_call + rp) + pp_all = pp_expr | pp_macro_def + + @staticmethod + def parse_expression(line): + return Parser.pp_expr.parse_string(line, parse_all = True)[0] + + @staticmethod + def parse_assign(line): + return ( + Parser.pp_macro_def + ).parse_string(line, parse_all = True)[0] + + @staticmethod + def run_tests(lines): + return Parser.pp_macro_def.run_tests(lines) \ No newline at end of file diff --git a/tokens.py b/tokens.py new file mode 100755 index 0000000..0b3b2d1 --- /dev/null +++ b/tokens.py @@ -0,0 +1,420 @@ +from typing import Type + + +class free_variable: + """ + Represents a free variable. + + This object does not reduce to + anything, since it has no meaning. + + Any name in an expression that isn't + a macro or a bound variable is assumed + to be a free variable. + """ + + def __init__(self, label: str): + self.label = label + + def __repr__(self): + return f"" + + def __str__(self): + return f"{self.label}" + + +class macro: + """ + Represents a "macro" in lambda calculus, + a variable that expands to an expression. + + These don't have inherent logic, they + just make writing and reading expressions + easier. + + These are defined as follows: + = + """ + + @staticmethod + def from_parse(result): + return macro( + result[0], + ) + + def __init__(self, name): + self.name = name + def __repr__(self): + return f"<{self.name}>" + def __str__(self): + return self.name + + def __eq__(self, other): + if not isinstance(other, macro): + raise TypeError("Can only compare macro with macro") + return self.name == other.name + + def expand(self, macro_table = {}, *, auto_free_vars = True): + if self.name in macro_table: + return macro_table[self.name] + elif not auto_free_vars: + raise NameError(f"Name {self.name} is not defined!") + else: + return free_variable(self.name) + + +class macro_expression: + """ + Represents a line that looks like + = + + Doesn't do anything particularly interesting, + just holds an expression until it is stored + in the runner's macro table. + """ + + @staticmethod + def from_parse(result): + return macro_expression( + result[0].name, + result[1] + ) + + def __init__(self, label, exp): + self.label = label + self.exp = exp + + def __repr__(self): + return f"<{self.label} := {self.exp!r}>" + + def __str__(self): + return f"{self.label} := {self.exp}" + + + + + +bound_variable_counter = 0 +class bound_variable: + def __init__(self, forced_id = None): + global bound_variable_counter + + if forced_id is None: + self.identifier = bound_variable_counter + bound_variable_counter += 1 + else: + self.identifier = forced_id + + def __eq__(self, other): + if not isinstance(other, bound_variable): + raise TypeError(f"Cannot compare bound_variable with {type(other)}") + return self.identifier == other.identifier + + def __repr__(self): + return f"" + +class lambda_func: + """ + Represents a function. + Defined like λa.aa + + After being created by the parser, a function + needs to have its variables bound. This cannot + happen during parsing, since the parser creates + functions "inside-out," and we need all inner + functions before we bind variables. + """ + + @staticmethod + def from_parse(result): + return lambda_func( + result[0], + result[1] + ) + + def __init__(self, input_var, output): + self.input = input_var + self.output = output + + def __repr__(self) -> str: + return f"<{self.input!r} → {self.output!r}>" + + def __str__(self) -> str: + return f"λ{self.input}.{self.output}" + + + def bind_variables( + self, + placeholder: macro | None = None, + val: bound_variable | None = None, + *, + binding_self: bool = False + ) -> None: + """ + Go through this function and all the functions inside it, + and replace the strings generated by the parser with bound + variables or free variables. + + If values are passed to `placeholder` and `val,` + we're binding the variable of a function containing + this one. If they are both none, start the binding + chain with this function. + + If only one of those arguments is None, something is very wrong. + + `placeholder` is a macro, NOT A STRING! + The parser assumes all names are macros at first, variable + binding fixes those that are actually bound variables. + + If `binding_self` is True, don't throw an error on a name conflict + and don't bind this function's input variable. + This is used when we're calling this method to bind this function's + variable. + """ + + + if (placeholder is None) and (val != placeholder): + raise Exception( + "Error while binding variables: placeholder and val are both None." + ) + + # 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 not binding_self and isinstance(self.input, macro): + if self.input == placeholder: + raise NameError("Bound variable name conflict.") + + # If this function's variables haven't been bound yet, + # bind them BEFORE binding the outer function's. + # + # If we bind inner functions' variables before outer + # 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() + self.bind_variables( + self.input, + new_bound_var, + binding_self = True + ) + self.input = new_bound_var + + + # Bind variables inside this function. + if isinstance(self.output, macro): + if self.output == placeholder: + self.output = val + elif isinstance(self.output, lambda_func): + self.output.bind_variables(placeholder, val) + elif isinstance(self.output, lambda_apply): + self.output.bind_variables(placeholder, val) + + # Expand this function's output. + # For functions, this isn't done unless + # its explicitly asked for. + def expand(self, macro_table = {}): + new_out = self.output + if isinstance(self.output, macro): + new_out = self.output.expand(macro_table) + + # If the macro becomes a free variable, expand again. + if isinstance(new_out, free_variable): + lambda_func( + self.input, + new_out + ).expand(macro_table) + + elif isinstance(self.output, lambda_func): + new_out = self.output.expand(macro_table) + elif isinstance(self.output, lambda_apply): + new_out = self.output.expand(macro_table) + return lambda_func( + self.input, + new_out + ) + + def apply( + self, + val, + *, + bound_var: bound_variable | None = None + ): + """ + Substitute `bound_var` into all instances of a bound variable `var`. + If `bound_var` is none, use this functions bound variable. + Returns a new object. + """ + + calling_self = False + if bound_var is None: + calling_self = True + bound_var = self.input + new_out = self.output + if isinstance(self.output, bound_variable): + if self.output == bound_var: + new_out = val + elif isinstance(self.output, lambda_func): + new_out = self.output.apply(val, bound_var = bound_var) + elif isinstance(self.output, lambda_apply): + new_out = self.output.sub_bound_var(val, bound_var = bound_var) + else: + raise TypeError("Cannot apply a function to {self.output!r}") + + # If we're applying THIS function, + # just give the output + if calling_self: + return new_out + + # If we're applying another function, + # return this one with substitutions + else: + return lambda_func( + self.input, + new_out + ) + + +class lambda_apply: + """ + Represents a function application. + Has two elements: fn, the function, + and arg, the thing it acts upon. + + Parentheses are handled by the parser, and + chained functions are handled by from_parse. + """ + + @staticmethod + def from_parse(result): + if len(result) == 2: + return lambda_apply( + result[0], + result[1] + ) + elif len(result) > 2: + return lambda_apply.from_parse([ + lambda_apply( + result[0], + result[1] + )] + result[2:] + ) + + def __init__( + self, + fn, + arg + ): + self.fn = fn + self.arg = arg + + def __repr__(self) -> str: + return f"<{self.fn!r} | {self.arg!r}>" + + def __str__(self) -> str: + return f"({self.fn} {self.arg})" + + def bind_variables( + self, + placeholder: macro | None = None, + val: bound_variable | None = None + ) -> None: + """ + 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): + raise Exception( + "Error while binding variables: placeholder and val are both None." + ) + + # If val and placeholder are None, + # everything below should still work as expected. + if isinstance(self.fn, macro) and placeholder is not None: + if self.fn == placeholder: + self.fn = val + elif isinstance(self.fn, lambda_func): + self.fn.bind_variables(placeholder, val) + elif isinstance(self.fn, lambda_apply): + self.fn.bind_variables(placeholder, val) + + if isinstance(self.arg, macro) and placeholder is not None: + if self.arg == placeholder: + self.arg = val + elif isinstance(self.arg, lambda_func): + self.arg.bind_variables(placeholder, val) + elif isinstance(self.arg, lambda_apply): + self.arg.bind_variables(placeholder, val) + + def sub_bound_var( + self, + val, + *, + bound_var: bound_variable + ): + + new_fn = self.fn + if isinstance(self.fn, bound_variable): + if self.fn == bound_var: + new_fn = val + elif isinstance(self.fn, lambda_func): + new_fn = self.fn.apply(val, bound_var = bound_var) + elif isinstance(self.fn, lambda_apply): + new_fn = self.fn.sub_bound_var(val, bound_var = bound_var) + + new_arg = self.arg + if isinstance(self.arg, bound_variable): + if self.arg == bound_var: + new_arg = val + elif isinstance(self.arg, lambda_func): + new_arg = self.arg.apply(val, bound_var = bound_var) + elif isinstance(self.arg, lambda_apply): + new_arg = self.arg.sub_bound_var(val, bound_var = bound_var) + + return lambda_apply( + new_fn, + new_arg + ) + + def expand(self, macro_table = {}): + # If fn is a function, apply it. + if isinstance(self.fn, lambda_func): + return self.fn.apply(self.arg) + # If fn is an application or macro, expand it. + elif isinstance(self.fn, macro): + f = lambda_apply( + m := self.fn.expand(macro_table), + self.arg + ) + + # If a macro becomes a free variable, + # expand twice. + if isinstance(m, free_variable): + return f.expand(macro_table) + else: + return f + + elif isinstance(self.fn, lambda_apply): + return lambda_apply( + self.fn.expand(macro_table), + self.arg + ) + + # If we get to this point, the function we're applying + # can't be expanded. That means it's a free or bound + # variable. If that happens, expand the arg instead. + elif ( + isinstance(self.arg, lambda_apply) or + isinstance(self.arg, lambda_func) + ): + return lambda_apply( + self.fn, + self.arg.expand(macro_table) + ) + + return self \ No newline at end of file