0
votes

I'm playing with type unification, I got the algorithm from here, https://eli.thegreenplace.net/2018/unification/

My problem is: I want to use this as type inference step, I tweaked the code from the above page and added Lark parser to parse and unify X -> Y == W -> Z expressions, meaning something like unify(_(X, Y), _(Y, W)). Also I use poly is for meaning poly types, or type variables, mono is for meaning mono types like int, str, bool in my case.

I expected a -> b == int -> int to be reject. I can understand why is not rejected because unify(f(A, B), f(1, 1)) is perfect fine. But I want it to fail. I don't know if this should be done in a later step, at type checking type, that would happen after type inference time, but to my application this seem just wrong.

Is the unification algorithm that I used the right one for type inference? If not, how to fix this unification unify(a -> b, int -> int) problem so it's rejected? (any papers on subject would be nice) Or I'm just mixing the balls and this should be done latter on type checking?

Here is my code, to execute you need Lark pip install lark-parser, I'm using python 3.8 for testing

from typing import (
    Dict,
    Any,
    NamedTuple,
    Optional,
    Callable,
    TypedDict,
    Generic,
    TypeVar,
    Iterable,
    Sequence,
    Tuple,
    Union,
)
from lark import Lark, Transformer as LarkTransformer

Subst = Dict[str, "TTerm"]


class TTerm:
    "Type term"

    def unify_eq(self, other) -> bool:
        pass


class TArrow(TTerm):
    def __init__(self, t1, t2):
        self.t1 = t1
        self.t2 = t2

    def unify_eq(self, other) -> bool:
        return (
            other.__class__ is TArrow
            and self.t1.unify_eq(other.t1)
            and self.t2.unify_eq(other.t2)
        )

    def __repr__(self):
        return f"({self.t1} -> {self.t2})"


class TPoly(TTerm):
    def __init__(self, name):
        self.name = name

    def unify_eq(self, other):
        return other.__class__ is TPoly and self.name == other.name

    def __eq__(self, other):
        return other.__class__ is self.__class__ and self.name == other.name

    def __repr__(self):
        return self.name


class TMono(TTerm):
    def __init__(self, val):
        self.val = val

    def unify_eq(self, other):
        return self.__class__ is other.__class__ and self.val == other.val

    def __eq__(self, other):
        return other.__class__ is self.__class__ and self.val == other.val

    def __repr__(self):
        return self.val


class TUnification:
    def __init__(self, t1: TTerm, t2: TTerm):
        self.t1 = t1
        self.t2 = t2

    def unify(self):
        return unify(self.t1, self.t2, {})

    def __repr__(self):
        return f"unify({self.t1}, {self.t2})"


def unify(x: TTerm, y: TTerm, subst: Optional[Subst]) -> Optional[Subst]:
    print(f"unify({x}, {y}, {subst})")
    if subst is None:
        return None
    elif x.unify_eq(y):
        return subst
    elif isinstance(x, TPoly):
        return unify_var(x, y, subst)
    elif isinstance(y, TPoly):
        return unify_var(y, x, subst)
    elif isinstance(x, TArrow) and isinstance(y, TArrow):
        subst = unify(x.t1, y.t1, subst)
        subst = unify(x.t2, y.t2, subst)
        return subst
    else:
        return None


def unify_var(v: TPoly, x: TTerm, subst: Subst) -> Optional[Subst]:
    print(f"unify_var({v}, {x}, {subst})")
    if v.name in subst:
        return unify(subst[v.name], x, subst)
    elif isinstance(x, TPoly) and x.name in subst:
        return unify(v, subst[x.name], subst)
    elif occurs_check(v, x, subst):
        return None
    else:
        return {**subst, v.name: x}


def occurs_check(v: TPoly, term: TTerm, subst: Subst) -> bool:
    if v == term:
        return True
    elif isinstance(term, TPoly) and term.name in subst:
        return occurs_check(v, subst[term.name], subst)
    elif isinstance(term, TArrow):
        return occurs_check(v, term.t1, subst) or occurs_check(v, term.t2, subst)
    else:
        return False


grammar = r"""
    unification : term "==" term
    ?term       : tarrow | POLY -> poly | MONO -> mono
    ?tarrow     : term "->" term | "(" term ")"
    POLY        : /[a-z]/
    MONO        : /(int|str|bool)/

    %import common.WS
    %import common.SH_COMMENT
    %import common.INT
    %ignore WS
    %ignore SH_COMMENT
"""

parser = Lark(grammar, start="unification", parser="lalr")


class Transformer(LarkTransformer):
    def unification(self, tree):
        return TUnification(tree[0], tree[1])

    def poly(self, tree):
        return TPoly(tree[0].value)

    def mono(self, tree):
        return TMono(tree[0].value)

    def tarrow(self, tree):
        return TArrow(tree[0], tree[1])


def parse(input_):
    return Transformer().transform(parser.parse(input_))


print(parse("a -> b == int -> int").unify())

Output

unify((a -> b), (int -> int), {})
unify(a, int, {})
unify_var(a, int, {})
unify(b, int, {'a': int})
unify_var(b, int, {'a': int})
{'a': int, 'b': int}
1

1 Answers

1
votes

But I want it to fail

This is the correct behaviour, though. In a -> b, a and b don't have to be different types, so a == b is perfectly fine. Thus, int -> int is fine too.

If you really need to reject int -> int, you can create an additional constraint for this type, like TArrow(a, b, constraints=[NotEqual(a, b)]) and then propagate it down the tree.