This repository has been archived on 2024-11-05. You can view files and clone it, but cannot push or open issues/pull-requests.
lamb/lamb_engine/runner/runner.py

304 lines
7.7 KiB
Python
Raw Normal View History

from prompt_toolkit import PromptSession
2022-10-22 18:53:40 -07:00
from prompt_toolkit.formatted_text import FormattedText
2022-11-11 15:30:21 -08:00
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit import prompt
2022-10-22 18:53:40 -07:00
from prompt_toolkit import print_formatted_text as printf
2022-11-11 17:04:05 -08:00
import collections
2022-10-28 18:58:25 -07:00
import math
2022-10-28 19:05:38 -07:00
import time
2022-10-21 17:55:31 -07:00
2022-11-11 17:20:59 -08:00
import lamb_engine
2022-10-22 18:53:40 -07:00
2022-11-11 17:20:59 -08:00
from lamb_engine.runner.misc import MacroDef
from lamb_engine.runner.misc import Command
from lamb_engine.runner.misc import StopReason
from lamb_engine.runner import commands as cmd
2022-10-22 18:53:40 -07:00
2022-10-28 14:19:29 -07:00
2022-11-11 15:30:21 -08:00
# Keybindings for step prompt.
# Prevents any text from being input.
step_bindings = KeyBindings()
@step_bindings.add("<any>")
def _(event):
pass
2022-10-21 17:05:25 -07:00
2022-10-21 14:40:49 -07:00
class Runner:
2022-10-28 14:19:29 -07:00
def __init__(
self,
prompt_session: PromptSession,
prompt_message
):
2022-10-21 14:40:49 -07:00
self.macro_table = {}
self.prompt_session = prompt_session
self.prompt_message = prompt_message
2022-11-11 17:20:59 -08:00
self.parser = lamb_engine.parser.LambdaParser(
action_func = lamb_engine.nodes.Func.from_parse,
action_bound = lamb_engine.nodes.Macro.from_parse,
action_macro = lamb_engine.nodes.Macro.from_parse,
action_call = lamb_engine.nodes.Call.from_parse,
action_church = lamb_engine.nodes.Church.from_parse,
2022-10-28 14:19:29 -07:00
action_macro_def = MacroDef.from_parse,
2022-10-29 15:44:17 -07:00
action_command = Command.from_parse,
2022-11-11 17:20:59 -08:00
action_history = lamb_engine.nodes.History.from_parse
2022-10-22 12:59:42 -07:00
)
2022-10-21 17:05:25 -07:00
# Maximum amount of reductions.
# If None, no maximum is enforced.
2022-10-22 18:53:40 -07:00
# Must be at least 1.
2022-10-22 18:20:48 -07:00
self.reduction_limit: int | None = 1_000_000
2022-10-21 14:40:49 -07:00
2022-10-22 12:59:42 -07:00
# Ensure bound variables are unique.
# This is automatically incremented whenever we make
# a bound variable.
self.bound_variable_counter = 0
2022-10-28 18:58:25 -07:00
# Update iteration after this many iterations
# Make sure every place value has a non-zero digit
# so that all digits appear to be changing.
self.iter_update = 231
2022-11-11 17:04:05 -08:00
self.history = collections.deque(
[None] * 10,
10)
2022-10-29 15:44:17 -07:00
2022-11-11 15:30:21 -08:00
# If true, reduce step-by-step.
self.step_reduction = False
2022-11-11 15:45:22 -08:00
# If true, expand ALL macros when printing output
self.full_expansion = False
def prompt(self):
2022-10-28 19:47:50 -07:00
return self.prompt_session.prompt(
message = self.prompt_message
)
2022-11-11 17:20:59 -08:00
def parse(self, line) -> tuple[lamb_engine.nodes.Root | MacroDef | Command, list]:
2022-10-22 18:53:40 -07:00
e = self.parser.parse_line(line)
2022-10-28 14:19:29 -07:00
2022-11-09 21:47:55 -08:00
w = []
2022-10-28 14:19:29 -07:00
if isinstance(e, MacroDef):
2022-11-11 17:20:59 -08:00
e.expr = lamb_engine.nodes.Root(e.expr)
2022-10-29 15:44:17 -07:00
e.set_runner(self)
2022-11-11 17:20:59 -08:00
w = lamb_engine.nodes.prepare(e.expr, ban_macro_name = e.label)
elif isinstance(e, lamb_engine.nodes.Node):
e = lamb_engine.nodes.Root(e)
2022-10-29 15:44:17 -07:00
e.set_runner(self)
2022-11-11 17:20:59 -08:00
w = lamb_engine.nodes.prepare(e)
2022-11-09 21:47:55 -08:00
return e, w
2022-10-29 15:44:17 -07:00
2022-11-11 17:20:59 -08:00
def reduce(self, node: lamb_engine.nodes.Root, *, warnings = []) -> None:
2022-10-29 15:44:17 -07:00
2022-10-31 08:20:27 -07:00
# Reduction Counter.
# We also count macro (and church) expansions,
# and subtract those from the final count.
k = 0
macro_expansions = 0
stop_reason = StopReason.MAX_EXCEEDED
start_time = time.time()
out_text = []
2022-11-01 21:29:39 -07:00
only_macro = (
2022-11-11 17:20:59 -08:00
isinstance(node.left, lamb_engine.nodes.Macro) or
isinstance(node.left, lamb_engine.nodes.Church)
2022-11-01 21:29:39 -07:00
)
2022-10-31 08:20:27 -07:00
if only_macro:
stop_reason = StopReason.SHOW_MACRO
2022-11-11 17:20:59 -08:00
m, node = lamb_engine.nodes.expand(node, force_all = only_macro)
2022-10-31 08:27:56 -07:00
macro_expansions += m
2022-10-31 08:20:27 -07:00
2022-11-09 21:47:55 -08:00
if len(warnings) != 0:
2022-11-11 17:20:59 -08:00
printf(FormattedText(warnings), style = lamb_engine.utils.style)
2022-10-21 14:44:52 -07:00
2022-11-11 15:30:21 -08:00
if self.step_reduction:
printf(FormattedText([
("class:warn", "Step-by-step reduction is enabled.\n"),
("class:muted", "Press "),
("class:cmd_key", "ctrl-c"),
("class:muted", " to continue automatically.\n"),
("class:muted", "Press "),
("class:cmd_key", "enter"),
("class:muted", " to step.\n"),
2022-11-11 17:20:59 -08:00
]), style = lamb_engine.utils.style)
2022-10-21 17:05:25 -07:00
2022-11-11 15:30:21 -08:00
skip_to_end = False
2022-11-12 17:47:41 -08:00
try:
while (
(
(self.reduction_limit is None) or
(k < self.reduction_limit)
) and not only_macro
2022-11-11 15:30:21 -08:00
):
2022-10-28 18:58:25 -07:00
2022-11-12 17:47:41 -08:00
# Show reduction count
if (
( (k >= self.iter_update) and (k % self.iter_update == 0) )
and not (self.step_reduction and not skip_to_end)
):
print(f" Reducing... {k:,}", end = "\r")
# Reduce
2022-11-11 17:20:59 -08:00
red_type, node = lamb_engine.nodes.reduce(node)
2022-11-12 17:47:41 -08:00
# If we can't reduce this expression anymore,
# it's in beta-normal form.
if red_type == lamb_engine.nodes.ReductionType.NOTHING:
stop_reason = StopReason.BETA_NORMAL
break
# Count reductions
k += 1
if red_type == lamb_engine.nodes.ReductionType.FUNCTION_APPLY:
macro_expansions += 1
# Pause after step if necessary
if self.step_reduction and not skip_to_end:
try:
s = prompt(
message = FormattedText([
("class:prompt", lamb_engine.nodes.reduction_text[red_type]),
("class:prompt", f":{k:03} ")
] + lamb_engine.utils.lex_str(str(node))),
style = lamb_engine.utils.style,
key_bindings = step_bindings
)
except KeyboardInterrupt or EOFError:
skip_to_end = True
printf(FormattedText([
("class:warn", "Skipping to end."),
]), style = lamb_engine.utils.style)
# Gracefully catch keyboard interrupts
except KeyboardInterrupt:
stop_reason = StopReason.INTERRUPT
2022-11-11 15:30:21 -08:00
2022-11-11 15:45:22 -08:00
# Print a space between step messages
2022-11-11 15:30:21 -08:00
if self.step_reduction:
print("")
2022-11-11 15:45:22 -08:00
# Clear reduction counter if it was printed
2022-10-31 08:20:27 -07:00
if k >= self.iter_update:
print(" " * round(14 + math.log10(k)), end = "\r")
2022-10-21 17:05:25 -07:00
2022-11-11 15:45:22 -08:00
# Expand fully if necessary
if self.full_expansion:
2022-11-11 17:20:59 -08:00
o, node = lamb_engine.nodes.expand(node, force_all = True)
2022-11-11 15:45:22 -08:00
macro_expansions += o
if only_macro:
out_text += [
2022-11-01 21:29:39 -07:00
("class:ok", f"Displaying macro content")
]
else:
2022-11-12 19:31:12 -08:00
if not self.step_reduction:
out_text += [
("class:ok", f"Runtime: "),
("class:text", f"{time.time() - start_time:.03f} seconds"),
("class:text", "\n")
]
2022-11-12 19:31:12 -08:00
out_text += [
("class:ok", f"Exit reason: "),
stop_reason.value,
2022-11-12 19:31:12 -08:00
("class:text", "\n"),
2022-11-12 19:31:12 -08:00
("class:ok", f"Macro expansions: "),
("class:text", f"{macro_expansions:,}"),
2022-11-12 19:31:12 -08:00
("class:text", "\n"),
2022-10-22 18:53:40 -07:00
2022-11-12 19:31:12 -08:00
("class:ok", f"Reductions: "),
("class:text", f"{k:,}\t"),
("class:muted", f"(Limit: {self.reduction_limit:,})")
]
2022-10-21 17:05:25 -07:00
2022-11-11 15:45:22 -08:00
if self.full_expansion:
out_text += [
2022-11-12 19:31:12 -08:00
("class:text", "\n"),
("class:ok", "All macros have been expanded")
2022-11-11 15:45:22 -08:00
]
if (
stop_reason == StopReason.BETA_NORMAL or
stop_reason == StopReason.LOOP_DETECTED or
only_macro
):
2022-10-28 19:05:38 -07:00
out_text += [
2022-11-11 17:04:05 -08:00
("class:ok", "\n\n => ")
2022-11-11 17:20:59 -08:00
] + lamb_engine.utils.lex_str(str(node))
2022-10-22 18:53:40 -07:00
2022-10-29 15:44:17 -07:00
2022-10-28 19:05:38 -07:00
printf(
FormattedText(out_text),
2022-11-11 17:20:59 -08:00
style = lamb_engine.utils.style
2022-10-28 19:05:38 -07:00
)
2022-10-28 14:19:29 -07:00
2022-11-11 15:45:22 -08:00
# Save to history
# Do this at the end so we don't always fully expand.
2022-11-11 17:04:05 -08:00
self.history.appendleft(
2022-11-11 17:20:59 -08:00
lamb_engine.nodes.expand( # type: ignore
2022-11-11 17:04:05 -08:00
node,
force_all = True
)[1]
)
2022-11-11 15:45:22 -08:00
2022-10-28 14:19:29 -07:00
def save_macro(
self,
macro: MacroDef,
*,
silent = False
) -> None:
2022-10-22 18:53:40 -07:00
was_rewritten = macro.label in self.macro_table
self.macro_table[macro.label] = macro.expr
if not silent:
printf(FormattedText([
("class:text", "Set "),
2022-11-01 21:29:39 -07:00
("class:code", macro.label),
2022-10-22 18:53:40 -07:00
("class:text", " to "),
2022-11-01 21:31:18 -07:00
("class:code", str(macro.expr))
2022-11-11 17:20:59 -08:00
]), style = lamb_engine.utils.style)
2022-10-21 17:05:25 -07:00
2022-10-21 14:40:49 -07:00
# Apply a list of definitions
2022-10-28 14:19:29 -07:00
def run(
self,
line: str,
*,
silent = False
) -> None:
2022-11-09 21:47:55 -08:00
e, w = self.parse(line)
2022-10-21 14:40:49 -07:00
2022-10-21 17:05:25 -07:00
# If this line is a macro definition, save the macro.
2022-10-28 14:19:29 -07:00
if isinstance(e, MacroDef):
2022-10-22 18:53:40 -07:00
self.save_macro(e, silent = silent)
2022-10-21 17:05:25 -07:00
# If this line is a command, do the command.
2022-10-28 14:19:29 -07:00
elif isinstance(e, Command):
2022-11-07 20:15:04 -08:00
if e.name not in cmd.commands:
2022-10-28 16:01:58 -07:00
printf(
FormattedText([
("class:warn", f"Unknown command \"{e.name}\"")
]),
2022-11-11 17:20:59 -08:00
style = lamb_engine.utils.style
2022-10-28 16:01:58 -07:00
)
else:
2022-11-07 20:15:04 -08:00
cmd.commands[e.name](e, self)
2022-10-21 17:05:25 -07:00
# If this line is a plain expression, reduce it.
2022-11-11 17:20:59 -08:00
elif isinstance(e, lamb_engine.nodes.Node):
2022-11-09 21:47:55 -08:00
self.reduce(e, warnings = w)
# We shouldn't ever get here.
2022-10-21 19:39:45 -07:00
else:
raise TypeError(f"I don't know what to do with a {type(e)}")
2022-10-21 14:40:49 -07:00
2022-10-21 17:05:25 -07:00
def run_lines(self, lines: list[str]):
2022-10-21 14:40:49 -07:00
for l in lines:
2022-10-22 18:53:40 -07:00
self.run(l, silent = True)