new-lang/checker/terms.py

558 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""
Terms are values in an equation. They can be constants, variables,
functions, operators, and many more.
"""
from __future__ import annotations
from typing import Callable, Generator
import copy
from dataclasses import dataclass
# Known const values that can be substituted into other values.
_known_consts : dict[str, Term] = {}
# Known functions to normalize certain specific terms with given shapes.
_known_norms : list[Callable[[Term], Term]] = []
USABLE_VAR_NAMES = [
'x', 'y', 'z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ',
# 'λ', # Avoid confusion
'μ', 'ν', 'ξ', 'ο', 'π', 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω'
]
def contains(haystack : Term, needle : Term) -> bool:
"""
Determine whether a given small term value is contained in a bigger
term.
:param haystack: The term to search through.
:type haystack: Term
:param needle: The term to find.
:type needle: Term
:return: Whether the needle was found in the haystack.
:rtype: bool
"""
for term in haystack.rec_iter():
if term == needle:
return True
else:
return False
def ecsl(s : str) -> str:
"""
Encapsulate something if it contains non-encapsulated spaces.
:param s: The string to potentially encapsulate.
:type s: str
:return: An encapsulated string.
:rtype: str
"""
def needs_encapsulation() -> bool:
if " " not in s:
return False
level = 0
for c in s:
match c:
case " ":
if level <= 0:
return True
case "(":
level += 1
case ")":
level -= 1
else:
return False
if needs_encapsulation():
return "(" + s + ")"
else:
return s
def normalize(term : Term) -> Term:
"""
Create a normalization function. This function simplifies functions as
much as possible.
"""
def norm_iter(term : Term) -> Term:
"""
Normalize a single term. Use both the standard normalization scheme
"""
for func in _known_norms:
term = func(term)
return term
return traverse(f=norm_iter, term=term)
def register_known_const(name : str, value : Term) -> None:
_known_consts[name] = value
def register_known_norm(func : Callable[[Term], Term]) -> None:
_known_norms.append(func)
def std_norm_iter(term : Term) -> Term:
"""
Normalize a single term. Trust that all children values have
already been normalized, or that they will be normalized later on.
:param term: The term to normalize.
:type term: Term
:return: A normalized version of the term.
:rtype: Term
"""
match term:
case App():
match term.f:
case Lambda():
return term.f.substitute(value=term.x)
case _:
return term
case Const():
return _known_consts.get(term.name, term)
case Lambda():
return term
case Var():
return term
case Term():
return term
_known_norms.append(std_norm_iter)
def replace(term : Term, old : Term, new : Term) -> Term:
"""
Find and replace occurrences of a sub-term in a given term.
:param term: The original term to update.
:type term: Term
:param old: The term to find and replace.
:type old: Term
:param new: The new term to replace the old one with.
:type new: Term
:return: The replaced term result.
:rtype: Term
"""
return traverse_bottom_up(f=lambda t : new if t == old else t, term=term)
def traverse(f : Callable[[Term], Term], term : Term) -> Term:
"""
Traverse through the tree back-and-forth, aiming to run the given
function as thoroughly as possible to manipulate the term tree.
The function quits once no more operations are created.
**WARNING:** This means the tree might traverse forever.
:param f: The traversal function that replaces terms.
:type f: Callable[[Term], Term]
:param term: The term to traverse.
:type term: Term
:return: The result of the traversed term.
:rtype: Term
"""
a, b = Term(), term
while a != b:
a, b = b, traverse_bottom_up(f=f, term=traverse_top_down(f=f, term=b))
return b
def traverse_bottom_up(f : Callable[[Term], Term], term : Term) -> Term:
"""
Traverse a term from the bottom up, replacing values with whatever the
inserted function returns.
:param f: The traversal function that replaces terms.
:type f: Callable[[Term], Term]
:param term: The term to traverse.
:type term: Term
:return: The result of the traversed term.
:rtype: Term
"""
match term:
case App():
return f(App(
f=traverse_bottom_up(f=f, term=term.f),
x=traverse_bottom_up(f=f, term=term.x)),
)
case Const():
return f(term)
case Lambda():
return term.replace_content(
traverse_bottom_up(f=f, term=term.out)
)
case Var():
return f(term)
case Term():
return f(term)
def traverse_top_down(f : Callable[[Term], Term], term : Term) -> Term:
"""
Traverse a term from the top down, replacing values with whatever the
inserted function returns.
:param f: The traversal function that replaces terms.
:type f: Callable[[Term], Term]
:param term: The term to traverse.
:type term: Term
:return: The result of the traversed term.
:rtype: Term
"""
t = f(term)
match t:
case App():
return App(
f=traverse_top_down(f=f, term=t.f),
x=traverse_top_down(f=f, term=t.x),
)
case Const():
return t
case Lambda():
return t.replace_content(traverse_top_down(f=f, term=t.out))
case Var():
return t
case Term():
return t
class Term:
"""
Base class for all terms.
"""
def __repr__(self) -> str:
"""
Create a representation of a term.
"""
return self.to_str(vars={})
def contains(self, needle : Term) -> bool:
"""
Determine whether this term is or contains any given term.
:param needle: The term to look for.
:type needle: Term
:return: Whether the term is found.
:rtype: bool
"""
return contains(haystack=self, needle=needle)
def normalize(self) -> Term:
return normalize(term=self)
def replace(self, old : Term, new : Term) -> Term:
return replace(term=self, old=old, new=new)
def rec_iter(self) -> Generator[Term, None, None]:
yield self
def to_str(self, vars : dict[str, Var]) -> str:
return "<undefined term>"
@dataclass(frozen=True)
class App(Term):
"""
An App is the application of a function with an argument.
You could consider it the inverse of the lambda function.
"""
f : Term
x : Term
def __repr__(self) -> str:
"""
Create a representation of a term.
"""
return self.to_str(vars={})
def rec_iter(self) -> Generator[Term, None, None]:
yield self
yield from self.f.rec_iter()
yield from self.x.rec_iter()
def to_str(self, vars: dict[str, Var]) -> str:
# Reformat pre-defined common functions
match self:
case App(f=App(f=Const("Nat.add"), x=a), x=b):
return f"{ecsl(a.to_str(vars=vars))} + {ecsl(b.to_str(vars=vars))}"
# Display numbers
n, c = 0, self
while isinstance(c, App) and c.f == Const("Nat.Succ"):
n, c = n + 1, c.x
if c == Const("Nat.Zero"):
return str(n)
# elif n > 0:
# return f"{ecsl(c.to_str(vars=vars))} + {n}"
f_str = self.f.to_str(vars=vars)
x_str = self.x.to_str(vars=vars)
return f_str + " " + ecsl(x_str)
A1 = lambda fn, a : App(f=fn, x=a)
A2 = lambda fn, a, b : App(f=A1(fn, a), x=b)
A3 = lambda fn, a, b, c : App(f=A2(fn, a, b), x=c)
A4 = lambda fn, a, b, c, d : App(f=A3(fn, a, b, c), x=d)
A5 = lambda fn, a, b, c, d, e : App(f=A4(fn, a, b, c, d), x=e)
A6 = lambda fn, a, b, c, d, e, f : App(f=A5(fn, a, b, c, d, e), x=f)
A7 = lambda fn, a, b, c, d, e, f, g : App(f=A6(fn, a, b, c, d, e, f), x=g)
A8 = lambda fn, a, b, c, d, e, f, g, h : App(f=A7(fn, a, b, c, d, e, f, g), x=h)
@dataclass(frozen=True)
class Const(Term):
"""
Constants are 0-ary functions. They can represent variables, constants
or other values that generally take no input.
"""
name : str
def __repr__(self) -> str:
"""
Create a representation of a term.
"""
return self.to_str(vars={})
def to_str(self, vars: dict[str, Var]) -> str:
if self.name == "Nat.Zero":
return "0"
return self.name
class Lambda(Term):
"""
Lambdas a nameless 1-ary functions.
"""
out : Term
var : Var
def __eq__(self, value: object) -> bool:
if not isinstance(value, Lambda):
return False
for c in USABLE_VAR_NAMES:
if not contains(self.out, Const(c)):
return self.substitute(Const(c)) == value.substitute((Const(c)))
else:
raise ValueError(
"Ran out of usable variables for comparing lambda structures"
)
def __init__(self, f : Callable[[Var], Term]) -> None:
self.var = Var()
self.out = f(self.var)
def __repr__(self) -> str:
"""
Create a representation of a term.
"""
return self.to_str(vars={})
def rec_iter(self) -> Generator[Term, None, None]:
yield self
yield from self.out.rec_iter()
def replace_content(self, term : Term) -> Lambda:
"""
Replace the content of the lambda function with the given term.
The new term may still contain the lambda's var as an argument.
:param term: The term to insert in a lambda function.
:type term: Term
:return: The new Lambda function.
:rtype: Lambda
"""
return Lambda(
lambda x : traverse_bottom_up(
f=lambda t : x if t is self.var else t,
term=term,
)
)
def substitute(self, value : Term) -> Term:
"""
Substitute all variables of a given index number.
:param value: The value to replace the variable with.
:type value: Term
:return: A resolved lambda where the term has been substituted.
:rtype: Term
"""
return traverse_bottom_up(
f=lambda t : value if t is self.var else t,
term=self.out,
)
def to_str(self, vars: dict[str, Var]) -> str:
char = ''
for c in USABLE_VAR_NAMES:
if c not in vars:
char = c
vars[c] = self.var
break
else:
raise ValueError(
f"Cannot represent a lambda function that's over {len(USABLE_VAR_NAMES)} layers deep!"
)
s = self.out.to_str(vars=vars)
del vars[char]
return "λ" + char + "." + ecsl(s)
L1 = lambda f : Lambda(lambda x : f(x))
L2 = lambda f : Lambda(lambda x : L1(lambda y : f(x, y)))
L3 = lambda f : Lambda(lambda x : L2(lambda y, z : f(x, y, z)))
L4 = lambda f : Lambda(lambda x : L3(lambda y, z, a : f(x, y, z, a)))
L5 = lambda f : Lambda(lambda x : L4(lambda y, z, a, b : f(x, y, z, a, b)))
L6 = lambda f : Lambda(lambda x : L5(lambda y, z, a, b, c : f(x, y, z, a, b, c)))
L7 = lambda f : Lambda(lambda x : L6(lambda y, z, a, b, c, d : f(x, y, z, a, b, c, d)))
L8 = lambda f : Lambda(lambda x : L7(lambda y, z, a, b, c, d, e : f(x, y, z, a, b, c, d, e)))
@dataclass(frozen=True)
class Var(Term):
"""
A variable is a value that can be substituted by a lambda function.
Each lambda function stores a unique var, which can then point to a new
structure later on.
"""
def __repr__(self) -> str:
"""
Create a representation of a term.
"""
return self.to_str(vars={})
def to_str(self, vars: dict[str, Var]) -> str:
for i, v in vars.items():
if v is self:
return i
else:
return "[ERROR:UNKNOWN_VAR]"
if __name__ == "__main__":
print("REPRESENTATIONS")
print(30 * "=")
# x
print(Const("x"))
# λx.x
print(Lambda(lambda a : a))
# λx.(f x)
print(Lambda(lambda a : App(f=Const("f"), x=a)))
# λx.λy.(x y)
print(Lambda(lambda b : Lambda(lambda c : App(f=b, x=c))))
# Nat.Succ (Nat.Succ Nat.Zero)
print(App(f=Const("Nat.Succ"), x=App(f=Const("Nat.Succ"), x=Const("Nat.Zero"))))
print("\nNORMALIZATIONS")
print(30 * "=")
# foo
a = App(f=Lambda(lambda x : x), x=Const("foo"))
print(a)
print(a.normalize())
# f x
b = App(
f=App(
f=Lambda(
lambda f : Lambda(
lambda x : App(
f=f, x=x
)
)
),
x=Const("f"),
),
x= Const("x"),
)
print(b)
print(b.normalize())
# a b c
c = App(
f=App(
f=App(
f=Lambda(
lambda x : Lambda(
lambda y : Lambda(
lambda z : App(
f=App(f=x, x=y), x=z
)
)
)
),
x=Const("a"),
),
x=Const("b"),
),
x=Const("c"),
)
print(c)
print(c.normalize())
#
d = L3(lambda x, y, z : A2(x, y, z))
print(d)
print(d.normalize())
print("\nSHORTHAND NOTATION")
print(30 * "=")
# λx.λy.λz.(x y z)
e = L3(lambda a, b, c : App(f=App(f=a, x=b), x=c))
print(e)
# λx.λy.λz.(x y z)
f = L3(lambda a, b, c : A3(L3(lambda a, b, c : A2(a, b, c)), a, b, c))
print(f)
print(f.normalize())
assert e == f.normalize()
# not
bool_t = L2(lambda x, y : x).normalize()
bool_f = L2(lambda x, y : y).normalize()
g = L1(lambda b : A2(b, bool_f, bool_t))
print(g)
print(g.normalize())
print("\nFIND & REPLACE")
print(30 * "=")
h = A3(Const("f"), Const("x"), Const("y"), Const("z"))
i = A3(Const("f"), Const("x"), Const("b"), Const("z")).replace(
old=Const("b"), new=Const("y"),
)
print(h)
print(i)
assert h == i