JSON-Lang/ir.py

271 lines
8 KiB
Python

from emis_funky_funktions import *
from dataclasses import dataclass
from functools import reduce
from typing import Collection, FrozenSet, Sequence, TypeAlias
@dataclass(frozen=True)
class Subst:
"""
A substitution which may be performed on a term.
Only variables may be substituted, but they may be substituted with any term. That
is, `x1/Chris` is valid, but `Chris/x1` is not.
It is strongly recommended to avoid recursive substitutions, that is, substitutions
which contain the variable they are replacing
"""
variable: str
replacement: 'IRTerm'
def __str__(self) -> str:
return repr(self)
def __repr__(self) -> str:
return f'{self.replacement}/{self.variable}'
Substitutions: TypeAlias = Sequence[Subst]
@dataclass(frozen=True)
class UnificationMismatch:
"""
Indicates that two terms failed to unify
Contains the two terms, each of which is a valid subterm of one of the two original
terms, and which do not superficially unify.
"""
term1: 'IRTerm'
term2: 'IRTerm'
@dataclass(frozen=True)
class LengthMismatch:
"""
Indicates that two clauses/argument lists failed to unify due to a length mismatch
Contains the first element of the two lists which didn't have a corresponding term on
the other side
"""
term: 'IRTerm'
UnificationError = UnificationMismatch | LengthMismatch
@dataclass(frozen=True)
class IRProp:
"""
Represents a proposition or object for resolution
Can have any number of arguments, each of which should be a `IRTerm`. Note that no
distinction is made between predicates (n-arity, logical statements), functions
(positive-arity functions over objects), and constants (stand-ins for objects)
"""
name: str
"""
The identifier of this thing, including its location in the source
"""
arguments: 'Tuple[IRTerm, ...]' = tuple()
def subst(self, subst: Subst) -> 'IRTerm':
"""
Perform substitution on a proposition
Returns the same proposition, but with any instances of the variable named in the
substitution replaced with the contents of the substitution.
>>> original = IRProp('angry', (IRVar('x1'),))
>>> original
angry(*x1)
>>> original.subst(Subst('x1', IRProp('Alex')))
angry(Alex())
"""
return IRProp(self.name, tuple(arg.subst(subst) for arg in self.arguments))
def __str__(self) -> str:
return repr(self)
def __repr__(self) -> str:
return f'{self.name}({",".join(str(arg) for arg in self.arguments)})'
def __contains__(self, var: str) -> bool:
"""
Test if a variable with a given name exists in this term
>>> 'x1' in IRProp('friends', [IRProp('John'), IRProp('mother_of', [IRVar('x1')])])
True
"""
return any(var in term for term in self.arguments)
@dataclass(frozen=True)
class IRVar:
"""
A variable which may be substituted for any other term
"""
name: str
def subst(self, subst: Subst) -> 'IRTerm':
"""
Perform substitution on a proposition
Returns the same proposition, but with any instances of the variable named in the
substitution replaced with the contents of the substitution.
>>> IRVar('x1').subst(Subst('x1', IRProp('Alex')))
Alex()
>>> IRVar('x1').subst(Subst('x2', IRProp('Alex')))
*x1
"""
if self.name == subst.variable:
return subst.replacement
else:
return self
def __str__(self) -> str:
return repr(self)
def __repr__(self) -> str:
return f'*{self.name}'
def __contains__(self, var: str) -> bool:
"""
Test if a variable with a given name exists in this term
>>> 'x1' in IRVar('x1')
True
>>> 'x1' in IRVar('x2')
False
"""
return var == self.name
@dataclass(frozen=True)
class IRNeg:
"""
A negated proposition
"""
inner: 'IRTerm'
def subst(self, subst: Subst) -> 'IRTerm':
"""
Perform substitution on a proposition
Returns the same proposition, but with any instances of the variable named in the
substitution replaced with the contents of the substitution.
>>> original = IRNeg(IRProp('happy', [IRVar('x1')]))
>>> original
¬happy(*x1)
>>> original.subst(Subst('x1', IRProp('parent', [IRProp('Susie')])))
¬happy(parent(Susie()))
"""
return IRNeg(self.inner.subst(subst))
def __str__(self) -> str:
return repr(self)
def __repr__(self) -> str:
return f'¬{self.inner}'
def __contains__(self, var: str) -> bool:
"""
Test if a variable with a given name exists in this term
>>> 'x1' in IRNeg(IRProp('round', [IRVar('x1')]))
True
"""
return var in self.inner
IRTerm: TypeAlias = IRVar | IRProp | IRNeg
Clause: TypeAlias = FrozenSet[IRTerm]
Clause_: TypeAlias = Collection[IRTerm]
"""
A more general definition of `Clause` which uses a collection rather than a frozen set
Every `Clause` is a `Clause_`, but not vice versa. In other words, true `Clause` is a
subclass of `Clause_`.
Due to this generalization, `Clause_` does not necessarily benefit from hashability or
deduplication. It exists mostly to make inputing things easier, e.g. in doctests.
"""
sub_all: Callable[[Substitutions, IRTerm], IRTerm] = p(reduce, lambda t, s: t.subst(s)) #type:ignore
"""
Perform a series of substitutions on a term
Applies every substitution to the term in order
>>> sub_all(
... [Subst('x1', IRVar('x2')), Subst('x2', IRProp('Karkat'))],
... IRProp('kismesis', [IRVar('x1'), IRVar('x2')]),
... )
kismesis(Karkat(), Karkat())
"""
def unify(t1: IRTerm, t2: IRTerm) -> Result[Substitutions, UnificationError]:
"""
Attempt to find a substitution that unifies two terms
If successful, the returned substitutions will cause both term to be equal, when
applied to both.
If this method fails, then the pair of subterms which caused the unification to fail
are returned.
>>> unify(
... IRProp('imaginary', [IRProp('Rufio')]),
... IRProp('imaginary', [IRVar('x1')])
... )
Ok((Rufio()/x1,))
>>> unify(
... IRProp('dating', [IRProp('Jade'), IRVar('x1')]),
... IRProp('dating', [IRVar('x1'), IRProp('John')])
... )
Err(UnificationMismatch(term1=Jade(), term2=John()))
>>> unify(
... IRProp('mother_of', [IRVar('x1')]),
... IRVar('x1')
... )
Err(UnificationMismatch(term1=mother_of(*x1), term2=*x1))
"""
match (t1, t2):
case (IRVar(v1), IRVar(v2)) if v1 == v2:
return Ok(tuple())
case (IRVar(v), t_other) | (t_other, IRVar(v)) if v not in t_other: #type: ignore
return Ok((Subst(v, t_other),))
case (IRProp(n1, a1), IRProp(n2, a2)) if n1 == n2 and len(a1) == len(a2):
return unify_lists(a1, a2)
case (IRNeg(i1), IRNeg(i2)):
return unify(i1, i2)
return Err(UnificationMismatch(t1, t2))
def unify_lists(c1: Sequence[IRTerm], c2: Sequence[IRTerm]) -> Result[Substitutions, UnificationError]:
"""
Attempt to perform unification on two term/argument lists
See `unify()` for the details of how this works. When working with lists, the same
rules apply. The substitutions, when applied to every term of both lists, will
cause the lists to become exactly the same.
Lists which are not the same length cannot be unified, and will always fail.
Notice the difference between a list of `IRTerm`s and a `Clause`: Namely, that a
`Clause` is unordered, while a list of `IRTerm`s is ordered.
>>> unify_lists(
... [ IRProp('imaginary', [IRProp('Rufio')]), IRProp('friend', [IRVar('x1'), IRVar('x3')]) ],
... [ IRProp('imaginary', [IRVar('x1')]), IRProp('friend', [IRVar('x2'), IRProp('Tavros')]) ]
... )
Ok((Rufio()/x1, Rufio()/x2, Tavros()/x3))
>>> unify_lists(
... [ IRProp('imaginary', [IRProp('Rufio')]), IRProp('friend', [IRVar('x1'), IRVar('x3')]) ],
... [ IRProp('imaginary', [IRVar('x1')]) ]
... )
Err(LengthMismatch(term=friend(Rufio(),*x3)))
"""
match (c1, c2):
case ([], []):
return Ok(tuple())
case ([h1, *t1], [h2, *t2]):
return unify(h1, h2) << (lambda subs:
unify_lists((*map(p(sub_all,subs),t1),), (*map(p(sub_all,subs),t2),)) <= (
lambda final_subs: (*subs, *final_subs)))
case ([h, *t], []) | ([], [h, *t]):
return Err(LengthMismatch(h))
raise Exception('Unreachable')
if __name__ == '__main__':
import doctest
doctest.testmod()