Compare commits

...

5 Commits

Author SHA1 Message Date
Mark 1a4ed032c2
Added basic command handling 2022-10-21 14:44:52 -07:00
Mark f8ff0a0c3e
Changed parsing syntax 2022-10-21 14:41:24 -07:00
Mark 5f4f3db48f
Improved reduction process 2022-10-21 14:40:59 -07:00
Mark c671fc6f9a
Added runner 2022-10-21 14:40:49 -07:00
Mark 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 runner import Runner
import tokens
import colorama as cr
import greeting
class lambda_runner:
def __init__(self):
self.macro_table = {}
self.expr = None
# Replace "\" with a pretty "λ" in the prompt
bindings = KeyBindings()
@bindings.add("\\")
def _(event):
event.current_buffer.insert_text("λ")
# Apply a list of definitions
def run_names(self, lines):
print("Added names:")
for l in lines:
if isinstance(l, str):
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)))"
+ ")"
session = PromptSession(
message = FormattedText([
("#00FFFF", "~~> ")
]),
key_bindings = bindings
)
print(repr(r.expr))
print("")
outs = [str(r.expr)]
for i in range(300):
x = r.run()
s = str(x)
p = s if len(s) < 100 else s[:97] + "..."
greeting.show()
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
if x is None:
print("\nCannot evaluate any further.")
break
if i.strip() == "":
continue
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("(")
rp = pp.Suppress(")")
func_char = pp.Suppress("->")
macro_char = pp.Suppress("=")
# Simple tokens
pp_expr = pp.Forward()
pp_name = pp.Word(pp.alphas + "_")
pp_name.set_parse_action(tokens.macro.from_parse)
pp_macro = pp.Word(pp.alphas + "_")
pp_macro.set_parse_action(tokens.macro.from_parse)
# Function definitions.
# Right associative.
#
# <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)
# Assignment.
# Can only be found at the start of a line.
#
# <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)
# Function calls.
@ -47,18 +45,24 @@ class Parser:
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_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
@staticmethod
def parse_expression(line):
return Parser.pp_expr.parse_string(line, parse_all = True)[0]
pp_command = pp.Suppress(":") + pp.Word(pp.alphas + "_")
pp_command.set_parse_action(tokens.command.from_parse)
@staticmethod
def parse_assign(line):
return (
Parser.pp_macro_def
).parse_string(line, parse_all = True)[0]
def parse_line(line):
k = (
Parser.pp_expr ^
Parser.pp_macro_def ^
Parser.pp_command ^ Parser.pp_call
).parse_string(
line,
parse_all = True
)[0]
print(k)
return k
@staticmethod
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.
@ -22,13 +68,22 @@ class free_variable:
def __str__(self):
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,
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
easier.
@ -54,14 +109,21 @@ class macro:
raise TypeError("Can only compare macro with macro")
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:
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:
raise NameError(f"Name {self.name} is not defined!")
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:
"""
@ -80,7 +142,7 @@ class macro_expression:
result[1]
)
def __init__(self, label, exp):
def __init__(self, label: str, exp: LambdaToken):
self.label = label
self.exp = exp
@ -91,11 +153,8 @@ class macro_expression:
return f"{self.label} := {self.exp}"
bound_variable_counter = 0
class bound_variable:
class bound_variable(LambdaToken):
def __init__(self, forced_id = None):
global bound_variable_counter
@ -113,7 +172,7 @@ class bound_variable:
def __repr__(self):
return f"<in {self.identifier}>"
class lambda_func:
class lambda_func(LambdaToken):
"""
Represents a function.
Defined like λa.aa
@ -132,9 +191,13 @@ class lambda_func:
result[1]
)
def __init__(self, input_var, output):
self.input = input_var
self.output = output
def __init__(
self,
input_var: macro | bound_variable,
output: LambdaToken
):
self.input: macro | bound_variable = input_var
self.output: LambdaToken = output
def __repr__(self) -> str:
return f"<{self.input!r}{self.output!r}>"
@ -142,7 +205,6 @@ class lambda_func:
def __str__(self) -> str:
return f"λ{self.input}.{self.output}"
def bind_variables(
self,
placeholder: macro | None = None,
@ -211,30 +273,26 @@ class lambda_func:
elif isinstance(self.output, lambda_apply):
self.output.bind_variables(placeholder, val)
# Expand this function's output.
# 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)
def reduce(self, macro_table = {}) -> ReductionStatus:
# If the macro becomes a free variable, expand again.
if isinstance(new_out, free_variable):
lambda_func(
self.input,
new_out
).expand(macro_table)
r = self.output.reduce(macro_table)
elif isinstance(self.output, lambda_func):
new_out = self.output.expand(macro_table)
elif isinstance(self.output, lambda_apply):
new_out = self.output.expand(macro_table)
return lambda_func(
self.input,
new_out
# If a macro becomes a free variable,
# reduce twice.
if r.reduction_type == ReductionType.MACRO_TO_FREE:
self.output = r.output
return self.reduce(macro_table)
return ReductionStatus(
was_reduced = r.was_reduced,
reduction_type = r.reduction_type,
output = lambda_func(
self.input,
r.output
)
)
def apply(
self,
val,
@ -259,8 +317,6 @@ class 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)
else:
raise TypeError("Cannot apply a function to {self.output!r}")
# If we're applying THIS function,
# just give the output
@ -276,7 +332,7 @@ class lambda_func:
)
class lambda_apply:
class lambda_apply(LambdaToken):
"""
Represents a function application.
Has two elements: fn, the function,
@ -303,11 +359,11 @@ class lambda_apply:
def __init__(
self,
fn,
arg
fn: LambdaToken,
arg: LambdaToken
):
self.fn = fn
self.arg = arg
self.fn: LambdaToken = fn
self.arg: LambdaToken = arg
def __repr__(self) -> str:
return f"<{self.fn!r} | {self.arg!r}>"
@ -381,40 +437,48 @@ class lambda_apply:
new_arg
)
def expand(self, macro_table = {}):
# If fn is a function, apply it.
def reduce(self, macro_table = {}) -> ReductionStatus:
# If we can directly apply self.fn, do so.
if isinstance(self.fn, lambda_func):
return self.fn.apply(self.arg)
# If fn is an application or macro, expand it.
elif isinstance(self.fn, macro):
f = lambda_apply(
m := self.fn.expand(macro_table),
self.arg
return ReductionStatus(
was_reduced = True,
reduction_type = ReductionType.FUNCTION_APPLY,
output = self.fn.apply(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,
# expand twice.
if isinstance(m, free_variable):
return f.expand(macro_table)
# reduce twice.
if r.reduction_type == ReductionType.MACRO_TO_FREE:
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:
return f
r = self.arg.reduce(macro_table)
elif isinstance(self.fn, lambda_apply):
return lambda_apply(
self.fn.expand(macro_table),
self.arg
)
if r.reduction_type == ReductionType.MACRO_TO_FREE:
self.arg = r.output
return self.reduce(macro_table)
# If we get to this point, the function we're applying
# can't be expanded. That means it's a free or bound
# variable. If that happens, expand the arg instead.
elif (
isinstance(self.arg, lambda_apply) or
isinstance(self.arg, lambda_func)
):
return lambda_apply(
self.fn,
self.arg.expand(macro_table)
)
return self
return ReductionStatus(
was_reduced = r.was_reduced,
reduction_type = r.reduction_type,
output = lambda_apply(
self.fn,
r.output
)
)