diff --git a/lamb/__main__.py b/lamb/__main__.py index 58f5717..0ac7209 100755 --- a/lamb/__main__.py +++ b/lamb/__main__.py @@ -46,6 +46,8 @@ r = runner.Runner( ) """ +macro_table = {} + class Direction(enum.Enum): UP = enum.auto() LEFT = enum.auto() @@ -74,7 +76,11 @@ class Node: self.left: Node | None = None self.right: Node | None = None - def set_parent(self, parent, side: Direction): + def set_parent(self, parent, side): + if (parent is not None) and (side is None): + raise Exception("If a node has a parent, it must have a direction.") + if (parent is None) and (side is not None): + raise Exception("If a node has no parent, it cannot have a direction.") self.parent = parent self.parent_side = side @@ -89,15 +95,33 @@ class Node: return Direction.UP, self.right def go_up(self): - if self.parent is None: - raise Exception("Can't go up when parent is None") return self.parent_side, self.parent -class EndNode: - def print_value(self): - raise NotImplementedError("EndNodes MUST have a print_value method!") + def clone(self): + raise NotImplementedError("Nodes MUST provide a `clone` method!") -class Macro(Node, EndNode): +class EndNode(Node): + def print_value(self): + raise NotImplementedError("EndNodes MUST provide a `print_value` method!") + +class ExpandableEndNode(EndNode): + def expand(self): + raise NotImplementedError("ExpandableEndNodes MUST provide an `expand` method!") + +class FreeVar(EndNode): + def __init__(self, name: str): + self.name = name + + def __repr__(self): + return f"" + + def print_value(self): + return f"{self.name}" + + def clone(self): + return FreeVar(self.name) + +class Macro(ExpandableEndNode): @staticmethod def from_parse(results): return Macro(results[0]) @@ -114,10 +138,21 @@ class Macro(Node, EndNode): def print_value(self): return self.name -class Church(Node, EndNode): + def expand(self): + if self.name in macro_table: + return macro_table[self.name] + else: + f = FreeVar(self.name) + f.set_parent(self.parent, self.parent_side) # type: ignore + return f + + def clone(self): + return Macro(self.name) + +class Church(ExpandableEndNode): @staticmethod def from_parse(results): - return Church(results[0]) + return Church(int(results[0])) def __init__(self, value: int) -> None: super().__init__() @@ -131,10 +166,7 @@ class Church(Node, EndNode): def print_value(self): return str(self.value) - def to_church(self): - """ - Return this number as an expanded church numeral. - """ + def expand(self): f = Bound("f") a = Bound("a") chain = a @@ -147,9 +179,11 @@ class Church(Node, EndNode): Func(a, chain) ) + def clone(self): + return Church(self.value) bound_counter = 0 -class Bound(Node, EndNode): +class Bound(EndNode): def __init__(self, name: str, *, forced_id = None): self.name = name global bound_counter @@ -161,13 +195,7 @@ class Bound(Node, EndNode): self.identifier = forced_id def clone(self): - """ - Return a new bound variable equivalent to this one. - """ - return Bound( - self.name, - forced_id = self.identifier - ) + return Bound(self.name, forced_id = self.identifier) def __eq__(self, other): if not isinstance(other, Bound): @@ -207,6 +235,9 @@ class Func(Node): def __repr__(self): return f"" + def clone(self): + return Func(self.input, None) # type: ignore + class Call(Node): @staticmethod def from_parse(results): @@ -237,6 +268,9 @@ class Call(Node): def __repr__(self): return f"" + def clone(self): + return Call(None, None) # type: ignore + class MacroDef: @staticmethod def from_parse(result): @@ -278,137 +312,285 @@ p = lamb.parser.LambdaParser( ) -def print_expr(expr) -> str: - if isinstance(expr, MacroDef): - return f"{expr.label} = {print_expr(expr.expr)}" +def clone_one(ptr, out): + if ptr.parent_side == Direction.LEFT: + out.left = ptr.clone() + out.left.set_parent(out, Direction.LEFT) + else: + out.right = ptr.clone() + out.right.set_parent(out, Direction.RIGHT) - elif isinstance(expr, Node): - ptr = expr - from_side = Direction.UP +def clone(expr: Node): + if not isinstance(expr, Node): + raise TypeError(f"I don't know what to do with a {type(expr)}") - out = "" + # Disconnect parent while cloning + old_parent = expr.parent + expr.parent = None - while True: - if isinstance(ptr, EndNode): - out += ptr.print_value() - if ptr.parent is not None: - from_side, ptr = ptr.go_up() + out = expr.clone() + out_ptr = out # Stays one step behind ptr, in the new tree. + ptr = expr + from_side = Direction.UP - elif isinstance(ptr, Func): - if from_side == Direction.UP: - if isinstance(ptr.parent, Func): - out += ptr.input.name - else: - out += "λ" + ptr.input.name - if not isinstance(ptr.left, Func): - out += "." - from_side, ptr = ptr.go_left() - elif from_side == Direction.LEFT: - if ptr.parent is not None: - from_side, ptr = ptr.go_up() - - elif isinstance(ptr, Call): - if from_side == Direction.UP: - out += "(" - from_side, ptr = ptr.go_left() - elif from_side == Direction.LEFT: - out += " " - from_side, ptr = ptr.go_right() - elif from_side == Direction.RIGHT: - out += ")" - if ptr.parent is not None: - from_side, ptr = ptr.go_up() - - if ptr.parent is None: - break + if isinstance(expr, EndNode): return out - else: + while True: + if isinstance(ptr, EndNode): + from_side, ptr = ptr.go_up() + _, out_ptr = out_ptr.go_up() + elif isinstance(ptr, Func): + if from_side == Direction.UP: + from_side, ptr = ptr.go_left() + clone_one(ptr, out_ptr) + _, out_ptr = out_ptr.go_left() + elif from_side == Direction.LEFT: + from_side, ptr = ptr.go_up() + _, out_ptr = out_ptr.go_up() + elif isinstance(ptr, Call): + if from_side == Direction.UP: + from_side, ptr = ptr.go_left() + clone_one(ptr, out_ptr) + _, out_ptr = out_ptr.go_left() + elif from_side == Direction.LEFT: + from_side, ptr = ptr.go_right() + clone_one(ptr, out_ptr) + _, out_ptr = out_ptr.go_right() + elif from_side == Direction.RIGHT: + from_side, ptr = ptr.go_up() + _, out_ptr = out_ptr.go_up() + if ptr is None: + break + + expr.parent = old_parent + return out + +def print_expr(expr) -> str: + + out = "" + + # Type check + if isinstance(expr, MacroDef): + out = expr.label + " = " + expr = expr.expr + elif not isinstance(expr, Node): raise TypeError(f"I don't know what to do with a {type(expr)}") + ptr = expr + from_side = Direction.UP + + while True: + print(ptr) + if isinstance(ptr, EndNode): + out += ptr.print_value() + from_side, ptr = ptr.go_up() + + elif isinstance(ptr, Func): + if from_side == Direction.UP: + if isinstance(ptr.parent, Func): + out += ptr.input.name + else: + out += "λ" + ptr.input.name + if not isinstance(ptr.left, Func): + out += "." + from_side, ptr = ptr.go_left() + elif from_side == Direction.LEFT: + from_side, ptr = ptr.go_up() + + elif isinstance(ptr, Call): + if from_side == Direction.UP: + out += "(" + from_side, ptr = ptr.go_left() + elif from_side == Direction.LEFT: + out += " " + from_side, ptr = ptr.go_right() + elif from_side == Direction.RIGHT: + out += ")" + from_side, ptr = ptr.go_up() + + if ptr is None: + break + return out + def bind_variables(expr) -> None: + # Type check if isinstance(expr, MacroDef): - bind_variables(expr.expr) - - elif isinstance(expr, Node): - ptr = expr - from_side = Direction.UP - - bound_variables = {} - - while True: - if isinstance(ptr, Func): - if from_side == Direction.UP: - # Add this function's input to the table of bound variables. - # If it is already there, raise an error. - if (ptr.input.name in bound_variables): - raise ReductionError(f"Bound variable name conflict: \"{ptr.input.name}\"") - else: - bound_variables[ptr.input.name] = Bound(ptr.input.name) - ptr.input = bound_variables[ptr.input.name] - - # If output is a macro, swap it with a bound variable. - if isinstance(ptr.left, Macro): - if ptr.left.name in bound_variables: - ptr.left = bound_variables[ptr.left.name].clone() - ptr.left.set_parent(ptr, Direction.LEFT) - - # If we can't move down the tree, move up. - if isinstance(ptr.left, EndNode): - del bound_variables[ptr.input.name] - if ptr.parent is not None: - from_side, ptr = ptr.go_up() - else: - from_side, ptr = ptr.go_left() - - elif from_side == Direction.LEFT: - del bound_variables[ptr.input.name] - if ptr.parent is not None: - from_side, ptr = ptr.go_up() - - elif isinstance(ptr, Call): - if from_side == Direction.UP: - # Bind macros - if isinstance(ptr.left, Macro): - if ptr.left.name in bound_variables: - ptr.left = bound_variables[ptr.left.name].clone() - ptr.left.set_parent(ptr, Direction.LEFT) - if isinstance(ptr.right, Macro): - if ptr.right.name in bound_variables: - ptr.right = bound_variables[ptr.right.name].clone() - ptr.right.set_parent(ptr, Direction.RIGHT) - - if not isinstance(ptr.left, EndNode): - from_side, ptr = ptr.go_left() - elif not isinstance(ptr.right, EndNode): - from_side, ptr = ptr.go_right() - elif ptr.parent is not None: - from_side, ptr = ptr.go_up() - - elif from_side == Direction.LEFT: - if isinstance(ptr.right, Macro): - if ptr.right.name in bound_variables: - ptr.right = bound_variables[ptr.right.name].clone() - ptr.right.set_parent(ptr, Direction.RIGHT) - - if not isinstance(ptr.right, EndNode): - from_side, ptr = ptr.go_right() - elif ptr.parent is not None: - from_side, ptr = ptr.go_up() - - elif from_side == Direction.RIGHT: - if ptr.parent is not None: - from_side, ptr = ptr.go_up() - - if ptr.parent is None: - break - - else: + expr = expr.expr + elif not isinstance(expr, Node): raise TypeError(f"I don't know what to do with a {type(expr)}") + ptr = expr + from_side = Direction.UP + bound_variables = {} + + while True: + if isinstance(ptr, Func): + if from_side == Direction.UP: + # Add this function's input to the table of bound variables. + # If it is already there, raise an error. + if (ptr.input.name in bound_variables): + raise ReductionError(f"Bound variable name conflict: \"{ptr.input.name}\"") + else: + bound_variables[ptr.input.name] = Bound(ptr.input.name) + ptr.input = bound_variables[ptr.input.name] + + # If output is a macro, swap it with a bound variable. + if isinstance(ptr.left, Macro): + if ptr.left.name in bound_variables: + ptr.left = bound_variables[ptr.left.name].clone() + ptr.left.set_parent(ptr, Direction.LEFT) + + # If we can't move down the tree, move up. + if isinstance(ptr.left, EndNode): + del bound_variables[ptr.input.name] + from_side, ptr = ptr.go_up() + else: + from_side, ptr = ptr.go_left() + + elif from_side == Direction.LEFT: + del bound_variables[ptr.input.name] + from_side, ptr = ptr.go_up() + + elif isinstance(ptr, Call): + if from_side == Direction.UP: + # Bind macros + if isinstance(ptr.left, Macro): + if ptr.left.name in bound_variables: + ptr.left = bound_variables[ptr.left.name].clone() + ptr.left.set_parent(ptr, Direction.LEFT) + if isinstance(ptr.right, Macro): + if ptr.right.name in bound_variables: + ptr.right = bound_variables[ptr.right.name].clone() + ptr.right.set_parent(ptr, Direction.RIGHT) + + if not isinstance(ptr.left, EndNode): + from_side, ptr = ptr.go_left() + elif not isinstance(ptr.right, EndNode): + from_side, ptr = ptr.go_right() + else: + from_side, ptr = ptr.go_up() + + elif from_side == Direction.LEFT: + if not isinstance(ptr.right, EndNode): + from_side, ptr = ptr.go_right() + else: + from_side, ptr = ptr.go_up() + + elif from_side == Direction.RIGHT: + from_side, ptr = ptr.go_up() + + if ptr is None: + break + + +# Apply a function. +# Returns the function's output. +def call_func(fn: Func, arg: Node): + ptr = fn + + # Temporarily disconnect this function's + # parent to keep our pointer inside this + # subtree. + old_parent = fn.parent + fn.parent = None + + from_side = Direction.UP + + while True: + if isinstance(ptr, Bound): + if ptr == fn.input: + if ptr.parent is None: + raise Exception("Tried to substitute a None bound variable.") + + if ptr.parent_side == Direction.LEFT: + ptr.parent.left = clone(arg) + ptr.parent.left.set_parent(ptr, Direction.LEFT) + else: + ptr.parent.right = clone(arg) + ptr.parent.right.set_parent(ptr, Direction.RIGHT) + + from_side, ptr = ptr.go_up() + + elif isinstance(ptr, Func): + if from_side == Direction.UP: + from_side, ptr = ptr.go_left() + elif from_side == Direction.LEFT: + from_side, ptr = ptr.go_up() + + elif isinstance(ptr, Call): + if from_side == Direction.UP: + from_side, ptr = ptr.go_left() + elif from_side == Direction.LEFT: + from_side, ptr = ptr.go_right() + elif from_side == Direction.RIGHT: + from_side, ptr = ptr.go_up() + else: + from_side, ptr = ptr.go_up() + + if ptr is None: + break + + fn.parent = old_parent + return fn.left + + +# Do a single reduction step +def reduce(expr) -> tuple[bool, Node]: + + if not isinstance(expr, Node): + raise TypeError(f"I can't reduce a {type(expr)}") + + ptr = expr + from_side = Direction.UP + reduced = False + + while True: + print("redu", ptr) + + if isinstance(ptr, Call): + if from_side == Direction.UP: + if isinstance(ptr.left, Func): + if ptr.parent is None: + expr = call_func(ptr.left, ptr.right) + expr.set_parent(None, None) + else: + ptr.parent.left = call_func(ptr.left, ptr.right) + ptr.parent.left.set_parent(ptr.parent, Direction.LEFT) + reduced = True + break + elif isinstance(ptr.left, ExpandableEndNode): + ptr.left = ptr.left.expand() + reduced = True + break + elif isinstance(ptr.left, Call): + from_side, ptr = ptr.go_left() + else: + from_side, ptr = ptr.go_right() + + else: + from_side, ptr = ptr.go_up() + + elif isinstance(ptr, Func): + if from_side == Direction.UP: + from_side, ptr = ptr.go_left() + else: + from_side, ptr = ptr.go_up() + + elif isinstance(ptr, EndNode): + from_side, ptr = ptr.go_up() + + if ptr is None: + break + + return reduced, expr + + for l in [ + "T = λab.a", + "F = λab.b", "NOT = λa.(a F T)", "AND = λab.(a F b)", "OR = λab.(a T b)", @@ -420,8 +602,21 @@ for l in [ "S = λnfa.(f (n f a))", "Z = λn.n (λa.F) T", "MULT = λnmf.n (m f)", - "H = λp.((PAIR (p F)) (S (p 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)))", + "S (λfa.f a)" ]: n = p.parse_line(l) bind_variables(n) - print(print_expr(n)) \ No newline at end of file + + if isinstance(n, MacroDef): + macro_table[n.label] = n.expr + print(print_expr(n)) + else: + for i in range(10): + r, n = reduce(n) + if not r: + break + print(print_expr(n)) + #print(print_expr(clone(n))) \ No newline at end of file diff --git a/lamb/parser.py b/lamb/parser.py index 096fe3c..2f7b219 100755 --- a/lamb/parser.py +++ b/lamb/parser.py @@ -47,7 +47,7 @@ class LambdaParser: ) self.pp_expr <<= ( - #self.pp_church ^ + self.pp_church ^ self.pp_lambda_fun ^ self.pp_name ^ (self.lp + self.pp_expr + self.rp) ^ diff --git a/lamb/tokens.py b/lamb/tokens.py deleted file mode 100755 index bb93dd8..0000000 --- a/lamb/tokens.py +++ /dev/null @@ -1,239 +0,0 @@ -import enum -import lamb.utils as utils - - - -class ReductionType(enum.Enum): - MACRO_EXPAND = enum.auto() - MACRO_TO_FREE = enum.auto() - FUNCTION_APPLY = enum.auto() - AUTOCHURCH = enum.auto() - - -class ReductionStatus: - """ - This object helps organize reduction output. - An instance is returned after every reduction step. - """ - - def __init__( - self, - *, - output, - was_reduced: bool, - reduction_type: ReductionType | None = None - ): - # The new expression - self.output = output - - # What did we do? - # Will be None if was_reduced is false. - self.reduction_type = reduction_type - - # Did this reduction change anything? - # If we try to reduce an irreducible expression, - # this will be false. - self.was_reduced = was_reduced - - -class church_num(LambdaToken): - def reduce(self, *, force_substitute = False) -> ReductionStatus: - if force_substitute: # Only expand macros if we NEED to - return ReductionStatus( - output = self.to_church(), - was_reduced = True, - reduction_type = ReductionType.AUTOCHURCH - ) - else: # Otherwise, do nothing. - return ReductionStatus( - output = self, - was_reduced = False - ) - - -class free_variable(LambdaToken): - """ - 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(LambdaToken): - def reduce( - self, - *, - # To keep output readable, we avoid expanding macros as often as possible. - # Macros are irreducible if force_substitute is false. - force_substitute = False, - - # If this is false, error when macros aren't defined instead of - # invisibly making a free variable. - auto_free_vars = True - ) -> ReductionStatus: - - if (self.name in self.runner.macro_table) and force_substitute: - if force_substitute: # Only expand macros if we NEED to - return ReductionStatus( - output = self.runner.macro_table[self.name], - reduction_type = ReductionType.MACRO_EXPAND, - was_reduced = True - ) - else: # Otherwise, do nothing. - return ReductionStatus( - output = self, - was_reduced = False - ) - - elif not auto_free_vars: - raise ReductionError(f"Macro {self.name} is not defined") - - else: - return ReductionStatus( - output = free_variable(self.name), - reduction_type = ReductionType.MACRO_TO_FREE, - was_reduced = True - ) - - -class lambda_func(LambdaToken): - def reduce(self) -> ReductionStatus: - - r = self.output.reduce() - - return ReductionStatus( - was_reduced = r.was_reduced, - reduction_type = r.reduction_type, - output = lambda_func( - self.input, - r.output - ) - ) - - - 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 # type: ignore - 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) # type: ignore - - # 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(LambdaToken): - 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 reduce(self) -> ReductionStatus: - - # If we can directly apply self.fn, do so. - if isinstance(self.fn, lambda_func): - return ReductionStatus( - was_reduced = True, - reduction_type = ReductionType.FUNCTION_APPLY, - output = self.fn.apply(self.arg) - ) - - # Otherwise, try to reduce self.fn. - # If that is impossible, try to reduce self.arg. - else: - if isinstance(self.fn, macro) or isinstance(self.fn, church_num): - # Macros must be reduced before we apply them as functions. - # This is the only place we force substitution. - r = self.fn.reduce( - force_substitute = True - ) - else: - r = self.fn.reduce() - - if r.was_reduced: - return ReductionStatus( - was_reduced = True, - reduction_type = r.reduction_type, - output = lambda_apply( - r.output, - self.arg - ) - ) - - else: - r = self.arg.reduce() - - return ReductionStatus( - was_reduced = r.was_reduced, - reduction_type = r.reduction_type, - output = lambda_apply( - self.fn, - r.output - ) - )