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": [ "cSpell.words": [
"autochurch", "autochurch",
"freevar", "freevar",
"mdel",
"onefile", "onefile",
"Packrat",
"pyparsing", "pyparsing",
"runstatus", "runstatus",
"subvar" "subvar"

View File

@ -2,12 +2,11 @@
## 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
- Delete macros
- Handle or avoid recursion errors - Handle or avoid recursion errors
- Fix colors
## Todo: ## Todo:
- live syntax check - live syntax check
@ -17,8 +16,8 @@
- Warn when overwriting macro - Warn when overwriting macro
- 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
- Parser is a bit slow. Maybe we can do better? - 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

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

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

View File

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

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,26 +6,18 @@ 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 exec_command(self, command: tokens.command) -> rs.CommandStatus: def prompt(self):
if command.name in commands.commands: return self.prompt_session.prompt(message = self.prompt_message)
return commands.run(command, self)
# Handle unknown commands
else:
return rs.CommandStatus(
formatted_text = FormattedText([
("#FFFF00", f"Unknown command \"{command}\"")
])
)
def reduce_expression(self, expr: tokens.LambdaToken) -> rs.ReduceStatus: def reduce_expression(self, expr: tokens.LambdaToken) -> rs.ReduceStatus:
@ -63,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.
@ -79,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 self.exec_command(e) 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

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

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(): def show_greeting():
# | _.._ _.|_ # | _.._ _.|_
@ -62,19 +95,4 @@ def show_greeting():
"<_s> A λ calculus engine</_s>", "<_s> A λ calculus engine</_s>",
"<_p> Type :help for help</_p>", "<_p> Type :help for help</_p>",
"" ""
])), style = Style.from_dict({ ])), style = style)
# Heading
"_h": "#FFFFFF bold",
# Version
"_v": "#B4EC85 bold",
# Lambda
"_l": "#FF6600 bold",
# Subtitle
"_s": "#B4EC85 bold",
# :help message
"_p": "#AAAAAA"
}))