Skip to content

Commit 23533d7

Browse files
committed
Add bounded type and more constraint solving
1 parent 349c770 commit 23533d7

2 files changed

Lines changed: 131 additions & 17 deletions

File tree

src/typechecker.erl

Lines changed: 131 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,10 +1499,12 @@ allow_empty_list(Ty) ->
14991499
end.
15001500

15011501
-type fun_ty() :: fun_ty_simple()
1502+
| fun_ty_bounded()
15021503
| fun_ty_intersection()
15031504
| fun_ty_union().
15041505

15051506
-type fun_ty_simple() :: {fun_ty, [type()], type()}.
1507+
-type fun_ty_bounded() :: {fun_ty_bounded, [type()], type(), #{atom() => type()}}.
15061508
-type fun_ty_intersection() :: {fun_ty_intersection, [fun_ty_simple()]}.
15071509
-type fun_ty_union() :: {fun_ty_union, [fun_ty()]}.
15081510

@@ -1527,11 +1529,51 @@ expect_fun_type(Env, Type, Arity) ->
15271529
-spec expect_fun_type1(env(), type() | [type()], arity()) -> fun_ty() | type_error.
15281530
expect_fun_type1(Env, BTy = {type, _, bounded_fun, [Ft, _Fc]}, Arity) ->
15291531
Sub = bounded_type_subst(Env, BTy),
1530-
Ft = ?assert_type(Ft, type()),
1531-
{fun_ty, ArgsTy, ResTy} = expect_fun_type1(Env, Ft, Arity),
1532-
ArgsTy1 = subst_ty(Sub, ArgsTy),
1533-
ResTy1 = subst_ty(Sub, ResTy),
1534-
{fun_ty, ArgsTy1, ResTy1};
1532+
Ft1 = ?assert_type(Ft, type()),
1533+
{fun_ty, ArgsTy, ResTy} = expect_fun_type1(Env, Ft1, Arity),
1534+
case Env#env.solve_constraints of
1535+
true ->
1536+
%% When constraint solving is enabled, check if we can treat bounded
1537+
%% type variables polymorphically. This is only useful when type variables
1538+
%% in the spec have concrete (variable-free) bounds, like:
1539+
%% -spec f([A]) -> [A] when A :: {top(), top()}.
1540+
%% For specs where bounds reference other type variables (like lists:map),
1541+
%% we fall through to the standard substitution.
1542+
BoundedVars = maps:filter(fun (_Var, {var, _, _}) -> false;
1543+
(_Var, Bound) ->
1544+
%% Only keep if bound is variable-free
1545+
maps:size(free_vars(Bound)) == 0
1546+
end, Sub),
1547+
case maps:size(BoundedVars) > 0 of
1548+
true ->
1549+
%% Keep bounded type variables, substitute only
1550+
%% unconstrained ones and those with variable-containing bounds
1551+
PartialSub = maps:without(maps:keys(BoundedVars), Sub),
1552+
ArgsTy1 = subst_ty(PartialSub, ArgsTy),
1553+
ResTy1 = subst_ty(PartialSub, ResTy),
1554+
HasRemainingVars = lists:any(fun contains_type_variables/1, [ResTy1 | ArgsTy1]),
1555+
case HasRemainingVars of
1556+
true ->
1557+
UBounds = maps:map(fun (_Var, _) ->
1558+
%% Get the bound from the constraint list
1559+
maps:get(_Var, BoundedVars)
1560+
end, BoundedVars),
1561+
{fun_ty_bounded, ArgsTy1, ResTy1, UBounds};
1562+
false ->
1563+
%% All vars were substituted away, no poly needed
1564+
{fun_ty, ArgsTy1, ResTy1}
1565+
end;
1566+
false ->
1567+
%% No concrete-bounded vars, use standard substitution
1568+
ArgsTy1 = subst_ty(Sub, ArgsTy),
1569+
ResTy1 = subst_ty(Sub, ResTy),
1570+
{fun_ty, ArgsTy1, ResTy1}
1571+
end;
1572+
false ->
1573+
ArgsTy1 = subst_ty(Sub, ArgsTy),
1574+
ResTy1 = subst_ty(Sub, ResTy),
1575+
{fun_ty, ArgsTy1, ResTy1}
1576+
end;
15351577
expect_fun_type1(_Env, {type, _, 'fun', [{type, _, product, ArgsTy}, ResTy]}, _Arity) ->
15361578
{fun_ty, ArgsTy, ?assert_type(ResTy, type())};
15371579
expect_fun_type1(_Env, {type, _, 'fun', []}, Arity) ->
@@ -1591,6 +1633,14 @@ expect_intersection_type(Env, [FunTy|Tys], Arity) ->
15911633
{fun_ty_union, _} ->
15921634
%% We can't have a union of functions as a spec clause
15931635
type_error;
1636+
{fun_ty_bounded, ArgsTy, ResTy, _} ->
1637+
%% Bounded poly within intersection: fall back to substituted form
1638+
Ty = {fun_ty, replace_type_vars_with_any(ArgsTy),
1639+
replace_type_vars_with_any(ResTy)},
1640+
case expect_intersection_type(Env, Tys, Arity) of
1641+
type_error -> type_error;
1642+
Tyss -> [Ty | Tyss]
1643+
end;
15941644
Ty ->
15951645
Ty = ?assert_type(Ty, fun_ty_simple()),
15961646
case expect_intersection_type(Env, Tys, Arity) of
@@ -2532,6 +2582,14 @@ type_check_call_ty(Env, {fun_ty, ArgsTy, ResTy} = Ty, Args, {_, P, _} = E) ->
25322582
{LenTy, LenArgs} ->
25332583
throw(argument_length_mismatch(P, arity(LenTy), arity(LenArgs)))
25342584
end;
2585+
type_check_call_ty(Env, {fun_ty_bounded, ArgsTy, ResTy, UBounds}, Args, {_, P, _} = E) ->
2586+
case {length(ArgsTy), length(Args)} of
2587+
{L, L} ->
2588+
FunTy = {fun_ty, ArgsTy, ResTy},
2589+
type_check_poly_call_bounded(Env, FunTy, Args, E, UBounds);
2590+
{LenTy, LenArgs} ->
2591+
throw(argument_length_mismatch(P, arity(LenTy), arity(LenArgs)))
2592+
end;
25352593
type_check_call_ty(Env, {fun_ty_intersection, ClauseTys}, Args, E) ->
25362594
type_check_call_ty_intersect(Env, ClauseTys, Args, E);
25372595
type_check_call_ty(Env, {fun_ty_union, Tyss}, Args, E) ->
@@ -2596,6 +2654,36 @@ type_check_poly_call(Env, {fun_ty, ParamTys, ResTy}, Args, {Name, P, FunTy}) ->
25962654
NewEnv = union_var_binds(VarBindsList, Env),
25972655
{NewResTy, NewEnv}.
25982656

2657+
%% Like type_check_poly_call but seeds the constraint system with upper bounds
2658+
%% from bounded type variable declarations (e.g. `when A :: {top(), top()}`).
2659+
-spec type_check_poly_call_bounded(env(), fun_ty_simple(), [type()], {_, anno(), type()},
2660+
#{atom() => type()}) -> {type(), env()}.
2661+
type_check_poly_call_bounded(Env, {fun_ty, ParamTys, ResTy}, Args, {Name, P, FunTy}, UBounds) ->
2662+
{ArgTys, VarBindsList} = lists:unzip([ type_check_expr(Env, Arg) || Arg <- Args ]),
2663+
Css = lists:map(fun ({ParamTy, Arg, ArgTy}) ->
2664+
case subtype_with_constraints(ArgTy, ParamTy, Env) of
2665+
{true, Cs} -> Cs;
2666+
false -> throw(type_error(Arg, ArgTy, ParamTy))
2667+
end
2668+
end, lists:zip3(ParamTys, Args, ArgTys)),
2669+
%% Seed upper bounds from the `when` clause
2670+
InitialUBoundCs = maps:fold(fun (Var, Bound, Acc) ->
2671+
constraints:combine(Acc, constraints:upper(Var, Bound), Env)
2672+
end, constraints:empty(), UBounds),
2673+
CombinedCs = constraints:combine([InitialUBoundCs | Css], Env),
2674+
CallExpr = {call, P, Name, Args},
2675+
?verbose(Env, "~sBounded constraints: ~p~n", [gradualizer_fmt:format_location(CallExpr, brief), CombinedCs]),
2676+
case constraints:satisfiable(CombinedCs, Env) of
2677+
true -> ok;
2678+
{false, Var, LBound, UBound} ->
2679+
throw({constraint_error, Var, LBound, UBound, CallExpr, FunTy, ArgTys})
2680+
end,
2681+
Subst = minimal_substitution(CombinedCs, ResTy),
2682+
?verbose(Env, "~sBounded substitution: ~p~n", [gradualizer_fmt:format_location(CallExpr, brief), Subst]),
2683+
NewResTy = subst_ty(Subst, ?assert_type(ResTy, type())),
2684+
NewEnv = union_var_binds(VarBindsList, Env),
2685+
{NewResTy, NewEnv}.
2686+
25992687
-spec type_check_call_ty_intersect(env(), _, _, _) -> {type(), env()}.
26002688
type_check_call_ty_intersect(Env, ClauseTys, Args, E = {Name, P, FunTy}) ->
26012689
check_call_arity(hd(ClauseTys), Args, E),
@@ -3137,6 +3225,10 @@ do_type_check_expr_in(Env, Ty, {'fun', _, {clauses, Clauses}} = Fun) ->
31373225
case expect_fun_type(Env, Ty, clause_arity(hd(Clauses))) of
31383226
{fun_ty, ArgsTys, ResTy} ->
31393227
check_clauses(Env, ArgsTys, ResTy, Clauses, bind_vars);
3228+
{fun_ty_bounded, ArgsTys, ResTy, _UBounds} ->
3229+
ArgsTys1 = replace_type_vars_with_any(ArgsTys),
3230+
ResTy1 = replace_type_vars_with_any(ResTy),
3231+
check_clauses(Env, ArgsTys1, ResTy1, Clauses, bind_vars);
31403232
%% TODO: Can this case actually happen?
31413233
{fun_ty_intersection, Tyss} ->
31423234
check_clauses_intersect(Env, Tyss, Clauses);
@@ -3186,6 +3278,10 @@ do_type_check_expr_in(Env, Ty, {named_fun, _, FunName, Clauses} = Fun) ->
31863278
case expect_fun_type(Env, Ty, clause_arity(hd(Clauses))) of
31873279
{fun_ty, ArgsTy, ResTy} ->
31883280
check_clauses(Env1, ArgsTy, ResTy, Clauses, bind_vars);
3281+
{fun_ty_bounded, ArgsTy, ResTy, _UBounds} ->
3282+
ArgsTy1 = replace_type_vars_with_any(ArgsTy),
3283+
ResTy1 = replace_type_vars_with_any(ResTy),
3284+
check_clauses(Env1, ArgsTy1, ResTy1, Clauses, bind_vars);
31893285
%% TODO: Can this case actually happen?
31903286
{fun_ty_intersection, Tyss} ->
31913287
check_clauses_intersect(Env1, Tyss, Clauses);
@@ -3893,14 +3989,15 @@ type_check_call_intersection(Env, ResTy, OrigExpr, ClauseTys, Args, {Name, P, Fu
38933989
end.
38943990

38953991
-spec check_call_arity(_, _, _) -> ok.
3896-
check_call_arity({fun_ty, ArgsTy, _FunResTy}, Args, {Name, P, _}) ->
3897-
case length(ArgsTy) =:= length(Args) of
3898-
true -> ok;
3899-
false ->
3900-
LenTys = arity(length(ArgsTy)),
3901-
LenArgs = arity(length(Args)),
3902-
throw(type_error(call_arity, P, Name, LenTys, LenArgs))
3903-
end.
3992+
check_call_arity({fun_ty, ArgsTy, _FunResTy}, Args, E) ->
3993+
check_call_arity_len(length(ArgsTy), Args, E);
3994+
check_call_arity({fun_ty_bounded, ArgsTy, _FunResTy, _UBounds}, Args, E) ->
3995+
check_call_arity_len(length(ArgsTy), Args, E).
3996+
3997+
-spec check_call_arity_len(non_neg_integer(), list(), {_, _, _}) -> ok.
3998+
check_call_arity_len(LenTys, Args, _) when LenTys =:= length(Args) -> ok;
3999+
check_call_arity_len(LenTys, Args, {Name, P, _}) ->
4000+
throw(type_error(call_arity, P, Name, arity(LenTys), arity(length(Args)))).
39044001

39054002
-spec type_check_call(env(), type(), _, _, _, _) -> env().
39064003
type_check_call(_Env, _ResTy, _, {fun_ty, ArgsTy, _FunResTy}, Args, {Name, P, _})
@@ -3926,6 +4023,18 @@ type_check_call(Env, ResTy, OrigExpr, {fun_ty, ArgsTy, FunResTy} = FunTy, Args,
39264023
false ->
39274024
throw(type_error(OrigExpr, CallResTy, ResTy))
39284025
end;
4026+
type_check_call(Env, ResTy, OrigExpr, {fun_ty_bounded, ArgsTy, FunResTy, UBounds}, Args, {Name, P, OrigFunTy}) ->
4027+
%% Bounded polymorphic call: type variables have upper bounds from `when` clauses.
4028+
%% We seed the constraint system with these upper bounds before solving.
4029+
FunTy = {fun_ty, ArgsTy, FunResTy},
4030+
{CallResTy, VarBinds} =
4031+
type_check_poly_call_bounded(Env, FunTy, Args, {Name, P, OrigFunTy}, UBounds),
4032+
case subtype(CallResTy, ResTy, Env) of
4033+
true ->
4034+
VarBinds;
4035+
false ->
4036+
throw(type_error(OrigExpr, CallResTy, ResTy))
4037+
end;
39294038
type_check_call(Env, ResTy, OrigExpr, {fun_ty_intersection, Tys}, Args, E) ->
39304039
type_check_call_intersection(Env, ResTy, OrigExpr, Tys, Args, E);
39314040
type_check_call(Env, ResTy, OrigExpr, {fun_ty_union, Tys}, Args, E) ->
@@ -4070,6 +4179,8 @@ make_rigid_type_vars({ann_type, P, [AnnoVar, Type]}) ->
40704179
{ann_type, P, [AnnoVar, make_rigid_type_vars(Type)]};
40714180
make_rigid_type_vars({fun_ty, ArgTys, ResTy}) ->
40724181
{fun_ty, make_rigid_type_vars(ArgTys), make_rigid_type_vars(ResTy)};
4182+
make_rigid_type_vars({fun_ty_bounded, ArgTys, ResTy, UBounds}) ->
4183+
{fun_ty_bounded, make_rigid_type_vars(ArgTys), make_rigid_type_vars(ResTy), UBounds};
40734184
make_rigid_type_vars({fun_ty_intersection, FunTys}) ->
40744185
{fun_ty_intersection, make_rigid_type_vars(FunTys)};
40754186
make_rigid_type_vars({fun_ty_union, FunTys}) ->
@@ -4109,6 +4220,13 @@ infer_clause(Env, {clause, _, Args, Guards, Block}) ->
41094220
R :: env().
41104221
check_clauses_fun(Env, {fun_ty, ArgsTy, FunResTy}, Clauses) ->
41114222
check_clauses(Env, ArgsTy, FunResTy, Clauses, bind_vars);
4223+
check_clauses_fun(Env, {fun_ty_bounded, ArgsTy, FunResTy, _UBounds}, Clauses) ->
4224+
%% When checking the function definition, treat bounded type variables
4225+
%% the same as their bounds (substitute back). The bounded poly behavior
4226+
%% is only relevant at call sites.
4227+
ArgsTy1 = replace_type_vars_with_any(ArgsTy),
4228+
FunResTy1 = replace_type_vars_with_any(FunResTy),
4229+
check_clauses(Env, ArgsTy1, FunResTy1, Clauses, bind_vars);
41124230
check_clauses_fun(Env, {fun_ty_intersection, Tys}, Clauses) ->
41134231
check_clauses_intersect(Env, Tys, Clauses);
41144232
check_clauses_fun(Env, {fun_ty_union, Tys}, Clauses) ->

test/known_problems/should_pass/poly_type_vars.erl renamed to test/should_pass/poly_type_vars.erl

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44

55
-gradualizer([solve_constraints]).
66

7-
%% When a spec contains bounded type variables (bound quantification),
8-
%% we currently only substitute these type variables with their bounds,
9-
%% effectively making these functions monomorphic.
10-
117
-spec foo([{integer(), integer()}]) -> [{integer(), integer()}].
128
foo(Pairs) ->
139
pair_sort(Pairs).

0 commit comments

Comments
 (0)