From c66cfb408bd7498d62fafe6db5d421f34fc556d7 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 13:58:02 +0200 Subject: [PATCH 01/27] Sketch a Common Test dynamic suite shape --- test/should_fail_SUITE.erl | 243 +++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 test/should_fail_SUITE.erl diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl new file mode 100644 index 00000000..f249a0b0 --- /dev/null +++ b/test/should_fail_SUITE.erl @@ -0,0 +1,243 @@ +-module(should_fail_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). + +%% Test server callbacks +-export([suite/0, + all/0, + groups/0, + init_per_suite/1, end_per_suite/1, + init_per_group/2, end_per_group/2, + init_per_testcase/2, end_per_testcase/2]). + +%% Test cases +-export([ + ]). + +%%-------------------------------------------------------------------- +%% COMMON TEST CALLBACK FUNCTIONS +%%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% Function: suite() -> Info +%% +%% Info = [tuple()] +%% List of key/value pairs. +%% +%% Description: Returns list of tuples to set default properties +%% for the suite. +%% +%% Note: The suite/0 function is only meant to be used to return +%% default data values, not perform any other operations. +%%-------------------------------------------------------------------- +suite() -> + [{timetrap,{minutes,10}}]. + +%%-------------------------------------------------------------------- +%% Function: init_per_suite(Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for skipping the suite. +%% +%% Description: Initialization before the suite. +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + {ok, _} = application:ensure_all_started(gradualizer), + ok = load_prerequisites(), + ok = dynamic_suite_reload(?MODULE), + Config. + +load_prerequisites() -> + %% user_types.erl is referenced by opaque_fail.erl. + %% It is not in the sourcemap of the DB so let's import it manually + %gradualizer_db:import_erl_files(["test/should_pass/user_types.erl"]), + %% exhaustive_user_type.erl is referenced by exhaustive_remote_user_type.erl + %gradualizer_db:import_erl_files(["test/should_fail/exhaustive_user_type.erl"]), + ok. + +dynamic_suite_reload(Module) -> + Forms = get_forms(Module), + TestTemplate = merl:quote("'@Name'(_) -> _@Body."), + {'fun', _, {clauses, [{clause, _, _, _, Body}]}} = merl:quote(" + fun (_) -> + 1 / 0 + end + "), + TestName = "test1", + TestEnv = [ + {'Name', erl_syntax:atom(TestName)}, + {'Body', Body} + ], + TestForm = erl_syntax:revert(merl:subst(TestTemplate, TestEnv)), + NewForms = Forms ++ [TestForm, {eof, 0}], + merl:compile_and_load(NewForms), + ok. + +%should_fail_template() -> +% Errors = gradualizer:type_check_file(File, [return_errors]), +% Timeouts = [ E || {_File, {form_check_timeout, _}} = E <- Errors], +% 0 = length(Timeouts), +% %% Test that error formatting doesn't crash +% Opts = [{fmt_location, brief}, +% {fmt_expr_fun, fun erl_prettypr:format/1}], +% lists:foreach(fun({_, Error}) -> gradualizer_fmt:handle_type_error(Error, Opts) end, Errors), +% {ok, Forms} = gradualizer_file_utils:get_forms_from_erl(File, []), +% ExpectedErrors = typechecker:number_of_exported_functions(Forms), +% ExpectedErrors = length(Errors). + +get_forms(Module) -> + ModPath = code:which(Module), + {ok, {Module, [Abst]}} = beam_lib:chunks(ModPath, [abstract_code]), + {abstract_code, {raw_abstract_v1, Forms}} = Abst, + StripEnd = fun + ({eof, _}) -> false; + (_) -> true + end, + lists:filter(StripEnd, Forms). + +%%-------------------------------------------------------------------- +%% Function: end_per_suite(Config0) -> void() | {save_config,Config1} +%% +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% +%% Description: Cleanup after the suite. +%%-------------------------------------------------------------------- +end_per_suite(_Config) -> + ok. + +%%-------------------------------------------------------------------- +%% Function: init_per_group(GroupName, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% GroupName = atom() +%% Name of the test case group that is about to run. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding configuration data for the group. +%% Reason = term() +%% The reason for skipping all test cases and subgroups in the group. +%% +%% Description: Initialization before each test case group. +%%-------------------------------------------------------------------- +init_per_group(_GroupName, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% Function: end_per_group(GroupName, Config0) -> +%% void() | {save_config,Config1} +%% +%% GroupName = atom() +%% Name of the test case group that is finished. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding configuration data for the group. +%% +%% Description: Cleanup after each test case group. +%%-------------------------------------------------------------------- +end_per_group(_GroupName, _Config) -> + ok. + +%%-------------------------------------------------------------------- +%% Function: init_per_testcase(TestCase, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% +%% TestCase = atom() +%% Name of the test case that is about to run. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for skipping the test case. +%% +%% Description: Initialization before each test case. +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%%-------------------------------------------------------------------- +init_per_testcase(_TestCase, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% Function: end_per_testcase(TestCase, Config0) -> +%% void() | {save_config,Config1} | {fail,Reason} +%% +%% TestCase = atom() +%% Name of the test case that is finished. +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for failing the test case. +%% +%% Description: Cleanup after each test case. +%%-------------------------------------------------------------------- +end_per_testcase(_TestCase, _Config) -> + ok. + +%%-------------------------------------------------------------------- +%% Function: groups() -> [Group] +%% +%% Group = {GroupName,Properties,GroupsAndTestCases} +%% GroupName = atom() +%% The name of the group. +%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] +%% Group properties that may be combined. +%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] +%% TestCase = atom() +%% The name of a test case. +%% Shuffle = shuffle | {shuffle,Seed} +%% To get cases executed in random order. +%% Seed = {integer(),integer(),integer()} +%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | +%% repeat_until_any_ok | repeat_until_any_fail +%% To get execution of cases repeated. +%% N = integer() | forever +%% +%% Description: Returns a list of test case group definitions. +%%-------------------------------------------------------------------- +groups() -> + []. + +%%-------------------------------------------------------------------- +%% Function: all() -> GroupsAndTestCases | {skip,Reason} +%% +%% GroupsAndTestCases = [{group,GroupName} | TestCase] +%% GroupName = atom() +%% Name of a test case group. +%% TestCase = atom() +%% Name of a test case. +%% Reason = term() +%% The reason for skipping all groups and test cases. +%% +%% Description: Returns the list of groups and test cases that +%% are to be executed. +%%-------------------------------------------------------------------- +all() -> + [test1]. + + +%%-------------------------------------------------------------------- +%% TEST CASES +%%-------------------------------------------------------------------- + +%%-------------------------------------------------------------------- +%% Function: TestCase(Config0) -> +%% ok | exit() | {skip,Reason} | {comment,Comment} | +%% {save_config,Config1} | {skip_and_save,Reason,Config1} +%% +%% Config0 = Config1 = [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Reason = term() +%% The reason for skipping the test case. +%% Comment = term() +%% A comment about the test case that will be printed in the html log. +%% +%% Description: Test case function. (The name of it must be specified in +%% the all/0 list or in a test case group for the test case +%% to be executed). +%%-------------------------------------------------------------------- From 3993820b1c92803bedea85247ba3e6acae5e66fe Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 14:12:57 +0200 Subject: [PATCH 02/27] Add Makefile ct rule which labels a test run with git commit --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 8485ff54..bd4ed1b0 100644 --- a/Makefile +++ b/Makefile @@ -149,6 +149,9 @@ eunit: compile-tests erl $(ERL_OPTS) -noinput -pa ebin -pa test -eval \ '$(erl_run_eunit), halt().' +ct: + @rebar3 ct --label "git: $$(git describe --tags --always) $$(git diff --no-ext-diff --quiet --exit-code || echo '(modified)')" + cli-tests: bin/gradualizer test/arg.beam # CLI test cases # 1. When checking a dir with erl files, erl file names are printed From d97a9f4f04a4f8da27d33ed5bf3d214cefc8ab91 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 14:40:37 +0200 Subject: [PATCH 03/27] Get should_fail test generation to work --- test/should_fail_SUITE.erl | 39 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl index f249a0b0..4696a682 100644 --- a/test/should_fail_SUITE.erl +++ b/test/should_fail_SUITE.erl @@ -66,32 +66,31 @@ load_prerequisites() -> dynamic_suite_reload(Module) -> Forms = get_forms(Module), TestTemplate = merl:quote("'@Name'(_) -> _@Body."), - {'fun', _, {clauses, [{clause, _, _, _, Body}]}} = merl:quote(" - fun (_) -> - 1 / 0 - end - "), - TestName = "test1", + {function, _Anno, _Name, 1, Clauses} = lists:keyfind(should_fail_template, 3, Forms), + [{clause, _, _Args, _Guards, ClauseBodyTemplate}] = Clauses, + TestName = "unary_op", + TestFile = "/Users/erszcz/work/erszcz/gradualizer/test/should_fail/unary_op.erl", + ClauseBody = merl:subst(ClauseBodyTemplate, [{'File', erl_syntax:string(TestFile)}]), TestEnv = [ {'Name', erl_syntax:atom(TestName)}, - {'Body', Body} + {'Body', ClauseBody} ], TestForm = erl_syntax:revert(merl:subst(TestTemplate, TestEnv)), NewForms = Forms ++ [TestForm, {eof, 0}], - merl:compile_and_load(NewForms), + {ok, _} = merl:compile_and_load(NewForms), ok. -%should_fail_template() -> -% Errors = gradualizer:type_check_file(File, [return_errors]), -% Timeouts = [ E || {_File, {form_check_timeout, _}} = E <- Errors], -% 0 = length(Timeouts), -% %% Test that error formatting doesn't crash -% Opts = [{fmt_location, brief}, -% {fmt_expr_fun, fun erl_prettypr:format/1}], -% lists:foreach(fun({_, Error}) -> gradualizer_fmt:handle_type_error(Error, Opts) end, Errors), -% {ok, Forms} = gradualizer_file_utils:get_forms_from_erl(File, []), -% ExpectedErrors = typechecker:number_of_exported_functions(Forms), -% ExpectedErrors = length(Errors). +should_fail_template(_@File) -> + Errors = gradualizer:type_check_file(_@File, [return_errors]), + Timeouts = [ E || {_File, {form_check_timeout, _}} = E <- Errors], + 0 = length(Timeouts), + %% Test that error formatting doesn't crash + Opts = [{fmt_location, brief}, + {fmt_expr_fun, fun erl_prettypr:format/1}], + lists:foreach(fun({_, Error}) -> gradualizer_fmt:handle_type_error(Error, Opts) end, Errors), + {ok, Forms} = gradualizer_file_utils:get_forms_from_erl(_@File, []), + ExpectedErrors = typechecker:number_of_exported_functions(Forms), + ExpectedErrors = length(Errors). get_forms(Module) -> ModPath = code:which(Module), @@ -218,7 +217,7 @@ groups() -> %% are to be executed. %%-------------------------------------------------------------------- all() -> - [test1]. + [unary_op]. %%-------------------------------------------------------------------- From a46d2a094de04a639bfeaed121ba7db483a0cd62 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 14:56:59 +0200 Subject: [PATCH 04/27] Generate all tests for a given path --- test/should_fail_SUITE.erl | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl index 4696a682..a5cc4dbe 100644 --- a/test/should_fail_SUITE.erl +++ b/test/should_fail_SUITE.erl @@ -65,20 +65,29 @@ load_prerequisites() -> dynamic_suite_reload(Module) -> Forms = get_forms(Module), + FilesForms = map_erl_files(fun (File) -> + make_test_form(Forms, File) + end, "/Users/erszcz/work/erszcz/gradualizer/test/should_fail"), + {_TestFiles, TestForms} = lists:unzip(FilesForms), + NewForms = Forms ++ TestForms ++ [{eof, 0}], + {ok, _} = merl:compile_and_load(NewForms), + ok. + +map_erl_files(Fun, Dir) -> + Files = filelib:wildcard(filename:join(Dir, "*.erl")), + [{filename:basename(File), Fun(File)} || File <- Files]. + +make_test_form(Forms, File) -> TestTemplate = merl:quote("'@Name'(_) -> _@Body."), {function, _Anno, _Name, 1, Clauses} = lists:keyfind(should_fail_template, 3, Forms), [{clause, _, _Args, _Guards, ClauseBodyTemplate}] = Clauses, - TestName = "unary_op", - TestFile = "/Users/erszcz/work/erszcz/gradualizer/test/should_fail/unary_op.erl", - ClauseBody = merl:subst(ClauseBodyTemplate, [{'File', erl_syntax:string(TestFile)}]), + TestName = filename:basename(File, ".erl"), + ClauseBody = merl:subst(ClauseBodyTemplate, [{'File', erl_syntax:string(File)}]), TestEnv = [ {'Name', erl_syntax:atom(TestName)}, {'Body', ClauseBody} ], - TestForm = erl_syntax:revert(merl:subst(TestTemplate, TestEnv)), - NewForms = Forms ++ [TestForm, {eof, 0}], - {ok, _} = merl:compile_and_load(NewForms), - ok. + erl_syntax:revert(merl:subst(TestTemplate, TestEnv)). should_fail_template(_@File) -> Errors = gradualizer:type_check_file(_@File, [return_errors]), From 193dcb07450796bca787b589e02394062a0e63dd Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 15:11:03 +0200 Subject: [PATCH 05/27] Generate SUITE:all/0 dynamically Sadly, this doesn't work, as init_per_suite/1 is called after all/0, so a suite with all/0 not returning any tests is not run :( --- test/should_fail_SUITE.erl | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl index a5cc4dbe..1a1e1c29 100644 --- a/test/should_fail_SUITE.erl +++ b/test/should_fail_SUITE.erl @@ -68,8 +68,16 @@ dynamic_suite_reload(Module) -> FilesForms = map_erl_files(fun (File) -> make_test_form(Forms, File) end, "/Users/erszcz/work/erszcz/gradualizer/test/should_fail"), - {_TestFiles, TestForms} = lists:unzip(FilesForms), - NewForms = Forms ++ TestForms ++ [{eof, 0}], + {TestFiles, TestForms} = lists:unzip(FilesForms), + TestNames = [ list_to_atom(filename:basename(File, ".erl")) || File <- TestFiles ], + AllTestsTemplate = merl:quote("all() -> _@AllTests."), + AllTestsForm = merl:subst(AllTestsTemplate, [{'AllTests', merl:term(TestNames)}]), + DropAllFunction = fun + ({function, _, all, _, _}) -> false; + (_) -> true + end, + Forms1 = lists:filter(DropAllFunction, Forms), + NewForms = Forms1 ++ [AllTestsForm] ++ TestForms ++ [{eof, 0}], {ok, _} = merl:compile_and_load(NewForms), ok. @@ -226,7 +234,8 @@ groups() -> %% are to be executed. %%-------------------------------------------------------------------- all() -> - [unary_op]. + %% Body of this function is dynamically replaced in init_per_suite/1. + []. %%-------------------------------------------------------------------- From 81e85264b07f8dfefc52f3e5211b22375d70f91f Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 15:25:56 +0200 Subject: [PATCH 06/27] Define all/0 tests manually --- test/should_fail_SUITE.erl | 57 +++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl index 1a1e1c29..3958e60d 100644 --- a/test/should_fail_SUITE.erl +++ b/test/should_fail_SUITE.erl @@ -52,7 +52,11 @@ suite() -> init_per_suite(Config) -> {ok, _} = application:ensure_all_started(gradualizer), ok = load_prerequisites(), - ok = dynamic_suite_reload(?MODULE), + {ok, TestNames} = dynamic_suite_reload(?MODULE), + case all() of + TestNames -> ok; + _ -> ct:fail("Please update all/0 to list all should_fail tests") + end, Config. load_prerequisites() -> @@ -65,21 +69,16 @@ load_prerequisites() -> dynamic_suite_reload(Module) -> Forms = get_forms(Module), + Path = "/Users/erszcz/work/erszcz/gradualizer/test/should_fail", FilesForms = map_erl_files(fun (File) -> make_test_form(Forms, File) - end, "/Users/erszcz/work/erszcz/gradualizer/test/should_fail"), + end, Path), {TestFiles, TestForms} = lists:unzip(FilesForms), TestNames = [ list_to_atom(filename:basename(File, ".erl")) || File <- TestFiles ], - AllTestsTemplate = merl:quote("all() -> _@AllTests."), - AllTestsForm = merl:subst(AllTestsTemplate, [{'AllTests', merl:term(TestNames)}]), - DropAllFunction = fun - ({function, _, all, _, _}) -> false; - (_) -> true - end, - Forms1 = lists:filter(DropAllFunction, Forms), - NewForms = Forms1 ++ [AllTestsForm] ++ TestForms ++ [{eof, 0}], + ct:pal("All tests found under ~s:\n~p\n", [Path, TestNames]), + NewForms = Forms ++ TestForms ++ [{eof, 0}], {ok, _} = merl:compile_and_load(NewForms), - ok. + {ok, TestNames}. map_erl_files(Fun, Dir) -> Files = filelib:wildcard(filename:join(Dir, "*.erl")), @@ -234,8 +233,40 @@ groups() -> %% are to be executed. %%-------------------------------------------------------------------- all() -> - %% Body of this function is dynamically replaced in init_per_suite/1. - []. + [annotated_types_fail,arg,arith_op_fail,arity_mismatch, + bc_fail,bin_expression,bin_type_error,branch,branch2,call, + call_intersection_function_with_union_arg_fail,case_pattern, + case_pattern2,catch_expr_fail,cons,covariant_map_keys_fail, + cyclic_type_vars,depth,exhaustive,exhaustive_float, + exhaustive_list_variants,exhaustive_refinable_map_variants, + exhaustive_remote_user_type,exhaustive_string_variants, + exhaustive_type,exhaustive_user_type, + exhaustiveness_check_toggling,generator,guard_fail, + imported_undef,infer_enabled,intersection_check, + intersection_fail,intersection_infer, + intersection_with_any_fail,iodata_fail,lambda_not_fun, + lc_generator_not_none_fail,lc_not_list,list_infer_fail, + list_op,list_op_should_fail,list_union_fail, + lists_map_nonempty_fail,literal_char,literal_patterns, + logic_op,map_entry,map_fail,map_failing_expr, + map_failing_subtyping,map_field_invalid_update,map_literal, + map_pattern_fail,map_refinement_fail,map_type_error,match, + messaging_fail,module_info_fail,named_fun_fail, + named_fun_infer_fail,nil,no_idempotent_xor, + non_neg_plus_pos_is_pos_fail, + nonempty_list_match_in_head_nonexhaustive, + nonempty_string_fail,opaque_fail,operator_pattern_fail, + pattern,pattern_record_fail,poly_fail,poly_lists_map_fail, + poly_union_lower_bound_fail,pp_intersection,record, + record_exhaustive,record_field,record_index, + record_info_fail,record_refinement_fail,record_update, + record_wildcard_fail,recursive_type_fail, + recursive_types_failing,rel_op,return_fun_fail, + rigid_type_variables_fail,send_fail,shortcut_ops_fail, + spec_and_fun_clause_intersection_fail,string_literal, + tuple_union_arg_fail,tuple_union_fail,tuple_union_pattern, + tuple_union_refinement,type_refinement_fail,unary_op, + unary_plus_fail,union_with_any,unreachable_after_refinement]. %%-------------------------------------------------------------------- From 421368768caec3ece88f908222c08ab2e52ef771 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 15:35:05 +0200 Subject: [PATCH 07/27] Make paths relative to app location --- test/should_fail_SUITE.erl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl index 3958e60d..819a827c 100644 --- a/test/should_fail_SUITE.erl +++ b/test/should_fail_SUITE.erl @@ -51,25 +51,26 @@ suite() -> %%-------------------------------------------------------------------- init_per_suite(Config) -> {ok, _} = application:ensure_all_started(gradualizer), - ok = load_prerequisites(), - {ok, TestNames} = dynamic_suite_reload(?MODULE), + AppBase = code:lib_dir(gradualizer), + ok = load_prerequisites(AppBase), + {ok, TestNames} = dynamic_suite_reload(?MODULE, AppBase), case all() of TestNames -> ok; _ -> ct:fail("Please update all/0 to list all should_fail tests") end, Config. -load_prerequisites() -> +load_prerequisites(AppBase) -> %% user_types.erl is referenced by opaque_fail.erl. %% It is not in the sourcemap of the DB so let's import it manually - %gradualizer_db:import_erl_files(["test/should_pass/user_types.erl"]), + gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_pass/user_types.erl")]), %% exhaustive_user_type.erl is referenced by exhaustive_remote_user_type.erl - %gradualizer_db:import_erl_files(["test/should_fail/exhaustive_user_type.erl"]), + gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_fail/exhaustive_user_type.erl")]), ok. -dynamic_suite_reload(Module) -> +dynamic_suite_reload(Module, AppBase) -> Forms = get_forms(Module), - Path = "/Users/erszcz/work/erszcz/gradualizer/test/should_fail", + Path = filename:join(AppBase, "test/should_fail"), FilesForms = map_erl_files(fun (File) -> make_test_form(Forms, File) end, Path), From 57b9ee2a1d7003dfd491ac492c280b1e73cefa51 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 15:38:59 +0200 Subject: [PATCH 08/27] Use EUnit macros for CLI readability --- test/should_fail_SUITE.erl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl index 819a827c..786f455f 100644 --- a/test/should_fail_SUITE.erl +++ b/test/should_fail_SUITE.erl @@ -4,6 +4,9 @@ -include_lib("common_test/include/ct.hrl"). +%% EUnit has some handy macros, so let's use it, too +-include_lib("eunit/include/eunit.hrl"). + %% Test server callbacks -export([suite/0, all/0, @@ -100,14 +103,14 @@ make_test_form(Forms, File) -> should_fail_template(_@File) -> Errors = gradualizer:type_check_file(_@File, [return_errors]), Timeouts = [ E || {_File, {form_check_timeout, _}} = E <- Errors], - 0 = length(Timeouts), + ?assertEqual(0, length(Timeouts)), %% Test that error formatting doesn't crash Opts = [{fmt_location, brief}, {fmt_expr_fun, fun erl_prettypr:format/1}], lists:foreach(fun({_, Error}) -> gradualizer_fmt:handle_type_error(Error, Opts) end, Errors), {ok, Forms} = gradualizer_file_utils:get_forms_from_erl(_@File, []), ExpectedErrors = typechecker:number_of_exported_functions(Forms), - ExpectedErrors = length(Errors). + ?assertEqual(ExpectedErrors, length(Errors)). get_forms(Module) -> ModPath = code:which(Module), From fcb443daec05ea7f5015cb77fe0121e81ac80fea Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 15:50:42 +0200 Subject: [PATCH 09/27] Clean up --- test/should_fail_SUITE.erl | 154 +------------------------------------ 1 file changed, 1 insertion(+), 153 deletions(-) diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl index 786f455f..010367cb 100644 --- a/test/should_fail_SUITE.erl +++ b/test/should_fail_SUITE.erl @@ -1,6 +1,6 @@ -module(should_fail_SUITE). --compile(export_all). +-compile([export_all, nowarn_export_all]). -include_lib("common_test/include/ct.hrl"). @@ -15,43 +15,9 @@ init_per_group/2, end_per_group/2, init_per_testcase/2, end_per_testcase/2]). -%% Test cases --export([ - ]). - -%%-------------------------------------------------------------------- -%% COMMON TEST CALLBACK FUNCTIONS -%%-------------------------------------------------------------------- - -%%-------------------------------------------------------------------- -%% Function: suite() -> Info -%% -%% Info = [tuple()] -%% List of key/value pairs. -%% -%% Description: Returns list of tuples to set default properties -%% for the suite. -%% -%% Note: The suite/0 function is only meant to be used to return -%% default data values, not perform any other operations. -%%-------------------------------------------------------------------- suite() -> [{timetrap,{minutes,10}}]. -%%-------------------------------------------------------------------- -%% Function: init_per_suite(Config0) -> -%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} -%% -%% Config0 = Config1 = [tuple()] -%% A list of key/value pairs, holding the test case configuration. -%% Reason = term() -%% The reason for skipping the suite. -%% -%% Description: Initialization before the suite. -%% -%% Note: This function is free to add any key/value pairs to the Config -%% variable, but should NOT alter/remove any existing entries. -%%-------------------------------------------------------------------- init_per_suite(Config) -> {ok, _} = application:ensure_all_started(gradualizer), AppBase = code:lib_dir(gradualizer), @@ -122,120 +88,24 @@ get_forms(Module) -> end, lists:filter(StripEnd, Forms). -%%-------------------------------------------------------------------- -%% Function: end_per_suite(Config0) -> void() | {save_config,Config1} -%% -%% Config0 = Config1 = [tuple()] -%% A list of key/value pairs, holding the test case configuration. -%% -%% Description: Cleanup after the suite. -%%-------------------------------------------------------------------- end_per_suite(_Config) -> ok. -%%-------------------------------------------------------------------- -%% Function: init_per_group(GroupName, Config0) -> -%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} -%% -%% GroupName = atom() -%% Name of the test case group that is about to run. -%% Config0 = Config1 = [tuple()] -%% A list of key/value pairs, holding configuration data for the group. -%% Reason = term() -%% The reason for skipping all test cases and subgroups in the group. -%% -%% Description: Initialization before each test case group. -%%-------------------------------------------------------------------- init_per_group(_GroupName, Config) -> Config. -%%-------------------------------------------------------------------- -%% Function: end_per_group(GroupName, Config0) -> -%% void() | {save_config,Config1} -%% -%% GroupName = atom() -%% Name of the test case group that is finished. -%% Config0 = Config1 = [tuple()] -%% A list of key/value pairs, holding configuration data for the group. -%% -%% Description: Cleanup after each test case group. -%%-------------------------------------------------------------------- end_per_group(_GroupName, _Config) -> ok. -%%-------------------------------------------------------------------- -%% Function: init_per_testcase(TestCase, Config0) -> -%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} -%% -%% TestCase = atom() -%% Name of the test case that is about to run. -%% Config0 = Config1 = [tuple()] -%% A list of key/value pairs, holding the test case configuration. -%% Reason = term() -%% The reason for skipping the test case. -%% -%% Description: Initialization before each test case. -%% -%% Note: This function is free to add any key/value pairs to the Config -%% variable, but should NOT alter/remove any existing entries. -%%-------------------------------------------------------------------- init_per_testcase(_TestCase, Config) -> Config. -%%-------------------------------------------------------------------- -%% Function: end_per_testcase(TestCase, Config0) -> -%% void() | {save_config,Config1} | {fail,Reason} -%% -%% TestCase = atom() -%% Name of the test case that is finished. -%% Config0 = Config1 = [tuple()] -%% A list of key/value pairs, holding the test case configuration. -%% Reason = term() -%% The reason for failing the test case. -%% -%% Description: Cleanup after each test case. -%%-------------------------------------------------------------------- end_per_testcase(_TestCase, _Config) -> ok. -%%-------------------------------------------------------------------- -%% Function: groups() -> [Group] -%% -%% Group = {GroupName,Properties,GroupsAndTestCases} -%% GroupName = atom() -%% The name of the group. -%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] -%% Group properties that may be combined. -%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] -%% TestCase = atom() -%% The name of a test case. -%% Shuffle = shuffle | {shuffle,Seed} -%% To get cases executed in random order. -%% Seed = {integer(),integer(),integer()} -%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | -%% repeat_until_any_ok | repeat_until_any_fail -%% To get execution of cases repeated. -%% N = integer() | forever -%% -%% Description: Returns a list of test case group definitions. -%%-------------------------------------------------------------------- groups() -> []. -%%-------------------------------------------------------------------- -%% Function: all() -> GroupsAndTestCases | {skip,Reason} -%% -%% GroupsAndTestCases = [{group,GroupName} | TestCase] -%% GroupName = atom() -%% Name of a test case group. -%% TestCase = atom() -%% Name of a test case. -%% Reason = term() -%% The reason for skipping all groups and test cases. -%% -%% Description: Returns the list of groups and test cases that -%% are to be executed. -%%-------------------------------------------------------------------- all() -> [annotated_types_fail,arg,arith_op_fail,arity_mismatch, bc_fail,bin_expression,bin_type_error,branch,branch2,call, @@ -271,25 +141,3 @@ all() -> tuple_union_arg_fail,tuple_union_fail,tuple_union_pattern, tuple_union_refinement,type_refinement_fail,unary_op, unary_plus_fail,union_with_any,unreachable_after_refinement]. - - -%%-------------------------------------------------------------------- -%% TEST CASES -%%-------------------------------------------------------------------- - -%%-------------------------------------------------------------------- -%% Function: TestCase(Config0) -> -%% ok | exit() | {skip,Reason} | {comment,Comment} | -%% {save_config,Config1} | {skip_and_save,Reason,Config1} -%% -%% Config0 = Config1 = [tuple()] -%% A list of key/value pairs, holding the test case configuration. -%% Reason = term() -%% The reason for skipping the test case. -%% Comment = term() -%% A comment about the test case that will be printed in the html log. -%% -%% Description: Test case function. (The name of it must be specified in -%% the all/0 list or in a test case group for the test case -%% to be executed). -%%-------------------------------------------------------------------- From f150d53c2ee37670b7c49f576a45a6815e2789ce Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 15:51:21 +0200 Subject: [PATCH 10/27] Make test template function configurable, pass params from the top --- test/should_fail_SUITE.erl | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl index 010367cb..a859edf5 100644 --- a/test/should_fail_SUITE.erl +++ b/test/should_fail_SUITE.erl @@ -16,20 +16,25 @@ init_per_testcase/2, end_per_testcase/2]). suite() -> - [{timetrap,{minutes,10}}]. - -init_per_suite(Config) -> + [{timetrap, {minutes, 10}}]. + +init_per_suite(Config0) -> + Config = [ + {app_base, code:lib_dir(gradualizer)}, + {dynamic_suite_module, ?MODULE}, + {dynamic_test_template, should_fail_template} + ] ++ Config0, {ok, _} = application:ensure_all_started(gradualizer), - AppBase = code:lib_dir(gradualizer), - ok = load_prerequisites(AppBase), - {ok, TestNames} = dynamic_suite_reload(?MODULE, AppBase), + ok = load_prerequisites(Config), + {ok, TestNames} = dynamic_suite_reload(Config), case all() of TestNames -> ok; _ -> ct:fail("Please update all/0 to list all should_fail tests") end, Config. -load_prerequisites(AppBase) -> +load_prerequisites(Config) -> + AppBase = ?config(app_base, Config), %% user_types.erl is referenced by opaque_fail.erl. %% It is not in the sourcemap of the DB so let's import it manually gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_pass/user_types.erl")]), @@ -37,11 +42,13 @@ load_prerequisites(AppBase) -> gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_fail/exhaustive_user_type.erl")]), ok. -dynamic_suite_reload(Module, AppBase) -> +dynamic_suite_reload(Config) -> + Module = ?config(dynamic_suite_module, Config), + AppBase = ?config(app_base, Config), Forms = get_forms(Module), Path = filename:join(AppBase, "test/should_fail"), FilesForms = map_erl_files(fun (File) -> - make_test_form(Forms, File) + make_test_form(Forms, File, Config) end, Path), {TestFiles, TestForms} = lists:unzip(FilesForms), TestNames = [ list_to_atom(filename:basename(File, ".erl")) || File <- TestFiles ], @@ -54,9 +61,10 @@ map_erl_files(Fun, Dir) -> Files = filelib:wildcard(filename:join(Dir, "*.erl")), [{filename:basename(File), Fun(File)} || File <- Files]. -make_test_form(Forms, File) -> +make_test_form(Forms, File, Config) -> + TestTemplateName = ?config(dynamic_test_template, Config), TestTemplate = merl:quote("'@Name'(_) -> _@Body."), - {function, _Anno, _Name, 1, Clauses} = lists:keyfind(should_fail_template, 3, Forms), + {function, _Anno, _Name, 1, Clauses} = lists:keyfind(TestTemplateName, 3, Forms), [{clause, _, _Args, _Guards, ClauseBodyTemplate}] = Clauses, TestName = filename:basename(File, ".erl"), ClauseBody = merl:subst(ClauseBodyTemplate, [{'File', erl_syntax:string(File)}]), From 34aa4a490b61d33d7150f336153d5da701166342 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 16:15:05 +0200 Subject: [PATCH 11/27] Extract gradualizer_dynamic_suite --- test/gradualizer_dynamic_suite.erl | 50 ++++++++++++++++++ test/should_fail_SUITE.erl | 81 +++++++----------------------- 2 files changed, 69 insertions(+), 62 deletions(-) create mode 100644 test/gradualizer_dynamic_suite.erl diff --git a/test/gradualizer_dynamic_suite.erl b/test/gradualizer_dynamic_suite.erl new file mode 100644 index 00000000..b002d613 --- /dev/null +++ b/test/gradualizer_dynamic_suite.erl @@ -0,0 +1,50 @@ +-module(gradualizer_dynamic_suite). + +-export([reload/1]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +reload(Config) -> + Module = ?config(dynamic_suite_module, Config), + Path = ?config(dynamic_suite_test_path, Config), + ?assert(Module /= undefined), + ?assert(Path /= undefined), + Forms = get_forms(Module), + FilesForms = map_erl_files(fun (File) -> + make_test_form(Forms, File, Config) + end, Path), + {TestFiles, TestForms} = lists:unzip(FilesForms), + TestNames = [ list_to_atom(filename:basename(File, ".erl")) || File <- TestFiles ], + ct:pal("All tests found under ~s:\n~p\n", [Path, TestNames]), + NewForms = Forms ++ TestForms ++ [{eof, 0}], + {ok, _} = merl:compile_and_load(NewForms), + {ok, TestNames}. + +map_erl_files(Fun, Dir) -> + Files = filelib:wildcard(filename:join(Dir, "*.erl")), + [{filename:basename(File), Fun(File)} || File <- Files]. + +make_test_form(Forms, File, Config) -> + TestTemplateName = ?config(dynamic_test_template, Config), + ?assert(TestTemplateName /= undefined), + TestTemplate = merl:quote("'@Name'(_) -> _@Body."), + {function, _Anno, _Name, 1, Clauses} = lists:keyfind(TestTemplateName, 3, Forms), + [{clause, _, _Args, _Guards, ClauseBodyTemplate}] = Clauses, + TestName = filename:basename(File, ".erl"), + ClauseBody = merl:subst(ClauseBodyTemplate, [{'File', erl_syntax:string(File)}]), + TestEnv = [ + {'Name', erl_syntax:atom(TestName)}, + {'Body', ClauseBody} + ], + erl_syntax:revert(merl:subst(TestTemplate, TestEnv)). + +get_forms(Module) -> + ModPath = code:which(Module), + {ok, {Module, [Abst]}} = beam_lib:chunks(ModPath, [abstract_code]), + {abstract_code, {raw_abstract_v1, Forms}} = Abst, + StripEnd = fun + ({eof, _}) -> false; + (_) -> true + end, + lists:filter(StripEnd, Forms). diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl index a859edf5..40c5b2d4 100644 --- a/test/should_fail_SUITE.erl +++ b/test/should_fail_SUITE.erl @@ -2,8 +2,6 @@ -compile([export_all, nowarn_export_all]). --include_lib("common_test/include/ct.hrl"). - %% EUnit has some handy macros, so let's use it, too -include_lib("eunit/include/eunit.hrl"). @@ -19,22 +17,22 @@ suite() -> [{timetrap, {minutes, 10}}]. init_per_suite(Config0) -> + AppBase = code:lib_dir(gradualizer), Config = [ - {app_base, code:lib_dir(gradualizer)}, {dynamic_suite_module, ?MODULE}, + {dynamic_suite_test_path, filename:join(AppBase, "test/should_fail")}, {dynamic_test_template, should_fail_template} ] ++ Config0, {ok, _} = application:ensure_all_started(gradualizer), - ok = load_prerequisites(Config), - {ok, TestNames} = dynamic_suite_reload(Config), + ok = load_prerequisites(AppBase), + {ok, TestNames} = gradualizer_dynamic_suite:reload(Config), case all() of TestNames -> ok; - _ -> ct:fail("Please update all/0 to list all should_fail tests") + _ -> ct:fail("Please update all/0 to list all tests") end, Config. -load_prerequisites(Config) -> - AppBase = ?config(app_base, Config), +load_prerequisites(AppBase) -> %% user_types.erl is referenced by opaque_fail.erl. %% It is not in the sourcemap of the DB so let's import it manually gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_pass/user_types.erl")]), @@ -42,61 +40,8 @@ load_prerequisites(Config) -> gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_fail/exhaustive_user_type.erl")]), ok. -dynamic_suite_reload(Config) -> - Module = ?config(dynamic_suite_module, Config), - AppBase = ?config(app_base, Config), - Forms = get_forms(Module), - Path = filename:join(AppBase, "test/should_fail"), - FilesForms = map_erl_files(fun (File) -> - make_test_form(Forms, File, Config) - end, Path), - {TestFiles, TestForms} = lists:unzip(FilesForms), - TestNames = [ list_to_atom(filename:basename(File, ".erl")) || File <- TestFiles ], - ct:pal("All tests found under ~s:\n~p\n", [Path, TestNames]), - NewForms = Forms ++ TestForms ++ [{eof, 0}], - {ok, _} = merl:compile_and_load(NewForms), - {ok, TestNames}. - -map_erl_files(Fun, Dir) -> - Files = filelib:wildcard(filename:join(Dir, "*.erl")), - [{filename:basename(File), Fun(File)} || File <- Files]. - -make_test_form(Forms, File, Config) -> - TestTemplateName = ?config(dynamic_test_template, Config), - TestTemplate = merl:quote("'@Name'(_) -> _@Body."), - {function, _Anno, _Name, 1, Clauses} = lists:keyfind(TestTemplateName, 3, Forms), - [{clause, _, _Args, _Guards, ClauseBodyTemplate}] = Clauses, - TestName = filename:basename(File, ".erl"), - ClauseBody = merl:subst(ClauseBodyTemplate, [{'File', erl_syntax:string(File)}]), - TestEnv = [ - {'Name', erl_syntax:atom(TestName)}, - {'Body', ClauseBody} - ], - erl_syntax:revert(merl:subst(TestTemplate, TestEnv)). - -should_fail_template(_@File) -> - Errors = gradualizer:type_check_file(_@File, [return_errors]), - Timeouts = [ E || {_File, {form_check_timeout, _}} = E <- Errors], - ?assertEqual(0, length(Timeouts)), - %% Test that error formatting doesn't crash - Opts = [{fmt_location, brief}, - {fmt_expr_fun, fun erl_prettypr:format/1}], - lists:foreach(fun({_, Error}) -> gradualizer_fmt:handle_type_error(Error, Opts) end, Errors), - {ok, Forms} = gradualizer_file_utils:get_forms_from_erl(_@File, []), - ExpectedErrors = typechecker:number_of_exported_functions(Forms), - ?assertEqual(ExpectedErrors, length(Errors)). - -get_forms(Module) -> - ModPath = code:which(Module), - {ok, {Module, [Abst]}} = beam_lib:chunks(ModPath, [abstract_code]), - {abstract_code, {raw_abstract_v1, Forms}} = Abst, - StripEnd = fun - ({eof, _}) -> false; - (_) -> true - end, - lists:filter(StripEnd, Forms). - end_per_suite(_Config) -> + ok = application:stop(gradualizer), ok. init_per_group(_GroupName, Config) -> @@ -149,3 +94,15 @@ all() -> tuple_union_arg_fail,tuple_union_fail,tuple_union_pattern, tuple_union_refinement,type_refinement_fail,unary_op, unary_plus_fail,union_with_any,unreachable_after_refinement]. + +should_fail_template(_@File) -> + Errors = gradualizer:type_check_file(_@File, [return_errors]), + Timeouts = [ E || {_File, {form_check_timeout, _}} = E <- Errors], + ?assertEqual(0, length(Timeouts)), + %% Test that error formatting doesn't crash + Opts = [{fmt_location, brief}, + {fmt_expr_fun, fun erl_prettypr:format/1}], + lists:foreach(fun({_, Error}) -> gradualizer_fmt:handle_type_error(Error, Opts) end, Errors), + {ok, Forms} = gradualizer_file_utils:get_forms_from_erl(_@File, []), + ExpectedErrors = typechecker:number_of_exported_functions(Forms), + ?assertEqual(ExpectedErrors, length(Errors)). From 2c4df557d79089950235a413708a7a8fcd562036 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 16:15:23 +0200 Subject: [PATCH 12/27] Add test/should_pass_SUITE.erl --- test/should_pass_SUITE.erl | 104 +++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 test/should_pass_SUITE.erl diff --git a/test/should_pass_SUITE.erl b/test/should_pass_SUITE.erl new file mode 100644 index 00000000..b89277f8 --- /dev/null +++ b/test/should_pass_SUITE.erl @@ -0,0 +1,104 @@ +-module(should_pass_SUITE). + +-compile([export_all, nowarn_export_all]). + +%% EUnit has some handy macros, so let's use it, too +-include_lib("eunit/include/eunit.hrl"). + +%% Test server callbacks +-export([suite/0, + all/0, + groups/0, + init_per_suite/1, end_per_suite/1, + init_per_group/2, end_per_group/2, + init_per_testcase/2, end_per_testcase/2]). + +suite() -> + [{timetrap, {minutes, 10}}]. + +init_per_suite(Config0) -> + AppBase = code:lib_dir(gradualizer), + Config = [ + {dynamic_suite_module, ?MODULE}, + {dynamic_suite_test_path, filename:join(AppBase, "test/should_pass")}, + {dynamic_test_template, should_pass_template} + ] ++ Config0, + {ok, _} = application:ensure_all_started(gradualizer), + ok = load_prerequisites(AppBase), + {ok, TestNames} = gradualizer_dynamic_suite:reload(Config), + case all() of + TestNames -> ok; + _ -> ct:fail("Please update all/0 to list all tests") + end, + Config. + +load_prerequisites(AppBase) -> + %% user_types.erl is referenced by remote_types.erl and opaque.erl. + %% It is not in the sourcemap of the DB so let's import it manually + gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_pass/user_types.erl")]), + gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_pass/other_module.erl")]), + %% imported.erl references any.erl + gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_pass/any.erl")]), + ok. + +end_per_suite(_Config) -> + ok = application:stop(gradualizer), + ok. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, _Config) -> + ok. + +init_per_testcase(_TestCase, Config) -> + Config. + +end_per_testcase(_TestCase, _Config) -> + ok. + +groups() -> + []. + +all() -> + [alias_in_pattern,andalso_any,ann_types,annotated_types,any, + any_doesnt_have_type_none_pass,any_pattern,bc_pass, + binary_exhaustiveness_checking,binary_in_union,binary_literal_pattern, + bitstring,block_scope,bool,bounded_funs, + call_intersection_function_with_union_arg_pass,'case', + case_of_record_with_user_defined,catch_expr_pass,covariant_map_keys_pass, + cyclic_otp_specs,erlang_error_args_none_pass,exhaustiveness_union_types, + factorial,float,flow,fun_capture,fun_spec,guard,guard_sequences_pass,if_expr, + imported,int,intersection_pass,intersection_with_any_pass,iodata,issue131,lc, + lc_generator_not_none,lc_var_binds_in_filters,list,list_concat_op_pass, + list_exhaustiveness_checking_regressions, + list_exhaustiveness_checking_regressions2, + list_exhaustiveness_checking_unreachable_clause_regression,list_infer_pass, + list_op_pass,listsspecs,map,map_as_argument_update,map_creation, + map_field_valid_update,map_infer_pass,map_passing_expr,map_passing_subtyping, + map_pattern,map_refinement,map_update,map_update_with_record_field, + messaging_pass,minimised_gradualizer_fmt,minus,module_info_higher_arity, + module_info_pass,named_fun_infer_pass,named_fun_pass,negate_none, + nested_pattern_match,non_neg_plus_pos_is_pos_pass,nonempty_cons, + nonempty_list_match_in_head_exhaustive,nonempty_string, + nonexhaustive_record_pattern,opaque,operator_pattern_pass,operator_subtypes, + other_module,pattern_bind_reuse,pattern_record,pattern_with_ty_vars, + poly_lists_map_constraints_pass,poly_lists_map_pass,poly_map_pattern, + poly_pass,poly_pass_infer,poly_pass_no_solve_constraints, + poly_union_lower_bound_pass,preludes,qlc_test,record_info,record_refinement, + record_union_pass,record_union_with_any_should_pass,record_var, + record_wildcard_pass,record_with_user_defined,records, + recursive_call_with_remote_union_return_type_pass,recursive_types_passing, + refine_comparison,refine_mismatch_using_guard_bifs,remote_types, + remote_types_pass,return_fun,rigid_type_variables,rigid_type_variables_pass, + scope,send_pass,sets_set,shortcut_ops_pass, + spec_and_fun_clause_intersection_pass,stuff_as_top,'try',try_expr,tuple, + tuple_union_pass,tuple_union_pat,tuple_union_pattern_pass,type_decl, + type_pattern,type_refinement_pass,type_variable,type_vars_term, + typed_record_field_access,unary_negate_union_with_user_type_pass,unary_plus, + underscore,user_type_in_pattern_body,user_types,var,var_fun,varbind_in_block, + varbind_in_case,varbind_in_function_head,varbind_in_lc,variable_binding, + variable_binding_leaks]. + +should_pass_template(_@File) -> + ?assertEqual(ok, gradualizer:type_check_file(_@File)). From 0983a7aa5910d3fa720621f8650cfb0d548f666f Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 20:52:02 +0200 Subject: [PATCH 13/27] Rename: test/should_pass/module_info.erl -> test/should_pass/module_info_pass.erl This is necessary to avoid a problem with the test module name being used as a generated test name, which is a function name. It lead to a name clash with preexisting module_info/1, which is defined for every module. --- test/should_pass/{module_info.erl => module_info_pass.erl} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename test/should_pass/{module_info.erl => module_info_pass.erl} (92%) diff --git a/test/should_pass/module_info.erl b/test/should_pass/module_info_pass.erl similarity index 92% rename from test/should_pass/module_info.erl rename to test/should_pass/module_info_pass.erl index d4d1c028..8e781741 100644 --- a/test/should_pass/module_info.erl +++ b/test/should_pass/module_info_pass.erl @@ -1,4 +1,4 @@ --module(module_info). +-module(module_info_pass). -compile([export_all, nowarn_export_all]). @@ -18,4 +18,4 @@ unary_direct() -> -spec unary_var() -> atom(). unary_var() -> I = erlang:module_info(module), - I. \ No newline at end of file + I. From b1d8222ed85b71f9d7a240b4613d592c141b038e Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 21:02:30 +0200 Subject: [PATCH 14/27] Don't explicitly enable maybe_expr --- rebar.config | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rebar.config b/rebar.config index 155bd7cc..0de6c2cb 100644 --- a/rebar.config +++ b/rebar.config @@ -4,10 +4,7 @@ {deps, [ {proper, {git, "https://github.com/proper-testing/proper.git", {branch, "master"}}} - ]}, - %% see the maybe expression fail; - %% the VM also needs to be configured to load the module - {erl_opts, [{feature,maybe_expr,enable}]} + ]} ]} ]}. From e345dc10801543beea2c05c5021518ccd0ae8636 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 21:25:36 +0200 Subject: [PATCH 15/27] Add test/known_problems_should_fail_SUITE.erl --- test/known_problems_should_fail_SUITE.erl | 87 +++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 test/known_problems_should_fail_SUITE.erl diff --git a/test/known_problems_should_fail_SUITE.erl b/test/known_problems_should_fail_SUITE.erl new file mode 100644 index 00000000..9f1eb42a --- /dev/null +++ b/test/known_problems_should_fail_SUITE.erl @@ -0,0 +1,87 @@ +-module(known_problems_should_fail_SUITE). + +-compile([export_all, nowarn_export_all]). + +%% EUnit has some handy macros, so let's use it, too +-include_lib("eunit/include/eunit.hrl"). + +%% Test server callbacks +-export([suite/0, + all/0, + groups/0, + init_per_suite/1, end_per_suite/1, + init_per_group/2, end_per_group/2, + init_per_testcase/2, end_per_testcase/2]). + +suite() -> + [{timetrap, {minutes, 10}}]. + +init_per_suite(Config0) -> + AppBase = code:lib_dir(gradualizer), + Config = [ + {dynamic_suite_module, ?MODULE}, + {dynamic_suite_test_path, filename:join(AppBase, "test/known_problems/should_fail")}, + {dynamic_test_template, known_problems_should_fail_template} + ] ++ Config0, + {ok, _} = application:ensure_all_started(gradualizer), + ok = load_prerequisites(AppBase), + {ok, TestNames} = gradualizer_dynamic_suite:reload(Config), + case all() of + TestNames -> ok; + _ -> ct:fail("Please update all/0 to list all tests") + end, + Config. + +load_prerequisites(AppBase) -> + %% exhaustive_user_type.erl is referenced by exhaustive_remote_user_type.erl + gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_fail/exhaustive_user_type.erl")]), + ok. + +end_per_suite(_Config) -> + ok = application:stop(gradualizer), + ok. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, _Config) -> + ok. + +init_per_testcase(_TestCase, Config) -> + Config. + +end_per_testcase(_TestCase, _Config) -> + ok. + +groups() -> + []. + +all() -> + [arith_op,binary_comprehension,case_pattern_should_fail, + exhaustive_argumentwise,exhaustive_expr,exhaustive_map_variants, + exhaustive_remote_map_variants,guard_should_fail,infer_any_pattern, + intersection_with_any_should_fail,intersection_with_unreachable, + lambda_wrong_args,map_refinement_fancy,poly_lists_map_should_fail, + poly_should_fail,recursive_types_should_fail,refine_ty_vars,sample]. + +known_problems_should_fail_template(_@File) -> + Result = safe_type_check_file(_@File, [return_errors]), + case Result of + crash -> + ok; + Errors -> + ErrorsExceptTimeouts = lists:filter( + fun ({_File, {form_check_timeout, _}}) -> false; (_) -> true end, + Errors), + ?assertEqual(0, length(ErrorsExceptTimeouts)) + end. + +safe_type_check_file(File) -> + safe_type_check_file(File, []). + +safe_type_check_file(File, Opts) -> + try + gradualizer:type_check_file(File, Opts) + catch + _:_ -> crash + end. From 314ca8b1d6adacb5116fa08161b0f6d51278a99f Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 21:32:14 +0200 Subject: [PATCH 16/27] Add test/known_problems_should_pass_SUITE.erl --- test/known_problems_should_pass_SUITE.erl | 82 +++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test/known_problems_should_pass_SUITE.erl diff --git a/test/known_problems_should_pass_SUITE.erl b/test/known_problems_should_pass_SUITE.erl new file mode 100644 index 00000000..4a7115b1 --- /dev/null +++ b/test/known_problems_should_pass_SUITE.erl @@ -0,0 +1,82 @@ +-module(known_problems_should_pass_SUITE). + +-compile([export_all, nowarn_export_all]). + +%% EUnit has some handy macros, so let's use it, too +-include_lib("eunit/include/eunit.hrl"). + +%% Test server callbacks +-export([suite/0, + all/0, + groups/0, + init_per_suite/1, end_per_suite/1, + init_per_group/2, end_per_group/2, + init_per_testcase/2, end_per_testcase/2]). + +suite() -> + [{timetrap, {minutes, 10}}]. + +init_per_suite(Config0) -> + AppBase = code:lib_dir(gradualizer), + Config = [ + {dynamic_suite_module, ?MODULE}, + {dynamic_suite_test_path, filename:join(AppBase, "test/known_problems/should_pass")}, + {dynamic_test_template, known_problems_should_pass_template} + ] ++ Config0, + {ok, _} = application:ensure_all_started(gradualizer), + ok = load_prerequisites(AppBase), + {ok, TestNames} = gradualizer_dynamic_suite:reload(Config), + case all() of + TestNames -> ok; + _ -> ct:fail("Please update all/0 to list all tests") + end, + Config. + +load_prerequisites(_AppBase) -> + ok. + +end_per_suite(_Config) -> + ok = application:stop(gradualizer), + ok. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, _Config) -> + ok. + +init_per_testcase(_TestCase, Config) -> + Config. + +end_per_testcase(_TestCase, _Config) -> + ok. + +groups() -> + []. + +all() -> + [arith_op_arg_types,binary_exhaustiveness_checking_should_pass, + call_intersection_function_with_union_arg_should_pass, + different_normalization_levels,elixir_list_first,error_in_guard, + fun_subtyping,generator_var_shadow,inner_union_subtype_of_root_union, + intersection_should_pass,intersection_with_any,list_concat_op_should_pass, + list_tail,map_pattern_duplicate_key,maybe_expr,poly_should_pass, + poly_type_vars,recursive_types,refine_bound_var_on_mismatch, + refine_bound_var_with_guard_should_pass,refine_comparison_should_pass, + refine_list_tail,union_fun]. + +known_problems_should_pass_template(_@File) -> + {ok, Forms} = gradualizer_file_utils:get_forms_from_erl(_@File, []), + ExpectedErrors = typechecker:number_of_exported_functions(Forms), + ReturnedErrors = length(safe_type_check_file(_@File, [return_errors])), + ?assertEqual(ExpectedErrors, ReturnedErrors). + +safe_type_check_file(File) -> + safe_type_check_file(File, []). + +safe_type_check_file(File, Opts) -> + try + gradualizer:type_check_file(File, Opts) + catch + _:_ -> crash + end. From b7d3c32db6a84d4261714a3e02323dec49db0778 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 21:52:17 +0200 Subject: [PATCH 17/27] Parallelize CT tests --- src/typechecker.erl | 6 ++++-- test/known_problems_should_fail_SUITE.erl | 11 +++++++---- test/known_problems_should_pass_SUITE.erl | 11 +++++++---- test/should_fail_SUITE.erl | 11 +++++++---- test/should_pass_SUITE.erl | 13 ++++++++----- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/typechecker.erl b/src/typechecker.erl index 9e9337c6..bad64b8a 100644 --- a/src/typechecker.erl +++ b/src/typechecker.erl @@ -5746,8 +5746,10 @@ type_check_forms(Forms, Opts) -> %% a Gradualizer (NOT the checked program!) error. -spec type_check_form_with_timeout(expr(), [any()], boolean(), env(), [any()]) -> [any()]. type_check_form_with_timeout(Function, Errors, StopOnFirstError, Env, Opts) -> - %% TODO: make FormCheckTimeOut configurable - FormCheckTimeOut = ?form_check_timeout_ms, + FormCheckTimeOut = case lists:keyfind(form_check_timeout_ms, 1, Opts) of + false -> ?form_check_timeout_ms; + {form_check_timeout_ms, MS} -> MS + end, ?verbose(Env, "Spawning async task...~n", []), Self = self(), Task = fun () -> diff --git a/test/known_problems_should_fail_SUITE.erl b/test/known_problems_should_fail_SUITE.erl index 9f1eb42a..c65211b9 100644 --- a/test/known_problems_should_fail_SUITE.erl +++ b/test/known_problems_should_fail_SUITE.erl @@ -26,9 +26,9 @@ init_per_suite(Config0) -> {ok, _} = application:ensure_all_started(gradualizer), ok = load_prerequisites(AppBase), {ok, TestNames} = gradualizer_dynamic_suite:reload(Config), - case all() of + case all_tests() of TestNames -> ok; - _ -> ct:fail("Please update all/0 to list all tests") + _ -> ct:fail("Please update all_tests/0 to list all tests") end, Config. @@ -53,10 +53,13 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> ok. +all() -> + [{group, all_tests}]. + groups() -> - []. + [{all_tests, [parallel], all_tests()}]. -all() -> +all_tests() -> [arith_op,binary_comprehension,case_pattern_should_fail, exhaustive_argumentwise,exhaustive_expr,exhaustive_map_variants, exhaustive_remote_map_variants,guard_should_fail,infer_any_pattern, diff --git a/test/known_problems_should_pass_SUITE.erl b/test/known_problems_should_pass_SUITE.erl index 4a7115b1..c97bca8b 100644 --- a/test/known_problems_should_pass_SUITE.erl +++ b/test/known_problems_should_pass_SUITE.erl @@ -26,9 +26,9 @@ init_per_suite(Config0) -> {ok, _} = application:ensure_all_started(gradualizer), ok = load_prerequisites(AppBase), {ok, TestNames} = gradualizer_dynamic_suite:reload(Config), - case all() of + case all_tests() of TestNames -> ok; - _ -> ct:fail("Please update all/0 to list all tests") + _ -> ct:fail("Please update all_tests/0 to list all tests") end, Config. @@ -51,10 +51,13 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> ok. +all() -> + [{group, all_tests}]. + groups() -> - []. + [{all_tests, [parallel], all_tests()}]. -all() -> +all_tests() -> [arith_op_arg_types,binary_exhaustiveness_checking_should_pass, call_intersection_function_with_union_arg_should_pass, different_normalization_levels,elixir_list_first,error_in_guard, diff --git a/test/should_fail_SUITE.erl b/test/should_fail_SUITE.erl index 40c5b2d4..95011f1b 100644 --- a/test/should_fail_SUITE.erl +++ b/test/should_fail_SUITE.erl @@ -26,9 +26,9 @@ init_per_suite(Config0) -> {ok, _} = application:ensure_all_started(gradualizer), ok = load_prerequisites(AppBase), {ok, TestNames} = gradualizer_dynamic_suite:reload(Config), - case all() of + case all_tests() of TestNames -> ok; - _ -> ct:fail("Please update all/0 to list all tests") + _ -> ct:fail("Please update all_tests/0 to list all tests") end, Config. @@ -56,10 +56,13 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> ok. +all() -> + [{group, all_tests}]. + groups() -> - []. + [{all_tests, [parallel], all_tests()}]. -all() -> +all_tests() -> [annotated_types_fail,arg,arith_op_fail,arity_mismatch, bc_fail,bin_expression,bin_type_error,branch,branch2,call, call_intersection_function_with_union_arg_fail,case_pattern, diff --git a/test/should_pass_SUITE.erl b/test/should_pass_SUITE.erl index b89277f8..b8618ab5 100644 --- a/test/should_pass_SUITE.erl +++ b/test/should_pass_SUITE.erl @@ -26,9 +26,9 @@ init_per_suite(Config0) -> {ok, _} = application:ensure_all_started(gradualizer), ok = load_prerequisites(AppBase), {ok, TestNames} = gradualizer_dynamic_suite:reload(Config), - case all() of + case all_tests() of TestNames -> ok; - _ -> ct:fail("Please update all/0 to list all tests") + _ -> ct:fail("Please update all_tests/0 to list all tests") end, Config. @@ -57,10 +57,13 @@ init_per_testcase(_TestCase, Config) -> end_per_testcase(_TestCase, _Config) -> ok. +all() -> + [{group, all_tests}]. + groups() -> - []. + [{all_tests, [parallel], all_tests()}]. -all() -> +all_tests() -> [alias_in_pattern,andalso_any,ann_types,annotated_types,any, any_doesnt_have_type_none_pass,any_pattern,bc_pass, binary_exhaustiveness_checking,binary_in_union,binary_literal_pattern, @@ -101,4 +104,4 @@ all() -> variable_binding_leaks]. should_pass_template(_@File) -> - ?assertEqual(ok, gradualizer:type_check_file(_@File)). + ?assertEqual(ok, gradualizer:type_check_file(_@File, [{form_check_timeout_ms, 2000}])). From a14c026ecf9527d13f80164d3faf9b9fc7921781 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Fri, 31 May 2024 01:06:50 +0200 Subject: [PATCH 18/27] Try defining subtype(union(), union()) with type_diff() This solves the problem captured in test/known_problems/should_pass/different_normalization_levels.erl but leads to a lot of other test failures. --- src/typechecker.erl | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/src/typechecker.erl b/src/typechecker.erl index bad64b8a..14c7baac 100644 --- a/src/typechecker.erl +++ b/src/typechecker.erl @@ -361,15 +361,21 @@ compat_ty({type, _, 'fun', [{type, _, product, Args1}, Res1]}, {Aps, constraints:combine(Cs, Css, Env)}; %% Unions -compat_ty({type, _, union, Tys1}, {type, _, union, Tys2}, Seen, Env) -> - lists:foldl(fun (Ty1, {Seen1, C1}) -> - {Seen2, C2} = any_type(Ty1, Tys2, Seen1, Env), - {Seen2, constraints:combine(C1, C2, Env)} - end, {Seen, constraints:empty()}, Tys1); -compat_ty(Ty1, {type, _, union, Tys2}, Seen, Env) -> - any_type(Ty1, Tys2, Seen, Env); -compat_ty({type, _, union, Tys1}, Ty2, Seen, Env) -> - all_type(Tys1, Ty2, Seen, Env); +compat_ty({type, _, union, _} = U1, {type, _, union, _} = U2, Seen, Env) -> + case type_diff(U1, U2, Env) of + ?type(none) -> ret(Seen); + false -> throw(nomatch) + end; + +%compat_ty({type, _, union, Tys1}, {type, _, union, Tys2}, Seen, Env) -> +% lists:foldl(fun (Ty1, {Seen1, C1}) -> +% {Seen2, C2} = any_type(Ty1, Tys2, Seen1, Env), +% {Seen2, constraints:combine(C1, C2, Env)} +% end, {Seen, constraints:empty()}, Tys1); +%compat_ty(Ty1, {type, _, union, Tys2}, Seen, Env) -> +% any_type(Ty1, Tys2, Seen, Env); +%compat_ty({type, _, union, Tys1}, Ty2, Seen, Env) -> +% all_type(Tys1, Ty2, Seen, Env); % Integer types compat_ty(Ty1, Ty2, Seen, _Env) when ?is_int_type(Ty1), ?is_int_type(Ty2) -> @@ -608,16 +614,16 @@ any_type(Ty1, Tys, Seen0, Env) -> %% @doc All types in `Tys' must be compatible with `Ty'. %% Returns all the gather memoizations and constraints. %% Does not return (throws `nomatch') if any of the types is not compatible. --spec all_type([type()], type(), map(), env()) -> compat_acc(). -all_type(Tys, Ty, Seen, Env) -> - all_type(Tys, Ty, Seen, [], Env). - --spec all_type([type()], type(), map(), [constraints:t()], env()) -> compat_acc(). -all_type([], _Ty, Seen, Css, Env) -> - {Seen, constraints:combine(Css, Env)}; -all_type([Ty1|Tys], Ty, AIn, Css, Env) -> - {AOut, Cs} = compat(Ty1, Ty, AIn, Env), - all_type(Tys, Ty, AOut, [Cs|Css], Env). +%-spec all_type([type()], type(), map(), env()) -> compat_acc(). +%all_type(Tys, Ty, Seen, Env) -> +% all_type(Tys, Ty, Seen, [], Env). + +%-spec all_type([type()], type(), map(), [constraints:t()], env()) -> compat_acc(). +%all_type([], _Ty, Seen, Css, Env) -> +% {Seen, constraints:combine(Css, Env)}; +%all_type([Ty1|Tys], Ty, AIn, Css, Env) -> +% {AOut, Cs} = compat(Ty1, Ty, AIn, Env), +% all_type(Tys, Ty, AOut, [Cs|Css], Env). %% Looks up the fields of a record by name and, if present, by the module where %% it belongs if a filename is included in the Anno. From 50fde326f9c93f1789be7e34a32c706c355c02c8 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Fri, 31 May 2024 09:43:50 +0200 Subject: [PATCH 19/27] Add gradualizer_cache:clear/1 --- src/gradualizer_cache.erl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/gradualizer_cache.erl b/src/gradualizer_cache.erl index b57f5e88..4a2cbb9a 100644 --- a/src/gradualizer_cache.erl +++ b/src/gradualizer_cache.erl @@ -9,6 +9,7 @@ %% API -export([start_link/1, + clear/1, get/2, store/3]). @@ -66,6 +67,13 @@ store_(Cache, Key, Value) -> ok end. +clear(glb) -> clear_(?GLB_CACHE); +clear(subtype) -> clear_(?SUB_CACHE). + +clear_(Cache) -> + ets:delete_all_objects(Cache), + ok. + %%=================================================================== %% gen_server callbacks %%=================================================================== From a26bb4ea3df7cd9956e03680eccc0d16a0e5b713 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Fri, 31 May 2024 09:44:33 +0200 Subject: [PATCH 20/27] fixup! Try defining subtype(union(), union()) with type_diff() --- src/typechecker.erl | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/typechecker.erl b/src/typechecker.erl index 14c7baac..c859868d 100644 --- a/src/typechecker.erl +++ b/src/typechecker.erl @@ -366,16 +366,10 @@ compat_ty({type, _, union, _} = U1, {type, _, union, _} = U2, Seen, Env) -> ?type(none) -> ret(Seen); false -> throw(nomatch) end; - -%compat_ty({type, _, union, Tys1}, {type, _, union, Tys2}, Seen, Env) -> -% lists:foldl(fun (Ty1, {Seen1, C1}) -> -% {Seen2, C2} = any_type(Ty1, Tys2, Seen1, Env), -% {Seen2, constraints:combine(C1, C2, Env)} -% end, {Seen, constraints:empty()}, Tys1); -%compat_ty(Ty1, {type, _, union, Tys2}, Seen, Env) -> -% any_type(Ty1, Tys2, Seen, Env); -%compat_ty({type, _, union, Tys1}, Ty2, Seen, Env) -> -% all_type(Tys1, Ty2, Seen, Env); +compat_ty(Ty1, {type, _, union, Tys2}, Seen, Env) -> + any_type(Ty1, Tys2, Seen, Env); +compat_ty({type, _, union, Tys1}, Ty2, Seen, Env) -> + all_type(Tys1, Ty2, Seen, Env); % Integer types compat_ty(Ty1, Ty2, Seen, _Env) when ?is_int_type(Ty1), ?is_int_type(Ty2) -> @@ -614,16 +608,16 @@ any_type(Ty1, Tys, Seen0, Env) -> %% @doc All types in `Tys' must be compatible with `Ty'. %% Returns all the gather memoizations and constraints. %% Does not return (throws `nomatch') if any of the types is not compatible. -%-spec all_type([type()], type(), map(), env()) -> compat_acc(). -%all_type(Tys, Ty, Seen, Env) -> -% all_type(Tys, Ty, Seen, [], Env). - -%-spec all_type([type()], type(), map(), [constraints:t()], env()) -> compat_acc(). -%all_type([], _Ty, Seen, Css, Env) -> -% {Seen, constraints:combine(Css, Env)}; -%all_type([Ty1|Tys], Ty, AIn, Css, Env) -> -% {AOut, Cs} = compat(Ty1, Ty, AIn, Env), -% all_type(Tys, Ty, AOut, [Cs|Css], Env). +-spec all_type([type()], type(), map(), env()) -> compat_acc(). +all_type(Tys, Ty, Seen, Env) -> + all_type(Tys, Ty, Seen, [], Env). + +-spec all_type([type()], type(), map(), [constraints:t()], env()) -> compat_acc(). +all_type([], _Ty, Seen, Css, Env) -> + {Seen, constraints:combine(Css, Env)}; +all_type([Ty1|Tys], Ty, AIn, Css, Env) -> + {AOut, Cs} = compat(Ty1, Ty, AIn, Env), + all_type(Tys, Ty, AOut, [Cs|Css], Env). %% Looks up the fields of a record by name and, if present, by the module where %% it belongs if a filename is included in the Anno. From e15dcbe7143fa7ba6904cfd74683bb12bc3529f5 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Fri, 31 May 2024 09:52:16 +0200 Subject: [PATCH 21/27] fixup! Try defining subtype(union(), union()) with type_diff() --- src/typechecker.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/typechecker.erl b/src/typechecker.erl index c859868d..57ac9207 100644 --- a/src/typechecker.erl +++ b/src/typechecker.erl @@ -364,7 +364,7 @@ compat_ty({type, _, 'fun', [{type, _, product, Args1}, Res1]}, compat_ty({type, _, union, _} = U1, {type, _, union, _} = U2, Seen, Env) -> case type_diff(U1, U2, Env) of ?type(none) -> ret(Seen); - false -> throw(nomatch) + _ -> throw(nomatch) end; compat_ty(Ty1, {type, _, union, Tys2}, Seen, Env) -> any_type(Ty1, Tys2, Seen, Env); From 23cfff8dda23761b9f94f266aa58d58957d0f035 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Fri, 31 May 2024 10:01:46 +0200 Subject: [PATCH 22/27] Move should-fail tests which now fail where they belong --- .../should_fail/poly_should_fail.erl | 26 +------------------ test/should_fail/poly_fail.erl | 23 +++++++++++++++- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/test/known_problems/should_fail/poly_should_fail.erl b/test/known_problems/should_fail/poly_should_fail.erl index 781f22b0..d28be89f 100644 --- a/test/known_problems/should_fail/poly_should_fail.erl +++ b/test/known_problems/should_fail/poly_should_fail.erl @@ -8,12 +8,10 @@ id_id_atom_is_int/1, id_fun_id_atom_is_int/1, use_flatten/1, - use_maps_get/3, use_generic_hd/1, use_generic_hd_var/1, inference1/1, - inference2/1, - invariant_tyvar/2 + inference2/1 ]). -spec id(A) -> A. @@ -56,11 +54,6 @@ pass_id_to_takes_int_to_bool_fun() -> use_flatten(ListOfListsOfAtoms) -> lists:flatten(ListOfListsOfAtoms). -%% Type variables in maps usually result in any(). --spec use_maps_get(atom(), #{atom() => binary()}, not_found) -> float() | not_found. -use_maps_get(Key, Map, NotFound) -> - maps:get(Key, Map, NotFound). - %% We do not support polymorphic intersection functions yet. %% When calling intersection functions, type variables are replaced with any(). @@ -88,20 +81,3 @@ inference1(L) -> -spec inference2([integer()]) -> [atom()]. inference2(L) -> lists:map(fun (I) -> I * 2 end, L). - -%% The type variable `A` in `id_fun_arg/2` is invariant in its result type. -%% Thus, if there are multiple possible substitutions, none of them is minimal. -%% In this case we choose `A = the_lower_bound_of_A | any()' which is a bit -%% lenient in some cases, as shown in invariant_tyvar/2. Hopefully, invariant -%% type variables are very rare. - --spec id_fun_arg(fun ((A) -> B), A) -> {fun ((A) -> B), A}. -id_fun_arg(Fun, Arg) -> {Fun, Arg}. - --spec positive(number()) -> boolean(). -positive(N) -> N > 0. - --spec invariant_tyvar(integer(), boolean()) -> any(). -invariant_tyvar(Int, Bool) -> - {Fun, _Arg} = id_fun_arg(fun positive/1, Int), - Fun(Bool). diff --git a/test/should_fail/poly_fail.erl b/test/should_fail/poly_fail.erl index b21b5143..7430c594 100644 --- a/test/should_fail/poly_fail.erl +++ b/test/should_fail/poly_fail.erl @@ -13,6 +13,7 @@ use_app_var/1, use_id/1, use_id_var/1, + use_maps_get/3, filter_positive_ints/1, filter_positive_concrete_numbers/1, append_floats_to_ints/2, @@ -34,7 +35,8 @@ use_enum_map1/1, use_enum_map1_var/1, use_enum_map2/1, - use_enum_map3/1 + use_enum_map3/1, + invariant_tyvar/2 ]). -gradualizer([solve_constraints]). @@ -110,6 +112,11 @@ use_id_var(Atom) -> X = id(Atom), X. +%% Type variables in maps usually result in any(). +-spec use_maps_get(atom(), #{atom() => binary()}, not_found) -> float() | not_found. +use_maps_get(Key, Map, NotFound) -> + maps:get(Key, Map, NotFound). + -spec positive(number()) -> boolean(). positive(N) -> N > 0. @@ -239,3 +246,17 @@ use_enum_map2(Atoms) -> -spec use_enum_map3(#{'__struct__' := some_struct}) -> [float()]. use_enum_map3(SomeStruct) -> enum_map(SomeStruct, fun positive/1). + +%% The type variable `A` in `id_fun_arg/2` is invariant in its result type. +%% Thus, if there are multiple possible substitutions, none of them is minimal. +%% In this case we choose `A = the_lower_bound_of_A | any()' which is a bit +%% lenient in some cases, as shown in invariant_tyvar/2. Hopefully, invariant +%% type variables are very rare. + +-spec id_fun_arg(fun ((A) -> B), A) -> {fun ((A) -> B), A}. +id_fun_arg(Fun, Arg) -> {Fun, Arg}. + +-spec invariant_tyvar(integer(), boolean()) -> any(). +invariant_tyvar(Int, Bool) -> + {Fun, _Arg} = id_fun_arg(fun positive/1, Int), + Fun(Bool). From 11a25b6a40c925b34031448362e90b1fa2d00ccc Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Fri, 31 May 2024 10:08:17 +0200 Subject: [PATCH 23/27] Move different_normalization_levels.erl as it now passes --- .../should_pass/different_normalization_levels.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename test/{known_problems => }/should_pass/different_normalization_levels.erl (64%) diff --git a/test/known_problems/should_pass/different_normalization_levels.erl b/test/should_pass/different_normalization_levels.erl similarity index 64% rename from test/known_problems/should_pass/different_normalization_levels.erl rename to test/should_pass/different_normalization_levels.erl index 1b44231f..7266dc60 100644 --- a/test/known_problems/should_pass/different_normalization_levels.erl +++ b/test/should_pass/different_normalization_levels.erl @@ -5,13 +5,13 @@ -type t() :: a | b | c. %% The problem is that the argument type stays the same (as top-level normalization -%% does not expand it) but the result type gets normalized to `a | b | c', -%% and `t() | a' is not a subtype of `a | b | c' because it currently checks whether -%% `t()' is a subtype of one of `a' or `b', or `c' (which it isn't). +%% does not expand it) but the result type gets normalized to `a | b | c'. +%% `t() | a' used not to be a subtype of `a | b | c' because it used to check whether +%% `t()' was a subtype of one of `a' or `b', or `c' (which it wasn't). %% It surfaced because, for instance, %% type() | any() -%% is not a subtype of +%% was not a subtype of %% type() -spec f(t() | a) -> t(). From 94906730e788adb606a41487ba075810eb6da8f8 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Fri, 31 May 2024 10:10:54 +0200 Subject: [PATCH 24/27] fixup! Move should-fail tests which now fail where they belong --- test/should_fail/poly_fail.erl | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/test/should_fail/poly_fail.erl b/test/should_fail/poly_fail.erl index 7430c594..24937efa 100644 --- a/test/should_fail/poly_fail.erl +++ b/test/should_fail/poly_fail.erl @@ -36,7 +36,7 @@ use_enum_map1_var/1, use_enum_map2/1, use_enum_map3/1, - invariant_tyvar/2 + invariant_tyvar2/2 ]). -gradualizer([solve_constraints]). @@ -250,13 +250,10 @@ use_enum_map3(SomeStruct) -> %% The type variable `A` in `id_fun_arg/2` is invariant in its result type. %% Thus, if there are multiple possible substitutions, none of them is minimal. %% In this case we choose `A = the_lower_bound_of_A | any()' which is a bit -%% lenient in some cases, as shown in invariant_tyvar/2. Hopefully, invariant +%% lenient in some cases, as shown in invariant_tyvar2/2. Hopefully, invariant %% type variables are very rare. --spec id_fun_arg(fun ((A) -> B), A) -> {fun ((A) -> B), A}. -id_fun_arg(Fun, Arg) -> {Fun, Arg}. - --spec invariant_tyvar(integer(), boolean()) -> any(). -invariant_tyvar(Int, Bool) -> +-spec invariant_tyvar2(integer(), boolean()) -> any(). +invariant_tyvar2(Int, Bool) -> {Fun, _Arg} = id_fun_arg(fun positive/1, Int), Fun(Bool). From c5942410bcac32c1248ee47a8090c802b2724bb7 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 22:41:08 +0200 Subject: [PATCH 25/27] Adjust CT test sets --- test/known_problems_should_pass_SUITE.erl | 12 ++++++------ test/should_pass_SUITE.erl | 9 +++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/test/known_problems_should_pass_SUITE.erl b/test/known_problems_should_pass_SUITE.erl index c97bca8b..41ec7515 100644 --- a/test/known_problems_should_pass_SUITE.erl +++ b/test/known_problems_should_pass_SUITE.erl @@ -59,12 +59,12 @@ groups() -> all_tests() -> [arith_op_arg_types,binary_exhaustiveness_checking_should_pass, - call_intersection_function_with_union_arg_should_pass, - different_normalization_levels,elixir_list_first,error_in_guard, - fun_subtyping,generator_var_shadow,inner_union_subtype_of_root_union, - intersection_should_pass,intersection_with_any,list_concat_op_should_pass, - list_tail,map_pattern_duplicate_key,maybe_expr,poly_should_pass, - poly_type_vars,recursive_types,refine_bound_var_on_mismatch, + call_intersection_function_with_union_arg_should_pass,elixir_list_first, + error_in_guard,fun_subtyping,generator_var_shadow, + inner_union_subtype_of_root_union,intersection_should_pass, + intersection_with_any,list_concat_op_should_pass,list_tail, + map_pattern_duplicate_key,maybe_expr,poly_should_pass,poly_type_vars, + recursive_types,refine_bound_var_on_mismatch, refine_bound_var_with_guard_should_pass,refine_comparison_should_pass, refine_list_tail,union_fun]. diff --git a/test/should_pass_SUITE.erl b/test/should_pass_SUITE.erl index b8618ab5..6fd36ed8 100644 --- a/test/should_pass_SUITE.erl +++ b/test/should_pass_SUITE.erl @@ -70,10 +70,11 @@ all_tests() -> bitstring,block_scope,bool,bounded_funs, call_intersection_function_with_union_arg_pass,'case', case_of_record_with_user_defined,catch_expr_pass,covariant_map_keys_pass, - cyclic_otp_specs,erlang_error_args_none_pass,exhaustiveness_union_types, - factorial,float,flow,fun_capture,fun_spec,guard,guard_sequences_pass,if_expr, - imported,int,intersection_pass,intersection_with_any_pass,iodata,issue131,lc, - lc_generator_not_none,lc_var_binds_in_filters,list,list_concat_op_pass, + cyclic_otp_specs,different_normalization_levels,erlang_error_args_none_pass, + exhaustiveness_union_types,factorial,float,flow,fun_capture,fun_spec,guard, + guard_sequences_pass,if_expr,imported,int,intersection_pass, + intersection_with_any_pass,iodata,issue131,lc,lc_generator_not_none, + lc_var_binds_in_filters,list,list_concat_op_pass, list_exhaustiveness_checking_regressions, list_exhaustiveness_checking_regressions2, list_exhaustiveness_checking_unreachable_clause_regression,list_infer_pass, From cd9603fe323729da97d9e1bb9a4034d024d6c853 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sun, 2 Jun 2024 23:02:13 +0200 Subject: [PATCH 26/27] Adjust tests of unions with any() member --- test/should_fail/union_with_any.erl | 10 ++++------ test/should_pass/union_with_any_pass.erl | 21 +++++++++++++++++++++ test/should_pass_SUITE.erl | 6 +++--- 3 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 test/should_pass/union_with_any_pass.erl diff --git a/test/should_fail/union_with_any.erl b/test/should_fail/union_with_any.erl index d9517587..6ddf23b2 100644 --- a/test/should_fail/union_with_any.erl +++ b/test/should_fail/union_with_any.erl @@ -3,17 +3,15 @@ %% T | any() means %% "at least values of type T are possible; maybe also other values" --export([f1/1, f2/0, f3/1]). +-export([f2/0, f3/1]). -%% With spec --spec f1(atom() | any()) -> any(). -f1(X) -> inc(X). +-gradualizer([infer]). %% Without spec f2() -> AtomOrAny = receive - 1 -> get_atom(); - Any -> Any + 1 -> get_atom(); + an_atom -> an_atom end, inc(AtomOrAny). %% Fails because atom() is possible diff --git a/test/should_pass/union_with_any_pass.erl b/test/should_pass/union_with_any_pass.erl new file mode 100644 index 00000000..0592db69 --- /dev/null +++ b/test/should_pass/union_with_any_pass.erl @@ -0,0 +1,21 @@ +-module(union_with_any_pass). + +-export([f1/1, f2/0]). + +%% With spec +-spec f1(atom() | any()) -> any(). +f1(X) -> inc(X). + +%% Without spec +f2() -> + AtomOrAny = receive + 1 -> get_atom(); + Any -> Any + end, + inc(AtomOrAny). %% Fails because atom() is possible + +-spec get_atom() -> atom(). +get_atom() -> banana. + +-spec inc(number()) -> number(). +inc(N) -> N + 1. diff --git a/test/should_pass_SUITE.erl b/test/should_pass_SUITE.erl index 6fd36ed8..b208df5c 100644 --- a/test/should_pass_SUITE.erl +++ b/test/should_pass_SUITE.erl @@ -100,9 +100,9 @@ all_tests() -> tuple_union_pass,tuple_union_pat,tuple_union_pattern_pass,type_decl, type_pattern,type_refinement_pass,type_variable,type_vars_term, typed_record_field_access,unary_negate_union_with_user_type_pass,unary_plus, - underscore,user_type_in_pattern_body,user_types,var,var_fun,varbind_in_block, - varbind_in_case,varbind_in_function_head,varbind_in_lc,variable_binding, - variable_binding_leaks]. + underscore,union_with_any_pass,user_type_in_pattern_body,user_types,var, + var_fun,varbind_in_block,varbind_in_case,varbind_in_function_head, + varbind_in_lc,variable_binding,variable_binding_leaks]. should_pass_template(_@File) -> ?assertEqual(ok, gradualizer:type_check_file(_@File, [{form_check_timeout_ms, 2000}])). From 4f1468fb194a0a6b30d7c349c2f5fe7c0dcbe701 Mon Sep 17 00:00:00 2001 From: Radek Szymczyszyn Date: Sat, 1 Jun 2024 19:54:42 +0200 Subject: [PATCH 27/27] wip --- src/typechecker.erl | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/typechecker.erl b/src/typechecker.erl index 57ac9207..efa7d00f 100644 --- a/src/typechecker.erl +++ b/src/typechecker.erl @@ -361,10 +361,22 @@ compat_ty({type, _, 'fun', [{type, _, product, Args1}, Res1]}, {Aps, constraints:combine(Cs, Css, Env)}; %% Unions -compat_ty({type, _, union, _} = U1, {type, _, union, _} = U2, Seen, Env) -> - case type_diff(U1, U2, Env) of - ?type(none) -> ret(Seen); - _ -> throw(nomatch) +compat_ty({type, _, union, Tys1} = U1, {type, _, union, Tys2} = U2, Seen, Env) -> + IsAny = fun + (?type(any)) -> true; + (_) -> false + end, + case lists:any(IsAny, Tys1) of + true -> ret(Seen); + false -> + case lists:any(IsAny, Tys2) of + true -> ret(Seen); + false -> + case type_diff(U1, U2, Env) of + ?type(none) -> ret(Seen); + _ -> throw(nomatch) + end + end end; compat_ty(Ty1, {type, _, union, Tys2}, Seen, Env) -> any_type(Ty1, Tys2, Seen, Env);