Compare commits

..

3 Commits

Author SHA1 Message Date
Mark 8558c484c5
Fixed parser, added macro printout 2022-10-21 19:55:15 -07:00
Mark ee744b5245
Type fixes 2022-10-21 19:39:45 -07:00
Mark b5d97cf5c6
Added Church numeral generation 2022-10-21 19:39:37 -07:00
8 changed files with 92 additions and 53 deletions

View File

@ -1,7 +1,9 @@
{
"cSpell.words": [
"autochurch",
"freevar",
"pyparsing",
"runstatus",
"subvar"
],
"python.analysis.typeCheckingMode": "basic"

View File

@ -2,15 +2,12 @@
## Todo (pre-release):
- Fix parser (call parentheses)
- Good command parsing (`:save`, `:load`, are a bare minimum)
- Python files: installable, package list, etc
- $\alpha$-equivalence check
- Versioning
- Automatic church numerals
- Prettyprint functions (combine args, rename bound variables)
- Documentation in README
- Don't crash on errors
## Todo:
- live syntax check
@ -20,6 +17,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?
## Mention in Docs
- lambda functions only work with single-letter arguments
- church numerals

10
main.py
View File

@ -43,7 +43,7 @@ r.run_lines([
"OR = λab.(a T b)",
"XOR = λab.(a (NOT a b) b)",
"w = λx.(x x)",
"W = (w w)",
"W = w w",
"Y = λf.( (λx.(f (x x))) (λx.(f (x x))) )",
"PAIR = λabi.( i a b )",
"inc = λnfa.(f (n f a))",
@ -90,7 +90,13 @@ while True:
# If this line defined a macro, print nothing.
if isinstance(x, runner.MacroStatus):
pass
printf(FormattedText([
("#FFFFFF", "Set "),
("#FF00FF", x.macro_label),
("#FFFFFF", " to "),
("#FFFFFF", str(x.macro_expr))
]))
if isinstance(x, runner.CommandStatus):
printf(x.formatted_text)

View File

@ -1,17 +1,8 @@
import pyparsing as pp
import tokens
import utils
class Parser:
"""
Macro_def must be on its own line.
macro_def :: var = expr
var :: word
lambda_fun :: var -> expr
call :: '(' (var | expr) ')' +
expr :: define | var | call | '(' expr ')'
"""
lp = pp.Suppress("(")
rp = pp.Suppress(")")
@ -20,6 +11,18 @@ class Parser:
pp_macro = pp.Word(pp.alphas + "_")
pp_macro.set_parse_action(tokens.macro.from_parse)
pp_church = pp.Word(pp.nums)
pp_church.set_parse_action(utils.autochurch)
# Function calls.
# `tokens.lambda_apply.from_parse` handles chained calls.
#
# <exp> <exp>
# <exp> <exp> <exp>
pp_call = pp.Forward()
pp_call <<= pp_expr[2, ...]
pp_call.set_parse_action(tokens.lambda_apply.from_parse)
# Function definitions.
# Right associative.
#
@ -28,41 +31,44 @@ class Parser:
(pp.Suppress("λ") | pp.Suppress("\\")) +
pp.Group(pp.Char(pp.alphas)[1, ...]) +
pp.Suppress(".") +
pp_expr
(pp_expr ^ pp_call)
)
pp_lambda_fun.set_parse_action(tokens.lambda_func.from_parse)
# Assignment.
# Can only be found at the start of a line.
#
# <var> = <exp>
pp_macro_def = pp.line_start() + pp_macro + pp.Suppress("=") + pp_expr
# <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)
# Function calls.
# `tokens.lambda_func.from_parse` handles chained calls.
#
# <var>(<exp>)
# <var>(<exp>)(<exp>)(<exp>)
# (<exp>)(<exp>)
# (<exp>)(<exp>)(<exp>)(<exp>)
pp_call = pp.Forward()
pp_call <<= pp_expr[2, ...]
pp_call.set_parse_action(tokens.lambda_apply.from_parse)
pp_expr <<= pp_lambda_fun ^ (lp + pp_expr + rp) ^ pp_macro ^ (lp + pp_call + rp)
pp_all = pp_expr | pp_macro_def
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_command.set_parse_action(tokens.command.from_parse)
pp_all = (
pp_expr ^
pp_macro_def ^
pp_command ^
pp_call
)
@staticmethod
def parse_line(line):
k = (
Parser.pp_expr ^
Parser.pp_macro_def ^
Parser.pp_command ^ Parser.pp_call
).parse_string(
k = Parser.pp_all.parse_string(
line,
parse_all = True
)[0]
@ -71,4 +77,4 @@ class Parser:
@staticmethod
def run_tests(lines):
return Parser.pp_macro_def.run_tests(lines)
return Parser.pp_all.run_tests(lines)

View File

@ -10,10 +10,6 @@ from runstatus import ReduceStatus
from runstatus import CommandStatus
class Runner:
def __init__(self):
self.macro_table = {}
@ -43,7 +39,7 @@ class Runner:
i = 0
macro_expansions = 0
while i < self.reduction_limit:
while (self.reduction_limit is None) or (i < self.reduction_limit):
r = expr.reduce(self.macro_table)
expr = r.output
@ -64,7 +60,7 @@ class Runner:
return ReduceStatus(
reduction_count = i - macro_expansions,
stop_reason = StopReason.MAX_EXCEEDED,
result = r.output
result = r.output # type: ignore
)
@ -81,7 +77,8 @@ class Runner:
return MacroStatus(
was_rewritten = was_rewritten,
macro_label = e.label
macro_label = e.label,
macro_expr = e.exp
)
# If this line is a command, do the command.
@ -89,9 +86,11 @@ class Runner:
return self.exec_command(e.name)
# If this line is a plain expression, reduce it.
else:
elif isinstance(e, tokens.LambdaToken):
e.bind_variables()
return self.reduce_expression(e)
else:
raise TypeError(f"I don't know what to do with a {type(e)}")
def run_lines(self, lines: list[str]):

View File

@ -17,16 +17,19 @@ class MacroStatus(RunStatus):
Values:
`was_rewritten`: If true, an old macro was replaced.
`macro_label`: The name of the macro we just made.
`macro_expr`: The expr of the macro we just made.
"""
def __init__(
self,
*,
was_rewritten: bool,
macro_label: str
macro_label: str,
macro_expr
):
self.was_rewritten = was_rewritten
self.macro_label = macro_label
self.macro_expr = macro_expr
class StopReason(enum.Enum):

View File

@ -302,9 +302,9 @@ class lambda_func(LambdaToken):
# Bind variables inside this function.
if isinstance(self.output, macro):
if isinstance(self.output, macro) and placeholder is not None:
if self.output == placeholder:
self.output = val
self.output = val # type: ignore
elif isinstance(self.output, lambda_func):
self.output.bind_variables(placeholder, val)
elif isinstance(self.output, lambda_apply):
@ -345,7 +345,7 @@ class lambda_func(LambdaToken):
calling_self = False
if bound_var is None:
calling_self = True
bound_var = self.input
bound_var = self.input # type: ignore
new_out = self.output
if isinstance(self.output, bound_variable):
if self.output == bound_var:
@ -353,7 +353,7 @@ class lambda_func(LambdaToken):
elif isinstance(self.output, lambda_func):
new_out = self.output.apply(val, bound_var = bound_var)
elif isinstance(self.output, lambda_apply):
new_out = self.output.sub_bound_var(val, bound_var = bound_var)
new_out = self.output.sub_bound_var(val, bound_var = bound_var) # type: ignore
# If we're applying THIS function,
# just give the output
@ -430,7 +430,7 @@ class lambda_apply(LambdaToken):
# everything below should still work as expected.
if isinstance(self.fn, macro) and placeholder is not None:
if self.fn == placeholder:
self.fn = val
self.fn = val # type: ignore
elif isinstance(self.fn, lambda_func):
self.fn.bind_variables(placeholder, val)
elif isinstance(self.fn, lambda_apply):
@ -438,7 +438,7 @@ class lambda_apply(LambdaToken):
if isinstance(self.arg, macro) and placeholder is not None:
if self.arg == placeholder:
self.arg = val
self.arg = val # type: ignore
elif isinstance(self.arg, lambda_func):
self.arg.bind_variables(placeholder, val)
elif isinstance(self.arg, lambda_apply):

24
utils.py Normal file
View File

@ -0,0 +1,24 @@
import tokens
def autochurch(results):
"""
Makes a church numeral from an integer.
"""
num = int(results[0])
f = tokens.bound_variable()
a = tokens.bound_variable()
chain = a
for i in range(num):
chain = tokens.lambda_apply(f, chain)
return tokens.lambda_func(
f,
tokens.lambda_func(
a,
chain
)
)