Compare commits

..

39 Commits

Author SHA1 Message Date
5bff77e4a7 Merge branch 'master' of https://github.com/rm-dr/lamb 2023-11-27 14:23:00 -08:00
8c8ea69890 Update README.md 2023-11-27 14:21:13 -08:00
6d14333e52 Update README.md 2023-11-27 14:19:24 -08:00
ab9057148a Merge branch 'master' of ssh://git.betalupi.com:33/Mark/lamb 2023-10-17 18:37:38 -07:00
c07edf1b50 Updated README.md 2023-10-16 21:17:27 -07:00
1bb0b91e20 Fixed a link in README 2023-10-16 15:31:49 -07:00
b20604de5c Fixed a path 2023-10-10 22:08:07 -07:00
d2f8b1c8fb Updated version 2023-04-02 20:54:26 -07:00
3022c2ffc0 Removed type hints to support older python versions 2023-04-02 20:52:59 -07:00
acbc247e10 Merge branch 'master' of ssh://git.betalupi.com:33/Mark/lamb 2023-04-02 07:56:55 -07:00
907d2d9e79 Added links to readme 2023-04-02 07:55:37 -07:00
73f4c60c06 Updated install instructions 2023-03-30 20:29:29 -07:00
fe7e6fca13 Added social media banner 2023-01-09 17:36:49 -08:00
866cb64485 Updated links 2023-01-09 17:15:52 -08:00
09a389857a Hide time when reducing by steps 2022-11-12 19:31:12 -08:00
da997b80c7 Added demo script 2022-11-12 19:15:08 -08:00
1b951813f4 Fixed command parser 2022-11-12 19:14:58 -08:00
12d6176f63 Updated README 2022-11-12 18:56:29 -08:00
787dbd9091 Added vhs tape 2022-11-12 18:16:35 -08:00
67b2332e1c Removed screenshot 2022-11-12 18:16:23 -08:00
115b7289e7 Updated README 2022-11-12 18:07:04 -08:00
69bf43f295 Updated README 2022-11-12 18:02:19 -08:00
495c947441 More reliable ctrl-c 2022-11-12 17:47:41 -08:00
b26d968884 Notes 2022-11-11 18:32:40 -08:00
e8bd6997b9 Added support for arguments 2022-11-11 18:31:37 -08:00
97aecb01f0 Fixed build files 2022-11-11 18:19:38 -08:00
45493c1093 Renamed package for pypi 2022-11-11 17:20:59 -08:00
a30bd7b870 Updated screenshot 2022-11-11 17:11:09 -08:00
f3d02721ad Version & README 2022-11-11 17:07:45 -08:00
48e7d405dd Improved history & highlighting 2022-11-11 17:04:05 -08:00
78cc118bfc Added :expand 2022-11-11 15:45:22 -08:00
dfbc5f3704 Added :step and fixed cloning bug 2022-11-11 15:30:21 -08:00
bd13b10f76 Added subscript parsing 2022-11-10 09:08:58 -08:00
0c215a5df4 Fixed print method 2022-11-10 08:02:28 -08:00
39d32a9925 Updated README 2022-11-09 21:54:10 -08:00
89bc4bd2c0 Minor warning cleanup 2022-11-09 21:47:55 -08:00
062a4b4d1b Updated README 2022-11-09 21:44:58 -08:00
4e3d5fa341 Fixed a macro bug 2022-11-09 21:44:32 -08:00
04ce18c891 Updated README 2022-11-07 20:18:33 -08:00
24 changed files with 975 additions and 490 deletions

6
.gitignore vendored
View File

@ -1,7 +1,13 @@
# Python dev files
venv venv
__pycache__ __pycache__
# Python build files
*.egg-info *.egg-info
*.spec *.spec
build build
dist dist
# Misc
*.gif
misc/secrets.sh

View File

@ -1,12 +1,14 @@
{ {
"cSpell.words": [ "cSpell.words": [
"appendleft",
"autochurch", "autochurch",
"clearmacros", "delmac",
"Endnodes", "Endnodes",
"freevar", "freevar",
"mdel", "mdel",
"onefile", "onefile",
"Packrat", "Packrat",
"printables",
"pyparsing", "pyparsing",
"rlimit", "rlimit",
"runstatus", "runstatus",

View File

@ -1,26 +1,43 @@
# Lamb: A Lambda Calculus Engine # 🐑 Lamb: A Lambda Calculus Engine
![Lamb screenshot](./misc/screenshot.png) ![Lamb Demo](https://github.com/rm-dr/lamb/assets/96270320/d518e344-e7c8-47ed-89c4-7ce273bf4e2d)
## Installation
### Method 1: PyPi (not yet)
1. Put this on PyPi ## :brain: What is lambda calculus?
2. Write these instructions
- [video 1](https://www.youtube.com/watch?v=3VQ382QG-y4): Introduction and boolean logic. The first few minutes are a bit confusing, but it starts to make sense at about [`6:50`](https://youtu.be/3VQ382QG-y4?t=400)
- [video 2](https://www.youtube.com/watch?v=pAnLQ9jwN-E): Continuation of video 1. Features combinators and numerals.
- [blog](https://www.driverlesscrocodile.com/technology/lambda-calculus-for-people-a-step-behind-me-1): Another introduction. Moves slower than the two videos above and doesn't assume CS knowledge. Four-part series.
- [handout](https://static.betalupi.com/ormc/Advanced/Lambda%20Calculus.pdf): A handout I've written on lambda calculus.
## :package: Installation
### Method 1: [PyPi](https://pypi.org/project/lamb-engine)
1. *(Optional but recommended)* make and enter a [venv](https://docs.python.org/3/library/venv.html)
- **On Windows, run the following in cmd or powershell:**
- `cd Desktop`
- `python -m venv lamb`
- `.\Scripts\activate`
2. `pip install lamb-engine`
3. `lamb`
### Method 2: Git ### Method 2: Git
1. Clone this repository. 1. Clone this repository.
2. Make and enter a [virtual environment](https://docs.python.org/3/library/venv.html). 2. Make and enter a [virtual environment](https://docs.python.org/3/library/venv.html).
3. ``cd`` into this directory 3. ``cd`` into this directory
4. Run ``pip install .`` 4. Run ``pip install .``
5. Run ``python .`` 5. Run ``lamb``
------------------------------------------------- -------------------------------------------------
## Usage ## 📖 Usage
Type lambda expressions into the prompt, and Lamb will evaluate them. \ Type expressions into the prompt, and Lamb will evaluate them. \
Use your `\` (backslash) key to type a `λ`. \ Use your `\` (backslash) key to type a `λ`. \
To define macros, use `=`. For example, To define macros, use `=`. For example,
``` ```
@ -43,30 +60,46 @@ Numbers will automatically be converted to Church numerals. For example, the fol
If an expression takes too long to evaluate, you may interrupt reduction with `Ctrl-C`. \ If an expression takes too long to evaluate, you may interrupt reduction with `Ctrl-C`. \
Exit the prompt with `Ctrl-C` or `Ctrl-D`. Exit the prompt with `Ctrl-C` or `Ctrl-D`.
There are many useful macros in [macros.lamb](./macros.lamb). Load them with the `:load` command: There are many useful macros in [macros.lamb](./macros.lamb). Download the file, then load them with the `:load` command:
``` ```
==> :load macros.lamb ==> :load macros.lamb
``` ```
You can also pass files to lamb directly to have them loaded at startup:
```
lamb file1 file2
```
Use your up/down arrows to recall history.
Have fun! Have fun!
------------------------------------------------- -------------------------------------------------
## Commands ## :card_file_box: Commands
Lamb understands many commands. Prefix them with a `:` in the prompt. Lamb understands many commands. Prefix them with a `:` in the prompt.
`:help` Prints a help message `:help` Print a help message
`:clear` Clear the screen `:clear` Clear the screen
`:rlimit [int | None]` Set maximum reduction limit. `:rlimit none` sets no limit. `:rlimit [int | None]` Set maximum reduction limit. `:rlimit none` sets no limit.
`:macros` List macros in the current environment. `:macros` List macros.
`:mdel [macro]` Delete a macro `:mdel [macro]` Delete a macro
`:clearmacros` Delete all macros `:delmac` Delete all macros
`:step [yes | no]` Enable or disable step-by-step reduction. Toggle if no argument is given. When reducing by steps, the prompt tells you what kind of reduction was done last:
- `M`: Macro expansion
- `C`: Church expansion
- `H`: History expansion
- `F`: Function application
`:expand [yes | no]` Enable or disable full expansion. Toggle if no argument is given. If full expansion is enabled, ALL macros will be expanded when printing output.
`:save [filename]` \ `:save [filename]` \
`:load [filename]` \ `:load [filename]` \
@ -75,36 +108,12 @@ The lines in a file look exactly the same as regular entries in the prompt, but
------------------------------------------------- -------------------------------------------------
## Internals ## Todo:
- Prevent macro-chaining recursion
Lamb treats each λ expression as a binary tree. Variable binding and reduction are all simple operations on that tree. All this magic happens in [`nodes.py`](./lamb/nodes.py).
**Highlights:**
- `TreeWalker` is the iterator we (usually) use to traverse our tree. It walks the "perimeter" of the tree, visiting some nodes multiple times.
- `Node` is the base class for all nodes. Any node has `.left` and `.right` elements, which may be `None` (empty). `Node`s also reference their parent and their direction relative to their parent, to make tree traversal easy.
- Before any reduction is done, variables are bound via `bind_variables`. This prevents accidental conflicts common in many lambda parsers.
-------------------------------------------------
## Todo (pre-release, in this order):
- Cleanup warnings - Cleanup warnings
- Truncate long expressions in warnings - Truncate long expressions in warnings
- Prevent macro-chaining recursion
- Full-reduce option (expand all macros)
- step-by-step reduction
- Update screenshot
- Update documentation & "internals" section.
- PyPi package
## Todo:
- History queue + command indexing
- Show history command
- Better class mutation: when is a node no longer valid?
- Loop detection - Loop detection
- $\alpha$-equivalence check - Unchurch command: make church numerals human-readable
- Command-line options (load a file, run a set of commands) - Better syntax highlighting
- Unchurch macro: make church numerals human-readable - Tab-complete file names and commands
- Syntax highlighting: parenthesis, bound variables, macros, etc - Load default macros without manually downloading `macros.lamb` (via `requests`, maybe?)
- Tests

View File

@ -1,10 +0,0 @@
import PyInstaller.__main__
# Run this file to build a standalone executable.
# pyinstaller does not build cross-platform.
PyInstaller.__main__.run([
"lamb/__main__.py",
"--onefile",
"--console"
])

View File

@ -1,61 +0,0 @@
if __name__ != "__main__":
raise ImportError("lamb.__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
lamb.utils.show_greeting()
r = lamb.Runner(
prompt_session = PromptSession(
style = lamb.utils.style,
lexer = lamb.utils.LambdaLexer(),
key_bindings = lamb.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.utils.style)
continue
except lamb.nodes.ReductionError as e:
printf(FormattedText([
("class:err", f"{e.msg}\n")
]), style = lamb.utils.style)
continue
printf("")

View File

@ -1,242 +0,0 @@
from prompt_toolkit import PromptSession
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit import print_formatted_text as printf
import enum
import math
import time
import lamb
from lamb.runner.misc import MacroDef
from lamb.runner.misc import Command
from lamb.runner.misc import StopReason
from lamb.runner import commands as cmd
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.parser.LambdaParser(
action_func = lamb.nodes.Func.from_parse,
action_bound = lamb.nodes.Macro.from_parse,
action_macro = lamb.nodes.Macro.from_parse,
action_call = lamb.nodes.Call.from_parse,
action_church = lamb.nodes.Church.from_parse,
action_macro_def = MacroDef.from_parse,
action_command = Command.from_parse,
action_history = lamb.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: list[lamb.nodes.Root] = []
def prompt(self):
return self.prompt_session.prompt(
message = self.prompt_message
)
def parse(self, line) -> tuple[lamb.nodes.Root | MacroDef | Command, dict]:
e = self.parser.parse_line(line)
o = {}
if isinstance(e, MacroDef):
e.expr = lamb.nodes.Root(e.expr)
e.set_runner(self)
o = lamb.nodes.prepare(e.expr, ban_macro_name = e.label)
elif isinstance(e, lamb.nodes.Node):
e = lamb.nodes.Root(e)
e.set_runner(self)
o = lamb.nodes.prepare(e)
return e, o
def reduce(self, node: lamb.nodes.Root, *, status = {}) -> None:
warning_text = []
# 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 = []
if status["has_history"] and len(self.history) != 0:
warning_text += [
("class:code", "$"),
("class:warn", " will be expanded to "),
("class:code", str(self.history[-1])),
("class:warn", "\n")
]
only_macro = (
isinstance(node.left, lamb.nodes.Macro) or
isinstance(node.left, lamb.nodes.Church)
)
if only_macro:
stop_reason = StopReason.SHOW_MACRO
m, node = lamb.nodes.expand(node, force_all = only_macro)
macro_expansions += m
for i in status["free_variables"]:
warning_text += [
("class:warn", "Name "),
("class:code", i),
("class:warn", " is a free variable\n"),
]
if len(warning_text) != 0:
printf(FormattedText(warning_text), style = lamb.utils.style)
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):
print(f" Reducing... {k:,}", end = "\r")
try:
red_type, node = lamb.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.nodes.ReductionType.NOTHING:
stop_reason = StopReason.BETA_NORMAL
break
# Count reductions
k += 1
if red_type == lamb.nodes.ReductionType.FUNCTION_APPLY:
macro_expansions += 1
if k >= self.iter_update:
# Clear reduction counter if it was printed
print(" " * round(14 + math.log10(k)), end = "\r")
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 (
stop_reason == StopReason.BETA_NORMAL or
stop_reason == StopReason.LOOP_DETECTED or
only_macro
):
out_text += [
("class:ok", "\n\n => "),
("class:text", str(node)), # type: ignore
]
self.history.append(lamb.nodes.expand(node, force_all = True)[1])
printf(
FormattedText(out_text),
style = lamb.utils.style
)
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.utils.style)
# Apply a list of definitions
def run(
self,
line: str,
*,
silent = False
) -> None:
e, o = 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.utils.style
)
else:
cmd.commands[e.name](e, self)
# If this line is a plain expression, reduce it.
elif isinstance(e, lamb.nodes.Node):
self.reduce(e, status = o)
# 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)

View File

@ -4,3 +4,5 @@ from . import parser
from .runner import Runner from .runner import Runner
from .runner import StopReason from .runner import StopReason
from .__main__ import main

79
lamb_engine/__main__.py Executable file
View File

@ -0,0 +1,79 @@
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 sys
import lamb_engine
def main():
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", "==> ")
])
)
# Load files passed as arguments
if len(sys.argv) > 1:
for i in range(1, len(sys.argv)):
try:
printf(FormattedText([
("class:warn", "\nLoading file "),
("class:code", sys.argv[i]),
]), style = lamb_engine.utils.style)
r.run(":load " + sys.argv[i])
except:
printf(FormattedText([
("class:err", "Error. Does this file exist?"),
]), style = lamb_engine.utils.style)
print("")
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("")
if __name__ == "__main__":
main()

View File

@ -1,5 +1,5 @@
import lamb import lamb_engine
import lamb.nodes as lbn import lamb_engine.nodes as lbn
def print_node(node: lbn.Node, *, export: bool = False) -> str: def print_node(node: lbn.Node, *, export: bool = False) -> str:
if not isinstance(node, lbn.Node): if not isinstance(node, lbn.Node):
@ -12,35 +12,41 @@ def print_node(node: lbn.Node, *, export: bool = False) -> str:
for s, n in node: for s, n in node:
if isinstance(n, lbn.EndNode): if isinstance(n, lbn.EndNode):
if isinstance(n, lbn.Bound): if isinstance(n, lbn.Bound):
if n.identifier not in bound_subs.keys():
o = n.print_value(export = export)
if o in bound_subs.items():
i = 1
while o in bound_subs.items():
o = lamb.utils.subscript(i := i + 1)
bound_subs[n.identifier] = o
else:
bound_subs[n.identifier] = n.print_value()
out += bound_subs[n.identifier] out += bound_subs[n.identifier]
else: else:
out += n.print_value(export = export) out += n.print_value(export = export)
elif isinstance(n, lbn.Func): 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: 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): if isinstance(n.parent, lbn.Call):
out += "(" out += "("
if isinstance(n.parent, lbn.Func): if isinstance(n.parent, lbn.Func):
out += n.input.name out += bound_subs[n.input.identifier]
else: else:
out += "λ" + n.input.name out += "λ" + bound_subs[n.input.identifier]
if not isinstance(n.left, lbn.Func): if not isinstance(n.left, lbn.Func):
out += "." out += "."
elif s == lbn.Direction.LEFT: elif s == lbn.Direction.LEFT:
if isinstance(n.parent, lbn.Call): if isinstance(n.parent, lbn.Call):
out += ")" out += ")"
del bound_subs[n.input.identifier]
elif isinstance(n, lbn.Call): elif isinstance(n, lbn.Call):
if s == lbn.Direction.UP: if s == lbn.Direction.UP:
@ -52,15 +58,24 @@ def print_node(node: lbn.Node, *, export: bool = False) -> str:
return out return out
def clone(node: lbn.Node): def clone(node: lbn.Node):
if not isinstance(node, lbn.Node): if not isinstance(node, lbn.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.copy() 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. out_ptr = out # Stays one step behind ptr, in the new tree.
ptr = node ptr = node
from_side = lbn.Direction.UP from_side = lbn.Direction.UP
if isinstance(node, lbn.EndNode): if isinstance(node, lbn.EndNode):
return out return out
@ -73,7 +88,18 @@ def clone(node: lbn.Node):
elif isinstance(ptr, lbn.Func) or isinstance(ptr, lbn.Root): elif isinstance(ptr, lbn.Func) or isinstance(ptr, lbn.Root):
if from_side == lbn.Direction.UP: if from_side == lbn.Direction.UP:
from_side, ptr = ptr.go_left() from_side, ptr = ptr.go_left()
out_ptr.set_side(ptr.parent_side, ptr.copy())
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() _, out_ptr = out_ptr.go_left()
elif from_side == lbn.Direction.LEFT: elif from_side == lbn.Direction.LEFT:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
@ -81,11 +107,33 @@ def clone(node: lbn.Node):
elif isinstance(ptr, lbn.Call): elif isinstance(ptr, lbn.Call):
if from_side == lbn.Direction.UP: if from_side == lbn.Direction.UP:
from_side, ptr = ptr.go_left() from_side, ptr = ptr.go_left()
out_ptr.set_side(ptr.parent_side, ptr.copy())
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() _, out_ptr = out_ptr.go_left()
elif from_side == lbn.Direction.LEFT: elif from_side == lbn.Direction.LEFT:
from_side, ptr = ptr.go_right() from_side, ptr = ptr.go_right()
out_ptr.set_side(ptr.parent_side, ptr.copy())
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() _, out_ptr = out_ptr.go_right()
elif from_side == lbn.Direction.RIGHT: elif from_side == lbn.Direction.RIGHT:
from_side, ptr = ptr.go_up() from_side, ptr = ptr.go_up()
@ -95,7 +143,7 @@ def clone(node: lbn.Node):
break break
return out return out
def prepare(root: lbn.Root, *, ban_macro_name = None) -> dict: def prepare(root: lbn.Root, *, ban_macro_name = None) -> list:
""" """
Prepare an expression for expansion. Prepare an expression for expansion.
This will does the following: This will does the following:
@ -109,15 +157,18 @@ def prepare(root: lbn.Root, *, ban_macro_name = None) -> dict:
bound_variables = {} bound_variables = {}
output = { warnings = []
"has_history": False,
"free_variables": set()
}
it = iter(root) it = iter(root)
for s, n in it: for s, n in it:
if isinstance(n, lbn.History): if isinstance(n, lbn.History):
output["has_history"] = True 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, # If this expression is part of a macro,
# make sure we don't reference it inside itself. # make sure we don't reference it inside itself.
@ -135,7 +186,11 @@ def prepare(root: lbn.Root, *, ban_macro_name = None) -> dict:
# Turn undefined macros into free variables # Turn undefined macros into free variables
elif n.name not in root.runner.macro_table: elif n.name not in root.runner.macro_table:
output["free_variables"].add(n.name) warnings += [
("class:warn", "Name "),
("class:code", n.name),
("class:warn", " is a free variable\n"),
]
n.parent.set_side( n.parent.set_side(
n.parent_side, n.parent_side,
n.to_freevar() n.to_freevar()
@ -152,13 +207,16 @@ def prepare(root: lbn.Root, *, ban_macro_name = None) -> dict:
if (n.input.name in bound_variables): if (n.input.name in bound_variables):
raise lbn.ReductionError(f"Bound variable name conflict: \"{n.input.name}\"") raise lbn.ReductionError(f"Bound variable name conflict: \"{n.input.name}\"")
else: else:
bound_variables[n.input.name] = lbn.Bound(n.input.name) 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] n.input = bound_variables[n.input.name]
elif s == lbn.Direction.LEFT: elif s == lbn.Direction.LEFT:
del bound_variables[n.input.name] del bound_variables[n.input.macro_name] # type: ignore
return output return warnings
# Apply a function. # Apply a function.
# Returns the function's output. # Returns the function's output.

View File

@ -5,6 +5,7 @@ class Direction(enum.Enum):
LEFT = enum.auto() LEFT = enum.auto()
RIGHT = enum.auto() RIGHT = enum.auto()
class ReductionType(enum.Enum): class ReductionType(enum.Enum):
# Nothing happened. This implies that # Nothing happened. This implies that
# an expression cannot be reduced further. # an expression cannot be reduced further.
@ -23,6 +24,16 @@ class ReductionType(enum.Enum):
# This is the only type of "formal" reduction step. # This is the only type of "formal" reduction step.
FUNCTION_APPLY = enum.auto() FUNCTION_APPLY = enum.auto()
# 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): class ReductionError(Exception):
""" """
Raised when we encounter an error while reducing. Raised when we encounter an error while reducing.

View File

@ -1,5 +1,5 @@
import lamb import lamb_engine
import lamb.nodes as lbn import lamb_engine.nodes as lbn
class TreeWalker: class TreeWalker:
""" """
@ -77,12 +77,12 @@ class Node:
self.parent_side: Direction = None # type: ignore self.parent_side: Direction = None # type: ignore
# Left and right nodes, None if empty # Left and right nodes, None if empty
self._left: Node | None = None self._left = None
self._right: Node | None = None self._right = None
# The runner this node is attached to. # The runner this node is attached to.
# Set by Node.set_runner() # Set by Node.set_runner()
self.runner: lamb.runner.Runner = None # type: ignore self.runner: lamb_engine.runner.Runner = None # type: ignore
def __iter__(self): def __iter__(self):
return TreeWalker(self) return TreeWalker(self)
@ -319,23 +319,30 @@ class History(ExpandableEndNode):
return "$" return "$"
def expand(self) -> tuple[lbn.ReductionType, Node]: def expand(self) -> tuple[lbn.ReductionType, Node]:
if len(self.runner.history) == 0: # We shouldn't ever get here, prepare()
raise lbn.ReductionError(f"There isn't any history to reference.") # catches empty history.
if self.runner.history[0] == None:
raise Exception(f"Tried to expand empty history.")
# .left is VERY important! # .left is VERY important!
# self.runner.history will contain Root nodes, # self.runner.history will contain Root nodes,
# and we don't want those *inside* our tree. # and we don't want those *inside* our tree.
return lbn.ReductionType.HIST_EXPAND, lbn.clone(self.runner.history[-1].left) return lbn.ReductionType.HIST_EXPAND, lbn.clone(self.runner.history[0].left)
def copy(self): def copy(self):
return History(runner = self.runner) return History(runner = self.runner)
bound_counter = 0 bound_counter = 0
class Bound(EndNode): class Bound(EndNode):
def __init__(self, name: str, *, forced_id = None, runner = None): def __init__(self, name: str, *, forced_id = None, runner = None, macro_name = None):
self.name = name self.name = name
global bound_counter global bound_counter
self.runner = runner # type: ignore 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 = macro_name
if forced_id is None: if forced_id is None:
self.identifier = bound_counter self.identifier = bound_counter
bound_counter += 1 bound_counter += 1
@ -343,7 +350,11 @@ class Bound(EndNode):
self.identifier = forced_id self.identifier = forced_id
def copy(self): def copy(self):
return Bound(self.name, forced_id = self.identifier, runner = self.runner) return Bound(
self.name,
forced_id = self.identifier,
runner = self.runner
)
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, Bound): if not isinstance(other, Bound):
@ -370,9 +381,9 @@ class Func(Node):
Func.from_parse(result) Func.from_parse(result)
) )
def __init__(self, input: Macro | Bound, output: Node, *, runner = None) -> None: def __init__(self, input, output: Node, *, runner = None) -> None:
super().__init__() super().__init__()
self.input: Macro | Bound = input self.input = input
self.left: Node = output self.left: Node = output
self.right: None = None self.right: None = None
self.runner = runner # type: ignore self.runner = runner # type: ignore
@ -381,7 +392,14 @@ class Func(Node):
return f"<func {self.input!r} {self.left!r}>" return f"<func {self.input!r} {self.left!r}>"
def copy(self): def copy(self):
return Func(self.input, None, runner = self.runner) # type: ignore return Func(
Bound(
self.input.name,
runner = self.runner
),
None, # type: ignore
runner = self.runner
)
class Root(Node): class Root(Node):
""" """

View File

@ -13,7 +13,7 @@ class LambdaParser:
# We still create macro objects from them, they are turned into # We still create macro objects from them, they are turned into
# bound variables after the expression is built. # bound variables after the expression is built.
self.pp_macro = pp.Word(pp.alphas + "_") self.pp_macro = pp.Word(pp.alphas + "_")
self.pp_bound = pp.Char(pp.srange("[a-z]")) self.pp_bound = pp.Regex("[a-z][₀₁₂₃₄₅₆₈₉]*")
self.pp_name = self.pp_bound ^ self.pp_macro self.pp_name = self.pp_bound ^ self.pp_macro
self.pp_church = pp.Word(pp.nums) self.pp_church = pp.Word(pp.nums)
self.pp_history = pp.Char("$") self.pp_history = pp.Char("$")
@ -56,7 +56,7 @@ class LambdaParser:
(self.lp + self.pp_history + 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_command = pp.Suppress(":") + pp.Word(pp.alphas + "_") + pp.Word(pp.printables)[0, ...]
self.pp_all = ( self.pp_all = (

View File

@ -2,18 +2,19 @@ from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.formatted_text import HTML from prompt_toolkit.formatted_text import HTML
from prompt_toolkit import print_formatted_text as printf 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
from prompt_toolkit import prompt
import os.path import os.path
from pyparsing import exceptions as ppx from pyparsing import exceptions as ppx
import lamb import lamb_engine
commands = {} commands = {}
help_texts = {} help_texts = {}
def lamb_command( def lamb_command(
*, *,
command_name: str | None = None, command_name = None,
help_text: str help_text: str
): ):
""" """
@ -27,6 +28,100 @@ def lamb_command(
help_texts[name] = help_text help_texts[name] = help_text
return inner 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( @lamb_command(
command_name = "save", command_name = "save",
@ -38,17 +133,18 @@ def cmd_save(command, runner) -> None:
HTML( HTML(
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>" f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
target = command.args[0] target = command.args[0]
if os.path.exists(target): if os.path.exists(target):
confirm = runner.prompt_session.prompt( confirm = prompt(
message = FormattedText([ message = FormattedText([
("class:warn", "File exists. Overwrite? "), ("class:warn", "File exists. Overwrite? "),
("class:text", "[yes/no]: ") ("class:text", "[yes/no]: ")
]) ]),
style = lamb_engine.utils.style
).lower() ).lower()
if confirm != "yes": if confirm != "yes":
@ -56,7 +152,7 @@ def cmd_save(command, runner) -> None:
HTML( HTML(
"<err>Cancelled.</err>" "<err>Cancelled.</err>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -69,7 +165,7 @@ def cmd_save(command, runner) -> None:
HTML( HTML(
f"Wrote {len(runner.macro_table)} macros to <code>{target}</code>" f"Wrote {len(runner.macro_table)} macros to <code>{target}</code>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
@ -83,7 +179,7 @@ def cmd_load(command, runner):
HTML( HTML(
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>" f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -93,7 +189,7 @@ def cmd_load(command, runner):
HTML( HTML(
f"<err>File {target} doesn't exist.</err>" f"<err>File {target} doesn't exist.</err>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -119,18 +215,18 @@ def cmd_load(command, runner):
("class:err", l[e.loc]), ("class:err", l[e.loc]),
("class:code", l[e.loc+1:]) ("class:code", l[e.loc+1:])
]), ]),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
if not isinstance(x, lamb.runner.runner.MacroDef): if not isinstance(x, lamb_engine.runner.runner.MacroDef):
printf( printf(
FormattedText([ FormattedText([
("class:warn", f"Skipping line {i+1:02}: "), ("class:warn", f"Skipping line {i+1:02}: "),
("class:code", l), ("class:code", l),
("class:warn", f" is not a macro definition.") ("class:warn", f" is not a macro definition.")
]), ]),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -138,10 +234,9 @@ def cmd_load(command, runner):
printf( printf(
FormattedText([ FormattedText([
("class:ok", f"Loaded {x.label}: "), ("class:ok", f"Loaded {x.label}: ")
("class:code", str(x.expr)) ] + lamb_engine.utils.lex_str(str(x.expr))),
]), style = lamb_engine.utils.style
style = lamb.utils.style
) )
@ -154,7 +249,7 @@ def mdel(command, runner) -> None:
HTML( HTML(
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>" f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -164,7 +259,7 @@ def mdel(command, runner) -> None:
HTML( HTML(
f"<warn>Macro \"{target}\" is not defined</warn>" f"<warn>Macro \"{target}\" is not defined</warn>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -173,12 +268,13 @@ def mdel(command, runner) -> None:
@lamb_command( @lamb_command(
help_text = "Delete all macros" help_text = "Delete all macros"
) )
def clearmacros(command, runner) -> None: def delmac(command, runner) -> None:
confirm = runner.prompt_session.prompt( confirm = prompt(
message = FormattedText([ message = FormattedText([
("class:warn", "Are you sure? "), ("class:warn", "Are you sure? "),
("class:text", "[yes/no]: ") ("class:text", "[yes/no]: ")
]) ]),
style = lamb_engine.utils.style
).lower() ).lower()
if confirm != "yes": if confirm != "yes":
@ -186,7 +282,7 @@ def clearmacros(command, runner) -> None:
HTML( HTML(
"<err>Cancelled.</err>" "<err>Cancelled.</err>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -201,7 +297,7 @@ def macros(command, runner) -> None:
printf(FormattedText([ printf(FormattedText([
("class:warn", "No macros are defined."), ("class:warn", "No macros are defined."),
]), ]),
style = lamb.utils.style style = lamb_engine.utils.style
) )
else: else:
printf(FormattedText([ printf(FormattedText([
@ -211,7 +307,7 @@ def macros(command, runner) -> None:
("class:text", f"\t{name} \t {exp}\n") ("class:text", f"\t{name} \t {exp}\n")
for name, exp in runner.macro_table.items() for name, exp in runner.macro_table.items()
]), ]),
style = lamb.utils.style style = lamb_engine.utils.style
) )
@lamb_command( @lamb_command(
@ -219,7 +315,7 @@ def macros(command, runner) -> None:
) )
def clear(command, runner) -> None: def clear(command, runner) -> None:
clear_screen() clear_screen()
lamb.utils.show_greeting() lamb_engine.utils.show_greeting()
@lamb_command( @lamb_command(
help_text = "Get or set reduction limit" help_text = "Get or set reduction limit"
@ -231,14 +327,14 @@ def rlimit(command, runner) -> None:
HTML( HTML(
"<ok>No reduction limit is set</ok>" "<ok>No reduction limit is set</ok>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
else: else:
printf( printf(
HTML( HTML(
f"<ok>Reduction limit is {runner.reduction_limit:,}</ok>" f"<ok>Reduction limit is {runner.reduction_limit:,}</ok>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -247,7 +343,7 @@ def rlimit(command, runner) -> None:
HTML( HTML(
f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>" f"<err>Command <code>:{command.name}</code> takes exactly one argument.</err>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -258,7 +354,7 @@ def rlimit(command, runner) -> None:
HTML( HTML(
f"<ok>Removed reduction limit</ok>" f"<ok>Removed reduction limit</ok>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -269,7 +365,7 @@ def rlimit(command, runner) -> None:
HTML( HTML(
"<err>Reduction limit must be a positive integer or \"none\".</err>" "<err>Reduction limit must be a positive integer or \"none\".</err>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -278,7 +374,7 @@ def rlimit(command, runner) -> None:
HTML( HTML(
"<err>Reduction limit must be at least 50.</err>" "<err>Reduction limit must be at least 50.</err>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
return return
@ -287,7 +383,7 @@ def rlimit(command, runner) -> None:
HTML( HTML(
f"<ok>Set reduction limit to {t:,}</ok>" f"<ok>Set reduction limit to {t:,}</ok>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )
@ -320,5 +416,5 @@ def help(command, runner) -> None:
"<muted>Detailed documentation can be found on this project's git page.</muted>" + "<muted>Detailed documentation can be found on this project's git page.</muted>" +
"</text>" "</text>"
), ),
style = lamb.utils.style style = lamb_engine.utils.style
) )

View File

@ -1,5 +1,5 @@
import enum import enum
import lamb import lamb_engine
class StopReason(enum.Enum): class StopReason(enum.Enum):
BETA_NORMAL = ("class:text", "β-normal form") BETA_NORMAL = ("class:text", "β-normal form")
@ -16,7 +16,7 @@ class MacroDef:
result[1] result[1]
) )
def __init__(self, label: str, expr: lamb.nodes.Node): def __init__(self, label: str, expr: lamb_engine.nodes.Node):
self.label = label self.label = label
self.expr = expr self.expr = expr

View File

@ -0,0 +1,303 @@
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 = 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
try:
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")
# Reduce
red_type, node = lamb_engine.nodes.reduce(node)
# 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)
# Gracefully catch keyboard interrupts
except KeyboardInterrupt:
stop_reason = StopReason.INTERRUPT
# 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:
if not self.step_reduction:
out_text += [
("class:ok", f"Runtime: "),
("class:text", f"{time.time() - start_time:.03f} seconds"),
("class:text", "\n")
]
out_text += [
("class:ok", f"Exit reason: "),
stop_reason.value,
("class:text", "\n"),
("class:ok", f"Macro expansions: "),
("class:text", f"{macro_expansions:,}"),
("class:text", "\n"),
("class:ok", f"Reductions: "),
("class:text", f"{k:,}\t"),
("class:muted", f"(Limit: {self.reduction_limit:,})")
]
if self.full_expansion:
out_text += [
("class:text", "\n"),
("class:ok", "All 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)

View File

@ -4,6 +4,9 @@ from prompt_toolkit.lexers import Lexer
from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit import print_formatted_text as printf from prompt_toolkit import print_formatted_text as printf
from importlib.metadata import version from importlib.metadata import version
from prompt_toolkit.document import Document
import re
style = Style.from_dict({ # type: ignore style = Style.from_dict({ # type: ignore
@ -75,6 +78,10 @@ class LambdaLexer(Lexer):
return inner return inner
def lex_str(s: str) -> list[tuple[str, str]]:
return LambdaLexer().lex_document(Document(s))(0)
def show_greeting(): def show_greeting():
# | _.._ _.|_ # | _.._ _.|_
# |_(_|| | ||_) # |_(_|| | ||_)
@ -94,7 +101,7 @@ def show_greeting():
"", "",
"<_h> | _.._ _.|_", "<_h> | _.._ _.|_",
" |_(_|| | ||_)</_h>", " |_(_|| | ||_)</_h>",
f" <_v>{version('lamb')}</_v>", f" <_v>{version('lamb_engine')}</_v>",
" __ __", " __ __",
" ,-` `` `,", " ,-` `` `,",
" (` <_l>\\</_l> )", " (` <_l>\\</_l> )",
@ -108,7 +115,25 @@ def show_greeting():
"" ""
])), style = style) ])), 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): def subscript(num: int):
# unicode subscripts ₀₁₂₃ and ₄₅₆₈₉
# usually look different,
# so we'll use base 4.
qb = base4(num)
sub = { sub = {
"0": "", "0": "",
"1": "", "1": "",
@ -136,5 +161,5 @@ def subscript(num: int):
} }
return "".join( return "".join(
[sup[x] for x in str(num)] [sub[str(x)] for x in qb]
) )

View File

@ -31,7 +31,7 @@
# Misc Combinators # Misc Combinators
M = λx.(x x) M = λx.(x x)
W = (M M) W = (M M)
Y = λf.((λx.(f (x x))) (λx.(f (x x)))) Y = λf.( (λx.(f (x x))) (λx.(f (x x))) )
# Booleans # Booleans
@ -67,11 +67,11 @@ XOR = λab.((a (NOT b)) b)
# `Y FAC 6` required 867,920 reductions and took 10 minutes to run. # `Y FAC 6` required 867,920 reductions and took 10 minutes to run.
PAIR = λabi.(i a b) PAIR = λabi.(i a b)
H = λp.((PAIR (p F)) (S (p F)))
S = λnfa.(f (n f a)) S = λnfa.(f (n f a))
D = λn.((n H) ((PAIR 0) 0) T) H = λp.((PAIR (p F)) (S (p F)))
D = λn.(n H (PAIR 0 0) T)
Z = λn.(n (λa.F) T) Z = λn.(n (λa.F) T)
NZ = λn.(n (λa.T) F) NZ = λn.(n (λa.T) F)
ADD = λmn.(m S n) ADD = λmn.(m S n)
MULT = λnmf.(n (m f)) MULT = λnmf.(n (m f))
FAC = λyn.( (Z n)(1)((MULT n) (y (D n))) ) FAC = λyn.(Z n) (1) (MULT n (y (D n)))

84
misc/demo.tape Normal file
View File

@ -0,0 +1,84 @@
# See makedemo.sh
#Output lambdemo.mp4
Output lambdemo.gif
Set FontSize 30
Set Width 2000
Set Height 1500
Set FontFamily "FantasqueSansMono NF"
Set TypingSpeed 60ms
Set Framerate 30
# Intro
Sleep 2000ms
Type "lamb"
Sleep 1000ms
Enter
Sleep 2000ms
# Demo 1: load
Type ":load ../macros.lamb"
Sleep 500ms
Enter
Sleep 2000ms
Type "NOT T"
Sleep 1000ms
Enter
Sleep 6s
Type ":clear"
Sleep 1000ms
Enter
Sleep 1500ms
# Demo 2: stepping
Type ":step"
Sleep 500ms
Enter
Sleep 1500ms
Type "NOT T"
Sleep 100ms
Enter
Sleep 1500ms
Enter
Sleep 760ms
Enter
Sleep 850ms
Enter
Sleep 650ms
Enter
Sleep 700ms
Enter
Sleep 3000ms
Type ":step"
Sleep 500ms
Enter
Sleep 6s
Type ":clear"
Sleep 1000ms
Enter
Sleep 1500ms
# Demo 3: macros
Type "M = \x.x x"
Sleep 500ms
Enter
Sleep 500ms
Type "M M"
Sleep 500ms
Enter
Sleep 3s
Ctrl+c
Sleep 1000ms
Type "Y FAC 3"
Sleep 500ms
Enter
Sleep 6s

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 30 KiB

34
misc/makedemo.sh Executable file
View File

@ -0,0 +1,34 @@
#!/bin/bash
# Should be run from the misc directory.
# Will not work with any other root.
# Create this file.
# Should define two variables:
# DAV_USER="name:password"
# DAV_URL="https://site.com/dav-path"
if [[ -f "secrets.sh" ]]; then
source secrets.sh
else
echo "Cannot run without secrets.sh"
exit
fi
# Activate venv if not in venv
if [[ "$VIRTUAL_ENV" == "" ]]; then
source ../venv/bin/activate
fi
# Make sure our venv is running the latest
# version of lamb.
pip install --editable ..
# Make gif
vhs < demo.tape
# Upload
curl \
--user $DAV_USER \
--url $DAV_URL \
--upload-file "lambdemo.gif"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

@ -1,30 +1,28 @@
[build-system]
requires = [ "setuptools>=61.0" ]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = [ "." ]
include = ["lamb_engine*"]
namespaces = false
[project.scripts]
lamb = "lamb_engine:main"
[project] [project]
name = "Lamb" name = "lamb_engine"
description = "A lambda calculus engine" description = "A lambda calculus engine"
version = "1.1.9"
# We use the standard semantic versioning:
# maj.min.pat
#
# Major release:
# 1.0.0 is the first stable release.
# Incremented on BIG breaking changes.
#
# Minor release:
# Large bug fixes, new features
#
# Patch release:
# Small, compatible fixes.
version = "0.1.3"
dependencies = [ dependencies = [
"prompt-toolkit==3.0.31", "prompt-toolkit==3.0.31",
"pyparsing==3.0.9" "pyparsing==3.0.9"
] ]
authors = [ authors = [
{ name="Mark", email="mark@betalupi.com" } { name="Mark", email="mark@betalupi.com" }
] ]
readme = "README.md" readme = "README.md"
requires-python = ">=3.7" requires-python = ">=3.7"
license = {text = "GNU General Public License v3 (GPLv3)"} license = {text = "GNU General Public License v3 (GPLv3)"}
@ -35,20 +33,10 @@ classifiers = [
"Environment :: Console" "Environment :: Console"
] ]
[project.urls] [project.urls]
"Homepage" = "https://git.betalupi.com/Mark/lamb" "Homepage" = "https://git.betalupi.com/Mark/lamb"
# To build:
[build-system] # pip install build twine
requires = [ "setuptools>=61.0" ] # python -m build
build-backend = "setuptools.build_meta" # twine upload dist/lamb_engine-1.1.6* (change the version)
[tool.setuptools.packages.find]
where = ["lamb"]
include = ["lamb*"]
namespaces = false
[project.optional-dependencies]
# Used to build a standalone executable
pyinstaller = [ "pyinstaller==5.5" ]