Cleaned up parser

master
Mark 2022-10-22 12:59:42 -07:00
parent 3cd0762d16
commit 218af2cd49
Signed by: Mark
GPG Key ID: AD62BB059C2AAEE4
7 changed files with 170 additions and 120 deletions

View File

@ -7,6 +7,7 @@
"Packrat", "Packrat",
"pyparsing", "pyparsing",
"runstatus", "runstatus",
"srange",
"subvar" "subvar"
], ],
"python.analysis.typeCheckingMode": "basic" "python.analysis.typeCheckingMode": "basic"

View File

@ -15,7 +15,6 @@
- Command and macro autocomplete - Command and macro autocomplete
- step-by-step reduction - step-by-step reduction
- Maybe a better icon? - Maybe a better icon?
- 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 - PyPi package

View File

@ -52,13 +52,11 @@ r.run_lines([
"AND = λab.(a F b)", "AND = λab.(a F b)",
"OR = λab.(a T b)", "OR = λab.(a T b)",
"XOR = λab.(a (NOT a b) b)", "XOR = λab.(a (NOT a b) b)",
"w = λx.(x x)", "M = λx.(x x)",
"W = w w", "W = M M",
"Y = λf.( (λx.(f (x x))) (λx.(f (x x))) )", "Y = λf.( (λx.(f (x x))) (λx.(f (x x))) )",
"PAIR = λabi.( i a b )", "PAIR = λabi.( i a b )",
"inc = λnfa.(f (n f a))", "S = λnfa.(f (n f a))",
"zero = λax.x",
"one = λfx.(f x)"
]) ])
@ -89,7 +87,7 @@ while True:
("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}."),
("class:text", "\n") ("class:text", "\n")
])) ]), style = utils.style)
continue continue
except tokens.ReductionError as e: except tokens.ReductionError as e:
printf(FormattedText([ printf(FormattedText([

View File

@ -1,82 +1,96 @@
import pyparsing as pp import pyparsing as pp
# Packrat gives a MAD speed boost. # Packrat gives a significant speed boost.
pp.ParserElement.enablePackrat() pp.ParserElement.enablePackrat()
import lamb.tokens as tokens class LambdaParser:
import lamb.utils as utils def make_parser(self):
self.lp = pp.Suppress("(")
self.rp = pp.Suppress(")")
self.pp_expr = pp.Forward()
class Parser: # Bound variables are ALWAYS lowercase and single-character.
lp = pp.Suppress("(") # We still create macro objects from them, they are turned into
rp = pp.Suppress(")") # bound variables after the expression is built.
self.pp_macro = pp.Word(pp.alphas + "_")
self.pp_bound = pp.Char(pp.srange("[a-z]"))
self.pp_name = self.pp_bound ^ self.pp_macro
self.pp_church = pp.Word(pp.nums)
# Simple tokens # Function calls.
pp_expr = pp.Forward() #
pp_macro = pp.Word(pp.alphas + "_") # <exp> <exp>
pp_macro.set_parse_action(tokens.macro.from_parse) # <exp> <exp> <exp>
self.pp_call = pp.Forward()
self.pp_call <<= (self.pp_expr | self.pp_bound)[2, ...]
pp_church = pp.Word(pp.nums) # Function definitions, right associative.
pp_church.set_parse_action(utils.autochurch) # Function args MUST be lowercase.
#
# <var> => <exp>
self.pp_lambda_fun = (
(pp.Suppress("λ") | pp.Suppress("\\")) +
pp.Group(self.pp_bound[1, ...]) +
pp.Suppress(".") +
(self.pp_expr ^ self.pp_call)
)
# Function calls. # Assignment.
# `tokens.lambda_apply.from_parse` handles chained calls. # Can only be found at the start of a line.
# #
# <exp> <exp> # <name> = <exp>
# <exp> <exp> <exp> self.pp_macro_def = (
pp_call = pp.Forward() pp.line_start() +
pp_call <<= pp_expr[2, ...] self.pp_macro +
pp_call.set_parse_action(tokens.lambda_apply.from_parse) pp.Suppress("=") +
(self.pp_expr ^ self.pp_call)
)
# Function definitions. self.pp_expr <<= (
# Right associative. self.pp_church ^
# self.pp_lambda_fun ^
# <var> => <exp> self.pp_name ^
pp_lambda_fun = ( (self.lp + self.pp_expr + self.rp) ^
(pp.Suppress("λ") | pp.Suppress("\\")) + (self.lp + self.pp_call + self.rp)
pp.Group(pp.Char(pp.alphas)[1, ...]) + )
pp.Suppress(".") +
(pp_expr ^ pp_call)
)
pp_lambda_fun.set_parse_action(tokens.lambda_func.from_parse)
# Assignment. self.pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_") + pp.Word(pp.alphas + "_")[0, ...]
# Can only be found at the start of a line.
#
# <name> = <exp>
pp_macro_def = (
pp.line_start() +
pp_macro +
pp.Suppress("=") +
(pp_expr ^ pp_call)
)
pp_macro_def.set_parse_action(tokens.macro_expression.from_parse)
pp_expr <<= (
pp_church ^
pp_lambda_fun ^
pp_macro ^
(lp + pp_expr + rp) ^
(lp + pp_call + rp)
)
pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_") + pp.Word(pp.alphas + "_")[0, ...]
pp_command.set_parse_action(tokens.command.from_parse)
pp_all = ( self.pp_all = (
pp_expr ^ self.pp_expr ^
pp_macro_def ^ self.pp_macro_def ^
pp_command ^ self.pp_command ^
pp_call self.pp_call
) )
@staticmethod def __init__(
def parse_line(line): self,
return Parser.pp_all.parse_string( *,
action_command,
action_macro_def,
action_church,
action_func,
action_bound,
action_macro,
action_apply
):
self.make_parser()
self.pp_command.set_parse_action(action_command)
self.pp_macro_def.set_parse_action(action_macro_def)
self.pp_church.set_parse_action(action_church)
self.pp_lambda_fun.set_parse_action(action_func)
self.pp_macro.set_parse_action(action_macro)
self.pp_bound.set_parse_action(action_bound)
self.pp_call.set_parse_action(action_apply)
def parse_line(self, line: str):
return self.pp_all.parse_string(
line, line,
parse_all = True parse_all = True
)[0] )[0]
@staticmethod def run_tests(self, lines: list[str]):
def run_tests(lines): return self.pp_all.run_tests(lines)
return Parser.pp_all.run_tests(lines)

View File

@ -1,8 +1,9 @@
from prompt_toolkit import PromptSession from prompt_toolkit import PromptSession
import lamb.commands as commands import lamb.commands as commands
from lamb.parser import Parser from lamb.parser import LambdaParser
import lamb.tokens as tokens import lamb.tokens as tokens
import lamb.utils as utils
import lamb.runstatus as rs import lamb.runstatus as rs
@ -11,11 +12,25 @@ class Runner:
self.macro_table = {} self.macro_table = {}
self.prompt_session = prompt_session self.prompt_session = prompt_session
self.prompt_message = prompt_message self.prompt_message = prompt_message
self.parser = LambdaParser(
action_command = tokens.command.from_parse,
action_macro_def = tokens.macro_expression.from_parse,
action_church = utils.autochurch(self),
action_func = tokens.lambda_func.from_parse,
action_bound = tokens.macro.from_parse,
action_macro = tokens.macro.from_parse,
action_apply = tokens.lambda_apply.from_parse
)
# 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
# Ensure bound variables are unique.
# This is automatically incremented whenever we make
# a bound variable.
self.bound_variable_counter = 0
def prompt(self): def prompt(self):
return self.prompt_session.prompt(message = self.prompt_message) return self.prompt_session.prompt(message = self.prompt_message)
@ -29,7 +44,8 @@ class Runner:
macro_expansions = 0 macro_expansions = 0
while (self.reduction_limit is None) or (i < self.reduction_limit): while (self.reduction_limit is None) or (i < self.reduction_limit):
r = expr.reduce(self.macro_table) print(repr(expr))
r = expr.reduce()
expr = r.output expr = r.output
# If we can't reduce this expression anymore, # If we can't reduce this expression anymore,
@ -55,13 +71,15 @@ 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, *, macro_only = False) -> rs.RunStatus:
e = Parser.parse_line(line) e = self.parser.parse_line(line)
# Give the elements of this expression access to the runner.
# Runner must be set BEFORE variables are bound.
e.set_runner(self)
e.bind_variables()
# If this line is a macro definition, save the macro. # If this line is a macro definition, save the macro.
if isinstance(e, tokens.macro_expression): if isinstance(e, tokens.macro_expression):
was_rewritten = e.label in self.macro_table was_rewritten = e.label in self.macro_table
e.exp.bind_variables()
self.macro_table[e.label] = e.exp self.macro_table[e.label] = e.exp
return rs.MacroStatus( return rs.MacroStatus(
@ -80,7 +98,6 @@ class Runner:
# 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()
return self.reduce_expression(e) return self.reduce_expression(e)
# We shouldn't ever get here. # We shouldn't ever get here.

View File

@ -40,16 +40,18 @@ class ReductionStatus:
# this will be false. # this will be false.
self.was_reduced = was_reduced self.was_reduced = was_reduced
class LambdaToken: class LambdaToken:
""" """
Base class for all lambda tokens. Base class for all lambda tokens.
""" """
def set_runner(self, runner):
self.runner = runner
def bind_variables(self) -> None: def bind_variables(self) -> None:
pass pass
def reduce(self, macro_table) -> ReductionStatus: def reduce(self) -> ReductionStatus:
return ReductionStatus( return ReductionStatus(
was_reduced = False, was_reduced = False,
output = self output = self
@ -121,7 +123,6 @@ class macro(LambdaToken):
def reduce( def reduce(
self, self,
macro_table = {},
*, *,
# To keep output readable, we avoid expanding macros as often as possible. # To keep output readable, we avoid expanding macros as often as possible.
# Macros are irreducible if force_substitute is false. # Macros are irreducible if force_substitute is false.
@ -132,10 +133,10 @@ class macro(LambdaToken):
auto_free_vars = True auto_free_vars = True
) -> ReductionStatus: ) -> ReductionStatus:
if (self.name in macro_table) and force_substitute: if (self.name in self.runner.macro_table) and force_substitute:
if force_substitute: # Only expand macros if we NEED to if force_substitute: # Only expand macros if we NEED to
return ReductionStatus( return ReductionStatus(
output = macro_table[self.name], output = self.runner.macro_table[self.name],
reduction_type = ReductionType.MACRO_EXPAND, reduction_type = ReductionType.MACRO_EXPAND,
was_reduced = True was_reduced = True
) )
@ -173,6 +174,11 @@ class macro_expression:
result[1] result[1]
) )
def set_runner(self, runner):
self.exp.set_runner(runner)
def bind_variables(self):
self.exp.bind_variables()
def __init__(self, label: str, exp: LambdaToken): def __init__(self, label: str, exp: LambdaToken):
self.label = label self.label = label
self.exp = exp self.exp = exp
@ -184,14 +190,14 @@ class macro_expression:
return f"{self.label} := {self.exp}" return f"{self.label} := {self.exp}"
bound_variable_counter = 0
class bound_variable(LambdaToken): class bound_variable(LambdaToken):
def __init__(self, forced_id = None): def __init__(self, name: str, *, runner, forced_id = None):
global bound_variable_counter self.original_name = name
self.runner = runner
if forced_id is None: if forced_id is None:
self.identifier = bound_variable_counter self.identifier = self.runner.bound_variable_counter
bound_variable_counter += 1 self.runner.bound_variable_counter += 1
else: else:
self.identifier = forced_id self.identifier = forced_id
@ -201,7 +207,7 @@ class bound_variable(LambdaToken):
return self.identifier == other.identifier return self.identifier == other.identifier
def __repr__(self): def __repr__(self):
return f"<in {self.identifier}>" return f"<{self.original_name} {self.identifier}>"
class lambda_func(LambdaToken): class lambda_func(LambdaToken):
""" """
@ -219,15 +225,20 @@ class lambda_func(LambdaToken):
def from_parse(result): def from_parse(result):
if len(result[0]) == 1: if len(result[0]) == 1:
return lambda_func( return lambda_func(
macro(result[0][0]), result[0][0],
result[1] result[1]
) )
else: else:
return lambda_func( return lambda_func(
macro(result[0].pop(0)), result[0].pop(0),
lambda_func.from_parse(result) lambda_func.from_parse(result)
) )
def set_runner(self, runner):
self.runner = runner
self.input.set_runner(runner)
self.output.set_runner(runner)
def __init__( def __init__(
self, self,
input_var: macro | bound_variable, input_var: macro | bound_variable,
@ -280,10 +291,14 @@ class lambda_func(LambdaToken):
# We only need to check for collisions if we're # We only need to check for collisions if we're
# binding another function's variable. If this # binding another function's variable. If this
# function starts the bind chain, skip that step. # function starts the bind chain, skip that step.
if not ((placeholder is None) and (val is None)): if placeholder is not None:
if not binding_self and isinstance(self.input, macro): if not binding_self and isinstance(self.input, macro):
if self.input == placeholder: if self.input == placeholder:
raise ReductionError(f"Variable name conflict: \"{self.input.name}\"") raise ReductionError(f"Bound variable name conflict: \"{self.input.name}\"")
if self.input.name in self.runner.macro_table:
raise ReductionError(f"Bound variable name conflict: \"{self.input.name}\" is a macro")
# If this function's variables haven't been bound yet, # If this function's variables haven't been bound yet,
# bind them BEFORE binding the outer function's. # bind them BEFORE binding the outer function's.
@ -292,7 +307,10 @@ class lambda_func(LambdaToken):
# functions' variables, we won't be able to detect # functions' variables, we won't be able to detect
# name conflicts. # name conflicts.
if isinstance(self.input, macro) and not binding_self: if isinstance(self.input, macro) and not binding_self:
new_bound_var = bound_variable() new_bound_var = bound_variable(
self.input.name,
runner = self.runner
)
self.bind_variables( self.bind_variables(
self.input, self.input,
new_bound_var, new_bound_var,
@ -310,15 +328,15 @@ class lambda_func(LambdaToken):
elif isinstance(self.output, lambda_apply): elif isinstance(self.output, lambda_apply):
self.output.bind_variables(placeholder, val) self.output.bind_variables(placeholder, val)
def reduce(self, macro_table = {}) -> ReductionStatus: def reduce(self) -> ReductionStatus:
r = self.output.reduce(macro_table) r = self.output.reduce()
# If a macro becomes a free variable, # If a macro becomes a free variable,
# reduce twice. # reduce twice.
if r.reduction_type == ReductionType.MACRO_TO_FREE: if r.reduction_type == ReductionType.MACRO_TO_FREE:
self.output = r.output self.output = r.output
return self.reduce(macro_table) return self.reduce()
return ReductionStatus( return ReductionStatus(
was_reduced = r.was_reduced, was_reduced = r.was_reduced,
@ -394,6 +412,11 @@ class lambda_apply(LambdaToken):
)] + result[2:] )] + result[2:]
) )
def set_runner(self, runner):
self.runner = runner
self.fn.set_runner(runner)
self.arg.set_runner(runner)
def __init__( def __init__(
self, self,
fn: LambdaToken, fn: LambdaToken,
@ -416,9 +439,6 @@ class lambda_apply(LambdaToken):
""" """
Does exactly what lambda_func.bind_variables does, Does exactly what lambda_func.bind_variables does,
but acts on applications instead. but acts on applications instead.
There will be little documentation in this method,
see lambda_func.bind_variables.
""" """
if (placeholder is None) and (val != placeholder): if (placeholder is None) and (val != placeholder):
@ -474,7 +494,7 @@ class lambda_apply(LambdaToken):
new_arg new_arg
) )
def reduce(self, macro_table = {}) -> ReductionStatus: def reduce(self) -> ReductionStatus:
# If we can directly apply self.fn, do so. # If we can directly apply self.fn, do so.
if isinstance(self.fn, lambda_func): if isinstance(self.fn, lambda_func):
@ -491,17 +511,16 @@ class lambda_apply(LambdaToken):
# Macros must be reduced before we apply them as functions. # Macros must be reduced before we apply them as functions.
# This is the only place we force substitution. # This is the only place we force substitution.
r = self.fn.reduce( r = self.fn.reduce(
macro_table,
force_substitute = True force_substitute = True
) )
else: else:
r = self.fn.reduce(macro_table) r = self.fn.reduce()
# If a macro becomes a free variable, # If a macro becomes a free variable,
# reduce twice. # reduce twice.
if r.reduction_type == ReductionType.MACRO_TO_FREE: if r.reduction_type == ReductionType.MACRO_TO_FREE:
self.fn = r.output self.fn = r.output
return self.reduce(macro_table) return self.reduce()
if r.was_reduced: if r.was_reduced:
return ReductionStatus( return ReductionStatus(
@ -514,11 +533,11 @@ class lambda_apply(LambdaToken):
) )
else: else:
r = self.arg.reduce(macro_table) r = self.arg.reduce()
if r.reduction_type == ReductionType.MACRO_TO_FREE: if r.reduction_type == ReductionType.MACRO_TO_FREE:
self.arg = r.output self.arg = r.output
return self.reduce(macro_table) return self.reduce()
return ReductionStatus( return ReductionStatus(
was_reduced = r.was_reduced, was_reduced = r.was_reduced,

View File

@ -6,31 +6,33 @@ from importlib.metadata import version
import lamb.tokens as tokens import lamb.tokens as tokens
def autochurch(results): def autochurch(runner):
""" """
Makes a church numeral from an integer. Makes a church numeral from an integer.
""" """
num = int(results[0]) def inner(results):
num = int(results[0])
f = tokens.bound_variable() f = tokens.bound_variable("f", runner = runner)
a = tokens.bound_variable() a = tokens.bound_variable("a", runner = runner)
chain = a chain = a
for i in range(num): for i in range(num):
chain = tokens.lambda_apply(f, chain) chain = tokens.lambda_apply(f, chain)
return tokens.lambda_func( return tokens.lambda_func(
f, f,
tokens.lambda_func( tokens.lambda_func(
a, a,
chain chain
)
) )
) return inner
style = Style.from_dict({ style = Style.from_dict({ # type: ignore
# Basic formatting # Basic formatting
"text": "#FFFFFF", "text": "#FFFFFF",
"warn": "#FFFF00", "warn": "#FFFF00",