Did the following:

- Added Root node
  - Created node.prepare() method
  - Added "macro show" warning
  - Rewrote TreeWalker & simplified a few methods
master
Mark 2022-11-01 20:46:45 -07:00
parent ecbb8661ce
commit 04880b7724
Signed by: Mark
GPG Key ID: AD62BB059C2AAEE4
3 changed files with 160 additions and 133 deletions

View File

@ -76,20 +76,21 @@ Lamb treats each λ expression as a binary tree. Variable binding and reduction
## Todo (pre-release): ## Todo (pre-release):
- Prettier colors - Prettier colors
- Prevent macro-chaining recursion
- step-by-step reduction
- Full-reduce option (expand all macros)
- PyPi package
- Cleanup warnings - Cleanup warnings
- Preprocess method: bind, macros to free, etc
- History queue
- Truncate long expressions in warnings - Truncate long expressions in warnings
- Prevent macro-chaining recursion
- Full-reduce option (expand all macros)
- step-by-step reduction
- Cleanup files
- PyPi package
## Todo: ## Todo:
- History queue + command indexing
- Show history command
- Better class mutation: when is a node no longer valid? - Better class mutation: when is a node no longer valid?
- Loop detection - Loop detection
- Command-line options (load a file, run a set of commands)
- $\alpha$-equivalence check - $\alpha$-equivalence check
- Command-line options (load a file, run a set of commands)
- Unchurch macro: make church numerals human-readable - Unchurch macro: make church numerals human-readable
- Syntax highlighting: parenthesis, bound variables, macros, etc - Syntax highlighting: parenthesis, bound variables, macros, etc

View File

@ -20,9 +20,6 @@ class ReductionType(enum.Enum):
# We turned a church numeral into an expression # We turned a church numeral into an expression
AUTOCHURCH = enum.auto() AUTOCHURCH = enum.auto()
# We replaced a macro with a free variable.
MACRO_TO_FREE = enum.auto()
# We applied a function. # We applied a function.
# This is the only type of "formal" reduction step. # This is the only type of "formal" reduction step.
FUNCTION_APPLY = enum.auto() FUNCTION_APPLY = enum.auto()
@ -50,27 +47,32 @@ class TreeWalker:
def __init__(self, expr): def __init__(self, expr):
self.expr = expr self.expr = expr
self.ptr = expr self.ptr = expr
self.first_step = True
self.from_side = Direction.UP self.from_side = Direction.UP
def __iter__(self):
return self
def __next__(self): def __next__(self):
# This could be implemented without checking the node type, # This could be implemented without checking the node type,
# but there's no reason to do that. # but there's no reason to do that.
# Maybe later? # Maybe later?
if self.ptr is self.expr.parent:
raise StopIteration
out = self.ptr if self.first_step:
out_side = self.from_side self.first_step = False
if isinstance(self.ptr, EndNode): return self.from_side, self.ptr
if isinstance(self.ptr, Root):
if self.from_side == Direction.UP:
self.from_side, self.ptr = self.ptr.go_left()
elif isinstance(self.ptr, EndNode):
self.from_side, self.ptr = self.ptr.go_up() self.from_side, self.ptr = self.ptr.go_up()
elif isinstance(self.ptr, Func): elif isinstance(self.ptr, Func):
if self.from_side == Direction.UP: if self.from_side == Direction.UP:
self.from_side, self.ptr = self.ptr.go_left() self.from_side, self.ptr = self.ptr.go_left()
elif self.from_side == Direction.LEFT: elif self.from_side == Direction.LEFT:
self.from_side, self.ptr = self.ptr.go_up() self.from_side, self.ptr = self.ptr.go_up()
elif isinstance(self.ptr, Call): elif isinstance(self.ptr, Call):
if self.from_side == Direction.UP: if self.from_side == Direction.UP:
self.from_side, self.ptr = self.ptr.go_left() self.from_side, self.ptr = self.ptr.go_left()
@ -78,11 +80,18 @@ class TreeWalker:
self.from_side, self.ptr = self.ptr.go_right() self.from_side, self.ptr = self.ptr.go_right()
elif self.from_side == Direction.RIGHT: elif self.from_side == Direction.RIGHT:
self.from_side, self.ptr = self.ptr.go_up() self.from_side, self.ptr = self.ptr.go_up()
else: else:
raise TypeError(f"I don't know how to iterate a {type(self.ptr)}") raise TypeError(f"I don't know how to iterate a {type(self.ptr)}")
return out_side, out # Stop conditions
if isinstance(self.expr, Root):
if self.ptr is self.expr:
raise StopIteration
else:
if self.ptr is self.expr.parent:
raise StopIteration
return self.from_side, self.ptr
class Node: class Node:
""" """
@ -93,11 +102,11 @@ class Node:
def __init__(self): def __init__(self):
# The node this one is connected to. # The node this one is connected to.
# None if this is the top objects. # None if this is the top objects.
self.parent: Node | None = None self.parent: Node = None # type: ignore
# What direction this is relative to the parent. # What direction this is relative to the parent.
# Left of Right. # Left of Right.
self.parent_side: Direction | None = None self.parent_side: Direction = None # type: ignore
# Left and right nodes, None if empty # Left and right nodes, None if empty
self._left: Node | None = None self._left: Node | None = None
@ -220,12 +229,6 @@ class Node:
""" """
return print_node(self, export = True) return print_node(self, export = True)
def bind_variables(self, *, ban_macro_name = None):
return bind_variables(
self,
ban_macro_name = ban_macro_name
)
def set_runner(self, runner): def set_runner(self, runner):
for s, n in self: for s, n in self:
if s == Direction.UP: if s == Direction.UP:
@ -279,9 +282,17 @@ class Macro(ExpandableEndNode):
def expand(self) -> tuple[ReductionType, Node]: def expand(self) -> tuple[ReductionType, Node]:
if self.name in self.runner.macro_table: if self.name in self.runner.macro_table:
return ReductionType.MACRO_EXPAND, clone(self.runner.macro_table[self.name]) # The element in the macro table will be a Root node,
# so we clone its left element.
return (
ReductionType.MACRO_EXPAND,
clone(self.runner.macro_table[self.name].left)
)
else: else:
return ReductionType.MACRO_TO_FREE, FreeVar(self.name, runner = self.runner) raise Exception(f"Macro {self.name} is not defined")
def to_freevar(self):
return FreeVar(self.name, runner = self.runner)
def copy(self): def copy(self):
return Macro(self.name, runner = self.runner) return Macro(self.name, runner = self.runner)
@ -342,7 +353,10 @@ class History(ExpandableEndNode):
def expand(self) -> tuple[ReductionType, Node]: def expand(self) -> tuple[ReductionType, Node]:
if len(self.runner.history) == 0: if len(self.runner.history) == 0:
raise ReductionError(f"There isn't any history to reference.") raise ReductionError(f"There isn't any history to reference.")
return ReductionType.HIST_EXPAND, clone(self.runner.history[-1]) # .left is VERY important!
# self.runner.history will contain Root nodes,
# and we don't want those *inside* our tree.
return ReductionType.HIST_EXPAND, clone(self.runner.history[-1].left)
def copy(self): def copy(self):
return History(runner = self.runner) return History(runner = self.runner)
@ -401,6 +415,23 @@ class Func(Node):
def copy(self): def copy(self):
return Func(self.input, None, runner = self.runner) # type: ignore return Func(self.input, None, runner = self.runner) # type: ignore
class Root(Node):
"""
Root node.
Used at the top of an expression.
"""
def __init__(self, left: Node, *, runner = None) -> None:
super().__init__()
self.left: Node = left
self.runner = runner # type: ignore
def __repr__(self):
return f"<Root {self.left!r}>"
def copy(self):
return Root(None, runner = self.runner) # type: ignore
class Call(Node): class Call(Node):
@staticmethod @staticmethod
def from_parse(results): def from_parse(results):
@ -498,7 +529,7 @@ def clone(node: Node):
if isinstance(ptr, EndNode): if isinstance(ptr, EndNode):
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
_, out_ptr = out_ptr.go_up() _, out_ptr = out_ptr.go_up()
elif isinstance(ptr, Func): elif isinstance(ptr, Func) or isinstance(ptr, Root):
if from_side == Direction.UP: if from_side == Direction.UP:
from_side, ptr = ptr.go_left() from_side, ptr = ptr.go_left()
out_ptr.set_side(ptr.parent_side, ptr.copy()) out_ptr.set_side(ptr.parent_side, ptr.copy())
@ -523,9 +554,17 @@ def clone(node: Node):
break break
return out return out
def bind_variables(node: Node, *, ban_macro_name = None) -> dict: def prepare(root: Root, *, ban_macro_name = None) -> dict:
if not isinstance(node, Node): """
raise TypeError(f"I don't know what to do with a {type(node)}") Prepare an expression for expansion.
This will does the following:
- Binds variables
- Turns unbound macros into free variables
- Generates warnings
"""
if not isinstance(root, Root):
raise TypeError(f"I don't know what to do with a {type(root)}")
bound_variables = {} bound_variables = {}
@ -534,7 +573,8 @@ def bind_variables(node: Node, *, ban_macro_name = None) -> dict:
"free_variables": set() "free_variables": set()
} }
for s, n in node: it = iter(root)
for s, n in it:
if isinstance(n, History): if isinstance(n, History):
output["has_history"] = True output["has_history"] = True
@ -544,9 +584,26 @@ def bind_variables(node: Node, *, ban_macro_name = None) -> dict:
if (n.name == ban_macro_name) and (ban_macro_name is not None): if (n.name == ban_macro_name) and (ban_macro_name is not None):
raise ReductionError("Macro cannot reference self") raise ReductionError("Macro cannot reference self")
if n.name not in node.runner.macro_table: # Bind variables
output["free_variables"].add(n.name) 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:
output["free_variables"].add(n.name)
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, Func): elif isinstance(n, Func):
if s == Direction.UP: if s == Direction.UP:
# Add this function's input to the table of bound variables. # Add this function's input to the table of bound variables.
@ -557,23 +614,9 @@ def bind_variables(node: Node, *, ban_macro_name = None) -> dict:
bound_variables[n.input.name] = Bound(n.input.name) bound_variables[n.input.name] = Bound(n.input.name)
n.input = bound_variables[n.input.name] n.input = bound_variables[n.input.name]
# If output is a macro, swap it with a bound variable.
if isinstance(n.left, Macro):
if n.left.name in bound_variables:
n.left = clone(bound_variables[n.left.name])
elif s == Direction.LEFT: elif s == Direction.LEFT:
del bound_variables[n.input.name] del bound_variables[n.input.name]
elif isinstance(n, Call):
if s == Direction.UP:
# Bind macros
if isinstance(n.left, Macro):
if n.left.name in bound_variables:
n.left = clone(bound_variables[n.left.name])
if isinstance(n.right, Macro):
if n.right.name in bound_variables:
n.right = clone(bound_variables[n.right.name])
return output return output
# Apply a function. # Apply a function.
@ -589,18 +632,14 @@ def call_func(fn: Func, arg: Node):
return fn.left return fn.left
# Do a single reduction step # Do a single reduction step
def reduce(node: Node) -> tuple[ReductionType, Node]: def reduce(root: Root) -> tuple[ReductionType, Root]:
if not isinstance(node, Node): if not isinstance(root, Root):
raise TypeError(f"I can't reduce a {type(node)}") raise TypeError(f"I can't reduce a {type(root)}")
out = node out = root
for s, n in out: for s, n in out:
if isinstance(n, Call) and (s == Direction.UP): if isinstance(n, Call) and (s == Direction.UP):
if isinstance(n.left, Func): if isinstance(n.left, Func):
if n.parent is None:
out = call_func(n.left, n.right)
out._set_parent(None, None)
else:
n.parent.set_side( n.parent.set_side(
n.parent_side, # type: ignore n.parent_side, # type: ignore
call_func(n.left, n.right) call_func(n.left, n.right)
@ -614,7 +653,7 @@ def reduce(node: Node) -> tuple[ReductionType, Node]:
return ReductionType.NOTHING, out return ReductionType.NOTHING, out
def expand(node: Node, *, force_all = False) -> tuple[int, Node]: def expand(root: Root, *, force_all = False) -> tuple[int, Root]:
""" """
Expands expandable nodes in the given tree. Expands expandable nodes in the given tree.
@ -625,50 +664,25 @@ def expand(node: Node, *, force_all = False) -> tuple[int, Node]:
ExpandableEndnodes. ExpandableEndnodes.
""" """
if not isinstance(node, Node): if not isinstance(root, Root):
raise TypeError(f"I don't know what to do with a {type(node)}") raise TypeError(f"I don't know what to do with a {type(root)}")
out = clone(node) out = root
ptr = out
from_side = Direction.UP
macro_expansions = 0 macro_expansions = 0
while True: it = iter(root)
for s, n in it:
if ( if (
isinstance(ptr, ExpandableEndNode) and isinstance(n, ExpandableEndNode) and
(force_all or ptr.always_expand) (force_all or n.always_expand)
): ):
if ptr.parent is None:
ptr = ptr.expand()[1] n.parent.set_side(
out = ptr n.parent_side, # type: ignore
ptr._set_parent(None, None) n.expand()[1]
else:
ptr.parent.set_side(
ptr.parent_side, # type: ignore
ptr.expand()[1]
) )
ptr = ptr.parent.get_side( it.ptr = n.parent.get_side(
ptr.parent_side # type: ignore n.parent_side # type: ignore
) )
macro_expansions += 1 macro_expansions += 1
# Tree walk logic
if isinstance(ptr, EndNode):
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()
if ptr is node.parent:
break
return macro_expansions, out return macro_expansions, out

View File

@ -13,6 +13,7 @@ class StopReason(enum.Enum):
LOOP_DETECTED = ("class:warn", "Loop detected") LOOP_DETECTED = ("class:warn", "Loop detected")
MAX_EXCEEDED = ("class:err", "Too many reductions") MAX_EXCEEDED = ("class:err", "Too many reductions")
INTERRUPT = ("class:warn", "User interrupt") INTERRUPT = ("class:warn", "User interrupt")
SHOW_MACRO = ("class:text", "Displaying macro content")
class MacroDef: class MacroDef:
@staticmethod @staticmethod
@ -32,11 +33,6 @@ class MacroDef:
def __str__(self): def __str__(self):
return f"{self.label} := {self.expr}" return f"{self.label} := {self.expr}"
def bind_variables(self, *, ban_macro_name = None):
return self.expr.bind_variables(
ban_macro_name = ban_macro_name
)
def set_runner(self, runner): def set_runner(self, runner):
return self.expr.set_runner(runner) return self.expr.set_runner(runner)
@ -88,27 +84,30 @@ class Runner:
# so that all digits appear to be changing. # so that all digits appear to be changing.
self.iter_update = 231 self.iter_update = 231
self.history = [] self.history: list[lamb.node.Root] = []
def prompt(self): def prompt(self):
return self.prompt_session.prompt( return self.prompt_session.prompt(
message = self.prompt_message message = self.prompt_message
) )
def parse(self, line) -> tuple[lamb.node.Node | MacroDef | Command, dict]: def parse(self, line) -> tuple[lamb.node.Root | MacroDef | Command, dict]:
e = self.parser.parse_line(line) e = self.parser.parse_line(line)
o = {} o = {}
if isinstance(e, MacroDef): if isinstance(e, MacroDef):
e.expr = lamb.node.Root(e.expr)
e.set_runner(self) e.set_runner(self)
o = e.bind_variables(ban_macro_name = e.label) o = lamb.node.prepare(e.expr, ban_macro_name = e.label)
elif isinstance(e, lamb.node.Node): elif isinstance(e, lamb.node.Node):
e = lamb.node.Root(e)
e.set_runner(self) e.set_runner(self)
o = e.bind_variables() o = lamb.node.prepare(e)
return e, o return e, o
def reduce(self, node: lamb.node.Node, *, status = {}) -> None: def reduce(self, node: lamb.node.Root, *, status = {}) -> None:
warning_text = [] warning_text = []
@ -130,12 +129,9 @@ class Runner:
("class:warn", "\n") ("class:warn", "\n")
] ]
only_macro = isinstance(node, lamb.node.ExpandableEndNode) only_macro = isinstance(node.left, lamb.node.Macro)
if only_macro: if only_macro:
warning_text += [ stop_reason = StopReason.SHOW_MACRO
("class:warn", "All macros will be expanded"),
("class:warn", "\n")
]
m, node = lamb.node.expand(node, force_all = only_macro) m, node = lamb.node.expand(node, force_all = only_macro)
macro_expansions += m macro_expansions += m
@ -147,10 +143,16 @@ class Runner:
("class:warn", " is a free variable\n"), ("class:warn", " is a free variable\n"),
] ]
if len(warning_text) != 0:
printf(FormattedText(warning_text), style = lamb.utils.style) printf(FormattedText(warning_text), style = lamb.utils.style)
while (self.reduction_limit is None) or (k < self.reduction_limit): while (
(
(self.reduction_limit is None) or
(k < self.reduction_limit)
) and not only_macro
):
# Show reduction count # Show reduction count
if (k >= self.iter_update) and (k % self.iter_update == 0): if (k >= self.iter_update) and (k % self.iter_update == 0):
@ -177,6 +179,12 @@ class Runner:
# Clear reduction counter if it was printed # Clear reduction counter if it was printed
print(" " * round(14 + math.log10(k)), end = "\r") print(" " * round(14 + math.log10(k)), end = "\r")
if only_macro:
out_text += [
("class:result_header", f"Displaying macro content")
]
else:
out_text += [ out_text += [
("class:result_header", f"Runtime: "), ("class:result_header", f"Runtime: "),
("class:text", f"{time.time() - start_time:.03f} seconds"), ("class:text", f"{time.time() - start_time:.03f} seconds"),
@ -192,7 +200,11 @@ class Runner:
("class:muted", f"(Limit: {self.reduction_limit:,})") ("class:muted", f"(Limit: {self.reduction_limit:,})")
] ]
if (stop_reason == StopReason.BETA_NORMAL or stop_reason == StopReason.LOOP_DETECTED): if (
stop_reason == StopReason.BETA_NORMAL or
stop_reason == StopReason.LOOP_DETECTED or
only_macro
):
out_text += [ out_text += [
("class:result_header", "\n\n => "), ("class:result_header", "\n\n => "),
("class:text", str(node)), # type: ignore ("class:text", str(node)), # type: ignore