287 lines
7.6 KiB
Python
287 lines
7.6 KiB
Python
|
import lamb_engine
|
||
|
import lamb_engine.nodes as lbn
|
||
|
|
||
|
def print_node(node: lbn.Node, *, export: bool = False) -> str:
|
||
|
if not isinstance(node, lbn.Node):
|
||
|
raise TypeError(f"I don't know how to print a {type(node)}")
|
||
|
|
||
|
out = ""
|
||
|
|
||
|
bound_subs = {}
|
||
|
|
||
|
for s, n in node:
|
||
|
if isinstance(n, lbn.EndNode):
|
||
|
if isinstance(n, lbn.Bound):
|
||
|
out += bound_subs[n.identifier]
|
||
|
else:
|
||
|
out += n.print_value(export = export)
|
||
|
|
||
|
elif isinstance(n, lbn.Func):
|
||
|
# This should never be true, but
|
||
|
# keep this here to silence type checker.
|
||
|
if not isinstance(n.input, lbn.Bound):
|
||
|
raise Exception("input is macro, something is wrong.")
|
||
|
|
||
|
if s == lbn.Direction.UP:
|
||
|
o = n.input.print_value(export = export)
|
||
|
if o in bound_subs.values():
|
||
|
i = -1
|
||
|
p = o
|
||
|
while o in bound_subs.values():
|
||
|
o = p + lamb_engine.utils.subscript(i := i + 1)
|
||
|
bound_subs[n.input.identifier] = o
|
||
|
else:
|
||
|
bound_subs[n.input.identifier] = n.input.print_value()
|
||
|
|
||
|
if isinstance(n.parent, lbn.Call):
|
||
|
out += "("
|
||
|
|
||
|
if isinstance(n.parent, lbn.Func):
|
||
|
out += bound_subs[n.input.identifier]
|
||
|
else:
|
||
|
out += "λ" + bound_subs[n.input.identifier]
|
||
|
if not isinstance(n.left, lbn.Func):
|
||
|
out += "."
|
||
|
|
||
|
elif s == lbn.Direction.LEFT:
|
||
|
if isinstance(n.parent, lbn.Call):
|
||
|
out += ")"
|
||
|
del bound_subs[n.input.identifier]
|
||
|
|
||
|
elif isinstance(n, lbn.Call):
|
||
|
if s == lbn.Direction.UP:
|
||
|
out += "("
|
||
|
elif s == lbn.Direction.LEFT:
|
||
|
out += " "
|
||
|
elif s == lbn.Direction.RIGHT:
|
||
|
out += ")"
|
||
|
|
||
|
return out
|
||
|
|
||
|
|
||
|
def clone(node: lbn.Node):
|
||
|
if not isinstance(node, lbn.Node):
|
||
|
raise TypeError(f"I don't know what to do with a {type(node)}")
|
||
|
|
||
|
macro_map = {}
|
||
|
if isinstance(node, lbn.Func):
|
||
|
c = node.copy()
|
||
|
macro_map[node.input.identifier] = c.input.identifier # type: ignore
|
||
|
else:
|
||
|
c = node.copy()
|
||
|
|
||
|
out = c
|
||
|
out_ptr = out # Stays one step behind ptr, in the new tree.
|
||
|
ptr = node
|
||
|
from_side = lbn.Direction.UP
|
||
|
|
||
|
|
||
|
if isinstance(node, lbn.EndNode):
|
||
|
return out
|
||
|
|
||
|
# We're not using a TreeWalker here because
|
||
|
# we need more control over our pointer when cloning.
|
||
|
while True:
|
||
|
if isinstance(ptr, lbn.EndNode):
|
||
|
from_side, ptr = ptr.go_up()
|
||
|
_, out_ptr = out_ptr.go_up()
|
||
|
elif isinstance(ptr, lbn.Func) or isinstance(ptr, lbn.Root):
|
||
|
if from_side == lbn.Direction.UP:
|
||
|
from_side, ptr = ptr.go_left()
|
||
|
|
||
|
if isinstance(ptr, lbn.Func):
|
||
|
c = ptr.copy()
|
||
|
macro_map[ptr.input.identifier] = c.input.identifier # type: ignore
|
||
|
elif isinstance(ptr, lbn.Bound):
|
||
|
c = ptr.copy()
|
||
|
if c.identifier in macro_map:
|
||
|
c.identifier = macro_map[c.identifier]
|
||
|
else:
|
||
|
c = ptr.copy()
|
||
|
out_ptr.set_side(ptr.parent_side, c)
|
||
|
|
||
|
_, out_ptr = out_ptr.go_left()
|
||
|
elif from_side == lbn.Direction.LEFT:
|
||
|
from_side, ptr = ptr.go_up()
|
||
|
_, out_ptr = out_ptr.go_up()
|
||
|
elif isinstance(ptr, lbn.Call):
|
||
|
if from_side == lbn.Direction.UP:
|
||
|
from_side, ptr = ptr.go_left()
|
||
|
|
||
|
if isinstance(ptr, lbn.Func):
|
||
|
c = ptr.copy()
|
||
|
macro_map[ptr.input.identifier] = c.input.identifier # type: ignore
|
||
|
elif isinstance(ptr, lbn.Bound):
|
||
|
c = ptr.copy()
|
||
|
if c.identifier in macro_map:
|
||
|
c.identifier = macro_map[c.identifier]
|
||
|
else:
|
||
|
c = ptr.copy()
|
||
|
out_ptr.set_side(ptr.parent_side, c)
|
||
|
|
||
|
_, out_ptr = out_ptr.go_left()
|
||
|
elif from_side == lbn.Direction.LEFT:
|
||
|
from_side, ptr = ptr.go_right()
|
||
|
|
||
|
if isinstance(ptr, lbn.Func):
|
||
|
c = ptr.copy()
|
||
|
macro_map[ptr.input.identifier] = c.input.identifier # type: ignore
|
||
|
elif isinstance(ptr, lbn.Bound):
|
||
|
c = ptr.copy()
|
||
|
if c.identifier in macro_map:
|
||
|
c.identifier = macro_map[c.identifier]
|
||
|
else:
|
||
|
c = ptr.copy()
|
||
|
out_ptr.set_side(ptr.parent_side, c)
|
||
|
|
||
|
_, out_ptr = out_ptr.go_right()
|
||
|
elif from_side == lbn.Direction.RIGHT:
|
||
|
from_side, ptr = ptr.go_up()
|
||
|
_, out_ptr = out_ptr.go_up()
|
||
|
|
||
|
if ptr is node.parent:
|
||
|
break
|
||
|
return out
|
||
|
|
||
|
def prepare(root: lbn.Root, *, ban_macro_name = None) -> list:
|
||
|
"""
|
||
|
Prepare an expression for expansion.
|
||
|
This will does the following:
|
||
|
- Binds variables
|
||
|
- Turns unbound macros into free variables
|
||
|
- Generates warnings
|
||
|
"""
|
||
|
|
||
|
if not isinstance(root, lbn.Root):
|
||
|
raise TypeError(f"I don't know what to do with a {type(root)}")
|
||
|
|
||
|
bound_variables = {}
|
||
|
|
||
|
warnings = []
|
||
|
|
||
|
it = iter(root)
|
||
|
for s, n in it:
|
||
|
if isinstance(n, lbn.History):
|
||
|
if root.runner.history[0] == None:
|
||
|
raise lbn.ReductionError("There isn't any history to reference.")
|
||
|
else:
|
||
|
warnings += [
|
||
|
("class:code", "$"),
|
||
|
("class:warn", " will be expanded to ")
|
||
|
] + lamb_engine.utils.lex_str(str(n.expand()[1]))
|
||
|
|
||
|
# If this expression is part of a macro,
|
||
|
# make sure we don't reference it inside itself.
|
||
|
elif isinstance(n, lbn.Macro):
|
||
|
if (n.name == ban_macro_name) and (ban_macro_name is not None):
|
||
|
raise lbn.ReductionError("Macro cannot reference self")
|
||
|
|
||
|
# Bind variables
|
||
|
if n.name in bound_variables:
|
||
|
n.parent.set_side(
|
||
|
n.parent_side,
|
||
|
clone(bound_variables[n.name])
|
||
|
)
|
||
|
it.ptr = n.parent.get_side(n.parent_side)
|
||
|
|
||
|
# Turn undefined macros into free variables
|
||
|
elif n.name not in root.runner.macro_table:
|
||
|
warnings += [
|
||
|
("class:warn", "Name "),
|
||
|
("class:code", n.name),
|
||
|
("class:warn", " is a free variable\n"),
|
||
|
]
|
||
|
n.parent.set_side(
|
||
|
n.parent_side,
|
||
|
n.to_freevar()
|
||
|
)
|
||
|
it.ptr = n.parent.get_side(n.parent_side)
|
||
|
|
||
|
|
||
|
# Save bound variables when we enter a function's sub-tree,
|
||
|
# delete them when we exit it.
|
||
|
elif isinstance(n, lbn.Func):
|
||
|
if s == lbn.Direction.UP:
|
||
|
# Add this function's input to the table of bound variables.
|
||
|
# If it is already there, raise an error.
|
||
|
if (n.input.name in bound_variables):
|
||
|
raise lbn.ReductionError(f"Bound variable name conflict: \"{n.input.name}\"")
|
||
|
else:
|
||
|
bound_variables[n.input.name] = lbn.Bound(
|
||
|
lamb_engine.utils.remove_sub(n.input.name),
|
||
|
macro_name = n.input.name
|
||
|
)
|
||
|
n.input = bound_variables[n.input.name]
|
||
|
|
||
|
elif s == lbn.Direction.LEFT:
|
||
|
del bound_variables[n.input.macro_name] # type: ignore
|
||
|
|
||
|
return warnings
|
||
|
|
||
|
# Apply a function.
|
||
|
# Returns the function's output.
|
||
|
def call_func(fn: lbn.Func, arg: lbn.Node):
|
||
|
for s, n in fn:
|
||
|
if isinstance(n, lbn.Bound) and (s == lbn.Direction.UP):
|
||
|
if n == fn.input:
|
||
|
if n.parent is None:
|
||
|
raise Exception("Tried to substitute a None bound variable.")
|
||
|
|
||
|
n.parent.set_side(n.parent_side, clone(arg)) # type: ignore
|
||
|
return fn.left
|
||
|
|
||
|
# Do a single reduction step
|
||
|
def reduce(root: lbn.Root) -> tuple[lbn.ReductionType, lbn.Root]:
|
||
|
if not isinstance(root, lbn.Root):
|
||
|
raise TypeError(f"I can't reduce a {type(root)}")
|
||
|
|
||
|
out = root
|
||
|
for s, n in out:
|
||
|
if isinstance(n, lbn.Call) and (s == lbn.Direction.UP):
|
||
|
if isinstance(n.left, lbn.Func):
|
||
|
n.parent.set_side(
|
||
|
n.parent_side, # type: ignore
|
||
|
call_func(n.left, n.right)
|
||
|
)
|
||
|
|
||
|
return lbn.ReductionType.FUNCTION_APPLY, out
|
||
|
|
||
|
elif isinstance(n.left, lbn.ExpandableEndNode):
|
||
|
r, n.left = n.left.expand()
|
||
|
return r, out
|
||
|
return lbn.ReductionType.NOTHING, out
|
||
|
|
||
|
|
||
|
def expand(root: lbn.Root, *, force_all = False) -> tuple[int, lbn.Root]:
|
||
|
"""
|
||
|
Expands expandable nodes in the given tree.
|
||
|
|
||
|
If force_all is false, this only expands
|
||
|
ExpandableEndnodes that have "always_expand" set to True.
|
||
|
|
||
|
If force_all is True, this expands ALL
|
||
|
ExpandableEndnodes.
|
||
|
"""
|
||
|
|
||
|
if not isinstance(root, lbn.Root):
|
||
|
raise TypeError(f"I don't know what to do with a {type(root)}")
|
||
|
|
||
|
out = root
|
||
|
macro_expansions = 0
|
||
|
|
||
|
it = iter(root)
|
||
|
for s, n in it:
|
||
|
if (
|
||
|
isinstance(n, lbn.ExpandableEndNode) and
|
||
|
(force_all or n.always_expand)
|
||
|
):
|
||
|
|
||
|
n.parent.set_side(
|
||
|
n.parent_side, # type: ignore
|
||
|
n.expand()[1]
|
||
|
)
|
||
|
it.ptr = n.parent.get_side(
|
||
|
n.parent_side # type: ignore
|
||
|
)
|
||
|
macro_expansions += 1
|
||
|
return macro_expansions, out
|