Skip to content

Commit c44e393

Browse files
authored
Merge pull request #570 from erszcz/skip-too-complex-guards
Skip too complex guards
2 parents 3bdca60 + c63080a commit c44e393

File tree

8 files changed

+139
-85
lines changed

8 files changed

+139
-85
lines changed

.github/workflows/proptests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
name: Erlang ${{matrix.otp}}
1414
strategy:
1515
matrix:
16-
otp: ['24.3.4.4']
16+
otp: ['26.2.5']
1717
steps:
1818
- uses: actions/checkout@v2
1919
- uses: erlef/setup-beam@v1

src/typechecker.erl

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -417,10 +417,6 @@ compat_ty({type, Anno1, record, [{atom, _, Name} | Fields1]},
417417
{type, Anno2, record, [{atom, _, Name} | Fields2]}, Seen, Env) ->
418418
AllFields1 = case Fields1 of [] -> get_record_fields_types(Name, Anno1, Env); _ -> Fields1 end,
419419
AllFields2 = case Fields2 of [] -> get_record_fields_types(Name, Anno2, Env); _ -> Fields2 end,
420-
%% We can assert because we explicitly match {atom, _, Name}
421-
%% out of the field list in both cases above.
422-
AllFields1 = ?assert_type(AllFields1, [record_field_type()]),
423-
AllFields2 = ?assert_type(AllFields2, [record_field_type()]),
424420
compat_record_tys(AllFields1, AllFields2, Seen, Env);
425421
compat_ty({type, _, record, _}, {type, _, tuple, any}, Seen, _Env) ->
426422
ret(Seen);
@@ -440,20 +436,15 @@ compat_ty(Ty1, Ty2, Seen, Env) when ?is_list_type(Ty1), ?is_list_type(Ty2) ->
440436
compat_ty({type, _, tuple, any}, {type, _, tuple, any}, Seen, _Env) ->
441437
ret(Seen);
442438
compat_ty({type, _, tuple, any}, {type, _, tuple, Args2}, Seen, Env) ->
443-
%% We can assert because we match out `any' in previous clauses.
444-
%% TODO: it would be perfect if Gradualizer could refine this type automatically in such a case
445-
Args2 = ?assert_type(Args2, [type()]),
446439
Args1 = lists:duplicate(length(Args2), type(any)),
447440
% We check the argument types because Args2 may contain type variables
448441
% and in that case, we want to constrain them
449442
compat_tys(Args1, Args2, Seen, Env);
450443
compat_ty({type, _, tuple, Args1}, {type, _, tuple, any}, Seen, Env) ->
451-
Args1 = ?assert_type(Args1, [type()]),
452444
Args2 = lists:duplicate(length(Args1), type(any)),
453445
compat_tys(Args1, Args2, Seen, Env);
454446
compat_ty({type, _, tuple, Args1}, {type, _, tuple, Args2}, Seen, Env) ->
455-
compat_tys(?assert_type(Args1, [type()]),
456-
?assert_type(Args2, [type()]), Seen, Env);
447+
compat_tys(Args1, Args2, Seen, Env);
457448

458449
%% Maps
459450
compat_ty({type, _, map, [?any_assoc]}, {type, _, map, _Assocs}, Seen, _Env) ->
@@ -469,10 +460,6 @@ compat_ty({type, _, map, Assocs1}, {type, _, map, Assocs2}, Seen, Env) ->
469460
({type, _, map_field_exact, _}) -> true;
470461
(_) -> false
471462
end,
472-
%% We can assert because {type, _, map, any} is normalized away by normalize/2,
473-
%% whereas ?any_assoc associations are matched out explicitly in the previous clauses.
474-
Assocs1 = ?assert_type(Assocs1, [gradualizer_type:af_assoc_type()]),
475-
Assocs2 = ?assert_type(Assocs2, [gradualizer_type:af_assoc_type()]),
476463
MandatoryAssocs1 = lists:filter(IsMandatory, Assocs1),
477464
MandatoryAssocs2 = lists:filter(IsMandatory, Assocs2),
478465
{Seen3, Cs3} = lists:foldl(fun ({type, _, map_field_exact, _} = Assoc2, {Seen2, Cs2}) ->
@@ -507,10 +494,6 @@ compat_ty({type, _, AssocTag1, [Key1, Val1]},
507494
AssocTag1 == map_field_exact, AssocTag2 == map_field_exact;
508495
AssocTag1 == map_field_exact, AssocTag2 == map_field_assoc ->
509496
%% For M1 <: M2, mandatory fields in M2 must be mandatory fields in M1
510-
Key1 = ?assert_type(Key1, type()),
511-
Key2 = ?assert_type(Key2, type()),
512-
Val1 = ?assert_type(Val1, type()),
513-
Val2 = ?assert_type(Val2, type()),
514497
{Seen1, Cs1} = compat(Key1, Key2, Seen, Env),
515498
{Seen2, Cs2} = compat(Val1, Val2, Seen1, Env),
516499
{Seen2, constraints:combine(Cs1, Cs2, Env)};
@@ -4015,7 +3998,6 @@ disable_exhaustiveness_check(#env{} = Env) ->
40153998
check_arg_exhaustiveness(Env, ArgTys, Clauses, RefinedArgTys) ->
40163999
case exhaustiveness_checking(Env) andalso
40174000
all_refinable(ArgTys, Env) andalso
4018-
no_clause_has_guards(Clauses) andalso
40194001
some_type_not_none(RefinedArgTys)
40204002
of
40214003
true ->
@@ -4036,10 +4018,6 @@ exhaustiveness_checking(#env{} = Env) ->
40364018
all_refinable(any, _Env) -> false;
40374019
all_refinable(Types, Env) -> lists:all(fun (Ty) -> refinable(Ty, Env) end, Types).
40384020

4039-
-spec no_clause_has_guards(_) -> boolean().
4040-
no_clause_has_guards(Clauses) ->
4041-
lists:all(fun no_guards/1, Clauses).
4042-
40434021
-spec some_type_not_none([type()]) -> boolean().
40444022
some_type_not_none(Types) when is_list(Types) ->
40454023
lists:any(fun (T) -> T =/= type(none) end, Types).
@@ -4106,6 +4084,8 @@ refine_mismatch_using_guards(PatTys,
41064084
do_refine_mismatch_using_guards(Guards, PatTys, Pats, VEnv, Env);
41074085
[_|_] ->
41084086
%% we don't know yet how to do this in guard sequences
4087+
Env#env.no_skip_complex_guards orelse throw({skip, too_complex_guards}),
4088+
%% TODO: Invalid lack of pattern refinement
41094089
PatTys
41104090
end.
41114091

@@ -4579,16 +4559,15 @@ mta({user_type, Anno, Name, Args}, Env) ->
45794559
mta(Type, _Env) ->
45804560
Type.
45814561

4582-
-spec no_guards(_) -> boolean().
4583-
no_guards({clause, _, _, Guards, _}) ->
4584-
Guards == [].
4585-
45864562
%% Refines the types of bound variables using the assumption that a clause has
45874563
%% mismatched.
45884564
-spec refine_vars_by_mismatching_clause(gradualizer_type:af_clause(), venv(), env()) -> venv().
45894565
refine_vars_by_mismatching_clause({clause, _, Pats, Guards, _Block}, VEnv, Env) ->
45904566
PatternCantFail = are_patterns_matching_all_input(Pats, VEnv),
45914567
case Guards of
4568+
[] ->
4569+
%% No guards, so no refinement
4570+
VEnv;
45924571
[[{call, _, {atom, _, Fun}, Args = [{var, _, Var}]}]] when PatternCantFail ->
45934572
%% Simple case: A single guard on the form `when is_TYPE(Var)'.
45944573
%% If Var was bound before the clause, which failed because of a
@@ -4602,7 +4581,8 @@ refine_vars_by_mismatching_clause({clause, _, Pats, Guards, _Block}, VEnv, Env)
46024581
VEnv
46034582
end;
46044583
_OtherGuards ->
4605-
%% No refinement
4584+
Env#env.no_skip_complex_guards orelse throw({skip, too_complex_guards}),
4585+
%% TODO: Invalid lack of refinement
46064586
VEnv
46074587
end.
46084588

@@ -4746,17 +4726,23 @@ type_check_function(Env, {function, _Anno, Name, NArgs, Clauses}) ->
47464726
?verbose(Env, "Checking function ~p/~p~n", [Name, NArgs]),
47474727
case maps:find({Name, NArgs}, Env#env.fenv) of
47484728
{ok, FunTy} ->
4749-
NewEnv = Env#env{current_spec = FunTy},
4750-
FunTyNoPos = [ typelib:remove_pos(?assert_type(Ty, type())) || Ty <- FunTy ],
4751-
Arity = clause_arity(hd(Clauses)),
4752-
case expect_fun_type(NewEnv, FunTyNoPos, Arity) of
4753-
{type_error, NotFunTy} ->
4754-
%% This can only happen if `create_fenv/2' creates garbage.
4755-
erlang:error({invalid_function_type, NotFunTy});
4756-
FTy ->
4757-
FTy1 = make_rigid_type_vars(FTy),
4758-
_Vars = check_clauses_fun(NewEnv, FTy1, Clauses),
4759-
NewEnv
4729+
try
4730+
NewEnv = Env#env{current_spec = FunTy},
4731+
FunTyNoPos = [ typelib:remove_pos(?assert_type(Ty, type())) || Ty <- FunTy ],
4732+
Arity = clause_arity(hd(Clauses)),
4733+
case expect_fun_type(NewEnv, FunTyNoPos, Arity) of
4734+
{type_error, NotFunTy} ->
4735+
%% This can only happen if `create_fenv/2' creates garbage.
4736+
erlang:error({invalid_function_type, NotFunTy});
4737+
FTy ->
4738+
FTy1 = make_rigid_type_vars(FTy),
4739+
_Vars = check_clauses_fun(NewEnv, FTy1, Clauses),
4740+
NewEnv
4741+
end
4742+
catch
4743+
throw:{skip, Reason} ->
4744+
?verbose(Env, "Skipping function ~p/~p due to ~s~n", [Name, NArgs, Reason]),
4745+
Env
47604746
end;
47614747
error ->
47624748
throw(internal_error(missing_type_spec, Name, NArgs))
@@ -4771,18 +4757,17 @@ arity(I) ->
47714757
?assert(I < 256, arity_overflow),
47724758
?assert_type(I, arity()).
47734759

4774-
-spec position_info_from_spec(form() | forms() | none) -> erl_anno:anno().
4760+
-spec position_info_from_spec(none | form() | forms()) -> erl_anno:anno().
47754761
position_info_from_spec(none) ->
47764762
%% This simplifies testing internal functions.
47774763
%% In these cases we don't go through type_check_function,
47784764
%% but call deeper into the typechecker directly.
47794765
erl_anno:new(0);
4766+
position_info_from_spec(Form) when is_tuple(Form) ->
4767+
element(2, Form);
47804768
position_info_from_spec([_|_] = Forms) ->
47814769
%% TODO: https://github.com/josefs/Gradualizer/issues/388
4782-
position_info_from_spec(hd(Forms));
4783-
position_info_from_spec(Form) ->
4784-
Form = ?assert_type(Form, form()),
4785-
element(2, Form).
4770+
position_info_from_spec(hd(Forms)).
47864771

47874772
%% Type check patterns against types (P1 :: T1, P2 :: T2, ...)
47884773
%% and add variable bindings for the patterns.
@@ -5835,7 +5820,8 @@ create_env(#parsedata{module = Module
58355820
verbose = proplists:get_bool(verbose, Opts),
58365821
union_size_limit = proplists:get_value(union_size_limit, Opts,
58375822
default_union_size_limit()),
5838-
solve_constraints = proplists:get_bool(solve_constraints, Opts)}.
5823+
solve_constraints = proplists:get_bool(solve_constraints, Opts),
5824+
no_skip_complex_guards = proplists:get_bool(no_skip_complex_guards, Opts)}.
58395825

58405826
-spec default_union_size_limit() -> non_neg_integer().
58415827
default_union_size_limit() -> 30.

src/typechecker.hrl

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,26 @@
33

44
-record(clauses_controls, {exhaust}).
55

6-
-record(env, {fenv = #{} :: #{{atom(), arity()} =>
7-
[gradualizer_type:af_constrained_function_type()]
8-
| [gradualizer_type:gr_any_type()]
9-
},
10-
imported = #{} :: #{{atom(), arity()} => module()},
11-
venv = #{} :: typechecker:venv(),
12-
tenv :: gradualizer_lib:tenv(),
13-
infer = false :: boolean(),
14-
verbose = false :: boolean(),
15-
exhaust = true :: boolean(),
6+
-record(env, {fenv = #{} :: #{{atom(), arity()} =>
7+
[gradualizer_type:af_constrained_function_type()]
8+
| [gradualizer_type:gr_any_type()]
9+
},
10+
imported = #{} :: #{{atom(), arity()} => module()},
11+
venv = #{} :: typechecker:venv(),
12+
tenv :: gradualizer_lib:tenv(),
13+
infer = false :: boolean(),
14+
verbose = false :: boolean(),
15+
exhaust = true :: boolean(),
1616
%% Controls driving the type checking algorithm
1717
%% per clauses-list (fun/case/try-catch/receive).
18-
clauses_stack = [] :: [#clauses_controls{}],
18+
clauses_stack = [] :: [#clauses_controls{}],
1919
%% Performance hack: Unions larger than this limit are replaced by any() in normalization.
20-
union_size_limit :: non_neg_integer(),
21-
current_spec = none :: erl_parse:abstract_form() | none,
22-
solve_constraints = false :: boolean()
20+
union_size_limit :: non_neg_integer(),
21+
current_spec = none :: erl_parse:abstract_form() | none,
22+
solve_constraints = false :: boolean(),
23+
%% Skip functions whose guards are too complex to be handled yet,
24+
%% which might result in false positives when checking rest of the functions
25+
no_skip_complex_guards = false :: boolean()
2326
}).
2427

2528
-endif. %% __TYPECHECKER_HRL__

src/typelib.erl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,15 @@ annotate_user_type_(_Filename, Type) ->
232232
-spec get_module_from_annotation(erl_anno:anno()) -> {ok, module()} | none.
233233
get_module_from_annotation(Anno) ->
234234
case erl_anno:file(Anno) of
235-
File when is_list(File) ->
236-
Basename = filename:basename(File, ".erl"),
237-
{ok, list_to_existing_atom(?assert_type(Basename, string()))};
238235
undefined ->
239-
none
236+
none;
237+
File ->
238+
case unicode:characters_to_binary(filename:basename(File, ".erl")) of
239+
Basename when is_binary(Basename) ->
240+
{ok, binary_to_existing_atom(Basename)};
241+
_ ->
242+
erlang:error({malformed_anno, Anno})
243+
end
240244
end.
241245

242246
-spec substitute_type_vars(type(),
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-module(type_refinement_should_fail).
2+
3+
-export([guard_prevents_refinement/2,
4+
guard_prevents_refinement2/1,
5+
pattern_prevents_refinement/2]).
6+
7+
-spec guard_prevents_refinement(1..2, boolean()) -> 2.
8+
guard_prevents_refinement(N, Guard) ->
9+
case N of
10+
1 when Guard -> 2;
11+
M -> M %% 1 cannot be eliminated
12+
end.
13+
14+
-spec guard_prevents_refinement2(integer()) -> ok.
15+
guard_prevents_refinement2(X) when is_integer(X), X rem 7 == 0 -> ok;
16+
guard_prevents_refinement2(X) -> ok. % X can still be an integer
17+
18+
-spec pattern_prevents_refinement(integer(), any()) -> atom().
19+
pattern_prevents_refinement(X, X) when is_integer(X) -> ok;
20+
pattern_prevents_refinement(X, {_Y}) when is_integer(X) -> ok;
21+
pattern_prevents_refinement(Inf, _) -> Inf. % Inf can still be an integer

test/known_problems/should_pass/refine_bound_var_with_guard_should_pass.erl

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
-module(refine_bound_var_with_guard_should_pass).
22

3-
-export([f/1]).
3+
-export([f/1,
4+
too_complex_guards/1]).
5+
6+
-gradualizer([no_skip_complex_guards]).
47

58
%% This type is simpler than gradualizer_type:abstract_type() by having less variants
69
%% and by using tuples to contain deeper nodes. The latter frees us from having to deal
@@ -35,3 +38,12 @@ g({type, nonempty_list, {InnerNode}}) ->
3538
InnerNode;
3639
g({type, atom, {_InnerNode}}) ->
3740
a.
41+
42+
%% See too_complex_guards thrown in src/typechecker.erl.
43+
-spec too_complex_guards(integer() | [integer()] | none) -> number().
44+
too_complex_guards([_|_] = Ints) ->
45+
lists:sum(Ints);
46+
too_complex_guards(EmptyOrNone) when EmptyOrNone =:= none orelse is_list(EmptyOrNone) ->
47+
0;
48+
too_complex_guards(Int) ->
49+
Int.
Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
-module(type_refinement_fail).
2-
-export([guard_prevents_refinement/2, imprecision_prevents_refinement/2,
3-
multi_pat_fail_1/2,
4-
guard_prevents_refinement2/1,
5-
pattern_prevents_refinement/2]).
62

7-
-spec guard_prevents_refinement(1..2, boolean()) -> 2.
8-
guard_prevents_refinement(N, Guard) ->
9-
case N of
10-
1 when Guard -> 2;
11-
M -> M %% 1 cannot be eliminated
12-
end.
3+
-export([imprecision_prevents_refinement/2,
4+
multi_pat_fail_1/2]).
135

146
-spec imprecision_prevents_refinement(float(), a|b) -> b.
157
imprecision_prevents_refinement(3.14, a) -> b;
@@ -18,12 +10,3 @@ imprecision_prevents_refinement(_, X) -> X.
1810
-spec multi_pat_fail_1(a|b, a|b) -> {b, b}.
1911
multi_pat_fail_1(a, a) -> {b, b};
2012
multi_pat_fail_1(A, B) -> {A, B}. %% Not only {b, b} here
21-
22-
-spec guard_prevents_refinement2(erlang:timestamp()) -> ok.
23-
guard_prevents_refinement2(X) when is_integer(X), X rem 7 == 0 -> ok;
24-
guard_prevents_refinement2(infinity) -> ok. % can still be an integer
25-
26-
-spec pattern_prevents_refinement(erlang:timestamp(), any()) -> atom().
27-
pattern_prevents_refinement(X, X) when is_integer(X) -> ok;
28-
pattern_prevents_refinement(X, {_Y}) when is_integer(X) -> ok;
29-
pattern_prevents_refinement(Inf, _) -> Inf. % Inf can still be an integer

test/should_pass/type_refinement_pass.erl

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
beside_match_all/2,
1212
beside_singleton/2,
1313
refine_bound_var_by_guard_bifs/1,
14-
refine_literals_by_guards/1]).
14+
refine_literals_by_guards/1,
15+
too_complex_guards/1,
16+
no_guards/1,
17+
no_complex_guards1/1,
18+
no_complex_guards2/1]).
1519

1620
%% Test that Value is not considered to be string() | false.
1721
-spec basic_type_refinement(string()) -> string().
@@ -99,3 +103,44 @@ refine_literals_by_guards(X) when is_integer(X) -> self();
99103
refine_literals_by_guards(X) when is_tuple(X) -> self();
100104
refine_literals_by_guards(X) when is_list(X) -> self();
101105
refine_literals_by_guards(X) -> X.
106+
107+
%% See also refine_bound_var_with_guard_should_pass known problem.
108+
-spec too_complex_guards(integer() | [integer()] | none) -> number().
109+
too_complex_guards([_|_] = Ints) ->
110+
lists:sum(Ints);
111+
%% Passes thanks to the whole function being skipped, but these guards are not handled yet.
112+
%% See too_complex_guards thrown in src/typechecker.erl.
113+
too_complex_guards(EmptyOrNone) when EmptyOrNone =:= none orelse is_list(EmptyOrNone) ->
114+
0;
115+
too_complex_guards(Int) ->
116+
Int.
117+
118+
%% This is based on dog-fooding typechecker:position_info_from_spec/1 @ 3bdca60,
119+
%% where we know that the list will never be empty.
120+
%% This is one way to refactor `too_complex_guards' to get typechecked with a little bit of
121+
%% programmer input.
122+
-spec no_guards(integer() | [integer()] | none) -> number().
123+
no_guards(none) ->
124+
0;
125+
no_guards([] = Ints) ->
126+
0;
127+
no_guards([_|_] = Ints) ->
128+
lists:sum(Ints);
129+
no_guards(Int) ->
130+
Int.
131+
132+
-spec no_complex_guards1(integer() | [integer()] | none) -> number().
133+
no_complex_guards1(none) ->
134+
0;
135+
no_complex_guards1(Ints) when is_list(Ints) ->
136+
lists:sum(Ints);
137+
no_complex_guards1(Num) ->
138+
Num.
139+
140+
-spec no_complex_guards2(integer() | [integer()] | none) -> number().
141+
no_complex_guards2(none) ->
142+
0;
143+
no_complex_guards2(Num) when is_integer(Num) ->
144+
Num;
145+
no_complex_guards2(List) ->
146+
lists:sum(List).

0 commit comments

Comments
 (0)