Rewrite continues

master
Mark 2022-10-25 13:26:55 -07:00
parent 455e447999
commit a991c3bb91
Signed by: Mark
GPG Key ID: AD62BB059C2AAEE4
3 changed files with 335 additions and 379 deletions

View File

@ -46,6 +46,8 @@ r = runner.Runner(
) )
""" """
macro_table = {}
class Direction(enum.Enum): class Direction(enum.Enum):
UP = enum.auto() UP = enum.auto()
LEFT = enum.auto() LEFT = enum.auto()
@ -74,7 +76,11 @@ class Node:
self.left: Node | None = None self.left: Node | None = None
self.right: 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 = parent
self.parent_side = side self.parent_side = side
@ -89,15 +95,33 @@ class Node:
return Direction.UP, self.right return Direction.UP, self.right
def go_up(self): 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 return self.parent_side, self.parent
class EndNode: def clone(self):
def print_value(self): raise NotImplementedError("Nodes MUST provide a `clone` method!")
raise NotImplementedError("EndNodes MUST have a print_value 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"<freevar {self.name}>"
def print_value(self):
return f"{self.name}"
def clone(self):
return FreeVar(self.name)
class Macro(ExpandableEndNode):
@staticmethod @staticmethod
def from_parse(results): def from_parse(results):
return Macro(results[0]) return Macro(results[0])
@ -114,10 +138,21 @@ class Macro(Node, EndNode):
def print_value(self): def print_value(self):
return self.name 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 @staticmethod
def from_parse(results): def from_parse(results):
return Church(results[0]) return Church(int(results[0]))
def __init__(self, value: int) -> None: def __init__(self, value: int) -> None:
super().__init__() super().__init__()
@ -131,10 +166,7 @@ class Church(Node, EndNode):
def print_value(self): def print_value(self):
return str(self.value) return str(self.value)
def to_church(self): def expand(self):
"""
Return this number as an expanded church numeral.
"""
f = Bound("f") f = Bound("f")
a = Bound("a") a = Bound("a")
chain = a chain = a
@ -147,9 +179,11 @@ class Church(Node, EndNode):
Func(a, chain) Func(a, chain)
) )
def clone(self):
return Church(self.value)
bound_counter = 0 bound_counter = 0
class Bound(Node, EndNode): class Bound(EndNode):
def __init__(self, name: str, *, forced_id = None): def __init__(self, name: str, *, forced_id = None):
self.name = name self.name = name
global bound_counter global bound_counter
@ -161,13 +195,7 @@ class Bound(Node, EndNode):
self.identifier = forced_id self.identifier = forced_id
def clone(self): def clone(self):
""" return Bound(self.name, forced_id = self.identifier)
Return a new bound variable equivalent to this one.
"""
return Bound(
self.name,
forced_id = self.identifier
)
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, Bound): if not isinstance(other, Bound):
@ -207,6 +235,9 @@ class Func(Node):
def __repr__(self): def __repr__(self):
return f"<func {self.input!r} {self.left!r}>" return f"<func {self.input!r} {self.left!r}>"
def clone(self):
return Func(self.input, None) # type: ignore
class Call(Node): class Call(Node):
@staticmethod @staticmethod
def from_parse(results): def from_parse(results):
@ -237,6 +268,9 @@ class Call(Node):
def __repr__(self): def __repr__(self):
return f"<call {self.left!r} {self.right!r}>" return f"<call {self.left!r} {self.right!r}>"
def clone(self):
return Call(None, None) # type: ignore
class MacroDef: class MacroDef:
@staticmethod @staticmethod
def from_parse(result): def from_parse(result):
@ -278,21 +312,79 @@ p = lamb.parser.LambdaParser(
) )
def print_expr(expr) -> str:
if isinstance(expr, MacroDef): def clone_one(ptr, out):
return f"{expr.label} = {print_expr(expr.expr)}" 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): def clone(expr: Node):
if not isinstance(expr, Node):
raise TypeError(f"I don't know what to do with a {type(expr)}")
# Disconnect parent while cloning
old_parent = expr.parent
expr.parent = None
out = expr.clone()
out_ptr = out # Stays one step behind ptr, in the new tree.
ptr = expr ptr = expr
from_side = Direction.UP from_side = Direction.UP
out = "" if isinstance(expr, EndNode):
return out
while True: 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): if isinstance(ptr, EndNode):
out += ptr.print_value() out += ptr.print_value()
if ptr.parent is not None:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
elif isinstance(ptr, Func): elif isinstance(ptr, Func):
@ -305,7 +397,6 @@ def print_expr(expr) -> str:
out += "." out += "."
from_side, ptr = ptr.go_left() from_side, ptr = ptr.go_left()
elif from_side == Direction.LEFT: elif from_side == Direction.LEFT:
if ptr.parent is not None:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
elif isinstance(ptr, Call): elif isinstance(ptr, Call):
@ -317,25 +408,22 @@ def print_expr(expr) -> str:
from_side, ptr = ptr.go_right() from_side, ptr = ptr.go_right()
elif from_side == Direction.RIGHT: elif from_side == Direction.RIGHT:
out += ")" out += ")"
if ptr.parent is not None:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
if ptr.parent is None: if ptr is None:
break break
return out return out
else:
raise TypeError(f"I don't know what to do with a {type(expr)}")
def bind_variables(expr) -> None: def bind_variables(expr) -> None:
# Type check
if isinstance(expr, MacroDef): if isinstance(expr, MacroDef):
bind_variables(expr.expr) expr = expr.expr
elif not isinstance(expr, Node):
raise TypeError(f"I don't know what to do with a {type(expr)}")
elif isinstance(expr, Node):
ptr = expr ptr = expr
from_side = Direction.UP from_side = Direction.UP
bound_variables = {} bound_variables = {}
while True: while True:
@ -358,14 +446,12 @@ def bind_variables(expr) -> None:
# If we can't move down the tree, move up. # If we can't move down the tree, move up.
if isinstance(ptr.left, EndNode): if isinstance(ptr.left, EndNode):
del bound_variables[ptr.input.name] del bound_variables[ptr.input.name]
if ptr.parent is not None:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
else: else:
from_side, ptr = ptr.go_left() from_side, ptr = ptr.go_left()
elif from_side == Direction.LEFT: elif from_side == Direction.LEFT:
del bound_variables[ptr.input.name] del bound_variables[ptr.input.name]
if ptr.parent is not None:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
elif isinstance(ptr, Call): elif isinstance(ptr, Call):
@ -384,31 +470,127 @@ def bind_variables(expr) -> None:
from_side, ptr = ptr.go_left() from_side, ptr = ptr.go_left()
elif not isinstance(ptr.right, EndNode): elif not isinstance(ptr.right, EndNode):
from_side, ptr = ptr.go_right() from_side, ptr = ptr.go_right()
elif ptr.parent is not None: else:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
elif from_side == Direction.LEFT: 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): if not isinstance(ptr.right, EndNode):
from_side, ptr = ptr.go_right() from_side, ptr = ptr.go_right()
elif ptr.parent is not None: else:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
elif from_side == Direction.RIGHT: elif from_side == Direction.RIGHT:
if ptr.parent is not None:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
if ptr.parent is None: if ptr is None:
break 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: else:
raise TypeError(f"I don't know what to do with a {type(expr)}") 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 [ for l in [
"T = λab.a",
"F = λab.b",
"NOT = λa.(a F T)", "NOT = λa.(a F T)",
"AND = λab.(a F b)", "AND = λab.(a F b)",
"OR = λab.(a T b)", "OR = λab.(a T b)",
@ -420,8 +602,21 @@ for l in [
"S = λnfa.(f (n f a))", "S = λnfa.(f (n f a))",
"Z = λn.n (λa.F) T", "Z = λn.n (λa.F) T",
"MULT = λnmf.n (m f)", "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) n = p.parse_line(l)
bind_variables(n) bind_variables(n)
if isinstance(n, MacroDef):
macro_table[n.label] = n.expr
print(print_expr(n)) 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)))

View File

@ -47,7 +47,7 @@ class LambdaParser:
) )
self.pp_expr <<= ( self.pp_expr <<= (
#self.pp_church ^ self.pp_church ^
self.pp_lambda_fun ^ self.pp_lambda_fun ^
self.pp_name ^ self.pp_name ^
(self.lp + self.pp_expr + self.rp) ^ (self.lp + self.pp_expr + self.rp) ^

View File

@ -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"<freevar {self.label}>"
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
)
)