Compare commits

...

4 Commits

Author SHA1 Message Date
fa02c2aa5b
Made commands more powerful, added :load and :save 2022-10-22 09:50:04 -07:00
1bbca094dd
Added basic lexer 2022-10-22 08:45:27 -07:00
0ef0e8e585
Cleaned up commands and styling 2022-10-22 08:37:19 -07:00
d11c9a5a7e
Improved parser 2022-10-22 08:28:05 -07:00
8 changed files with 280 additions and 158 deletions

View File

@ -2,7 +2,9 @@
"cSpell.words": [
"autochurch",
"freevar",
"mdel",
"onefile",
"Packrat",
"pyparsing",
"runstatus",
"subvar"

View File

@ -2,12 +2,11 @@
## 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
- Delete macros
- Handle or avoid recursion errors
- Fix colors
## Todo:
- live syntax check
@ -17,8 +16,8 @@
- Warn when overwriting macro
- Syntax highlighting: parenthesis, bound variables, macros, etc
- Pin header to top of screen
- Parser is a bit slow. Maybe we can do better?
- pypi package
- PyPi package
- Smart alignment in all printouts
## Mention in Docs
- lambda functions only work with single-letter arguments

View File

@ -4,34 +4,46 @@ 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 prompt_toolkit.key_binding import KeyBindings
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
import lamb.utils as utils
# Replace "\" with a pretty "λ" in the prompt
bindings = KeyBindings()
@bindings.add("\\")
def _(event):
event.current_buffer.insert_text("λ")
session = PromptSession(
message = FormattedText([
("#00FFFF", "~~> ")
]),
key_bindings = bindings
)
# Simple lexer for highlighting.
# Improve this later.
class LambdaLexer(Lexer):
def lex_document(self, document):
def inner(line_no):
return [("class:text", str(document.lines[line_no]))]
return inner
utils.show_greeting()
r = runner.Runner()
# Replace "\" with pretty "λ"s
bindings = KeyBindings()
@bindings.add("\\")
def _(event):
event.current_buffer.insert_text("λ")
r = runner.Runner(
prompt_session = PromptSession(
style = utils.style,
lexer = LambdaLexer(),
key_bindings = bindings
),
prompt_message = FormattedText([
("class:prompt", "~~> ")
]),
)
r.run_lines([
"T = λab.a",
@ -52,7 +64,7 @@ r.run_lines([
while True:
try:
i = session.prompt()
i = r.prompt()
# Catch Ctrl-C and Ctrl-D
except KeyboardInterrupt:
@ -72,46 +84,44 @@ 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([
("#FF0000", " "*(e.loc + l) + "^\n"),
("#FF0000", f"Syntax error at char {e.loc}."),
("#FFFFFF", "\n")
("class:err", " "*(e.loc + l) + "^\n"),
("class:err", f"Syntax error at char {e.loc}."),
("class:text", "\n")
]))
continue
except tokens.ReductionError as e:
printf(FormattedText([
("#FF0000", f"{e.msg}"),
("#FFFFFF", "\n")
]))
("class:err", f"{e.msg}\n")
]), style = utils.style)
continue
# If this line defined a macro, print nothing.
if isinstance(x, rs.MacroStatus):
printf(FormattedText([
("#FFFFFF", "Set "),
("#FF00FF", x.macro_label),
("#FFFFFF", " to "),
("#FFFFFF", str(x.macro_expr))
]))
("class:text", "Set "),
("class:syn_macro", x.macro_label),
("class:text", " to "),
("class:text", str(x.macro_expr))
]), style = utils.style)
if isinstance(x, rs.CommandStatus):
printf(x.formatted_text)
pass
# If this line was an expression, print reduction status
elif isinstance(x, rs.ReduceStatus):
printf(FormattedText([
("#00FF00 bold", f"\nExit reason: "),
("class:result_header", f"\nExit reason: "),
x.stop_reason.value,
("#00FF00 bold", f"\nReduction count: "),
("#FFFFFF", str(x.reduction_count)),
("class:result_header", f"\nReduction count: "),
("class:text", str(x.reduction_count)),
("#00FF00 bold", "\n\n => "),
("#FFFFFF", str(x.result)),
]))
("class:result_header", "\n\n => "),
("class:text", str(x.result)),
]), style = utils.style)
printf("")

View File

@ -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,100 +20,181 @@ def lamb_command(*, help_text: str):
help_texts[func.__name__] = help_text
return inner
def run(command, runner):
return commands[command.name](command, runner)
def run(command, runner) -> None:
if command.name not in commands:
printf(
FormattedText([
("class:warn", f"Unknown command \"{command.name}\"")
]),
style = utils.style
)
else:
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(
"<red>Command <grey>:mdel</grey> takes exactly one argument.</red>"
)
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(
f"<red>Macro \"{target}\" is not defined</red>"
)
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(
formatted_text = FormattedText([
("#FF6600 bold", "\nDefined Macros:\n"),
def macros(command, runner) -> None:
printf(FormattedText([
("class:cmd_h", "\nDefined Macros:\n"),
] +
[
("#FFFFFF", f"\t{name} \t {exp}\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 = FormattedText([
("#FF6600 bold", "\nUsage:\n"),
(
"#FFFFFF",
"\tWrite lambda expressions using your "
),
(
"#00FF00",
"\\"
),
(
"#FFFFFF",
" key.\n" +
"\tMacros can be defined using "
),
("#00FF00", "="),
(
"#FFFFFF",
", as in "
),
(
"#AAAAAA bold",
"T = λab.a\n"
),
(
"#FFFFFF",
"\tRun commands using "
),
(
"#00FF00",
":"
),
(
"#FFFFFF",
", for example "
),
(
"#AAAAAA bold",
":help"
),
("#FF6600 bold", "\n\nCommands:\n")
] +
[
("#FFFFFF", f"\t{name} \t {text}\n")
for name, text in help_texts.items()
]
)
def help(command, runner) -> None:
printf(
HTML(
"\n<cmd_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 <cmd_code>T = λab.a</cmd_code>" +
"\n" +
"\tRun commands using <cmd_key>:</cmd_key>, for example <cmd_code>:help</cmd_code>" +
"\n\n" +
"<cmd_h>Commands:</cmd_h>"+
"\n" +
"\n".join([
f"\t{name} \t {text}"
for name, text in help_texts.items()
]) +
"</cmd_text>"
),
style = utils.style
)

View File

@ -1,5 +1,8 @@
import pyparsing as pp
# Packrat gives a MAD speed boost.
pp.ParserElement.enablePackrat()
import lamb.tokens as tokens
import lamb.utils as utils

View File

@ -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,26 +6,18 @@ 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 exec_command(self, command: tokens.command) -> rs.CommandStatus:
if command.name in commands.commands:
return commands.run(command, self)
# Handle unknown commands
else:
return rs.CommandStatus(
formatted_text = FormattedText([
("#FFFF00", f"Unknown command \"{command}\"")
])
)
def prompt(self):
return self.prompt_session.prompt(message = self.prompt_message)
def reduce_expression(self, expr: tokens.LambdaToken) -> rs.ReduceStatus:
@ -58,12 +49,12 @@ class Runner:
return rs.ReduceStatus(
reduction_count = i - macro_expansions,
stop_reason = rs.StopReason.MAX_EXCEEDED,
result = r.output # type: ignore
result = r.output # type: ignore
)
# 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.
@ -79,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 self.exec_command(e)
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)}")

View File

@ -1,8 +1,19 @@
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.formatted_text import HTML
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.
@ -33,10 +44,10 @@ class MacroStatus(RunStatus):
class StopReason(enum.Enum):
BETA_NORMAL = ("#FFFFFF", "β-normal form")
LOOP_DETECTED = ("#FFFF00", "loop detected")
MAX_EXCEEDED = ("#FFFF00", "too many reductions")
INTERRUPT = ("#FF0000", "user interrupt")
BETA_NORMAL = ("class:text", "β-normal form")
LOOP_DETECTED = ("class:warn", "loop detected")
MAX_EXCEEDED = ("class:err", "too many reductions")
INTERRUPT = ("class:warn", "user interrupt")
class ReduceStatus(RunStatus):
@ -63,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
):
self.formatted_text = formatted_text
def __init__(self, *, cmd: str):
self.cmd = cmd

View File

@ -30,6 +30,39 @@ def autochurch(results):
)
style = Style.from_dict({
# Basic formatting
"text": "#FFFFFF",
"warn": "#FFFF00",
"err": "#FF0000",
"prompt": "#00FFFF",
"ok": "#B4EC85",
# Syntax
"syn_macro": "#FF00FF",
"syn_lambda": "#FF00FF",
"syn_bound": "#FF00FF",
# Titles for reduction results
"result_header": "#B4EC85 bold",
# Command formatting
# cmd_h: section titles
# cmd_code: example snippets
# cmd_text: regular text
# cmd_key: keyboard keys, usually one character
"cmd_h": "#FF6600 bold",
"cmd_code": "#AAAAAA italic",
"cmd_text": "#FFFFFF",
"cmd_key": "#B4EC85 bold",
# Only used in greeting
"_v": "#B4EC85 bold",
"_l": "#FF6600 bold",
"_s": "#B4EC85 bold",
"_p": "#AAAAAA"
})
def show_greeting():
# | _.._ _.|_
@ -62,19 +95,4 @@ def show_greeting():
"<_s> A λ calculus engine</_s>",
"<_p> Type :help for help</_p>",
""
])), style = Style.from_dict({
# Heading
"_h": "#FFFFFF bold",
# Version
"_v": "#B4EC85 bold",
# Lambda
"_l": "#FF6600 bold",
# Subtitle
"_s": "#B4EC85 bold",
# :help message
"_p": "#AAAAAA"
}))
])), style = style)