master
Mark 2022-10-28 16:01:58 -07:00
parent a1d8714f2f
commit affcbc33ee
Signed by: Mark
GPG Key ID: AD62BB059C2AAEE4
3 changed files with 171 additions and 92 deletions

View File

@ -4,7 +4,6 @@ from prompt_toolkit import print_formatted_text as printf
from prompt_toolkit.shortcuts import clear as clear_screen from prompt_toolkit.shortcuts import clear as clear_screen
import os.path import os.path
from pyparsing import exceptions as ppx from pyparsing import exceptions as ppx
import lamb import lamb
@ -12,30 +11,32 @@ import lamb
commands = {} commands = {}
help_texts = {} 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): def inner(func):
commands[func.__name__] = func name = func.__name__ if command_name is None else command_name
help_texts[func.__name__] = help_text
commands[name] = func
help_texts[name] = help_text
return inner 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(
@lamb_command(help_text = "Save macros to a file") command_name = "save",
def save(command, runner) -> None: help_text = "Save macros to a file"
)
def cmd_save(command, runner) -> None:
if len(command.args) != 1: if len(command.args) != 1:
printf( printf(
HTML( HTML(
"<err>Command <cmd_code>:save</cmd_code> takes exactly one argument.</err>" "<err>Command <cmd_code>:{command.name}</cmd_code> takes exactly one argument.</err>"
), ),
style = lamb.utils.style style = lamb.utils.style
) )
@ -72,12 +73,15 @@ def save(command, runner) -> None:
) )
@lamb_command(help_text = "Load macros from a file") @lamb_command(
def load(command, runner): command_name = "load",
help_text = "Load macros from a file"
)
def cmd_load(command, runner):
if len(command.args) != 1: if len(command.args) != 1:
printf( printf(
HTML( HTML(
"<err>Command <cmd_code>:load</cmd_code> takes exactly one argument.</err>" "<err>Command <cmd_code>:{command.name}</cmd_code> takes exactly one argument.</err>"
), ),
style = lamb.utils.style style = lamb.utils.style
) )
@ -134,13 +138,14 @@ def load(command, runner):
) )
@lamb_command(
@lamb_command(help_text = "Delete a macro") help_text = "Delete a macro"
)
def mdel(command, runner) -> None: def mdel(command, runner) -> None:
if len(command.args) != 1: if len(command.args) != 1:
printf( printf(
HTML( HTML(
"<err>Command <cmd_code>:mdel</cmd_code> takes exactly one argument.</err>" "<err>Command <cmd_code>:{command.name}</cmd_code> takes exactly one argument.</err>"
), ),
style = lamb.utils.style style = lamb.utils.style
) )
@ -159,8 +164,9 @@ def mdel(command, runner) -> None:
del runner.macro_table[target] del runner.macro_table[target]
@lamb_command(
@lamb_command(help_text = "Show macros") help_text = "Show macros"
)
def macros(command, runner) -> None: def macros(command, runner) -> None:
printf(FormattedText([ printf(FormattedText([
("class:cmd_h", "\nDefined Macros:\n"), ("class:cmd_h", "\nDefined Macros:\n"),
@ -172,13 +178,17 @@ def macros(command, runner) -> None:
style = lamb.utils.style style = lamb.utils.style
) )
@lamb_command(help_text = "Clear the screen") @lamb_command(
help_text = "Clear the screen"
)
def clear(command, runner) -> None: def clear(command, runner) -> None:
clear_screen() clear_screen()
lamb.utils.show_greeting() lamb.utils.show_greeting()
@lamb_command(help_text = "Print this help") @lamb_command(
help_text = "Print this help"
)
def help(command, runner) -> None: def help(command, runner) -> None:
printf( printf(
HTML( HTML(

View File

@ -5,6 +5,24 @@ class Direction(enum.Enum):
LEFT = enum.auto() LEFT = enum.auto()
RIGHT = 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): class ReductionError(Exception):
""" """
Raised when we encounter an error while reducing. Raised when we encounter an error while reducing.
@ -16,12 +34,26 @@ class ReductionError(Exception):
class TreeWalker: 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): def __init__(self, expr):
self.expr = expr self.expr = expr
self.ptr = expr self.ptr = expr
self.from_side = Direction.UP self.from_side = Direction.UP
def __next__(self): 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: if self.ptr is self.expr.parent:
raise StopIteration raise StopIteration
@ -50,6 +82,10 @@ class TreeWalker:
return out_side, out return out_side, out
class Node: class Node:
"""
Generic class for an element of an expression tree.
"""
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.
@ -67,6 +103,12 @@ class Node:
return TreeWalker(self) return TreeWalker(self)
def _set_parent(self, parent, side): 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): if (parent is not None) and (side is None):
raise Exception("If a node has a parent, it must have a direction.") raise Exception("If a node has a parent, it must have a direction.")
if (parent is None) and (side is not None): if (parent is None) and (side is not None):
@ -97,6 +139,11 @@ class Node:
def set_side(self, side: Direction, 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: if side == Direction.LEFT:
self.left = node self.left = node
elif side == Direction.RIGHT: elif side == Direction.RIGHT:
@ -106,20 +153,47 @@ class Node:
def go_left(self): 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: if self._left is None:
raise Exception("Can't go left when left is None") raise Exception("Can't go left when left is None")
return Direction.UP, self._left return Direction.UP, self._left
def go_right(self): 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: if self._right is None:
raise Exception("Can't go right when right is None") raise Exception("Can't go right when right is None")
return Direction.UP, self._right return Direction.UP, self._right
def go_up(self): 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 return self.parent_side, self.parent
def clone(self): def copy(self):
raise NotImplementedError("Nodes MUST provide a `clone` method!") """
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: def __str__(self) -> str:
return print_node(self) return print_node(self)
@ -135,7 +209,7 @@ class EndNode(Node):
raise NotImplementedError("EndNodes MUST provide a `print_value` method!") raise NotImplementedError("EndNodes MUST provide a `print_value` method!")
class ExpandableEndNode(EndNode): class ExpandableEndNode(EndNode):
def expand(self): def expand(self) -> tuple[ReductionType, Node]:
raise NotImplementedError("ExpandableEndNodes MUST provide an `expand` method!") raise NotImplementedError("ExpandableEndNodes MUST provide an `expand` method!")
class FreeVar(EndNode): class FreeVar(EndNode):
@ -148,7 +222,7 @@ class FreeVar(EndNode):
def print_value(self): def print_value(self):
return f"{self.name}" return f"{self.name}"
def clone(self): def copy(self):
return FreeVar(self.name) return FreeVar(self.name)
class Macro(ExpandableEndNode): class Macro(ExpandableEndNode):
@ -168,14 +242,13 @@ class Macro(ExpandableEndNode):
def print_value(self): def print_value(self):
return self.name return self.name
def expand(self, *, macro_table = {}): def expand(self, *, macro_table = {}) -> tuple[ReductionType, Node]:
if self.name in macro_table: if self.name in macro_table:
return clone(macro_table[self.name]) return ReductionType.MACRO_EXPAND, clone(macro_table[self.name])
else: else:
f = FreeVar(self.name) return ReductionType.MACRO_TO_FREE, FreeVar(self.name)
return f
def clone(self): def copy(self):
return Macro(self.name) return Macro(self.name)
class Church(ExpandableEndNode): class Church(ExpandableEndNode):
@ -195,7 +268,7 @@ class Church(ExpandableEndNode):
def print_value(self): def print_value(self):
return str(self.value) return str(self.value)
def expand(self): def expand(self) -> tuple[ReductionType, Node]:
f = Bound("f") f = Bound("f")
a = Bound("a") a = Bound("a")
chain = a chain = a
@ -203,12 +276,12 @@ class Church(ExpandableEndNode):
for i in range(self.value): for i in range(self.value):
chain = Call(clone(f), clone(chain)) chain = Call(clone(f), clone(chain))
return Func( return (
f, ReductionType.AUTOCHURCH,
Func(a, chain) Func(f, Func(a, chain))
) )
def clone(self): def copy(self):
return Church(self.value) return Church(self.value)
bound_counter = 0 bound_counter = 0
@ -223,7 +296,7 @@ class Bound(EndNode):
else: else:
self.identifier = forced_id self.identifier = forced_id
def clone(self): def copy(self):
return Bound(self.name, forced_id = self.identifier) return Bound(self.name, forced_id = self.identifier)
def __eq__(self, other): def __eq__(self, other):
@ -260,7 +333,7 @@ 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): def copy(self):
return Func(self.input, None) # type: ignore return Func(self.input, None) # type: ignore
class Call(Node): class Call(Node):
@ -292,14 +365,13 @@ 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): def copy(self):
return Call(None, None) # type: ignore return Call(None, None) # type: ignore
def print_node(node: Node) -> str: def print_node(node: Node) -> str:
if not isinstance(node, Node): 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: else:
out = "" out = ""
@ -330,7 +402,7 @@ def clone(node: Node):
if not isinstance(node, Node): 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 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. out_ptr = out # Stays one step behind ptr, in the new tree.
ptr = node ptr = node
from_side = Direction.UP from_side = Direction.UP
@ -347,7 +419,7 @@ def clone(node: Node):
elif isinstance(ptr, Func): 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.clone()) out_ptr.set_side(ptr.parent_side, ptr.copy())
_, out_ptr = out_ptr.go_left() _, out_ptr = out_ptr.go_left()
elif from_side == Direction.LEFT: elif from_side == Direction.LEFT:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
@ -355,12 +427,11 @@ def clone(node: Node):
elif isinstance(ptr, Call): elif isinstance(ptr, Call):
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.clone() out_ptr.set_side(ptr.parent_side, ptr.copy())
)
_, out_ptr = out_ptr.go_left() _, out_ptr = out_ptr.go_left()
elif from_side == Direction.LEFT: elif from_side == Direction.LEFT:
from_side, ptr = ptr.go_right() 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() _, out_ptr = out_ptr.go_right()
elif from_side == Direction.RIGHT: elif from_side == Direction.RIGHT:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
@ -371,7 +442,6 @@ def clone(node: Node):
return out return out
def bind_variables(node: Node, *, ban_macro_name = None) -> None: def bind_variables(node: Node, *, ban_macro_name = None) -> None:
if not isinstance(node, Node): 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 what to do with a {type(node)}")
@ -432,31 +502,29 @@ def call_func(fn: Func, arg: Node):
# Do a single reduction step # 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): if not isinstance(node, Node):
raise TypeError(f"I can't reduce a {type(node)}") raise TypeError(f"I can't reduce a {type(node)}")
reduced = False
out = node out = node
for s, n in out: for s, n in out:
if isinstance(n, Call): if isinstance(n, Call) and (s == Direction.UP):
if s == Direction.UP: if isinstance(n.left, Func):
if isinstance(n.left, Func): if n.parent is None:
if n.parent is None: out = call_func(n.left, n.right)
out = call_func(n.left, n.right) out._set_parent(None, None)
out._set_parent(None, None) else:
else: n.parent.left = call_func(n.left, n.right)
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
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

View File

@ -103,39 +103,32 @@ class Runner:
while (self.reduction_limit is None) or (i < self.reduction_limit): while (self.reduction_limit is None) or (i < self.reduction_limit):
try: try:
w, r = lamb.node.reduce( red_type, new_node = lamb.node.reduce(
node, node,
macro_table = self.macro_table macro_table = self.macro_table
) )
except RecursionError: except RecursionError:
stop_reason = StopReason.RECURSION stop_reason = StopReason.RECURSION
break break
node = r node = new_node
#print(expr)
#self.prompt()
# If we can't reduce this expression anymore, # If we can't reduce this expression anymore,
# it's in beta-normal form. # it's in beta-normal form.
if not w: if red_type == lamb.node.ReductionType.NOTHING:
stop_reason = StopReason.BETA_NORMAL stop_reason = StopReason.BETA_NORMAL
break break
# Count reductions # 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 i += 1
if red_type == lamb.node.ReductionType.FUNCTION_APPLY:
macro_expansions += 1
if ( if (
stop_reason == StopReason.BETA_NORMAL or stop_reason == StopReason.BETA_NORMAL or
stop_reason == StopReason.LOOP_DETECTED stop_reason == StopReason.LOOP_DETECTED
): ):
out_str = str(r) # type: ignore out_str = str(new_node) # type: ignore
printf(FormattedText([ printf(FormattedText([
("class:result_header", f"\nExit reason: "), ("class:result_header", f"\nExit reason: "),
@ -195,7 +188,15 @@ class Runner:
# If this line is a command, do the command. # If this line is a command, do the command.
elif isinstance(e, 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. # If this line is a plain expression, reduce it.
elif isinstance(e, lamb.node.Node): elif isinstance(e, lamb.node.Node):