Compare commits

..

No commits in common. "d3917b1f58cf65ab15091f97272b8bed2f231fca" and "ecbb8661ced3a7ae5d9ded7cb75e580649a46370" have entirely different histories.

4 changed files with 132 additions and 159 deletions

View File

@ -76,21 +76,20 @@ Lamb treats each λ expression as a binary tree. Variable binding and reduction
## Todo (pre-release): ## Todo (pre-release):
- Prettier colors - Prettier colors
- Cleanup warnings
- Truncate long expressions in warnings
- Prevent macro-chaining recursion - Prevent macro-chaining recursion
- Full-reduce option (expand all macros)
- step-by-step reduction - step-by-step reduction
- Cleanup files - Full-reduce option (expand all macros)
- PyPi package - PyPi package
- Cleanup warnings
- Preprocess method: bind, macros to free, etc
- History queue
- Truncate long expressions in warnings
## 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
- $\alpha$-equivalence check
- Command-line options (load a file, run a set of commands) - Command-line options (load a file, run a set of commands)
- $\alpha$-equivalence check
- 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,6 +20,9 @@ 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()
@ -47,32 +50,27 @@ 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
if self.first_step: out = self.ptr
self.first_step = False out_side = self.from_side
return self.from_side, self.ptr if isinstance(self.ptr, EndNode):
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()
@ -80,18 +78,11 @@ 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)}")
# Stop conditions return out_side, out
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:
""" """
@ -102,11 +93,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 # type: ignore self.parent: Node | None = None
# 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 # type: ignore self.parent_side: Direction | None = None
# Left and right nodes, None if empty # Left and right nodes, None if empty
self._left: Node | None = None self._left: Node | None = None
@ -229,6 +220,12 @@ 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:
@ -282,17 +279,9 @@ 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:
# The element in the macro table will be a Root node, return ReductionType.MACRO_EXPAND, clone(self.runner.macro_table[self.name])
# so we clone its left element.
return (
ReductionType.MACRO_EXPAND,
clone(self.runner.macro_table[self.name].left)
)
else: else:
raise Exception(f"Macro {self.name} is not defined") return ReductionType.MACRO_TO_FREE, FreeVar(self.name, runner = self.runner)
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)
@ -353,10 +342,7 @@ 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.")
# .left is VERY important! return ReductionType.HIST_EXPAND, clone(self.runner.history[-1])
# 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)
@ -415,23 +401,6 @@ 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):
@ -529,7 +498,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) or isinstance(ptr, Root): elif isinstance(ptr, Func):
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())
@ -554,17 +523,9 @@ def clone(node: Node):
break break
return out return out
def prepare(root: Root, *, ban_macro_name = None) -> dict: def bind_variables(node: Node, *, ban_macro_name = None) -> dict:
""" if not isinstance(node, Node):
Prepare an expression for expansion. raise TypeError(f"I don't know what to do with a {type(node)}")
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 = {}
@ -573,8 +534,7 @@ def prepare(root: Root, *, ban_macro_name = None) -> dict:
"free_variables": set() "free_variables": set()
} }
it = iter(root) for s, n in node:
for s, n in it:
if isinstance(n, History): if isinstance(n, History):
output["has_history"] = True output["has_history"] = True
@ -584,26 +544,9 @@ def prepare(root: Root, *, 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")
# Bind variables if n.name not in node.runner.macro_table:
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) 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.
@ -614,9 +557,23 @@ def prepare(root: Root, *, 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.
@ -632,18 +589,22 @@ 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(root: Root) -> tuple[ReductionType, Root]: def reduce(node: Node) -> tuple[ReductionType, Node]:
if not isinstance(root, Root): if not isinstance(node, Node):
raise TypeError(f"I can't reduce a {type(root)}") raise TypeError(f"I can't reduce a {type(node)}")
out = root out = node
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):
n.parent.set_side( if n.parent is None:
n.parent_side, # type: ignore out = call_func(n.left, n.right)
call_func(n.left, n.right) out._set_parent(None, None)
) else:
n.parent.set_side(
n.parent_side, # type: ignore
call_func(n.left, n.right)
)
return ReductionType.FUNCTION_APPLY, out return ReductionType.FUNCTION_APPLY, out
@ -653,7 +614,7 @@ def reduce(root: Root) -> tuple[ReductionType, Root]:
return ReductionType.NOTHING, out return ReductionType.NOTHING, out
def expand(root: Root, *, force_all = False) -> tuple[int, Root]: def expand(node: Node, *, force_all = False) -> tuple[int, Node]:
""" """
Expands expandable nodes in the given tree. Expands expandable nodes in the given tree.
@ -664,25 +625,50 @@ def expand(root: Root, *, force_all = False) -> tuple[int, Root]:
ExpandableEndnodes. ExpandableEndnodes.
""" """
if not isinstance(root, Root): if not isinstance(node, Node):
raise TypeError(f"I don't know what to do with a {type(root)}") raise TypeError(f"I don't know what to do with a {type(node)}")
out = root out = clone(node)
ptr = out
from_side = Direction.UP
macro_expansions = 0 macro_expansions = 0
it = iter(root) while True:
for s, n in it:
if ( if (
isinstance(n, ExpandableEndNode) and isinstance(ptr, ExpandableEndNode) and
(force_all or n.always_expand) (force_all or ptr.always_expand)
): ):
if ptr.parent is None:
n.parent.set_side( ptr = ptr.expand()[1]
n.parent_side, # type: ignore out = ptr
n.expand()[1] ptr._set_parent(None, None)
) else:
it.ptr = n.parent.get_side( ptr.parent.set_side(
n.parent_side # type: ignore ptr.parent_side, # type: ignore
) ptr.expand()[1]
)
ptr = ptr.parent.get_side(
ptr.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,7 +13,6 @@ 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
@ -33,6 +32,11 @@ 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)
@ -84,30 +88,27 @@ 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: list[lamb.node.Root] = [] self.history = []
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.Root | MacroDef | Command, dict]: def parse(self, line) -> tuple[lamb.node.Node | 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 = lamb.node.prepare(e.expr, ban_macro_name = e.label) o = e.bind_variables(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 = lamb.node.prepare(e) o = e.bind_variables()
return e, o return e, o
def reduce(self, node: lamb.node.Root, *, status = {}) -> None: def reduce(self, node: lamb.node.Node, *, status = {}) -> None:
warning_text = [] warning_text = []
@ -129,9 +130,12 @@ class Runner:
("class:warn", "\n") ("class:warn", "\n")
] ]
only_macro = isinstance(node.left, lamb.node.Macro) only_macro = isinstance(node, lamb.node.ExpandableEndNode)
if only_macro: if only_macro:
stop_reason = StopReason.SHOW_MACRO warning_text += [
("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
@ -143,16 +147,10 @@ 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 ( while (self.reduction_limit is None) or (k < self.reduction_limit):
(
(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):
@ -179,32 +177,22 @@ 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 += [
out_text += [ ("class:result_header", f"Runtime: "),
("class:result_header", f"Displaying macro content") ("class:text", f"{time.time() - start_time:.03f} seconds"),
]
else: ("class:result_header", f"\nExit reason: "),
out_text += [ stop_reason.value,
("class:result_header", f"Runtime: "),
("class:text", f"{time.time() - start_time:.03f} seconds"),
("class:result_header", f"\nExit reason: "), ("class:result_header", f"\nMacro expansions: "),
stop_reason.value, ("class:text", f"{macro_expansions:,}"),
("class:result_header", f"\nMacro expansions: "), ("class:result_header", f"\nReductions: "),
("class:text", f"{macro_expansions:,}"), ("class:text", f"{k:,}\t"),
("class:muted", f"(Limit: {self.reduction_limit:,})")
]
("class:result_header", f"\nReductions: "), if (stop_reason == StopReason.BETA_NORMAL or stop_reason == StopReason.LOOP_DETECTED):
("class:text", f"{k:,}\t"),
("class:muted", f"(Limit: {self.reduction_limit:,})")
]
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

View File

@ -14,7 +14,7 @@ description = "A lambda calculus engine"
# #
# Patch release: # Patch release:
# Small, compatible fixes. # Small, compatible fixes.
version = "0.1.2" version = "0.1.1"
dependencies = [ dependencies = [
"prompt-toolkit==3.0.31", "prompt-toolkit==3.0.31",