Compare commits

..

No commits in common. "fa02c2aa5b2a09c5722f88643c8dadce4a9d3507" and "a7078f9a778e09f52cd3b5aaff6db1da061561b5" have entirely different histories.

8 changed files with 156 additions and 278 deletions

View File

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

View File

@ -2,11 +2,12 @@
## 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
@ -16,8 +17,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
- PyPi package - Parser is a bit slow. Maybe we can do better?
- Smart alignment in all printouts - pypi package
## 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,47 +4,35 @@ 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
# Simple lexer for highlighting. # Replace "\" with a pretty "λ" in the prompt
# 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()
# 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(
r = runner.Runner( message = FormattedText([
prompt_session = PromptSession( ("#00FFFF", "~~> ")
style = utils.style,
lexer = LambdaLexer(),
key_bindings = bindings
),
prompt_message = FormattedText([
("class:prompt", "~~> ")
]), ]),
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",
@ -64,7 +52,7 @@ r.run_lines([
while True: while True:
try: try:
i = r.prompt() i = session.prompt()
# Catch Ctrl-C and Ctrl-D # Catch Ctrl-C and Ctrl-D
except KeyboardInterrupt: except KeyboardInterrupt:
@ -84,44 +72,46 @@ 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(r.prompt_session.message)) l = len(to_plain_text(session.message))
printf(FormattedText([ printf(FormattedText([
("class:err", " "*(e.loc + l) + "^\n"), ("#FF0000", " "*(e.loc + l) + "^\n"),
("class:err", f"Syntax error at char {e.loc}."), ("#FF0000", f"Syntax error at char {e.loc}."),
("class:text", "\n") ("#FFFFFF", "\n")
])) ]))
continue continue
except tokens.ReductionError as e: except tokens.ReductionError as e:
printf(FormattedText([ printf(FormattedText([
("class:err", f"{e.msg}\n") ("#FF0000", f"{e.msg}"),
]), style = utils.style) ("#FFFFFF", "\n")
]))
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([
("class:text", "Set "), ("#FFFFFF", "Set "),
("class:syn_macro", x.macro_label), ("#FF00FF", x.macro_label),
("class:text", " to "), ("#FFFFFF", " to "),
("class:text", str(x.macro_expr)) ("#FFFFFF", str(x.macro_expr))
]), style = utils.style) ]))
if isinstance(x, rs.CommandStatus): if isinstance(x, rs.CommandStatus):
pass printf(x.formatted_text)
# 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,
("class:result_header", f"\nReduction count: "), ("#00FF00 bold", f"\nReduction count: "),
("class:text", str(x.reduction_count)), ("#FFFFFF", str(x.reduction_count)),
("class:result_header", "\n\n => "), ("#00FF00 bold", "\n\n => "),
("class:text", str(x.result)), ("#FFFFFF", str(x.result)),
]), style = utils.style) ]))
printf("") printf("")

View File

@ -1,12 +1,8 @@
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
import os.path from lamb.runstatus import CommandStatus
from pyparsing import exceptions as ppx
import lamb.runstatus as rs
import lamb.utils as utils import lamb.utils as utils
@ -20,181 +16,100 @@ 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) -> None: def run(command, runner):
if command.name not in commands: return commands[command.name](command, runner)
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) -> None: def mdel(command, runner):
if len(command.args) != 1: if len(command.args) != 1:
printf( return CommandStatus(
HTML( formatted_text = HTML(
"<err>Command <cmd_code>:mdel</cmd_code> takes exactly one argument.</err>" "<red>Command <grey>:mdel</grey> takes exactly one argument.</red>"
), )
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:
printf( return CommandStatus(
HTML( formatted_text = HTML(
f"<warn>Macro \"{target}\" is not defined</warn>" f"<red>Macro \"{target}\" is not defined</red>"
), )
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) -> None: def macros(command, runner):
printf(FormattedText([ return CommandStatus(
("class:cmd_h", "\nDefined Macros:\n"), formatted_text = FormattedText([
("#FF6600 bold", "\nDefined Macros:\n"),
] + ] +
[ [
("class:cmd_text", f"\t{name} \t {exp}\n") ("#FFFFFF", 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) -> None: def clear(command, runner):
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) -> None: def help(command, runner):
printf( return CommandStatus(
HTML( formatted_text = FormattedText([
"\n<cmd_text>" + ("#FF6600 bold", "\nUsage:\n"),
"<cmd_h>Usage:</cmd_h>" + (
"\n" + "#FFFFFF",
"\tWrite lambda expressions using your <cmd_key>\\</cmd_key> key." + "\tWrite lambda expressions using your "
"\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 (
"#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()
]
)
) )

View File

@ -1,8 +1,5 @@
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,4 +1,5 @@
from prompt_toolkit import PromptSession from distutils.cmd import Command
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
@ -6,18 +7,26 @@ import lamb.tokens as tokens
import lamb.runstatus as rs import lamb.runstatus as rs
class Runner: class Runner:
def __init__(self, prompt_session: PromptSession, prompt_message): def __init__(self):
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): def exec_command(self, command: tokens.command) -> rs.CommandStatus:
return self.prompt_session.prompt(message = self.prompt_message) 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 reduce_expression(self, expr: tokens.LambdaToken) -> rs.ReduceStatus: def reduce_expression(self, expr: tokens.LambdaToken) -> rs.ReduceStatus:
@ -54,7 +63,7 @@ class Runner:
# Apply a list of definitions # Apply a list of definitions
def run(self, line: str, *, macro_only = False) -> rs.RunStatus: def run(self, line: str) -> 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.
@ -70,20 +79,14 @@ 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):
commands.run(e, self) return self.exec_command(e)
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,19 +1,8 @@
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.
@ -44,10 +33,10 @@ class MacroStatus(RunStatus):
class StopReason(enum.Enum): class StopReason(enum.Enum):
BETA_NORMAL = ("class:text", "β-normal form") BETA_NORMAL = ("#FFFFFF", "β-normal form")
LOOP_DETECTED = ("class:warn", "loop detected") LOOP_DETECTED = ("#FFFF00", "loop detected")
MAX_EXCEEDED = ("class:err", "too many reductions") MAX_EXCEEDED = ("#FFFF00", "too many reductions")
INTERRUPT = ("class:warn", "user interrupt") INTERRUPT = ("#FF0000", "user interrupt")
class ReduceStatus(RunStatus): class ReduceStatus(RunStatus):
@ -74,11 +63,14 @@ 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:
`cmd`: The command that was run, without a colon. `formatted_text`: What to print after this command is executed
""" """
def __init__(self, *, cmd: str): def __init__(
self.cmd = cmd self,
*,
formatted_text: FormattedText
):
self.formatted_text = formatted_text

View File

@ -30,39 +30,6 @@ 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():
# | _.._ _.|_ # | _.._ _.|_
@ -95,4 +62,19 @@ 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) ])), style = Style.from_dict({
# Heading
"_h": "#FFFFFF bold",
# Version
"_v": "#B4EC85 bold",
# Lambda
"_l": "#FF6600 bold",
# Subtitle
"_s": "#B4EC85 bold",
# :help message
"_p": "#AAAAAA"
}))