Compare commits

...

5 Commits

Author SHA1 Message Date
1a4ed032c2
Added basic command handling 2022-10-21 14:44:52 -07:00
f8ff0a0c3e
Changed parsing syntax 2022-10-21 14:41:24 -07:00
5f4f3db48f
Improved reduction process 2022-10-21 14:40:59 -07:00
c671fc6f9a
Added runner 2022-10-21 14:40:49 -07:00
9ed159dee9
Cleaned up greeting, add simple prompt, moved runner 2022-10-21 14:40:17 -07:00
5 changed files with 335 additions and 223 deletions

53
greeting.py Normal file
View File

@ -0,0 +1,53 @@
from prompt_toolkit.styles import Style
from prompt_toolkit.formatted_text import HTML, to_formatted_text
from prompt_toolkit import print_formatted_text
# | _.._ _.|_
# |_(_|| | ||_)
# 1.1.0
#
# __ __
# ,-` `` `,
# (` \ )
# (` \ `)
# (, / \ _)
# (` / \ )
# `'._.--._.'
#
# A λ calculus engine
style = Style.from_dict({
# Heading
"_h": "#FFFFFF bold",
# Version
"_v": "#B4EC85 bold",
# Lambda
"_l": "#FF6600 bold",
# Subtitle
"_s": "#B4EC85 bold"
})
html = HTML(f"""
<_h> | _.._ _.|_
|_(_|| | ||_)</_h>
<_v>1.1.0</_v>
__ __
,-` `` `,
(` <_l>\\</_l> )
(` <_l>\\</_l> `)
(, <_l>/ \\</_l> _)
(` <_l>/ \\</_l> )
`'._.--._.'
<_s> A λ calculus engine</_s>
"""[1:-1])
def show():
print_formatted_text(html, style = style)

207
main.py
View File

@ -1,146 +1,87 @@
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.formatted_text import to_plain_text
from prompt_toolkit.key_binding import KeyBindings
from pyparsing import exceptions as ppx
from parser import Parser from parser import Parser
from runner import Runner
import tokens import tokens
import colorama as cr import greeting
class lambda_runner: # Replace "\" with a pretty "λ" in the prompt
def __init__(self): bindings = KeyBindings()
self.macro_table = {} @bindings.add("\\")
self.expr = None def _(event):
event.current_buffer.insert_text("λ")
# Apply a list of definitions session = PromptSession(
def run_names(self, lines): message = FormattedText([
print("Added names:") ("#00FFFF", "~~> ")
for l in lines: ]),
if isinstance(l, str): key_bindings = bindings
e = Parser.parse_assign(l)
else:
e = l
if e.label in self.macro_table:
raise NameError(f"Label {e.label} exists!")
e.exp.bind_variables()
self.macro_table[e.label] = e.exp
print(f"\t{e}")
print("\n")
def set_expr(self, expr: str | None = None):
if expr == None:
self.expr = None
print("Removed expression.\n")
return
self.expr = Parser.parse_expression(expr)
self.expr.bind_variables()
print(f"Set expression to {self.expr}\n")
def run(self):
if isinstance(self.expr, tokens.lambda_apply):
self.expr = self.expr.expand(self.macro_table)
elif isinstance(self.expr, tokens.lambda_func):
self.expr = self.expr.expand(self.macro_table)
else:
return None
return self.expr
"""
| _.._ _.|_
|_(_|| | ||_)
1.1.0
__ __
,-` `` `,
(` \ )
(` \ `)
(, / \ _)
(` / \ )
`'._.--._.'
A λ calculus engine
"""
b = cr.Style.BRIGHT
v = cr.Fore.GREEN + cr.Style.BRIGHT
l = cr.Fore.RED + cr.Style.BRIGHT
n = cr.Style.RESET_ALL
t = cr.Fore.GREEN
print(f"""
{b} | _.._ _.|_
|_(_|| | ||_){n}
{v}1.1.0{n}
__ __
,-` `` `,
(` {l}\{n} )
(` {l}\{n} `)
(, {l}/ \{n} _)
(` {l}/ \{n} )
`'._.--._.'
{t} A λ calculus engine{n}
"""[1:-1])
r = lambda_runner()
r.run_names([
"T = a -> b -> a",
"F = a -> b -> a",
"NOT = a -> (a F T)",
"AND = a -> b -> (a F b)",
"OR = a -> b -> (a T b)",
"XOR = a -> b -> (a (NOT a b) b)"
])
r.run_names([
"w = x -> (x x)",
"W = (w w)",
"Y = f -> ( (x -> (f (x x))) (x -> (f (x x))) )",
#"l = if_true -> if_false -> which -> ( which if_true if_false )"
])
r.run_names([
"inc = n -> f -> x -> (f (n f x))",
"zero = a -> x -> x",
"one = f -> x -> (f x)",
])
print("\n")
#AND = r.run()
#OR = r.run()
#XOR = r.run()
r.set_expr(
"(" +
"inc (inc (inc (zero)))"
+ ")"
) )
print(repr(r.expr))
print("")
outs = [str(r.expr)] greeting.show()
for i in range(300):
x = r.run()
s = str(x)
p = s if len(s) < 100 else s[:97] + "..."
if s in outs:
print(p)
print("\nLoop detected, exiting.")
r = Runner()
r.run_lines([
"T = λa.λb.a",
"F = λa.λb.b",
"NOT = \\a.(a F T)",
#"AND = a -> b -> (a F b)",
#"OR = a -> b -> (a T b)",
#"XOR = a -> b -> (a (NOT a b) b)",
#"w = x -> (x x)",
#"W = (w w)",
#"Y = f -> ( (x -> (f (x x))) (x -> (f (x x))) )",
#"l = if_true -> if_false -> which -> ( which if_true if_false )"
#"inc = n -> f -> x -> (f (n f x))",
#"zero = a -> x -> x",
#"one = f -> x -> (f x)",
])
while True:
try:
i = session.prompt()
# Catch Ctrl-C and Ctrl-D
except KeyboardInterrupt:
print("")
break
except EOFError:
print("")
break break
if x is None: if i.strip() == "":
print("\nCannot evaluate any further.") continue
break
outs.append(s)
print(p)
print(f"Performed {i} {'operations' if i != 1 else 'operation'}.") try:
x = r.run(i)
except ppx.ParseException as e:
l = len(to_plain_text(session.message))
print_formatted_text(FormattedText([
("#FF0000", " "*(e.loc + l) + "^\n"),
("#FF0000", f"Syntax error at char {e.loc}."),
("#FFFFFF", "\n")
]))
continue
print_formatted_text(FormattedText([
("#00FF00", " = "),
("#FFFFFF", str(x))
]))
print("")

View File

@ -14,26 +14,24 @@ class Parser:
lp = pp.Suppress("(") lp = pp.Suppress("(")
rp = pp.Suppress(")") rp = pp.Suppress(")")
func_char = pp.Suppress("->")
macro_char = pp.Suppress("=")
# Simple tokens # Simple tokens
pp_expr = pp.Forward() pp_expr = pp.Forward()
pp_name = pp.Word(pp.alphas + "_") pp_macro = pp.Word(pp.alphas + "_")
pp_name.set_parse_action(tokens.macro.from_parse) pp_macro.set_parse_action(tokens.macro.from_parse)
# Function definitions. # Function definitions.
# Right associative. # Right associative.
# #
# <var> => <exp> # <var> => <exp>
pp_lambda_fun = pp_name + func_char + pp_expr pp_lambda_fun = (pp.Suppress("λ") | pp.Suppress("\\")) + pp_macro + pp.Suppress(".") + pp_expr
pp_lambda_fun.set_parse_action(tokens.lambda_func.from_parse) pp_lambda_fun.set_parse_action(tokens.lambda_func.from_parse)
# Assignment. # Assignment.
# Can only be found at the start of a line. # Can only be found at the start of a line.
# #
# <var> = <exp> # <var> = <exp>
pp_macro_def = pp.line_start() + pp_name + macro_char + pp_expr pp_macro_def = pp.line_start() + pp_macro + pp.Suppress("=") + pp_expr
pp_macro_def.set_parse_action(tokens.macro_expression.from_parse) pp_macro_def.set_parse_action(tokens.macro_expression.from_parse)
# Function calls. # Function calls.
@ -47,18 +45,24 @@ class Parser:
pp_call <<= pp_expr[2, ...] pp_call <<= pp_expr[2, ...]
pp_call.set_parse_action(tokens.lambda_apply.from_parse) pp_call.set_parse_action(tokens.lambda_apply.from_parse)
pp_expr <<= pp_lambda_fun ^ (lp + pp_expr + rp) ^ pp_name ^ (lp + pp_call + rp) pp_expr <<= pp_lambda_fun ^ (lp + pp_expr + rp) ^ pp_macro ^ (lp + pp_call + rp)
pp_all = pp_expr | pp_macro_def pp_all = pp_expr | pp_macro_def
@staticmethod pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_")
def parse_expression(line): pp_command.set_parse_action(tokens.command.from_parse)
return Parser.pp_expr.parse_string(line, parse_all = True)[0]
@staticmethod @staticmethod
def parse_assign(line): def parse_line(line):
return ( k = (
Parser.pp_macro_def Parser.pp_expr ^
).parse_string(line, parse_all = True)[0] Parser.pp_macro_def ^
Parser.pp_command ^ Parser.pp_call
).parse_string(
line,
parse_all = True
)[0]
print(k)
return k
@staticmethod @staticmethod
def run_tests(lines): def run_tests(lines):

50
runner.py Normal file
View File

@ -0,0 +1,50 @@
import tokens
from parser import Parser
class Runner:
def __init__(self):
self.macro_table = {}
self.expr = None
def exec_command(self, command: str):
if command == "help":
print("This is a help message.")
# Apply a list of definitions
def run(self, line: str):
e = Parser.parse_line(line)
if isinstance(e, tokens.macro_expression):
if e.label in self.macro_table:
raise NameError(f"Label {e.label} exists!")
e.exp.bind_variables()
self.macro_table[e.label] = e.exp
elif isinstance(e, tokens.command):
self.exec_command(e.name)
else:
e.bind_variables()
self.expr = e
outs = [str(e)]
for i in range(300):
r = self.expr.reduce(self.macro_table)
self.expr = r.output
s = str(r.output)
p = s if len(s) < 100 else s[:97] + "..."
#if s in outs:
#print(p)
#print("\nLoop detected, exiting.")
#break
if not r.was_reduced:
print("\nCannot evaluate any further.")
break
print(f"Performed {i} {'operations' if i != 1 else 'operation'}.")
return self.expr
def run_lines(self, lines):
for l in lines:
self.run(l)

216
tokens.py
View File

@ -1,7 +1,53 @@
from typing import Type from ast import Lambda
import enum
class ReductionType(enum.Enum):
MACRO_EXPAND = enum.auto()
MACRO_TO_FREE = enum.auto()
FUNCTION_APPLY = enum.auto()
class free_variable: class ReductionStatus:
"""
This object helps organize reduction output.
An instance is returned after every reduction step.
"""
def __init__(
self,
*,
output,
was_reduced: bool,
reduction_type: ReductionType | None = None
):
# The new expression
self.output = output
# What did we do?
# Will be None if was_reduced is false.
self.reduction_type = reduction_type
# Did this reduction change anything?
# If we try to reduce an irreducible expression,
# this will be false.
self.was_reduced = was_reduced
class LambdaToken:
"""
Base class for all lambda tokens.
"""
def bind_variables(self) -> None:
pass
def reduce(self, macro_table) -> ReductionStatus:
return ReductionStatus(
was_reduced = False,
output = self
)
class free_variable(LambdaToken):
""" """
Represents a free variable. Represents a free variable.
@ -22,13 +68,22 @@ class free_variable:
def __str__(self): def __str__(self):
return f"{self.label}" return f"{self.label}"
class command:
@staticmethod
def from_parse(result):
return command(
result[0],
)
class macro: def __init__(self, name):
self.name = name
class macro(LambdaToken):
""" """
Represents a "macro" in lambda calculus, Represents a "macro" in lambda calculus,
a variable that expands to an expression. a variable that reduces to an expression.
These don't have inherent logic, they These don't have any inherent logic, they
just make writing and reading expressions just make writing and reading expressions
easier. easier.
@ -54,14 +109,21 @@ class macro:
raise TypeError("Can only compare macro with macro") raise TypeError("Can only compare macro with macro")
return self.name == other.name return self.name == other.name
def expand(self, macro_table = {}, *, auto_free_vars = True): def reduce(self, macro_table = {}, *, auto_free_vars = True) -> ReductionStatus:
if self.name in macro_table: if self.name in macro_table:
return macro_table[self.name] return ReductionStatus(
output = macro_table[self.name],
reduction_type = ReductionType.MACRO_EXPAND,
was_reduced = True
)
elif not auto_free_vars: elif not auto_free_vars:
raise NameError(f"Name {self.name} is not defined!") raise NameError(f"Name {self.name} is not defined!")
else: else:
return free_variable(self.name) return ReductionStatus(
output = free_variable(self.name),
reduction_type = ReductionType.MACRO_TO_FREE,
was_reduced = True
)
class macro_expression: class macro_expression:
""" """
@ -80,7 +142,7 @@ class macro_expression:
result[1] result[1]
) )
def __init__(self, label, exp): def __init__(self, label: str, exp: LambdaToken):
self.label = label self.label = label
self.exp = exp self.exp = exp
@ -91,11 +153,8 @@ class macro_expression:
return f"{self.label} := {self.exp}" return f"{self.label} := {self.exp}"
bound_variable_counter = 0 bound_variable_counter = 0
class bound_variable: class bound_variable(LambdaToken):
def __init__(self, forced_id = None): def __init__(self, forced_id = None):
global bound_variable_counter global bound_variable_counter
@ -113,7 +172,7 @@ class bound_variable:
def __repr__(self): def __repr__(self):
return f"<in {self.identifier}>" return f"<in {self.identifier}>"
class lambda_func: class lambda_func(LambdaToken):
""" """
Represents a function. Represents a function.
Defined like λa.aa Defined like λa.aa
@ -132,9 +191,13 @@ class lambda_func:
result[1] result[1]
) )
def __init__(self, input_var, output): def __init__(
self.input = input_var self,
self.output = output input_var: macro | bound_variable,
output: LambdaToken
):
self.input: macro | bound_variable = input_var
self.output: LambdaToken = output
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.input!r}{self.output!r}>" return f"<{self.input!r}{self.output!r}>"
@ -142,7 +205,6 @@ class lambda_func:
def __str__(self) -> str: def __str__(self) -> str:
return f"λ{self.input}.{self.output}" return f"λ{self.input}.{self.output}"
def bind_variables( def bind_variables(
self, self,
placeholder: macro | None = None, placeholder: macro | None = None,
@ -211,30 +273,26 @@ class lambda_func:
elif isinstance(self.output, lambda_apply): elif isinstance(self.output, lambda_apply):
self.output.bind_variables(placeholder, val) self.output.bind_variables(placeholder, val)
# Expand this function's output. def reduce(self, macro_table = {}) -> ReductionStatus:
# For functions, this isn't done unless
# its explicitly asked for.
def expand(self, macro_table = {}):
new_out = self.output
if isinstance(self.output, macro):
new_out = self.output.expand(macro_table)
# If the macro becomes a free variable, expand again. r = self.output.reduce(macro_table)
if isinstance(new_out, free_variable):
lambda_func(
self.input,
new_out
).expand(macro_table)
elif isinstance(self.output, lambda_func): # If a macro becomes a free variable,
new_out = self.output.expand(macro_table) # reduce twice.
elif isinstance(self.output, lambda_apply): if r.reduction_type == ReductionType.MACRO_TO_FREE:
new_out = self.output.expand(macro_table) self.output = r.output
return lambda_func( return self.reduce(macro_table)
self.input,
new_out return ReductionStatus(
was_reduced = r.was_reduced,
reduction_type = r.reduction_type,
output = lambda_func(
self.input,
r.output
)
) )
def apply( def apply(
self, self,
val, val,
@ -259,8 +317,6 @@ class lambda_func:
new_out = self.output.apply(val, bound_var = bound_var) new_out = self.output.apply(val, bound_var = bound_var)
elif isinstance(self.output, lambda_apply): 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)
else:
raise TypeError("Cannot apply a function to {self.output!r}")
# If we're applying THIS function, # If we're applying THIS function,
# just give the output # just give the output
@ -276,7 +332,7 @@ class lambda_func:
) )
class lambda_apply: class lambda_apply(LambdaToken):
""" """
Represents a function application. Represents a function application.
Has two elements: fn, the function, Has two elements: fn, the function,
@ -303,11 +359,11 @@ class lambda_apply:
def __init__( def __init__(
self, self,
fn, fn: LambdaToken,
arg arg: LambdaToken
): ):
self.fn = fn self.fn: LambdaToken = fn
self.arg = arg self.arg: LambdaToken = arg
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.fn!r} | {self.arg!r}>" return f"<{self.fn!r} | {self.arg!r}>"
@ -381,40 +437,48 @@ class lambda_apply:
new_arg new_arg
) )
def expand(self, macro_table = {}): def reduce(self, macro_table = {}) -> ReductionStatus:
# If fn is a function, apply it.
# If we can directly apply self.fn, do so.
if isinstance(self.fn, lambda_func): if isinstance(self.fn, lambda_func):
return self.fn.apply(self.arg) return ReductionStatus(
# If fn is an application or macro, expand it. was_reduced = True,
elif isinstance(self.fn, macro): reduction_type = ReductionType.FUNCTION_APPLY,
f = lambda_apply( output = self.fn.apply(self.arg)
m := self.fn.expand(macro_table),
self.arg
) )
# Otherwise, try to reduce self.fn.
# If that is impossible, try to reduce self.arg.
else:
r = self.fn.reduce(macro_table)
# If a macro becomes a free variable, # If a macro becomes a free variable,
# expand twice. # reduce twice.
if isinstance(m, free_variable): if r.reduction_type == ReductionType.MACRO_TO_FREE:
return f.expand(macro_table) self.fn = r.output
return self.reduce(macro_table)
if r.was_reduced:
return ReductionStatus(
was_reduced = True,
reduction_type = r.reduction_type,
output = lambda_apply(
r.output,
self.arg
)
)
else: else:
return f r = self.arg.reduce(macro_table)
elif isinstance(self.fn, lambda_apply): if r.reduction_type == ReductionType.MACRO_TO_FREE:
return lambda_apply( self.arg = r.output
self.fn.expand(macro_table), return self.reduce(macro_table)
self.arg
)
# If we get to this point, the function we're applying return ReductionStatus(
# can't be expanded. That means it's a free or bound was_reduced = r.was_reduced,
# variable. If that happens, expand the arg instead. reduction_type = r.reduction_type,
elif ( output = lambda_apply(
isinstance(self.arg, lambda_apply) or self.fn,
isinstance(self.arg, lambda_func) r.output
): )
return lambda_apply( )
self.fn,
self.arg.expand(macro_table)
)
return self