타입 추론 규칙 문서의 검증 코드다. if __name__ == "__main__": 아래가 자체 테스트다.

# 35_type-inference-rules_verify.py
# 타입 추론 규칙 문서의 예를 확인한다. 규칙을 "가정들 / 결론" 꼴 그대로 적용해 증명 나무를 만들고,
# 문서의 예(A + B, A && B, 상수, x + 3, 함수 호출, 배열)에서
# 붙는 타입과 쓰인 규칙 수를 본다. 표준 라이브러리만 쓴다.
# 식: ("int", 3) ("double", 1.5) ("string", "s") ("bool", True) ("var", "x")
#     (연산자, 왼쪽, 오른쪽) ("call", f, [식...]) ("index", 식, 식)
# 스코프 S: {변수 이름: 타입, 함수 이름: ((인자 타입들), 반환 타입)}. None이면 스코프 정보가 없다.
# 실행: python3 35_type-inference-rules_verify.py

NUMERIC = ("short", "int", "long", "float", "double")
PRIMITIVE = ("byte", "short", "int", "long", "float", "double", "char", "bool")


class NoRule(Exception):
    pass


def derive(e, S):
    """(타입, 증명 나무)를 돌려준다. 증명 나무 = (규칙 이름, 결론, [가정의 증명 나무...])."""
    k = e[0]
    if k in ("int", "double", "string", "bool"):
        return k, ("상수", "%s : %s" % (e[1], k), [])
    if k == "var":
        if S is None or e[1] not in S:
            raise NoRule("자유 변수 %s의 타입을 정할 정보가 없다" % e[1])
        return S[e[1]], ("변수", "%s : %s" % (e[1], S[e[1]]), [])
    if k == "call":
        if S is None or e[1] not in S:
            raise NoRule(e[1])
        params, ret = S[e[1]]
        if len(params) != len(e[2]):
            raise NoRule("인자 수")
        proofs = []
        for want, arg in zip(params, e[2]):
            t, p = derive(arg, S)
            if t != want:
                raise NoRule("인자 타입")
            proofs.append(p)
        return ret, ("함수 호출", "%s(...) : %s" % (e[1], ret), proofs)
    t1, p1 = derive(e[1], S)
    t2, p2 = derive(e[2], S)
    if k == "index":
        if t1.endswith("[]") and t2 == "int":
            return t1[:-2], ("배열", "e1[e2] : %s" % t1[:-2], [p1, p2])
    elif k == "+":
        if t1 == t2 and t1 in NUMERIC:
            return t1, ("덧셈", "e1 + e2 : %s" % t1, [p1, p2])
    elif k == "&&":
        if t1 == t2 == "bool":
            return "bool", ("논리곱", "e1 && e2 : bool", [p1, p2])
    elif k in ("==", "!=", "<=", ">="):
        if t1 == t2 and t1 in PRIMITIVE:
            return "bool", ("비교", "e1 %s e2 : bool" % k, [p1, p2])
    raise NoRule("%s %s %s" % (t1, k, t2))


def size(proof):
    return 1 + sum(size(p) for p in proof[2])


def typed(e, S):
    try:
        return derive(e, S)[0]
    except NoRule:
        return None


if __name__ == "__main__":
    var = lambda n: ("var", n)

    # A, B가 int면 A + B는 int
    assert typed(("+", var("A"), var("B")), {"A": "int", "B": "int"}) == "int"
    # A가 bool, B가 int면 A && B에 맞는 규칙이 없다
    assert typed(("&&", var("A"), var("B")), {"A": "bool", "B": "int"}) is None
    assert typed(("&&", var("A"), var("B")), {"A": "bool", "B": "bool"}) == "bool"

    # 공리와 상수 규칙은 스코프 정보 없이도 쓸 수 있다
    assert typed(("bool", True), None) == "bool" and typed(("bool", False), None) == "bool"
    assert typed(("int", 1), None) == "int" and typed(("int", 1000), None) == "int"
    assert typed(("string", "hi"), None) == "string" and typed(("double", 2.5), None) == "double"
    # 덧셈은 T가 short, int, long, float, double일 때만. 두 쪽의 T가 같아야 한다
    for T in NUMERIC:
        assert typed(("+", var("p"), var("q")), {"p": T, "q": T}) == T
    for T in ("bool", "char", "string", "byte"):
        assert typed(("+", var("p"), var("q")), {"p": T, "q": T}) is None
    assert typed(("+", ("int", 1), ("double", 2.0)), None) is None
    # 비교는 원시 타입 T가 같을 때 bool
    for op in ("==", "!=", "<=", ">="):
        for T in PRIMITIVE:
            assert typed((op, var("p"), var("q")), {"p": T, "q": T}) == "bool"
        assert typed((op, var("p"), var("q")), {"p": "int", "q": "double"}) is None
        assert typed((op, var("p"), var("q")), {"p": "string", "q": "string"}) is None

    # 스코프 정보가 없으면 x + 3의 x는 자유 변수라 타입을 못 정한다
    assert typed(("+", var("x"), ("int", 3)), None) is None
    # 스코프 정보를 주면 정해진다. 같은 식이 스코프에 따라 다른 답을 낸다
    assert typed(("+", var("x"), ("int", 3)), {"x": "int"}) == "int"
    assert typed(("+", var("x"), ("int", 3)), {"x": "double"}) is None
    assert typed(("+", var("x"), ("int", 3)), {"x": "bool"}) is None

    # 함수 호출과 배열
    S = {"f": (("int", "bool"), "double"), "a": "int[]", "i": "int", "ok": "bool", "d": "double"}
    assert typed(("call", "f", [var("i"), var("ok")]), S) == "double"
    assert typed(("call", "f", [var("ok"), var("i")]), S) is None
    assert typed(("call", "f", [var("i")]), S) is None
    assert typed(("index", var("a"), var("i")), S) == "int"
    assert typed(("index", var("a"), var("d")), S) is None
    assert typed(("index", var("i"), var("i")), S) is None

    # 증명 나무: x + 3 == y  (x, y가 int) 는 규칙 다섯 번
    t, proof = derive(("==", ("+", var("x"), ("int", 3)), var("y")), {"x": "int", "y": "int"})
    assert t == "bool" and size(proof) == 5
    assert proof[0] == "비교" and proof[2][0][0] == "덧셈" and proof[2][1][0] == "변수"
    assert [p[0] for p in proof[2][0][2]] == ["변수", "상수"]
    # 규칙 수 = 식 나무의 노드 수
    t, proof = derive(("+", ("index", var("a"), var("i")), ("int", 1)), S)
    assert t == "int" and size(proof) == 5
    # 카드: f(a[i], i == 1) + d
    t, proof = derive(("+", ("call", "f", [("index", var("a"), var("i")), ("==", var("i"), ("int", 1))]), var("d")), S)
    assert t == "double" and size(proof) == 9
    # 카드: (1 == 2) && (x <= 3), x가 int
    assert typed(("&&", ("==", ("int", 1), ("int", 2)), ("<=", var("x"), ("int", 3))), {"x": "int"}) == "bool"
    assert typed(("==", ("&&", ("bool", True), ("bool", False)), ("int", 1)), None) is None

    print("ALL CHECKS PASSED")