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
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
@lamb_command(
command_name = "save",
help_text = "Save macros to a file"
)
else:
commands[command.name](command, runner)
@lamb_command(help_text = "Save macros to a file")
def save(command, runner) -> None:
def cmd_save(command, runner) -> None:
if len(command.args) != 1:
printf(
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
)
@ -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(
"<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
)
@ -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(
"<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
)
@ -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(

View File

@ -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"<func {self.input!r} {self.left!r}>"
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"<call {self.left!r} {self.right!r}>"
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, 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)
reduced = True
break
return ReductionType.FUNCTION_APPLY, out
elif isinstance(n.left, ExpandableEndNode):
if isinstance(n.left, Macro):
n.left = n.left.expand(macro_table = macro_table)
r, n.left = n.left.expand(
macro_table = macro_table
)
else:
n.left = n.left.expand()
reduced = True
break
r, n.left = n.left.expand()
return r, out
return reduced, out
return ReductionType.NOTHING, out

View File

@ -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
):
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):