271 lines
8 KiB
Python
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() |