diff --git a/README.md b/README.md
index f60a6f9..ecafee1 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,6 @@
## Todo (pre-release):
- - Good command parsing (`:save`, `:load`, are a bare minimum)
- $\alpha$-equivalence check
- Prettyprint functions (combine args, rename bound variables)
- Write a nice README
@@ -18,6 +17,7 @@
- Syntax highlighting: parenthesis, bound variables, macros, etc
- Pin header to top of screen
- PyPi package
+ - Smart alignment in all printouts
## Mention in Docs
- lambda functions only work with single-letter arguments
diff --git a/lamb/__main__.py b/lamb/__main__.py
index 7f615624..7c12200 100755
--- a/lamb/__main__.py
+++ b/lamb/__main__.py
@@ -8,7 +8,6 @@ from prompt_toolkit.lexers import Lexer
from pyparsing import exceptions as ppx
-from lamb.parser import Parser
import lamb.runner as runner
import lamb.runstatus as rs
import lamb.tokens as tokens
@@ -23,27 +22,29 @@ class LambdaLexer(Lexer):
return [("class:text", str(document.lines[line_no]))]
return inner
-# Replace "\" with a pretty "λ" in the prompt
+
+utils.show_greeting()
+
+
+# Replace "\" with pretty "λ"s
bindings = KeyBindings()
@bindings.add("\\")
def _(event):
event.current_buffer.insert_text("λ")
-session = PromptSession(
- message = FormattedText([
+
+r = runner.Runner(
+ prompt_session = PromptSession(
+ style = utils.style,
+ lexer = LambdaLexer(),
+ key_bindings = bindings
+ ),
+
+ prompt_message = FormattedText([
("class:prompt", "~~> ")
]),
- style = utils.style,
- lexer = LambdaLexer(),
- key_bindings = bindings
)
-
-utils.show_greeting()
-
-
-r = runner.Runner()
-
r.run_lines([
"T = λab.a",
"F = λab.b",
@@ -63,7 +64,7 @@ r.run_lines([
while True:
try:
- i = session.prompt()
+ i = r.prompt()
# Catch Ctrl-C and Ctrl-D
except KeyboardInterrupt:
@@ -83,7 +84,7 @@ while True:
try:
x = r.run(i)
except ppx.ParseException as e:
- l = len(to_plain_text(session.message))
+ 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}."),
@@ -107,7 +108,7 @@ while True:
if isinstance(x, rs.CommandStatus):
- printf(x.formatted_text, style = utils.style)
+ pass
# If this line was an expression, print reduction status
elif isinstance(x, rs.ReduceStatus):
diff --git a/lamb/commands.py b/lamb/commands.py
index fe53aaa..8566d2a 100644
--- a/lamb/commands.py
+++ b/lamb/commands.py
@@ -1,8 +1,12 @@
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 lamb.runstatus import CommandStatus
+import os.path
+
+from pyparsing import exceptions as ppx
+import lamb.runstatus as rs
import lamb.utils as utils
@@ -16,60 +20,165 @@ def lamb_command(*, help_text: str):
help_texts[func.__name__] = help_text
return inner
-def run(command, runner):
+def run(command, runner) -> None:
if command.name not in commands:
- return CommandStatus(
- formatted_text = FormattedText([
+ printf(
+ FormattedText([
("class:warn", f"Unknown command \"{command.name}\"")
- ])
+ ]),
+ style = utils.style
)
else:
- return commands[command.name](command, runner)
+ commands[command.name](command, runner)
+
+
+@lamb_command(help_text = "Save macros to a file")
+def save(command, runner) -> None:
+ if len(command.args) != 1:
+ printf(
+ HTML(
+ "Command :save takes exactly one argument."
+ ),
+ style = utils.style
+ )
+ return
+
+ target = command.args[0]
+ if os.path.exists(target):
+ confirm = runner.prompt_session.prompt(
+ message = FormattedText([
+ ("class:warn", "File exists. Overwrite? "),
+ ("class:text", "[yes/no]: ")
+ ])
+ ).lower()
+
+ if confirm != "yes":
+ printf(
+ HTML(
+ "Cancelled."
+ ),
+ style = utils.style
+ )
+ return
+
+ with open(target, "w") as f:
+ f.write("\n".join(
+ [f"{n} = {e}" for n, e in runner.macro_table.items()]
+ ))
+
+ printf(
+ HTML(
+ f"Wrote {len(runner.macro_table)} macros to {target}"
+ ),
+ style = utils.style
+ )
+
+
+@lamb_command(help_text = "Load macros from a file")
+def load(command, runner):
+ if len(command.args) != 1:
+ printf(
+ HTML(
+ "Command :load takes exactly one argument."
+ ),
+ style = utils.style
+ )
+ return
+
+ target = command.args[0]
+ if not os.path.exists(target):
+ printf(
+ HTML(
+ f"File {target} doesn't exist."
+ ),
+ style = 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]
+ try:
+ x = runner.run(l, macro_only = True)
+ except ppx.ParseException as e:
+ printf(
+ FormattedText([
+ ("class:warn", f"Syntax error on line {i+1:02}: "),
+ ("class:cmd_code", l[:e.loc]),
+ ("class:err", l[e.loc]),
+ ("class:cmd_code", l[e.loc+1:])
+ ]),
+ style = utils.style
+ )
+ except rs.NotAMacro:
+ printf(
+ FormattedText([
+ ("class:warn", f"Skipping line {i+1:02}: "),
+ ("class:cmd_code", l),
+ ("class:warn", f" is not a macro definition.")
+ ]),
+ style = utils.style
+ )
+ else:
+ printf(
+ FormattedText([
+ ("class:ok", f"Loaded {x.macro_label}: "),
+ ("class:cmd_code", str(x.macro_expr))
+ ]),
+ style = utils.style
+ )
+
+
@lamb_command(help_text = "Delete a macro")
-def mdel(command, runner):
+def mdel(command, runner) -> None:
if len(command.args) != 1:
- return CommandStatus(
- formatted_text = HTML(
- "Command :mdel takes exactly one argument."
- )
+ printf(
+ HTML(
+ "Command :mdel takes exactly one argument."
+ ),
+ style = utils.style
)
+ return
target = command.args[0]
if target not in runner.macro_table:
- return CommandStatus(
- formatted_text = HTML(
+ printf(
+ HTML(
f"Macro \"{target}\" is not defined"
- )
+ ),
+ style = utils.style
)
+ return
del runner.macro_table[target]
-@lamb_command(help_text = "Show macros")
-def macros(command, runner):
- return CommandStatus(
- # Can't use HTML here, certain characters might break it.
- formatted_text = FormattedText([
+
+@lamb_command(help_text = "Show macros")
+def macros(command, runner) -> None:
+ printf(FormattedText([
("class:cmd_h", "\nDefined Macros:\n"),
] +
[
("class:cmd_text", f"\t{name} \t {exp}\n")
for name, exp in runner.macro_table.items()
- ]
- )
+ ]),
+ style = utils.style
)
@lamb_command(help_text = "Clear the screen")
-def clear(command, runner):
+def clear(command, runner) -> None:
clear_screen()
utils.show_greeting()
@lamb_command(help_text = "Print this help")
-def help(command, runner):
- return CommandStatus(
- formatted_text = HTML(
+def help(command, runner) -> None:
+ printf(
+ HTML(
"\n" +
"Usage:" +
"\n" +
@@ -86,5 +195,6 @@ def help(command, runner):
for name, text in help_texts.items()
]) +
""
- )
+ ),
+ style = utils.style
)
\ No newline at end of file
diff --git a/lamb/runner.py b/lamb/runner.py
index bcc8e20..0da75b7 100644
--- a/lamb/runner.py
+++ b/lamb/runner.py
@@ -1,5 +1,4 @@
-from distutils.cmd import Command
-from prompt_toolkit.formatted_text import FormattedText
+from prompt_toolkit import PromptSession
import lamb.commands as commands
from lamb.parser import Parser
@@ -7,15 +6,19 @@ import lamb.tokens as tokens
import lamb.runstatus as rs
-
class Runner:
- def __init__(self):
+ def __init__(self, prompt_session: PromptSession, prompt_message):
self.macro_table = {}
+ self.prompt_session = prompt_session
+ self.prompt_message = prompt_message
# Maximum amount of reductions.
# If None, no maximum is enforced.
self.reduction_limit: int | None = 300
+ def prompt(self):
+ return self.prompt_session.prompt(message = self.prompt_message)
+
def reduce_expression(self, expr: tokens.LambdaToken) -> rs.ReduceStatus:
@@ -51,7 +54,7 @@ class Runner:
# Apply a list of definitions
- def run(self, line: str) -> rs.RunStatus:
+ def run(self, line: str, *, macro_only = False) -> rs.RunStatus:
e = Parser.parse_line(line)
# If this line is a macro definition, save the macro.
@@ -67,14 +70,20 @@ class Runner:
macro_expr = e.exp
)
+ elif macro_only:
+ raise rs.NotAMacro()
+
# If this line is a command, do the command.
elif isinstance(e, tokens.command):
- return commands.run(e, self)
+ commands.run(e, self)
+ return rs.CommandStatus(cmd = e.name)
# If this line is a plain expression, reduce it.
elif isinstance(e, tokens.LambdaToken):
e.bind_variables()
return self.reduce_expression(e)
+
+ # We shouldn't ever get here.
else:
raise TypeError(f"I don't know what to do with a {type(e)}")
diff --git a/lamb/runstatus.py b/lamb/runstatus.py
index 434703c..f307e6c 100644
--- a/lamb/runstatus.py
+++ b/lamb/runstatus.py
@@ -4,6 +4,16 @@ import enum
import lamb.tokens as tokens
+
+class NotAMacro(Exception):
+ """
+ Raised when we try to run a non-macro line
+ while enforcing macro_only in Runner.run().
+
+ This should be caught and elegantly presented to the user.
+ """
+ pass
+
class RunStatus:
"""
Base class for run status.
@@ -64,14 +74,11 @@ class ReduceStatus(RunStatus):
class CommandStatus(RunStatus):
"""
Returned when a command is executed.
+ Doesn't do anything interesting.
Values:
- `formatted_text`: What to print after this command is executed
+ `cmd`: The command that was run, without a colon.
"""
- def __init__(
- self,
- *,
- formatted_text: FormattedText | HTML
- ):
- self.formatted_text = formatted_text
\ No newline at end of file
+ def __init__(self, *, cmd: str):
+ self.cmd = cmd
\ No newline at end of file
diff --git a/lamb/utils.py b/lamb/utils.py
index fb55bb3..23fb1b9 100644
--- a/lamb/utils.py
+++ b/lamb/utils.py
@@ -36,6 +36,7 @@ style = Style.from_dict({
"warn": "#FFFF00",
"err": "#FF0000",
"prompt": "#00FFFF",
+ "ok": "#B4EC85",
# Syntax
"syn_macro": "#FF00FF",