Renamed package for pypi
This commit is contained in:
6
lamb_engine/__init__.py
Normal file
6
lamb_engine/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
from . import utils
|
||||
from . import nodes
|
||||
from . import parser
|
||||
|
||||
from .runner import Runner
|
||||
from .runner import StopReason
|
61
lamb_engine/__main__.py
Executable file
61
lamb_engine/__main__.py
Executable file
@ -0,0 +1,61 @@
|
||||
if __name__ != "__main__":
|
||||
raise ImportError("lamb_engine.__main__ should never be imported. Run it directly.")
|
||||
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit import print_formatted_text as printf
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
from prompt_toolkit.formatted_text import to_plain_text
|
||||
from pyparsing import exceptions as ppx
|
||||
|
||||
import lamb_engine
|
||||
|
||||
|
||||
lamb_engine.utils.show_greeting()
|
||||
|
||||
|
||||
r = lamb_engine.Runner(
|
||||
prompt_session = PromptSession(
|
||||
style = lamb_engine.utils.style,
|
||||
lexer = lamb_engine.utils.LambdaLexer(),
|
||||
key_bindings = lamb_engine.utils.bindings
|
||||
),
|
||||
prompt_message = FormattedText([
|
||||
("class:prompt", "==> ")
|
||||
])
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
i = r.prompt()
|
||||
|
||||
# Catch Ctrl-C and Ctrl-D
|
||||
except KeyboardInterrupt:
|
||||
printf("\n\nGoodbye.\n")
|
||||
break
|
||||
except EOFError:
|
||||
printf("\n\nGoodbye.\n")
|
||||
break
|
||||
|
||||
# Skip empty lines
|
||||
if i.strip() == "":
|
||||
continue
|
||||
|
||||
# Try to run an input line.
|
||||
# Catch parse errors and point them out.
|
||||
try:
|
||||
x = r.run(i)
|
||||
except ppx.ParseException as e:
|
||||
l = len(to_plain_text(r.prompt_session.message))
|
||||
printf(FormattedText([
|
||||
("class:err", " "*(e.loc + l) + "^\n"),
|
||||
("class:err", f"Syntax error at char {e.loc}."),
|
||||
("class:text", "\n")
|
||||
]), style = lamb_engine.utils.style)
|
||||
continue
|
||||
except lamb_engine.nodes.ReductionError as e:
|
||||
printf(FormattedText([
|
||||
("class:err", f"{e.msg}\n")
|
||||
]), style = lamb_engine.utils.style)
|
||||
continue
|
||||
|
||||
printf("")
|
3
lamb_engine/nodes/__init__.py
Normal file
3
lamb_engine/nodes/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .misc import *
|
||||
from .nodes import *
|
||||
from .functions import *
|
287
lamb_engine/nodes/functions.py
Normal file
287
lamb_engine/nodes/functions.py
Normal file
@ -0,0 +1,287 @@
|
||||
import lamb_engine
|
||||
import lamb_engine.nodes as lbn
|
||||
|
||||
def print_node(node: lbn.Node, *, export: bool = False) -> str:
|
||||
if not isinstance(node, lbn.Node):
|
||||
raise TypeError(f"I don't know how to print a {type(node)}")
|
||||
|
||||
out = ""
|
||||
|
||||
bound_subs = {}
|
||||
|
||||
for s, n in node:
|
||||
if isinstance(n, lbn.EndNode):
|
||||
if isinstance(n, lbn.Bound):
|
||||
out += bound_subs[n.identifier]
|
||||
else:
|
||||
out += n.print_value(export = export)
|
||||
|
||||
elif isinstance(n, lbn.Func):
|
||||
# This should never be true, but
|
||||
# keep this here to silence type checker.
|
||||
if not isinstance(n.input, lbn.Bound):
|
||||
raise Exception("input is macro, something is wrong.")
|
||||
|
||||
if s == lbn.Direction.UP:
|
||||
o = n.input.print_value(export = export)
|
||||
if o in bound_subs.values():
|
||||
i = -1
|
||||
p = o
|
||||
while o in bound_subs.values():
|
||||
o = p + lamb_engine.utils.subscript(i := i + 1)
|
||||
bound_subs[n.input.identifier] = o
|
||||
else:
|
||||
bound_subs[n.input.identifier] = n.input.print_value()
|
||||
|
||||
if isinstance(n.parent, lbn.Call):
|
||||
out += "("
|
||||
|
||||
if isinstance(n.parent, lbn.Func):
|
||||
out += bound_subs[n.input.identifier]
|
||||
else:
|
||||
out += "λ" + bound_subs[n.input.identifier]
|
||||
if not isinstance(n.left, lbn.Func):
|
||||
out += "."
|
||||
|
||||
elif s == lbn.Direction.LEFT:
|
||||
if isinstance(n.parent, lbn.Call):
|
||||
out += ")"
|
||||
del bound_subs[n.input.identifier]
|
||||
|
||||
elif isinstance(n, lbn.Call):
|
||||
if s == lbn.Direction.UP:
|
||||
out += "("
|
||||
elif s == lbn.Direction.LEFT:
|
||||
out += " "
|
||||
elif s == lbn.Direction.RIGHT:
|
||||
out += ")"
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def clone(node: lbn.Node):
|
||||
if not isinstance(node, lbn.Node):
|
||||
raise TypeError(f"I don't know what to do with a {type(node)}")
|
||||
|
||||
macro_map = {}
|
||||
if isinstance(node, lbn.Func):
|
||||
c = node.copy()
|
||||
macro_map[node.input.identifier] = c.input.identifier # type: ignore
|
||||
else:
|
||||
c = node.copy()
|
||||
|
||||
out = c
|
||||
out_ptr = out # Stays one step behind ptr, in the new tree.
|
||||
ptr = node
|
||||
from_side = lbn.Direction.UP
|
||||
|
||||
|
||||
if isinstance(node, lbn.EndNode):
|
||||
return out
|
||||
|
||||
# We're not using a TreeWalker here because
|
||||
# we need more control over our pointer when cloning.
|
||||
while True:
|
||||
if isinstance(ptr, lbn.EndNode):
|
||||
from_side, ptr = ptr.go_up()
|
||||
_, out_ptr = out_ptr.go_up()
|
||||
elif isinstance(ptr, lbn.Func) or isinstance(ptr, lbn.Root):
|
||||
if from_side == lbn.Direction.UP:
|
||||
from_side, ptr = ptr.go_left()
|
||||
|
||||
if isinstance(ptr, lbn.Func):
|
||||
c = ptr.copy()
|
||||
macro_map[ptr.input.identifier] = c.input.identifier # type: ignore
|
||||
elif isinstance(ptr, lbn.Bound):
|
||||
c = ptr.copy()
|
||||
if c.identifier in macro_map:
|
||||
c.identifier = macro_map[c.identifier]
|
||||
else:
|
||||
c = ptr.copy()
|
||||
out_ptr.set_side(ptr.parent_side, c)
|
||||
|
||||
_, out_ptr = out_ptr.go_left()
|
||||
elif from_side == lbn.Direction.LEFT:
|
||||
from_side, ptr = ptr.go_up()
|
||||
_, out_ptr = out_ptr.go_up()
|
||||
elif isinstance(ptr, lbn.Call):
|
||||
if from_side == lbn.Direction.UP:
|
||||
from_side, ptr = ptr.go_left()
|
||||
|
||||
if isinstance(ptr, lbn.Func):
|
||||
c = ptr.copy()
|
||||
macro_map[ptr.input.identifier] = c.input.identifier # type: ignore
|
||||
elif isinstance(ptr, lbn.Bound):
|
||||
c = ptr.copy()
|
||||
if c.identifier in macro_map:
|
||||
c.identifier = macro_map[c.identifier]
|
||||
else:
|
||||
c = ptr.copy()
|
||||
out_ptr.set_side(ptr.parent_side, c)
|
||||
|
||||
_, out_ptr = out_ptr.go_left()
|
||||
elif from_side == lbn.Direction.LEFT:
|
||||
from_side, ptr = ptr.go_right()
|
||||
|
||||
if isinstance(ptr, lbn.Func):
|
||||
c = ptr.copy()
|
||||
macro_map[ptr.input.identifier] = c.input.identifier # type: ignore
|
||||
elif isinstance(ptr, lbn.Bound):
|
||||
c = ptr.copy()
|
||||
if c.identifier in macro_map:
|
||||
c.identifier = macro_map[c.identifier]
|
||||
else:
|
||||
c = ptr.copy()
|
||||
out_ptr.set_side(ptr.parent_side, c)
|
||||
|
||||
_, out_ptr = out_ptr.go_right()
|
||||
elif from_side == lbn.Direction.RIGHT:
|
||||
from_side, ptr = ptr.go_up()
|
||||
_, out_ptr = out_ptr.go_up()
|
||||
|
||||
if ptr is node.parent:
|
||||
break
|
||||
return out
|
||||
|
||||
def prepare(root: lbn.Root, *, ban_macro_name = None) -> list:
|
||||
"""
|
||||
Prepare an expression for expansion.
|
||||
This will does the following:
|
||||
- Binds variables
|
||||
- Turns unbound macros into free variables
|
||||
- Generates warnings
|
||||
"""
|
||||
|
||||
if not isinstance(root, lbn.Root):
|
||||
raise TypeError(f"I don't know what to do with a {type(root)}")
|
||||
|
||||
bound_variables = {}
|
||||
|
||||
warnings = []
|
||||
|
||||
it = iter(root)
|
||||
for s, n in it:
|
||||
if isinstance(n, lbn.History):
|
||||
if root.runner.history[0] == None:
|
||||
raise lbn.ReductionError("There isn't any history to reference.")
|
||||
else:
|
||||
warnings += [
|
||||
("class:code", "$"),
|
||||
("class:warn", " will be expanded to ")
|
||||
] + lamb_engine.utils.lex_str(str(n.expand()[1]))
|
||||
|
||||
# If this expression is part of a macro,
|
||||
# make sure we don't reference it inside itself.
|
||||
elif isinstance(n, lbn.Macro):
|
||||
if (n.name == ban_macro_name) and (ban_macro_name is not None):
|
||||
raise lbn.ReductionError("Macro cannot reference self")
|
||||
|
||||
# Bind variables
|
||||
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:
|
||||
warnings += [
|
||||
("class:warn", "Name "),
|
||||
("class:code", n.name),
|
||||
("class:warn", " is a free variable\n"),
|
||||
]
|
||||
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, lbn.Func):
|
||||
if s == lbn.Direction.UP:
|
||||
# Add this function's input to the table of bound variables.
|
||||
# If it is already there, raise an error.
|
||||
if (n.input.name in bound_variables):
|
||||
raise lbn.ReductionError(f"Bound variable name conflict: \"{n.input.name}\"")
|
||||
else:
|
||||
bound_variables[n.input.name] = lbn.Bound(
|
||||
lamb_engine.utils.remove_sub(n.input.name),
|
||||
macro_name = n.input.name
|
||||
)
|
||||
n.input = bound_variables[n.input.name]
|
||||
|
||||
elif s == lbn.Direction.LEFT:
|
||||
del bound_variables[n.input.macro_name] # type: ignore
|
||||
|
||||
return warnings
|
||||
|
||||
# Apply a function.
|
||||
# Returns the function's output.
|
||||
def call_func(fn: lbn.Func, arg: lbn.Node):
|
||||
for s, n in fn:
|
||||
if isinstance(n, lbn.Bound) and (s == lbn.Direction.UP):
|
||||
if n == fn.input:
|
||||
if n.parent is None:
|
||||
raise Exception("Tried to substitute a None bound variable.")
|
||||
|
||||
n.parent.set_side(n.parent_side, clone(arg)) # type: ignore
|
||||
return fn.left
|
||||
|
||||
# Do a single reduction step
|
||||
def reduce(root: lbn.Root) -> tuple[lbn.ReductionType, lbn.Root]:
|
||||
if not isinstance(root, lbn.Root):
|
||||
raise TypeError(f"I can't reduce a {type(root)}")
|
||||
|
||||
out = root
|
||||
for s, n in out:
|
||||
if isinstance(n, lbn.Call) and (s == lbn.Direction.UP):
|
||||
if isinstance(n.left, lbn.Func):
|
||||
n.parent.set_side(
|
||||
n.parent_side, # type: ignore
|
||||
call_func(n.left, n.right)
|
||||
)
|
||||
|
||||
return lbn.ReductionType.FUNCTION_APPLY, out
|
||||
|
||||
elif isinstance(n.left, lbn.ExpandableEndNode):
|
||||
r, n.left = n.left.expand()
|
||||
return r, out
|
||||
return lbn.ReductionType.NOTHING, out
|
||||
|
||||
|
||||
def expand(root: lbn.Root, *, force_all = False) -> tuple[int, lbn.Root]:
|
||||
"""
|
||||
Expands expandable nodes in the given tree.
|
||||
|
||||
If force_all is false, this only expands
|
||||
ExpandableEndnodes that have "always_expand" set to True.
|
||||
|
||||
If force_all is True, this expands ALL
|
||||
ExpandableEndnodes.
|
||||
"""
|
||||
|
||||
if not isinstance(root, lbn.Root):
|
||||
raise TypeError(f"I don't know what to do with a {type(root)}")
|
||||
|
||||
out = root
|
||||
macro_expansions = 0
|
||||
|
||||
it = iter(root)
|
||||
for s, n in it:
|
||||
if (
|
||||
isinstance(n, lbn.ExpandableEndNode) and
|
||||
(force_all or n.always_expand)
|
||||
):
|
||||
|
||||
n.parent.set_side(
|
||||
n.parent_side, # type: ignore
|
||||
n.expand()[1]
|
||||
)
|
||||
it.ptr = n.parent.get_side(
|
||||
n.parent_side # type: ignore
|
||||
)
|
||||
macro_expansions += 1
|
||||
return macro_expansions, out
|
44
lamb_engine/nodes/misc.py
Normal file
44
lamb_engine/nodes/misc.py
Normal file
@ -0,0 +1,44 @@
|
||||
import enum
|
||||
|
||||
class Direction(enum.Enum):
|
||||
UP = enum.auto()
|
||||
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 expanded a history reference
|
||||
HIST_EXPAND = enum.auto()
|
||||
|
||||
# We turned a church numeral into an expression
|
||||
AUTOCHURCH = enum.auto()
|
||||
|
||||
# We applied a function.
|
||||
# This is the only type of "formal" reduction step.
|
||||
FUNCTION_APPLY = enum.auto()
|
||||
|
||||
# Pretty, short names for each reduction type.
|
||||
# These should all have the same length.
|
||||
reduction_text = {
|
||||
ReductionType.NOTHING: "N",
|
||||
ReductionType.MACRO_EXPAND: "M",
|
||||
ReductionType.HIST_EXPAND: "H",
|
||||
ReductionType.AUTOCHURCH: "C",
|
||||
ReductionType.FUNCTION_APPLY: "F",
|
||||
}
|
||||
|
||||
class ReductionError(Exception):
|
||||
"""
|
||||
Raised when we encounter an error while reducing.
|
||||
|
||||
These should be caught and elegantly presented to the user.
|
||||
"""
|
||||
def __init__(self, msg: str):
|
||||
self.msg = msg
|
452
lamb_engine/nodes/nodes.py
Normal file
452
lamb_engine/nodes/nodes.py
Normal file
@ -0,0 +1,452 @@
|
||||
import lamb_engine
|
||||
import lamb_engine.nodes as lbn
|
||||
|
||||
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.first_step = True
|
||||
self.from_side = lbn.Direction.UP
|
||||
|
||||
def __iter__(self):
|
||||
return 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.first_step:
|
||||
self.first_step = False
|
||||
return self.from_side, self.ptr
|
||||
|
||||
if isinstance(self.ptr, Root):
|
||||
if self.from_side == lbn.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()
|
||||
elif isinstance(self.ptr, Func):
|
||||
if self.from_side == lbn.Direction.UP:
|
||||
self.from_side, self.ptr = self.ptr.go_left()
|
||||
elif self.from_side == lbn.Direction.LEFT:
|
||||
self.from_side, self.ptr = self.ptr.go_up()
|
||||
elif isinstance(self.ptr, Call):
|
||||
if self.from_side == lbn.Direction.UP:
|
||||
self.from_side, self.ptr = self.ptr.go_left()
|
||||
elif self.from_side == lbn.Direction.LEFT:
|
||||
self.from_side, self.ptr = self.ptr.go_right()
|
||||
elif self.from_side == lbn.Direction.RIGHT:
|
||||
self.from_side, self.ptr = self.ptr.go_up()
|
||||
else:
|
||||
raise TypeError(f"I don't know how to iterate a {type(self.ptr)}")
|
||||
|
||||
# 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:
|
||||
"""
|
||||
Generic class for an element of an expression tree.
|
||||
All nodes are subclasses of this.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# The node this one is connected to.
|
||||
# None if this is the top objects.
|
||||
self.parent: Node = None # type: ignore
|
||||
|
||||
# What direction this is relative to the parent.
|
||||
# Left of Right.
|
||||
self.parent_side: Direction = None # type: ignore
|
||||
|
||||
# Left and right nodes, None if empty
|
||||
self._left: Node | None = None
|
||||
self._right: Node | None = None
|
||||
|
||||
# The runner this node is attached to.
|
||||
# Set by Node.set_runner()
|
||||
self.runner: lamb_engine.runner.Runner = None # type: ignore
|
||||
|
||||
def __iter__(self):
|
||||
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 lbn.direction.")
|
||||
if (parent is None) and (side is not None):
|
||||
raise Exception("If a node has no parent, it cannot have a lbn.direction.")
|
||||
self.parent = parent
|
||||
self.parent_side = side
|
||||
return self
|
||||
|
||||
@property
|
||||
def left(self):
|
||||
return self._left
|
||||
|
||||
@left.setter
|
||||
def left(self, node):
|
||||
if node is not None:
|
||||
node._set_parent(self, lbn.Direction.LEFT)
|
||||
self._left = node
|
||||
|
||||
@property
|
||||
def right(self):
|
||||
return self._right
|
||||
|
||||
@right.setter
|
||||
def right(self, node):
|
||||
if node is not None:
|
||||
node._set_parent(self, lbn.Direction.RIGHT)
|
||||
self._right = node
|
||||
|
||||
|
||||
def set_side(self, side: lbn.Direction, node):
|
||||
"""
|
||||
A wrapper around Node.left and Node.right that
|
||||
automatically selects a side.
|
||||
"""
|
||||
|
||||
if side == lbn.Direction.LEFT:
|
||||
self.left = node
|
||||
elif side == lbn.Direction.RIGHT:
|
||||
self.right = node
|
||||
else:
|
||||
raise TypeError("Can only set left or right side.")
|
||||
|
||||
def get_side(self, side: lbn.Direction):
|
||||
if side == lbn.Direction.LEFT:
|
||||
return self.left
|
||||
elif side == lbn.Direction.RIGHT:
|
||||
return self.right
|
||||
else:
|
||||
raise TypeError("Can only get left or right side.")
|
||||
|
||||
|
||||
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 lbn.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 lbn.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 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 lbn.print_node(self)
|
||||
|
||||
def export(self) -> str:
|
||||
"""
|
||||
Convert this tree to a parsable string.
|
||||
"""
|
||||
return lbn.print_node(self, export = True)
|
||||
|
||||
def set_runner(self, runner):
|
||||
for s, n in self:
|
||||
if s == lbn.Direction.UP:
|
||||
n.runner = runner # type: ignore
|
||||
return self
|
||||
|
||||
class EndNode(Node):
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
raise NotImplementedError("EndNodes MUST provide a `print_value` method!")
|
||||
|
||||
class ExpandableEndNode(EndNode):
|
||||
always_expand = False
|
||||
def expand(self) -> tuple[lbn.ReductionType, Node]:
|
||||
raise NotImplementedError("ExpandableEndNodes MUST provide an `expand` method!")
|
||||
|
||||
class FreeVar(EndNode):
|
||||
def __init__(self, name: str, *, runner = None):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<freevar {self.name}>"
|
||||
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
if export:
|
||||
return f"{self.name}'"
|
||||
else:
|
||||
return f"{self.name}'"
|
||||
|
||||
def copy(self):
|
||||
return FreeVar(self.name)
|
||||
|
||||
class Macro(ExpandableEndNode):
|
||||
@staticmethod
|
||||
def from_parse(results):
|
||||
return Macro(results[0])
|
||||
|
||||
def __init__(self, name: str, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.left = None
|
||||
self.right = None
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<macro {self.name}>"
|
||||
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
return self.name
|
||||
|
||||
def expand(self) -> tuple[lbn.ReductionType, Node]:
|
||||
if self.name in self.runner.macro_table:
|
||||
# The element in the macro table will be a Root node,
|
||||
# so we clone its left element.
|
||||
return (
|
||||
lbn.ReductionType.MACRO_EXPAND,
|
||||
lbn.clone(self.runner.macro_table[self.name].left)
|
||||
)
|
||||
else:
|
||||
raise Exception(f"Macro {self.name} is not defined")
|
||||
|
||||
def to_freevar(self):
|
||||
return FreeVar(self.name, runner = self.runner)
|
||||
|
||||
def copy(self):
|
||||
return Macro(self.name, runner = self.runner)
|
||||
|
||||
class Church(ExpandableEndNode):
|
||||
@staticmethod
|
||||
def from_parse(results):
|
||||
return Church(int(results[0]))
|
||||
|
||||
def __init__(self, value: int, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.value = value
|
||||
self.left = None
|
||||
self.right = None
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<church {self.value}>"
|
||||
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
return str(self.value)
|
||||
|
||||
def expand(self) -> tuple[lbn.ReductionType, Node]:
|
||||
f = Bound("f")
|
||||
a = Bound("a")
|
||||
chain = a
|
||||
|
||||
for i in range(self.value):
|
||||
chain = Call(lbn.clone(f), lbn.clone(chain))
|
||||
|
||||
return (
|
||||
lbn.ReductionType.AUTOCHURCH,
|
||||
Func(f, Func(a, chain)).set_runner(self.runner)
|
||||
)
|
||||
|
||||
def copy(self):
|
||||
return Church(self.value, runner = self.runner)
|
||||
|
||||
class History(ExpandableEndNode):
|
||||
always_expand = True
|
||||
|
||||
@staticmethod
|
||||
def from_parse(results):
|
||||
return History()
|
||||
|
||||
def __init__(self, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.left = None
|
||||
self.right = None
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<$>"
|
||||
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
return "$"
|
||||
|
||||
def expand(self) -> tuple[lbn.ReductionType, Node]:
|
||||
# We shouldn't ever get here, prepare()
|
||||
# catches empty history.
|
||||
if self.runner.history[0] == None:
|
||||
raise Exception(f"Tried to expand empty history.")
|
||||
# .left is VERY important!
|
||||
# self.runner.history will contain Root nodes,
|
||||
# and we don't want those *inside* our tree.
|
||||
return lbn.ReductionType.HIST_EXPAND, lbn.clone(self.runner.history[0].left)
|
||||
|
||||
def copy(self):
|
||||
return History(runner = self.runner)
|
||||
|
||||
bound_counter = 0
|
||||
class Bound(EndNode):
|
||||
def __init__(self, name: str, *, forced_id = None, runner = None, macro_name = None):
|
||||
self.name = name
|
||||
global bound_counter
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
# The name of the macro this bound came from.
|
||||
# Always equal to self.name, unless the macro
|
||||
# this came from had a subscript.
|
||||
self.macro_name: str | None = macro_name
|
||||
|
||||
if forced_id is None:
|
||||
self.identifier = bound_counter
|
||||
bound_counter += 1
|
||||
else:
|
||||
self.identifier = forced_id
|
||||
|
||||
def copy(self):
|
||||
return Bound(
|
||||
self.name,
|
||||
forced_id = self.identifier,
|
||||
runner = self.runner
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Bound):
|
||||
raise TypeError(f"Cannot compare bound_variable with {type(other)}")
|
||||
return self.identifier == other.identifier
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.name} {self.identifier}>"
|
||||
|
||||
def print_value(self, *, export: bool = False) -> str:
|
||||
return self.name
|
||||
|
||||
class Func(Node):
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
if len(result[0]) == 1:
|
||||
return Func(
|
||||
result[0][0],
|
||||
result[1]
|
||||
)
|
||||
else:
|
||||
return Func(
|
||||
result[0].pop(0),
|
||||
Func.from_parse(result)
|
||||
)
|
||||
|
||||
def __init__(self, input: Macro | Bound, output: Node, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.input: Macro | Bound = input
|
||||
self.left: Node = output
|
||||
self.right: None = None
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<func {self.input!r} {self.left!r}>"
|
||||
|
||||
def copy(self):
|
||||
return Func(
|
||||
Bound(
|
||||
self.input.name,
|
||||
runner = self.runner
|
||||
),
|
||||
None, # type: ignore
|
||||
runner = self.runner
|
||||
)
|
||||
|
||||
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):
|
||||
@staticmethod
|
||||
def from_parse(results):
|
||||
if len(results) == 2:
|
||||
return Call(
|
||||
results[0],
|
||||
results[1]
|
||||
)
|
||||
else:
|
||||
this = Call(
|
||||
results[0],
|
||||
results[1]
|
||||
)
|
||||
|
||||
return Call.from_parse(
|
||||
[Call(
|
||||
results[0],
|
||||
results[1]
|
||||
)] + results[2:]
|
||||
)
|
||||
|
||||
def __init__(self, fn: Node, arg: Node, *, runner = None) -> None:
|
||||
super().__init__()
|
||||
self.left: Node = fn
|
||||
self.right: Node = arg
|
||||
self.runner = runner # type: ignore
|
||||
|
||||
def __repr__(self):
|
||||
return f"<call {self.left!r} {self.right!r}>"
|
||||
|
||||
def copy(self):
|
||||
return Call(None, None, runner = self.runner) # type: ignore
|
101
lamb_engine/parser.py
Executable file
101
lamb_engine/parser.py
Executable file
@ -0,0 +1,101 @@
|
||||
import pyparsing as pp
|
||||
|
||||
# Packrat gives a significant speed boost.
|
||||
pp.ParserElement.enablePackrat()
|
||||
|
||||
class LambdaParser:
|
||||
def make_parser(self):
|
||||
self.lp = pp.Suppress("(")
|
||||
self.rp = pp.Suppress(")")
|
||||
self.pp_expr = pp.Forward()
|
||||
|
||||
# Bound variables are ALWAYS lowercase and single-character.
|
||||
# We still create macro objects from them, they are turned into
|
||||
# bound variables after the expression is built.
|
||||
self.pp_macro = pp.Word(pp.alphas + "_")
|
||||
self.pp_bound = pp.Regex("[a-z][₀₁₂₃₄₅₆₈₉]*")
|
||||
self.pp_name = self.pp_bound ^ self.pp_macro
|
||||
self.pp_church = pp.Word(pp.nums)
|
||||
self.pp_history = pp.Char("$")
|
||||
|
||||
# Function calls.
|
||||
#
|
||||
# <exp> <exp>
|
||||
# <exp> <exp> <exp>
|
||||
self.pp_call = pp.Forward()
|
||||
self.pp_call <<= (self.pp_expr | self.pp_bound | self.pp_history)[2, ...]
|
||||
|
||||
# Function definitions, right associative.
|
||||
# Function args MUST be lowercase.
|
||||
#
|
||||
# <var> => <exp>
|
||||
self.pp_lambda_fun = (
|
||||
(pp.Suppress("λ") | pp.Suppress("\\")) +
|
||||
pp.Group(self.pp_bound[1, ...]) +
|
||||
pp.Suppress(".") +
|
||||
(self.pp_expr ^ self.pp_call)
|
||||
)
|
||||
|
||||
# Assignment.
|
||||
# Can only be found at the start of a line.
|
||||
#
|
||||
# <name> = <exp>
|
||||
self.pp_macro_def = (
|
||||
pp.line_start() +
|
||||
self.pp_macro +
|
||||
pp.Suppress("=") +
|
||||
(self.pp_expr ^ self.pp_call ^ self.pp_history)
|
||||
)
|
||||
|
||||
self.pp_expr <<= (
|
||||
self.pp_church ^
|
||||
self.pp_lambda_fun ^
|
||||
self.pp_name ^
|
||||
(self.lp + self.pp_expr + self.rp) ^
|
||||
(self.lp + self.pp_call + self.rp) ^
|
||||
(self.lp + self.pp_history + self.rp)
|
||||
)
|
||||
|
||||
self.pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_") + pp.Word(pp.alphas + pp.nums + "_.")[0, ...]
|
||||
|
||||
|
||||
self.pp_all = (
|
||||
self.pp_expr ^
|
||||
self.pp_macro_def ^
|
||||
self.pp_command ^
|
||||
self.pp_call ^
|
||||
self.pp_history
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
action_command,
|
||||
action_macro_def,
|
||||
action_church,
|
||||
action_func,
|
||||
action_bound,
|
||||
action_macro,
|
||||
action_call,
|
||||
action_history
|
||||
):
|
||||
|
||||
self.make_parser()
|
||||
|
||||
self.pp_command.set_parse_action(action_command)
|
||||
self.pp_macro_def.set_parse_action(action_macro_def)
|
||||
self.pp_church.set_parse_action(action_church)
|
||||
self.pp_lambda_fun.set_parse_action(action_func)
|
||||
self.pp_macro.set_parse_action(action_macro)
|
||||
self.pp_bound.set_parse_action(action_bound)
|
||||
self.pp_call.set_parse_action(action_call)
|
||||
self.pp_history.set_parse_action(action_history)
|
||||
|
||||
def parse_line(self, line: str):
|
||||
return self.pp_all.parse_string(
|
||||
line,
|
||||
parse_all = True
|
||||
)[0]
|
||||
|
||||
def run_tests(self, lines: list[str]):
|
||||
return self.pp_all.run_tests(lines)
|
2
lamb_engine/runner/__init__.py
Normal file
2
lamb_engine/runner/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from .runner import Runner
|
||||
from .runner import StopReason
|
420
lamb_engine/runner/commands.py
Normal file
420
lamb_engine/runner/commands.py
Normal file
@ -0,0 +1,420 @@
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit import print_formatted_text as printf
|
||||
from prompt_toolkit.shortcuts import clear as clear_screen
|
||||
from prompt_toolkit import prompt
|
||||
|
||||
import os.path
|
||||
from pyparsing import exceptions as ppx
|
||||
|
||||
import lamb_engine
|
||||
|
||||
commands = {}
|
||||
help_texts = {}
|
||||
|
||||
def lamb_command(
|
||||
*,
|
||||
command_name: str | None = None,
|
||||
help_text: str
|
||||
):
|
||||
"""
|
||||
A decorator that allows us to easily make commands
|
||||
"""
|
||||
|
||||
def inner(func):
|
||||
name = func.__name__ if command_name is None else command_name
|
||||
|
||||
commands[name] = func
|
||||
help_texts[name] = help_text
|
||||
return inner
|
||||
|
||||
@lamb_command(
|
||||
command_name = "step",
|
||||
help_text = "Toggle step-by-step reduction"
|
||||
)
|
||||
def cmd_step(command, runner) -> None:
|
||||
if len(command.args) > 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes no more than one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = not runner.step_reduction
|
||||
if len(command.args) == 1:
|
||||
if command.args[0].lower() in ("y", "yes"):
|
||||
target = True
|
||||
elif command.args[0].lower() in ("n", "no"):
|
||||
target = False
|
||||
else:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Usage: <code>:step [yes|no]</code></err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
if target:
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Enabled step-by-step reduction.</warn>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
runner.step_reduction = True
|
||||
else:
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Disabled step-by-step reduction.</warn>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
runner.step_reduction = False
|
||||
|
||||
@lamb_command(
|
||||
command_name = "expand",
|
||||
help_text = "Toggle full expansion"
|
||||
)
|
||||
def cmd_expand(command, runner) -> None:
|
||||
if len(command.args) > 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes no more than one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = not runner.full_expansion
|
||||
if len(command.args) == 1:
|
||||
if command.args[0].lower() in ("y", "yes"):
|
||||
target = True
|
||||
elif command.args[0].lower() in ("n", "no"):
|
||||
target = False
|
||||
else:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Usage: <code>:expand [yes|no]</code></err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
if target:
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Enabled complete expansion.</warn>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
runner.full_expansion = True
|
||||
else:
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Disabled complete expansion.</warn>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
runner.full_expansion = False
|
||||
|
||||
|
||||
@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(
|
||||
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = command.args[0]
|
||||
if os.path.exists(target):
|
||||
confirm = prompt(
|
||||
message = FormattedText([
|
||||
("class:warn", "File exists. Overwrite? "),
|
||||
("class:text", "[yes/no]: ")
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
).lower()
|
||||
|
||||
if confirm != "yes":
|
||||
printf(
|
||||
HTML(
|
||||
"<err>Cancelled.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
with open(target, "w") as f:
|
||||
f.write("\n".join(
|
||||
[f"{n} = {e.export()}" for n, e in runner.macro_table.items()]
|
||||
))
|
||||
|
||||
printf(
|
||||
HTML(
|
||||
f"Wrote {len(runner.macro_table)} macros to <code>{target}</code>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
|
||||
|
||||
@lamb_command(
|
||||
command_name = "load",
|
||||
help_text = "Load macros from a file"
|
||||
)
|
||||
def cmd_load(command, runner):
|
||||
if len(command.args) != 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = command.args[0]
|
||||
if not os.path.exists(target):
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>File {target} doesn't exist.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
with open(target, "r") as f:
|
||||
lines = [x.strip() for x in f.readlines()]
|
||||
|
||||
for i in range(len(lines)):
|
||||
l = lines[i].strip()
|
||||
|
||||
# Skip comments and empty lines
|
||||
if l.startswith("#"):
|
||||
continue
|
||||
if l == "":
|
||||
continue
|
||||
|
||||
try:
|
||||
x = runner.parse(l)[0]
|
||||
except ppx.ParseException as e:
|
||||
printf(
|
||||
FormattedText([
|
||||
("class:warn", f"Syntax error on line {i+1:02}: "),
|
||||
("class:code", l[:e.loc]),
|
||||
("class:err", l[e.loc]),
|
||||
("class:code", l[e.loc+1:])
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
if not isinstance(x, lamb_engine.runner.runner.MacroDef):
|
||||
printf(
|
||||
FormattedText([
|
||||
("class:warn", f"Skipping line {i+1:02}: "),
|
||||
("class:code", l),
|
||||
("class:warn", f" is not a macro definition.")
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
runner.save_macro(x, silent = True)
|
||||
|
||||
printf(
|
||||
FormattedText([
|
||||
("class:ok", f"Loaded {x.label}: ")
|
||||
] + lamb_engine.utils.lex_str(str(x.expr))),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Delete a macro"
|
||||
)
|
||||
def mdel(command, runner) -> None:
|
||||
if len(command.args) != 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = command.args[0]
|
||||
if target not in runner.macro_table:
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Macro \"{target}\" is not defined</warn>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
del runner.macro_table[target]
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Delete all macros"
|
||||
)
|
||||
def delmac(command, runner) -> None:
|
||||
confirm = prompt(
|
||||
message = FormattedText([
|
||||
("class:warn", "Are you sure? "),
|
||||
("class:text", "[yes/no]: ")
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
).lower()
|
||||
|
||||
if confirm != "yes":
|
||||
printf(
|
||||
HTML(
|
||||
"<err>Cancelled.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
runner.macro_table = {}
|
||||
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Show macros"
|
||||
)
|
||||
def macros(command, runner) -> None:
|
||||
if len(runner.macro_table) == 0:
|
||||
printf(FormattedText([
|
||||
("class:warn", "No macros are defined."),
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
else:
|
||||
printf(FormattedText([
|
||||
("class:cmd_h", "\nDefined Macros:\n"),
|
||||
] +
|
||||
[
|
||||
("class:text", f"\t{name} \t {exp}\n")
|
||||
for name, exp in runner.macro_table.items()
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Clear the screen"
|
||||
)
|
||||
def clear(command, runner) -> None:
|
||||
clear_screen()
|
||||
lamb_engine.utils.show_greeting()
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Get or set reduction limit"
|
||||
)
|
||||
def rlimit(command, runner) -> None:
|
||||
if len(command.args) == 0:
|
||||
if runner.reduction_limit is None:
|
||||
printf(
|
||||
HTML(
|
||||
"<ok>No reduction limit is set</ok>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
else:
|
||||
printf(
|
||||
HTML(
|
||||
f"<ok>Reduction limit is {runner.reduction_limit:,}</ok>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
elif len(command.args) != 1:
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
t = command.args[0]
|
||||
if t.lower() == "none":
|
||||
runner.reduction_limit = None
|
||||
printf(
|
||||
HTML(
|
||||
f"<ok>Removed reduction limit</ok>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
t = int(t)
|
||||
except ValueError:
|
||||
printf(
|
||||
HTML(
|
||||
"<err>Reduction limit must be a positive integer or \"none\".</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
if 50 > t:
|
||||
printf(
|
||||
HTML(
|
||||
"<err>Reduction limit must be at least 50.</err>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
return
|
||||
|
||||
runner.reduction_limit = t
|
||||
printf(
|
||||
HTML(
|
||||
f"<ok>Set reduction limit to {t:,}</ok>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
|
||||
|
||||
|
||||
@lamb_command(
|
||||
help_text = "Print this help"
|
||||
)
|
||||
def help(command, runner) -> None:
|
||||
printf(
|
||||
HTML(
|
||||
"\n<text>" +
|
||||
|
||||
"<cmd_h>Usage:</cmd_h>" +
|
||||
"\n" +
|
||||
"\tWrite lambda expressions using your <cmd_key>\\</cmd_key> key." +
|
||||
"\n" +
|
||||
"\tMacros can be defined using <cmd_key>=</cmd_key>, as in <code>T = λab.a</code>" +
|
||||
"\n" +
|
||||
"\tRun commands using <cmd_key>:</cmd_key>, for example <code>:help</code>" +
|
||||
"\n" +
|
||||
"\tHistory can be accessed with <cmd_key>$</cmd_key>, which will expand to the result of the last successful reduction." +
|
||||
"\n\n" +
|
||||
"<cmd_h>Commands:</cmd_h>"+
|
||||
"\n" +
|
||||
"\n".join([
|
||||
f"\t<code>{name}</code> \t {text}"
|
||||
for name, text in help_texts.items()
|
||||
]) +
|
||||
"\n\n"
|
||||
"<muted>Detailed documentation can be found on this project's git page.</muted>" +
|
||||
"</text>"
|
||||
),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
42
lamb_engine/runner/misc.py
Normal file
42
lamb_engine/runner/misc.py
Normal file
@ -0,0 +1,42 @@
|
||||
import enum
|
||||
import lamb_engine
|
||||
|
||||
class StopReason(enum.Enum):
|
||||
BETA_NORMAL = ("class:text", "β-normal form")
|
||||
LOOP_DETECTED = ("class:warn", "Loop detected")
|
||||
MAX_EXCEEDED = ("class:err", "Too many reductions")
|
||||
INTERRUPT = ("class:warn", "User interrupt")
|
||||
SHOW_MACRO = ("class:text", "Displaying macro content")
|
||||
|
||||
class MacroDef:
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
return MacroDef(
|
||||
result[0].name,
|
||||
result[1]
|
||||
)
|
||||
|
||||
def __init__(self, label: str, expr: lamb_engine.nodes.Node):
|
||||
self.label = label
|
||||
self.expr = expr
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.label} := {self.expr!r}>"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.label} := {self.expr}"
|
||||
|
||||
def set_runner(self, runner):
|
||||
return self.expr.set_runner(runner)
|
||||
|
||||
class Command:
|
||||
@staticmethod
|
||||
def from_parse(result):
|
||||
return Command(
|
||||
result[0],
|
||||
result[1:]
|
||||
)
|
||||
|
||||
def __init__(self, name, args):
|
||||
self.name = name
|
||||
self.args = args
|
294
lamb_engine/runner/runner.py
Normal file
294
lamb_engine/runner/runner.py
Normal file
@ -0,0 +1,294 @@
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.formatted_text import FormattedText
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit import prompt
|
||||
from prompt_toolkit import print_formatted_text as printf
|
||||
import collections
|
||||
import math
|
||||
import time
|
||||
|
||||
import lamb_engine
|
||||
|
||||
from lamb_engine.runner.misc import MacroDef
|
||||
from lamb_engine.runner.misc import Command
|
||||
from lamb_engine.runner.misc import StopReason
|
||||
from lamb_engine.runner import commands as cmd
|
||||
|
||||
|
||||
# Keybindings for step prompt.
|
||||
# Prevents any text from being input.
|
||||
step_bindings = KeyBindings()
|
||||
@step_bindings.add("<any>")
|
||||
def _(event):
|
||||
pass
|
||||
|
||||
|
||||
class Runner:
|
||||
def __init__(
|
||||
self,
|
||||
prompt_session: PromptSession,
|
||||
prompt_message
|
||||
):
|
||||
self.macro_table = {}
|
||||
self.prompt_session = prompt_session
|
||||
self.prompt_message = prompt_message
|
||||
self.parser = lamb_engine.parser.LambdaParser(
|
||||
action_func = lamb_engine.nodes.Func.from_parse,
|
||||
action_bound = lamb_engine.nodes.Macro.from_parse,
|
||||
action_macro = lamb_engine.nodes.Macro.from_parse,
|
||||
action_call = lamb_engine.nodes.Call.from_parse,
|
||||
action_church = lamb_engine.nodes.Church.from_parse,
|
||||
action_macro_def = MacroDef.from_parse,
|
||||
action_command = Command.from_parse,
|
||||
action_history = lamb_engine.nodes.History.from_parse
|
||||
)
|
||||
|
||||
# Maximum amount of reductions.
|
||||
# If None, no maximum is enforced.
|
||||
# Must be at least 1.
|
||||
self.reduction_limit: int | None = 1_000_000
|
||||
|
||||
# Ensure bound variables are unique.
|
||||
# This is automatically incremented whenever we make
|
||||
# a bound variable.
|
||||
self.bound_variable_counter = 0
|
||||
|
||||
# Update iteration after this many iterations
|
||||
# Make sure every place value has a non-zero digit
|
||||
# so that all digits appear to be changing.
|
||||
self.iter_update = 231
|
||||
|
||||
self.history = collections.deque(
|
||||
[None] * 10,
|
||||
10)
|
||||
|
||||
|
||||
# If true, reduce step-by-step.
|
||||
self.step_reduction = False
|
||||
|
||||
# If true, expand ALL macros when printing output
|
||||
self.full_expansion = False
|
||||
|
||||
def prompt(self):
|
||||
return self.prompt_session.prompt(
|
||||
message = self.prompt_message
|
||||
)
|
||||
|
||||
def parse(self, line) -> tuple[lamb_engine.nodes.Root | MacroDef | Command, list]:
|
||||
e = self.parser.parse_line(line)
|
||||
|
||||
w = []
|
||||
if isinstance(e, MacroDef):
|
||||
e.expr = lamb_engine.nodes.Root(e.expr)
|
||||
e.set_runner(self)
|
||||
w = lamb_engine.nodes.prepare(e.expr, ban_macro_name = e.label)
|
||||
elif isinstance(e, lamb_engine.nodes.Node):
|
||||
e = lamb_engine.nodes.Root(e)
|
||||
e.set_runner(self)
|
||||
w = lamb_engine.nodes.prepare(e)
|
||||
|
||||
return e, w
|
||||
|
||||
|
||||
def reduce(self, node: lamb_engine.nodes.Root, *, warnings = []) -> None:
|
||||
|
||||
# Reduction Counter.
|
||||
# We also count macro (and church) expansions,
|
||||
# and subtract those from the final count.
|
||||
k = 0
|
||||
macro_expansions = 0
|
||||
|
||||
stop_reason = StopReason.MAX_EXCEEDED
|
||||
start_time = time.time()
|
||||
out_text = []
|
||||
|
||||
only_macro = (
|
||||
isinstance(node.left, lamb_engine.nodes.Macro) or
|
||||
isinstance(node.left, lamb_engine.nodes.Church)
|
||||
)
|
||||
if only_macro:
|
||||
stop_reason = StopReason.SHOW_MACRO
|
||||
m, node = lamb_engine.nodes.expand(node, force_all = only_macro)
|
||||
macro_expansions += m
|
||||
|
||||
if len(warnings) != 0:
|
||||
printf(FormattedText(warnings), style = lamb_engine.utils.style)
|
||||
|
||||
if self.step_reduction:
|
||||
printf(FormattedText([
|
||||
("class:warn", "Step-by-step reduction is enabled.\n"),
|
||||
("class:muted", "Press "),
|
||||
("class:cmd_key", "ctrl-c"),
|
||||
("class:muted", " to continue automatically.\n"),
|
||||
("class:muted", "Press "),
|
||||
("class:cmd_key", "enter"),
|
||||
("class:muted", " to step.\n"),
|
||||
]), style = lamb_engine.utils.style)
|
||||
|
||||
|
||||
skip_to_end = False
|
||||
while (
|
||||
(
|
||||
(self.reduction_limit is None) or
|
||||
(k < self.reduction_limit)
|
||||
) and not only_macro
|
||||
):
|
||||
|
||||
# Show reduction count
|
||||
if (
|
||||
( (k >= self.iter_update) and (k % self.iter_update == 0) )
|
||||
and not (self.step_reduction and not skip_to_end)
|
||||
):
|
||||
print(f" Reducing... {k:,}", end = "\r")
|
||||
|
||||
try:
|
||||
red_type, node = lamb_engine.nodes.reduce(node)
|
||||
except KeyboardInterrupt:
|
||||
stop_reason = StopReason.INTERRUPT
|
||||
break
|
||||
|
||||
# If we can't reduce this expression anymore,
|
||||
# it's in beta-normal form.
|
||||
if red_type == lamb_engine.nodes.ReductionType.NOTHING:
|
||||
stop_reason = StopReason.BETA_NORMAL
|
||||
break
|
||||
|
||||
# Count reductions
|
||||
k += 1
|
||||
if red_type == lamb_engine.nodes.ReductionType.FUNCTION_APPLY:
|
||||
macro_expansions += 1
|
||||
|
||||
# Pause after step if necessary
|
||||
if self.step_reduction and not skip_to_end:
|
||||
try:
|
||||
s = prompt(
|
||||
message = FormattedText([
|
||||
("class:prompt", lamb_engine.nodes.reduction_text[red_type]),
|
||||
("class:prompt", f":{k:03} ")
|
||||
] + lamb_engine.utils.lex_str(str(node))),
|
||||
style = lamb_engine.utils.style,
|
||||
key_bindings = step_bindings
|
||||
)
|
||||
except KeyboardInterrupt or EOFError:
|
||||
skip_to_end = True
|
||||
printf(FormattedText([
|
||||
("class:warn", "Skipping to end."),
|
||||
]), style = lamb_engine.utils.style)
|
||||
|
||||
# Print a space between step messages
|
||||
if self.step_reduction:
|
||||
print("")
|
||||
|
||||
# Clear reduction counter if it was printed
|
||||
if k >= self.iter_update:
|
||||
print(" " * round(14 + math.log10(k)), end = "\r")
|
||||
|
||||
# Expand fully if necessary
|
||||
if self.full_expansion:
|
||||
o, node = lamb_engine.nodes.expand(node, force_all = True)
|
||||
macro_expansions += o
|
||||
|
||||
if only_macro:
|
||||
out_text += [
|
||||
("class:ok", f"Displaying macro content")
|
||||
]
|
||||
|
||||
else:
|
||||
out_text += [
|
||||
("class:ok", f"Runtime: "),
|
||||
("class:text", f"{time.time() - start_time:.03f} seconds"),
|
||||
|
||||
("class:ok", f"\nExit reason: "),
|
||||
stop_reason.value,
|
||||
|
||||
("class:ok", f"\nMacro expansions: "),
|
||||
("class:text", f"{macro_expansions:,}"),
|
||||
|
||||
("class:ok", f"\nReductions: "),
|
||||
("class:text", f"{k:,}\t"),
|
||||
("class:muted", f"(Limit: {self.reduction_limit:,})")
|
||||
]
|
||||
|
||||
if self.full_expansion:
|
||||
out_text += [
|
||||
("class:ok", "\nAll macros have been expanded")
|
||||
]
|
||||
|
||||
if (
|
||||
stop_reason == StopReason.BETA_NORMAL or
|
||||
stop_reason == StopReason.LOOP_DETECTED or
|
||||
only_macro
|
||||
):
|
||||
out_text += [
|
||||
("class:ok", "\n\n => ")
|
||||
] + lamb_engine.utils.lex_str(str(node))
|
||||
|
||||
|
||||
printf(
|
||||
FormattedText(out_text),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
|
||||
# Save to history
|
||||
# Do this at the end so we don't always fully expand.
|
||||
self.history.appendleft(
|
||||
lamb_engine.nodes.expand( # type: ignore
|
||||
node,
|
||||
force_all = True
|
||||
)[1]
|
||||
)
|
||||
|
||||
def save_macro(
|
||||
self,
|
||||
macro: MacroDef,
|
||||
*,
|
||||
silent = False
|
||||
) -> None:
|
||||
was_rewritten = macro.label in self.macro_table
|
||||
self.macro_table[macro.label] = macro.expr
|
||||
|
||||
if not silent:
|
||||
printf(FormattedText([
|
||||
("class:text", "Set "),
|
||||
("class:code", macro.label),
|
||||
("class:text", " to "),
|
||||
("class:code", str(macro.expr))
|
||||
]), style = lamb_engine.utils.style)
|
||||
|
||||
# Apply a list of definitions
|
||||
def run(
|
||||
self,
|
||||
line: str,
|
||||
*,
|
||||
silent = False
|
||||
) -> None:
|
||||
e, w = self.parse(line)
|
||||
|
||||
# If this line is a macro definition, save the macro.
|
||||
if isinstance(e, MacroDef):
|
||||
self.save_macro(e, silent = silent)
|
||||
|
||||
# If this line is a command, do the command.
|
||||
elif isinstance(e, Command):
|
||||
if e.name not in cmd.commands:
|
||||
printf(
|
||||
FormattedText([
|
||||
("class:warn", f"Unknown command \"{e.name}\"")
|
||||
]),
|
||||
style = lamb_engine.utils.style
|
||||
)
|
||||
else:
|
||||
cmd.commands[e.name](e, self)
|
||||
|
||||
# If this line is a plain expression, reduce it.
|
||||
elif isinstance(e, lamb_engine.nodes.Node):
|
||||
self.reduce(e, warnings = w)
|
||||
|
||||
# We shouldn't ever get here.
|
||||
else:
|
||||
raise TypeError(f"I don't know what to do with a {type(e)}")
|
||||
|
||||
|
||||
def run_lines(self, lines: list[str]):
|
||||
for l in lines:
|
||||
self.run(l, silent = True)
|
165
lamb_engine/utils.py
Normal file
165
lamb_engine/utils.py
Normal file
@ -0,0 +1,165 @@
|
||||
from prompt_toolkit.styles import Style
|
||||
from prompt_toolkit.formatted_text import HTML
|
||||
from prompt_toolkit.lexers import Lexer
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit import print_formatted_text as printf
|
||||
from importlib.metadata import version
|
||||
from prompt_toolkit.document import Document
|
||||
|
||||
import re
|
||||
|
||||
|
||||
style = Style.from_dict({ # type: ignore
|
||||
# Basic formatting
|
||||
"text": "#FFFFFF",
|
||||
"warn": "#FFA700",
|
||||
"err": "#FF3809",
|
||||
"prompt": "#05CFFF",
|
||||
"ok": "#00EF7C",
|
||||
"code": "#AAAAAA italic",
|
||||
"muted": "#AAAAAA",
|
||||
|
||||
# Syntax highlighting colors
|
||||
"syn_cmd": "#FFFFFF italic",
|
||||
"syn_lambda": "#AAAAAA",
|
||||
"syn_paren": "#AAAAAA",
|
||||
|
||||
# Command formatting
|
||||
# cmd_h: section titles
|
||||
# cmd_key: keyboard keys, usually one character
|
||||
"cmd_h": "#FF3809 bold",
|
||||
"cmd_key": "#00EF7C bold",
|
||||
|
||||
# Only used in greeting
|
||||
"_v": "#00EF7C bold",
|
||||
"_l": "#FF3809 bold",
|
||||
"_s": "#00EF7C bold",
|
||||
"_p": "#AAAAAA"
|
||||
})
|
||||
|
||||
|
||||
# Replace "\" with pretty "λ"s
|
||||
bindings = KeyBindings()
|
||||
@bindings.add("\\")
|
||||
def _(event):
|
||||
event.current_buffer.insert_text("λ")
|
||||
|
||||
# Simple lexer for highlighting.
|
||||
# Improve this later.
|
||||
class LambdaLexer(Lexer):
|
||||
def lex_document(self, document):
|
||||
def inner(line_no):
|
||||
out = []
|
||||
tmp_str = []
|
||||
d = str(document.lines[line_no])
|
||||
|
||||
if d.startswith(":"):
|
||||
return [
|
||||
("class:syn_cmd", d)
|
||||
]
|
||||
|
||||
for c in d:
|
||||
if c in "\\λ.":
|
||||
if len(tmp_str) != 0:
|
||||
out.append(("class:text", "".join(tmp_str)))
|
||||
out.append(("class:syn_lambda", c))
|
||||
tmp_str = []
|
||||
elif c in "()":
|
||||
if len(tmp_str) != 0:
|
||||
out.append(("class:text", "".join(tmp_str)))
|
||||
out.append(("class:syn_paren", c))
|
||||
tmp_str = []
|
||||
else:
|
||||
tmp_str.append(c)
|
||||
|
||||
if len(tmp_str) != 0:
|
||||
out.append(("class:text", "".join(tmp_str)))
|
||||
return out
|
||||
return inner
|
||||
|
||||
|
||||
|
||||
def lex_str(s: str) -> list[tuple[str, str]]:
|
||||
return LambdaLexer().lex_document(Document(s))(0)
|
||||
|
||||
def show_greeting():
|
||||
# | _.._ _.|_
|
||||
# |_(_|| | ||_)
|
||||
# 0.0.0
|
||||
#
|
||||
# __ __
|
||||
# ,-` `` `,
|
||||
# (` \ )
|
||||
# (` \ `)
|
||||
# (, / \ _)
|
||||
# (` / \ )
|
||||
# `'._.--._.'
|
||||
#
|
||||
# A λ calculus engine
|
||||
|
||||
printf(HTML("\n".join([
|
||||
"",
|
||||
"<_h> | _.._ _.|_",
|
||||
" |_(_|| | ||_)</_h>",
|
||||
f" <_v>{version('lamb')}</_v>",
|
||||
" __ __",
|
||||
" ,-` `` `,",
|
||||
" (` <_l>\\</_l> )",
|
||||
" (` <_l>\\</_l> `)",
|
||||
" (, <_l>/ \\</_l> _)",
|
||||
" (` <_l>/ \\</_l> )",
|
||||
" `'._.--._.'",
|
||||
"",
|
||||
"<_s> A λ calculus engine</_s>",
|
||||
"<_p> Type :help for help</_p>",
|
||||
""
|
||||
])), style = style)
|
||||
|
||||
def remove_sub(s: str):
|
||||
return re.sub("[₀₁₂₃₄₅₆₈₉]*", "", s)
|
||||
|
||||
def base4(n: int):
|
||||
if n == 0:
|
||||
return [0]
|
||||
digits = []
|
||||
while n:
|
||||
digits.append(n % 4)
|
||||
n //= 4
|
||||
return digits[::-1]
|
||||
|
||||
def subscript(num: int):
|
||||
|
||||
# unicode subscripts ₀₁₂₃ and ₄₅₆₈₉
|
||||
# usually look different,
|
||||
# so we'll use base 4.
|
||||
qb = base4(num)
|
||||
|
||||
sub = {
|
||||
"0": "₀",
|
||||
"1": "₁",
|
||||
"2": "₂",
|
||||
"3": "₃",
|
||||
"4": "₄",
|
||||
"5": "₅",
|
||||
"6": "₆",
|
||||
"7": "₇",
|
||||
"8": "₈",
|
||||
"9": "₉"
|
||||
}
|
||||
|
||||
sup = {
|
||||
"0": "⁰",
|
||||
"1": "¹",
|
||||
"2": "²",
|
||||
"3": "³",
|
||||
"4": "⁴",
|
||||
"5": "⁵",
|
||||
"6": "⁶",
|
||||
"7": "⁷",
|
||||
"8": "⁸",
|
||||
"9": "⁹"
|
||||
}
|
||||
|
||||
return "".join(
|
||||
[sub[str(x)] for x in qb]
|
||||
)
|
Reference in New Issue
Block a user