Made commands more powerful, added :load and :save

master
Mark 2022-10-22 09:50:04 -07:00
parent 1bbca094dd
commit fa02c2aa5b
Signed by: Mark
GPG Key ID: AD62BB059C2AAEE4
6 changed files with 184 additions and 56 deletions

View File

@ -2,7 +2,6 @@
## Todo (pre-release): ## Todo (pre-release):
- Good command parsing (`:save`, `:load`, are a bare minimum)
- $\alpha$-equivalence check - $\alpha$-equivalence check
- Prettyprint functions (combine args, rename bound variables) - Prettyprint functions (combine args, rename bound variables)
- Write a nice README - Write a nice README
@ -18,6 +17,7 @@
- Syntax highlighting: parenthesis, bound variables, macros, etc - Syntax highlighting: parenthesis, bound variables, macros, etc
- Pin header to top of screen - Pin header to top of screen
- PyPi package - PyPi package
- Smart alignment in all printouts
## Mention in Docs ## Mention in Docs
- lambda functions only work with single-letter arguments - lambda functions only work with single-letter arguments

View File

@ -8,7 +8,6 @@ from prompt_toolkit.lexers import Lexer
from pyparsing import exceptions as ppx from pyparsing import exceptions as ppx
from lamb.parser import Parser
import lamb.runner as runner import lamb.runner as runner
import lamb.runstatus as rs import lamb.runstatus as rs
import lamb.tokens as tokens import lamb.tokens as tokens
@ -23,27 +22,29 @@ class LambdaLexer(Lexer):
return [("class:text", str(document.lines[line_no]))] return [("class:text", str(document.lines[line_no]))]
return inner return inner
# Replace "\" with a pretty "λ" in the prompt
utils.show_greeting()
# Replace "\" with pretty "λ"s
bindings = KeyBindings() bindings = KeyBindings()
@bindings.add("\\") @bindings.add("\\")
def _(event): def _(event):
event.current_buffer.insert_text("λ") 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", "~~> ") ("class:prompt", "~~> ")
]), ]),
style = utils.style,
lexer = LambdaLexer(),
key_bindings = bindings
) )
utils.show_greeting()
r = runner.Runner()
r.run_lines([ r.run_lines([
"T = λab.a", "T = λab.a",
"F = λab.b", "F = λab.b",
@ -63,7 +64,7 @@ r.run_lines([
while True: while True:
try: try:
i = session.prompt() i = r.prompt()
# Catch Ctrl-C and Ctrl-D # Catch Ctrl-C and Ctrl-D
except KeyboardInterrupt: except KeyboardInterrupt:
@ -83,7 +84,7 @@ while True:
try: try:
x = r.run(i) x = r.run(i)
except ppx.ParseException as e: except ppx.ParseException as e:
l = len(to_plain_text(session.message)) l = len(to_plain_text(r.prompt_session.message))
printf(FormattedText([ printf(FormattedText([
("class:err", " "*(e.loc + l) + "^\n"), ("class:err", " "*(e.loc + l) + "^\n"),
("class:err", f"Syntax error at char {e.loc}."), ("class:err", f"Syntax error at char {e.loc}."),
@ -107,7 +108,7 @@ while True:
if isinstance(x, rs.CommandStatus): if isinstance(x, rs.CommandStatus):
printf(x.formatted_text, style = utils.style) pass
# If this line was an expression, print reduction status # If this line was an expression, print reduction status
elif isinstance(x, rs.ReduceStatus): elif isinstance(x, rs.ReduceStatus):

View File

@ -1,8 +1,12 @@
from prompt_toolkit.formatted_text import FormattedText 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.shortcuts import clear as clear_screen 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 import lamb.utils as utils
@ -16,60 +20,165 @@ def lamb_command(*, help_text: str):
help_texts[func.__name__] = help_text help_texts[func.__name__] = help_text
return inner return inner
def run(command, runner): def run(command, runner) -> None:
if command.name not in commands: if command.name not in commands:
return CommandStatus( printf(
formatted_text = FormattedText([ FormattedText([
("class:warn", f"Unknown command \"{command.name}\"") ("class:warn", f"Unknown command \"{command.name}\"")
]) ]),
style = utils.style
) )
else: 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") @lamb_command(help_text = "Delete a macro")
def mdel(command, runner): def mdel(command, runner) -> None:
if len(command.args) != 1: if len(command.args) != 1:
return CommandStatus( printf(
formatted_text = HTML( HTML(
"<warn>Command <cmd_code>:mdel</cmd_code> takes exactly one argument.</warn>" "<err>Command <cmd_code>:mdel</cmd_code> takes exactly one argument.</err>"
) ),
style = utils.style
) )
return
target = command.args[0] target = command.args[0]
if target not in runner.macro_table: if target not in runner.macro_table:
return CommandStatus( printf(
formatted_text = HTML( HTML(
f"<warn>Macro \"{target}\" is not defined</warn>" f"<warn>Macro \"{target}\" is not defined</warn>"
) ),
style = utils.style
) )
return
del runner.macro_table[target] 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_h", "\nDefined Macros:\n"),
] + ] +
[ [
("class:cmd_text", f"\t{name} \t {exp}\n") ("class:cmd_text", f"\t{name} \t {exp}\n")
for name, exp in runner.macro_table.items() for name, exp in runner.macro_table.items()
] ]),
) style = utils.style
) )
@lamb_command(help_text = "Clear the screen") @lamb_command(help_text = "Clear the screen")
def clear(command, runner): def clear(command, runner) -> None:
clear_screen() clear_screen()
utils.show_greeting() utils.show_greeting()
@lamb_command(help_text = "Print this help") @lamb_command(help_text = "Print this help")
def help(command, runner): def help(command, runner) -> None:
return CommandStatus( printf(
formatted_text = HTML( HTML(
"\n<cmd_text>" + "\n<cmd_text>" +
"<cmd_h>Usage:</cmd_h>" + "<cmd_h>Usage:</cmd_h>" +
"\n" + "\n" +
@ -86,5 +195,6 @@ def help(command, runner):
for name, text in help_texts.items() for name, text in help_texts.items()
]) + ]) +
"</cmd_text>" "</cmd_text>"
) ),
style = utils.style
) )

View File

@ -1,5 +1,4 @@
from distutils.cmd import Command from prompt_toolkit import PromptSession
from prompt_toolkit.formatted_text import FormattedText
import lamb.commands as commands import lamb.commands as commands
from lamb.parser import Parser from lamb.parser import Parser
@ -7,15 +6,19 @@ import lamb.tokens as tokens
import lamb.runstatus as rs import lamb.runstatus as rs
class Runner: class Runner:
def __init__(self): def __init__(self, prompt_session: PromptSession, prompt_message):
self.macro_table = {} self.macro_table = {}
self.prompt_session = prompt_session
self.prompt_message = prompt_message
# Maximum amount of reductions. # Maximum amount of reductions.
# If None, no maximum is enforced. # If None, no maximum is enforced.
self.reduction_limit: int | None = 300 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: def reduce_expression(self, expr: tokens.LambdaToken) -> rs.ReduceStatus:
@ -51,7 +54,7 @@ class Runner:
# Apply a list of definitions # 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) e = Parser.parse_line(line)
# If this line is a macro definition, save the macro. # If this line is a macro definition, save the macro.
@ -67,14 +70,20 @@ class Runner:
macro_expr = e.exp macro_expr = e.exp
) )
elif macro_only:
raise rs.NotAMacro()
# If this line is a command, do the command. # If this line is a command, do the command.
elif isinstance(e, tokens.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. # If this line is a plain expression, reduce it.
elif isinstance(e, tokens.LambdaToken): elif isinstance(e, tokens.LambdaToken):
e.bind_variables() e.bind_variables()
return self.reduce_expression(e) return self.reduce_expression(e)
# We shouldn't ever get here.
else: else:
raise TypeError(f"I don't know what to do with a {type(e)}") raise TypeError(f"I don't know what to do with a {type(e)}")

View File

@ -4,6 +4,16 @@ import enum
import lamb.tokens as tokens 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: class RunStatus:
""" """
Base class for run status. Base class for run status.
@ -64,14 +74,11 @@ class ReduceStatus(RunStatus):
class CommandStatus(RunStatus): class CommandStatus(RunStatus):
""" """
Returned when a command is executed. Returned when a command is executed.
Doesn't do anything interesting.
Values: Values:
`formatted_text`: What to print after this command is executed `cmd`: The command that was run, without a colon.
""" """
def __init__( def __init__(self, *, cmd: str):
self, self.cmd = cmd
*,
formatted_text: FormattedText | HTML
):
self.formatted_text = formatted_text

View File

@ -36,6 +36,7 @@ style = Style.from_dict({
"warn": "#FFFF00", "warn": "#FFFF00",
"err": "#FF0000", "err": "#FF0000",
"prompt": "#00FFFF", "prompt": "#00FFFF",
"ok": "#B4EC85",
# Syntax # Syntax
"syn_macro": "#FF00FF", "syn_macro": "#FF00FF",