Made commands more powerful, added :load and :save
parent
1bbca094dd
commit
fa02c2aa5b
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
162
lamb/commands.py
162
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(
|
||||
"<err>Command <cmd_code>:save</cmd_code> takes exactly one argument.</err>"
|
||||
),
|
||||
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(
|
||||
"<err>Cancelled.</err>"
|
||||
),
|
||||
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 <cmd_code>{target}</cmd_code>"
|
||||
),
|
||||
style = utils.style
|
||||
)
|
||||
|
||||
|
||||
@lamb_command(help_text = "Load macros from a file")
|
||||
def load(command, runner):
|
||||
if len(command.args) != 1:
|
||||
printf(
|
||||
HTML(
|
||||
"<err>Command <cmd_code>:load</cmd_code> takes exactly one argument.</err>"
|
||||
),
|
||||
style = utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = command.args[0]
|
||||
if not os.path.exists(target):
|
||||
printf(
|
||||
HTML(
|
||||
f"<err>File {target} doesn't exist.</err>"
|
||||
),
|
||||
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(
|
||||
"<warn>Command <cmd_code>:mdel</cmd_code> takes exactly one argument.</warn>"
|
||||
)
|
||||
printf(
|
||||
HTML(
|
||||
"<err>Command <cmd_code>:mdel</cmd_code> takes exactly one argument.</err>"
|
||||
),
|
||||
style = utils.style
|
||||
)
|
||||
return
|
||||
|
||||
target = command.args[0]
|
||||
if target not in runner.macro_table:
|
||||
return CommandStatus(
|
||||
formatted_text = HTML(
|
||||
printf(
|
||||
HTML(
|
||||
f"<warn>Macro \"{target}\" is not defined</warn>"
|
||||
)
|
||||
),
|
||||
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<cmd_text>" +
|
||||
"<cmd_h>Usage:</cmd_h>" +
|
||||
"\n" +
|
||||
|
@ -86,5 +195,6 @@ def help(command, runner):
|
|||
for name, text in help_texts.items()
|
||||
]) +
|
||||
"</cmd_text>"
|
||||
)
|
||||
),
|
||||
style = utils.style
|
||||
)
|
|
@ -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)}")
|
||||
|
||||
|
|
|
@ -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
|
||||
def __init__(self, *, cmd: str):
|
||||
self.cmd = cmd
|
|
@ -36,6 +36,7 @@ style = Style.from_dict({
|
|||
"warn": "#FFFF00",
|
||||
"err": "#FF0000",
|
||||
"prompt": "#00FFFF",
|
||||
"ok": "#B4EC85",
|
||||
|
||||
# Syntax
|
||||
"syn_macro": "#FF00FF",
|
||||
|
|
Reference in New Issue