Current section
Files
Jump to
Current section
Files
test/phi_typechecker_test.exs
defmodule PhiTypecheckerTest do
use ExUnit.Case
alias Phi.Type.{TVar, TCon, TApp}
alias Phi.Typechecker
alias Phi.Typechecker.State
test "Unification of Identical Types" do
state = %State{}
t1 = %TCon{name: "Int"}
assert {:ok, _} = Typechecker.unify(t1, t1, state)
end
test "Unification of Type Variable and Concrete Type" do
state = %State{}
t_var = %TVar{id: 1}
t_int = %TCon{name: "Int"}
{:ok, state2} = Typechecker.unify(t_var, t_int, state)
assert Map.get(state2.subst, 1) == t_int
end
test "Unification of Arrows (Functions)" do
state = %State{}
# a -> Int
t1 = Phi.Type.arrow(%TVar{id: 1}, %TCon{name: "Int"})
# String -> b
t2 = Phi.Type.arrow(%TCon{name: "String"}, %TVar{id: 2})
{:ok, state2} = Typechecker.unify(t1, t2, state)
# After unification: 'a' is String, 'b' is Int
assert Map.get(state2.subst, 1) == %TCon{name: "String"}
assert Map.get(state2.subst, 2) == %TCon{name: "Int"}
end
test "Unification catches Infinite Types (Occurs Check)" do
state = %State{}
# Unifying `a` with `a -> Int` should fail because it results in an infinite type
t_var = %TVar{id: 1}
t_arrow = Phi.Type.arrow(%TVar{id: 1}, %TCon{name: "Int"})
assert {:error, "Occurs check failed: infinite type"} = Typechecker.unify(t_var, t_arrow, state)
end
test "Inference for Identity Function `\\x -> x`" do
# AST: \x -> x
expr = %Phi.AST.ExprLam{
binder: %Phi.AST.BinderVar{name: "x"},
body: %Phi.AST.ExprVar{name: "x"}
}
env = Phi.Typechecker.Env.new()
{:ok, type} = Phi.Typechecker.infer(env, expr)
# Should give `t1 -> t1`
assert %TApp{
func: %TApp{func: %TCon{name: "->"}, arg: %TVar{id: id1}},
arg: %TVar{id: id2}
} = type
assert id1 == id2
end
test "Inference applies polymorphism in let bindings" do
# AST: let id = \x -> x in id
id_func = %Phi.AST.ExprLam{
binder: %Phi.AST.BinderVar{name: "x"},
body: %Phi.AST.ExprVar{name: "x"}
}
expr = %Phi.AST.ExprLet{
bindings: [%Phi.AST.DeclValue{name: "id", binders: [], expr: id_func}],
body: %Phi.AST.ExprVar{name: "id"}
}
env = Phi.Typechecker.Env.new()
{:ok, type} = Phi.Typechecker.infer(env, expr)
# Polmorphic type should be instantiated, so it's `t2 -> t2`
assert %TApp{
func: %TApp{func: %TCon{name: "->"}, arg: %TVar{id: id1}},
arg: %TVar{id: id2}
} = type
assert id1 == id2
end
test "Unification of Row Types (Record Polymorphism)" do
state = %State{}
# { x :: Int | r1 }
r1 = %Phi.Type.TRowExtend{label: "x", type: %TCon{name: "Int"}, rest: %Phi.Type.TVar{id: 1}}
# { y :: String, x :: a | r2 }
r2 = %Phi.Type.TRowExtend{label: "y", type: %TCon{name: "String"},
rest: %Phi.Type.TRowExtend{label: "x", type: %Phi.Type.TVar{id: 2}, rest: %Phi.Type.TVar{id: 3}}}
{:ok, state2} = Typechecker.unify(r1, r2, state)
# TVar 2 (a) should unify with Int
assert Map.get(state2.subst, 2) == %TCon{name: "Int"}
# TVar 1 (r1) should unify with { y :: String | r_rest }
t1_subst = Map.get(state2.subst, 1)
assert %Phi.Type.TRowExtend{label: "y", type: %TCon{name: "String"}} = t1_subst
end
test "Unification of Constrained Types (Type Classes)" do
state = %State{}
# Eq a => a -> a
c1 = %Phi.Type.TConstrained{
class_name: "Eq",
args: [%Phi.Type.TVar{id: 1}],
type: Phi.Type.arrow(%Phi.Type.TVar{id: 1}, %Phi.Type.TVar{id: 1})
}
# Eq Int => Int -> Int
c2 = %Phi.Type.TConstrained{
class_name: "Eq",
args: [%TCon{name: "Int"}],
type: Phi.Type.arrow(%TCon{name: "Int"}, %TCon{name: "Int"})
}
{:ok, state2} = Typechecker.unify(c1, c2, state)
assert Map.get(state2.subst, 1) == %TCon{name: "Int"}
end
end