diff --git a/lamb/commands.py b/lamb/commands.py
index b77df98..08acd70 100644
--- a/lamb/commands.py
+++ b/lamb/commands.py
@@ -4,7 +4,6 @@ from prompt_toolkit import print_formatted_text as printf
from prompt_toolkit.shortcuts import clear as clear_screen
import os.path
-
from pyparsing import exceptions as ppx
import lamb
@@ -12,30 +11,32 @@ import lamb
commands = {}
help_texts = {}
-def lamb_command(*, help_text: str):
+def lamb_command(
+ *,
+ command_name: str | None = None,
+ help_text: str
+ ):
+ """
+ A decorator that allows us to easily make commands
+ """
+
def inner(func):
- commands[func.__name__] = func
- help_texts[func.__name__] = help_text
+ name = func.__name__ if command_name is None else command_name
+
+ commands[name] = func
+ help_texts[name] = help_text
return inner
-def run(command, runner) -> None:
- if command.name not in commands:
- printf(
- FormattedText([
- ("class:warn", f"Unknown command \"{command.name}\"")
- ]),
- style = lamb.utils.style
- )
- else:
- commands[command.name](command, runner)
-
-@lamb_command(help_text = "Save macros to a file")
-def save(command, runner) -> None:
+@lamb_command(
+ command_name = "save",
+ help_text = "Save macros to a file"
+)
+def cmd_save(command, runner) -> None:
if len(command.args) != 1:
printf(
HTML(
- "Command :save takes exactly one argument."
+ "Command :{command.name} takes exactly one argument."
),
style = lamb.utils.style
)
@@ -72,12 +73,15 @@ def save(command, runner) -> None:
)
-@lamb_command(help_text = "Load macros from a file")
-def load(command, runner):
+@lamb_command(
+ command_name = "load",
+ help_text = "Load macros from a file"
+)
+def cmd_load(command, runner):
if len(command.args) != 1:
printf(
HTML(
- "Command :load takes exactly one argument."
+ "Command :{command.name} takes exactly one argument."
),
style = lamb.utils.style
)
@@ -134,13 +138,14 @@ def load(command, runner):
)
-
-@lamb_command(help_text = "Delete a macro")
+@lamb_command(
+ help_text = "Delete a macro"
+)
def mdel(command, runner) -> None:
if len(command.args) != 1:
printf(
HTML(
- "Command :mdel takes exactly one argument."
+ "Command :{command.name} takes exactly one argument."
),
style = lamb.utils.style
)
@@ -159,8 +164,9 @@ def mdel(command, runner) -> None:
del runner.macro_table[target]
-
-@lamb_command(help_text = "Show macros")
+@lamb_command(
+ help_text = "Show macros"
+)
def macros(command, runner) -> None:
printf(FormattedText([
("class:cmd_h", "\nDefined Macros:\n"),
@@ -172,13 +178,17 @@ def macros(command, runner) -> None:
style = lamb.utils.style
)
-@lamb_command(help_text = "Clear the screen")
+@lamb_command(
+ help_text = "Clear the screen"
+)
def clear(command, runner) -> None:
clear_screen()
lamb.utils.show_greeting()
-@lamb_command(help_text = "Print this help")
+@lamb_command(
+ help_text = "Print this help"
+)
def help(command, runner) -> None:
printf(
HTML(
diff --git a/lamb/node.py b/lamb/node.py
index 32b470e..45195f3 100644
--- a/lamb/node.py
+++ b/lamb/node.py
@@ -5,6 +5,24 @@ class Direction(enum.Enum):
LEFT = enum.auto()
RIGHT = enum.auto()
+class ReductionType(enum.Enum):
+ # Nothing happened. This implies that
+ # an expression cannot be reduced further.
+ NOTHING = enum.auto()
+
+ # We replaced a macro with an expression.
+ MACRO_EXPAND = enum.auto()
+
+ # We turned a church numeral into an expression
+ AUTOCHURCH = enum.auto()
+
+ # We replaced a macro with a free variable.
+ MACRO_TO_FREE = enum.auto()
+
+ # We applied a function.
+ # This is the only type of "formal" reduction step.
+ FUNCTION_APPLY = enum.auto()
+
class ReductionError(Exception):
"""
Raised when we encounter an error while reducing.
@@ -16,12 +34,26 @@ class ReductionError(Exception):
class TreeWalker:
+ """
+ An iterator that walks the "outline" of a tree
+ defined by a chain of nodes.
+
+ It returns a tuple: (out_side, out)
+
+ out is the node we moved to,
+ out_side is the direction we came to the node from.
+ """
+
def __init__(self, expr):
self.expr = expr
self.ptr = expr
self.from_side = Direction.UP
def __next__(self):
+ # This could be implemented without checking the node type,
+ # but there's no reason to do that.
+ # Maybe later?
+
if self.ptr is self.expr.parent:
raise StopIteration
@@ -50,6 +82,10 @@ class TreeWalker:
return out_side, out
class Node:
+ """
+ Generic class for an element of an expression tree.
+ """
+
def __init__(self):
# The node this one is connected to.
# None if this is the top objects.
@@ -67,6 +103,12 @@ class Node:
return TreeWalker(self)
def _set_parent(self, parent, side):
+ """
+ Set this node's parent and parent side.
+ This method shouldn't be called explicitly unless
+ there's no other option. Use self.left and self.right instead.
+ """
+
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):
@@ -97,6 +139,11 @@ class Node:
def set_side(self, side: Direction, node):
+ """
+ A wrapper around Node.left and Node.right that
+ automatically selects a side.
+ """
+
if side == Direction.LEFT:
self.left = node
elif side == Direction.RIGHT:
@@ -106,20 +153,47 @@ class Node:
def go_left(self):
+ """
+ Go down the left branch of this node.
+ Returns a tuple (from_dir, node)
+
+ from_dir is the direction from which we came INTO the next node.
+ node is the node on the left of this one.
+ """
+
if self._left is None:
raise Exception("Can't go left when left is None")
return Direction.UP, self._left
def go_right(self):
+ """
+ Go down the right branch of this node.
+ Returns a tuple (from_dir, node)
+
+ from_dir is the direction from which we came INTO the next node.
+ node is the node on the right of this one.
+ """
if self._right is None:
raise Exception("Can't go right when right is None")
return Direction.UP, self._right
def go_up(self):
+ """
+ Go up th the parent of this node.
+ Returns a tuple (from_dir, node)
+
+ from_dir is the direction from which we came INTO the parent.
+ node is the node above of this one.
+ """
return self.parent_side, self.parent
- def clone(self):
- raise NotImplementedError("Nodes MUST provide a `clone` method!")
+ def copy(self):
+ """
+ Return a copy of this node.
+ parent, parent_side, left, and right should be left
+ as None, and will be filled later.
+ """
+ raise NotImplementedError("Nodes MUST provide a `copy` method!")
def __str__(self) -> str:
return print_node(self)
@@ -135,7 +209,7 @@ class EndNode(Node):
raise NotImplementedError("EndNodes MUST provide a `print_value` method!")
class ExpandableEndNode(EndNode):
- def expand(self):
+ def expand(self) -> tuple[ReductionType, Node]:
raise NotImplementedError("ExpandableEndNodes MUST provide an `expand` method!")
class FreeVar(EndNode):
@@ -148,7 +222,7 @@ class FreeVar(EndNode):
def print_value(self):
return f"{self.name}"
- def clone(self):
+ def copy(self):
return FreeVar(self.name)
class Macro(ExpandableEndNode):
@@ -168,14 +242,13 @@ class Macro(ExpandableEndNode):
def print_value(self):
return self.name
- def expand(self, *, macro_table = {}):
+ def expand(self, *, macro_table = {}) -> tuple[ReductionType, Node]:
if self.name in macro_table:
- return clone(macro_table[self.name])
+ return ReductionType.MACRO_EXPAND, clone(macro_table[self.name])
else:
- f = FreeVar(self.name)
- return f
+ return ReductionType.MACRO_TO_FREE, FreeVar(self.name)
- def clone(self):
+ def copy(self):
return Macro(self.name)
class Church(ExpandableEndNode):
@@ -195,7 +268,7 @@ class Church(ExpandableEndNode):
def print_value(self):
return str(self.value)
- def expand(self):
+ def expand(self) -> tuple[ReductionType, Node]:
f = Bound("f")
a = Bound("a")
chain = a
@@ -203,12 +276,12 @@ class Church(ExpandableEndNode):
for i in range(self.value):
chain = Call(clone(f), clone(chain))
- return Func(
- f,
- Func(a, chain)
+ return (
+ ReductionType.AUTOCHURCH,
+ Func(f, Func(a, chain))
)
- def clone(self):
+ def copy(self):
return Church(self.value)
bound_counter = 0
@@ -223,7 +296,7 @@ class Bound(EndNode):
else:
self.identifier = forced_id
- def clone(self):
+ def copy(self):
return Bound(self.name, forced_id = self.identifier)
def __eq__(self, other):
@@ -260,7 +333,7 @@ class Func(Node):
def __repr__(self):
return f""
- def clone(self):
+ def copy(self):
return Func(self.input, None) # type: ignore
class Call(Node):
@@ -292,14 +365,13 @@ class Call(Node):
def __repr__(self):
return f""
- def clone(self):
+ def copy(self):
return Call(None, None) # type: ignore
-
def print_node(node: Node) -> str:
if not isinstance(node, Node):
- raise TypeError(f"I don't know what to do with a {type(node)}")
+ raise TypeError(f"I don't know how to print a {type(node)}")
else:
out = ""
@@ -330,7 +402,7 @@ def clone(node: Node):
if not isinstance(node, Node):
raise TypeError(f"I don't know what to do with a {type(node)}")
- out = node.clone()
+ out = node.copy()
out_ptr = out # Stays one step behind ptr, in the new tree.
ptr = node
from_side = Direction.UP
@@ -347,7 +419,7 @@ def clone(node: Node):
elif isinstance(ptr, Func):
if from_side == Direction.UP:
from_side, ptr = ptr.go_left()
- out_ptr.set_side(ptr.parent_side, ptr.clone())
+ out_ptr.set_side(ptr.parent_side, ptr.copy())
_, out_ptr = out_ptr.go_left()
elif from_side == Direction.LEFT:
from_side, ptr = ptr.go_up()
@@ -355,12 +427,11 @@ def clone(node: Node):
elif isinstance(ptr, Call):
if from_side == Direction.UP:
from_side, ptr = ptr.go_left()
- out_ptr.set_side(ptr.parent_side, ptr.clone()
-)
+ out_ptr.set_side(ptr.parent_side, ptr.copy())
_, out_ptr = out_ptr.go_left()
elif from_side == Direction.LEFT:
from_side, ptr = ptr.go_right()
- out_ptr.set_side(ptr.parent_side, ptr.clone())
+ out_ptr.set_side(ptr.parent_side, ptr.copy())
_, out_ptr = out_ptr.go_right()
elif from_side == Direction.RIGHT:
from_side, ptr = ptr.go_up()
@@ -371,7 +442,6 @@ def clone(node: Node):
return out
def bind_variables(node: Node, *, ban_macro_name = None) -> None:
-
if not isinstance(node, Node):
raise TypeError(f"I don't know what to do with a {type(node)}")
@@ -432,31 +502,29 @@ def call_func(fn: Func, arg: Node):
# Do a single reduction step
-def reduce(node: Node, *, macro_table = {}) -> tuple[bool, Node]:
-
+def reduce(node: Node, *, macro_table = {}) -> tuple[ReductionType, Node]:
if not isinstance(node, Node):
raise TypeError(f"I can't reduce a {type(node)}")
- reduced = False
-
out = node
for s, n in out:
- if isinstance(n, Call):
- if s == Direction.UP:
- 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.left = call_func(n.left, n.right)
- reduced = True
- break
- elif isinstance(n.left, ExpandableEndNode):
- if isinstance(n.left, Macro):
- n.left = n.left.expand(macro_table = macro_table)
- else:
- n.left = n.left.expand()
- reduced = True
- break
+ if isinstance(n, Call) and (s == Direction.UP):
+ 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.left = call_func(n.left, n.right)
- return reduced, out
+ return ReductionType.FUNCTION_APPLY, out
+
+ elif isinstance(n.left, ExpandableEndNode):
+ if isinstance(n.left, Macro):
+ r, n.left = n.left.expand(
+ macro_table = macro_table
+ )
+ else:
+ r, n.left = n.left.expand()
+ return r, out
+
+ return ReductionType.NOTHING, out
diff --git a/lamb/runner.py b/lamb/runner.py
index 466113a..11d7350 100644
--- a/lamb/runner.py
+++ b/lamb/runner.py
@@ -103,39 +103,32 @@ class Runner:
while (self.reduction_limit is None) or (i < self.reduction_limit):
try:
- w, r = lamb.node.reduce(
+ red_type, new_node = lamb.node.reduce(
node,
macro_table = self.macro_table
)
except RecursionError:
stop_reason = StopReason.RECURSION
break
- node = r
-
- #print(expr)
- #self.prompt()
+ node = new_node
# If we can't reduce this expression anymore,
# it's in beta-normal form.
- if not w:
+ if red_type == lamb.node.ReductionType.NOTHING:
stop_reason = StopReason.BETA_NORMAL
break
# Count reductions
- #i += 1
- #if (
- # r.reduction_type == tokens.ReductionType.MACRO_EXPAND or
- # r.reduction_type == tokens.ReductionType.AUTOCHURCH
- # ):
- # macro_expansions += 1
- #else:
i += 1
+ if red_type == lamb.node.ReductionType.FUNCTION_APPLY:
+ macro_expansions += 1
+
if (
- stop_reason == StopReason.BETA_NORMAL or
- stop_reason == StopReason.LOOP_DETECTED
+ stop_reason == StopReason.BETA_NORMAL or
+ stop_reason == StopReason.LOOP_DETECTED
):
- out_str = str(r) # type: ignore
+ out_str = str(new_node) # type: ignore
printf(FormattedText([
("class:result_header", f"\nExit reason: "),
@@ -195,7 +188,15 @@ class Runner:
# If this line is a command, do the command.
elif isinstance(e, Command):
- lamb.commands.run(e, self)
+ if e.name not in lamb.commands.commands:
+ printf(
+ FormattedText([
+ ("class:warn", f"Unknown command \"{e.name}\"")
+ ]),
+ style = lamb.utils.style
+ )
+ else:
+ lamb.commands.commands[e.name](e, self)
# If this line is a plain expression, reduce it.
elif isinstance(e, lamb.node.Node):