From a9c803f5b3bfc9ce912985d094cbe1d68d078703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 4 Jan 2024 13:48:43 +0100 Subject: [PATCH 1/3] Initial type cleanup: remove guards and maps --- lib/elixir/lib/module.ex | 2 +- .../lib/module/{types => }/behaviour.ex | 2 +- lib/elixir/lib/module/parallel_checker.ex | 2 +- lib/elixir/lib/module/types/of.ex | 89 +-- lib/elixir/lib/module/types/pattern.ex | 552 +-------------- lib/elixir/src/elixir_compiler.erl | 2 +- lib/elixir/test/elixir/code_test.exs | 77 --- .../test/elixir/fixtures/checker_warning.exs | 3 - .../elixir/module/types/integration_test.exs | 14 - .../test/elixir/module/types/map_test.exs | 377 ----------- .../test/elixir/module/types/pattern_test.exs | 542 --------------- .../test/elixir/module/types/types_test.exs | 628 ------------------ 12 files changed, 12 insertions(+), 2278 deletions(-) rename lib/elixir/lib/module/{types => }/behaviour.ex (99%) delete mode 100644 lib/elixir/test/elixir/fixtures/checker_warning.exs delete mode 100644 lib/elixir/test/elixir/module/types/map_test.exs delete mode 100644 lib/elixir/test/elixir/module/types/pattern_test.exs diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index 6b0aca3a950..c5daf0cdaa6 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -1443,7 +1443,7 @@ defmodule Module do "to defoverridable/1 because #{error_explanation}" end - behaviour_callbacks = Module.Types.Behaviour.callbacks(behaviour) + behaviour_callbacks = Module.Behaviour.callbacks(behaviour) tuples = for definition <- definitions_in(module), diff --git a/lib/elixir/lib/module/types/behaviour.ex b/lib/elixir/lib/module/behaviour.ex similarity index 99% rename from lib/elixir/lib/module/types/behaviour.ex rename to lib/elixir/lib/module/behaviour.ex index e57c6500097..2cc857d132e 100644 --- a/lib/elixir/lib/module/types/behaviour.ex +++ b/lib/elixir/lib/module/behaviour.ex @@ -1,4 +1,4 @@ -defmodule Module.Types.Behaviour do +defmodule Module.Behaviour do # Checking functionality for @behaviours and @impl @moduledoc false diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index f1d3a36edc4..e5de7106757 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -249,7 +249,7 @@ defmodule Module.ParallelChecker do |> merge_compiler_no_warn_undefined() behaviour_warnings = - Module.Types.Behaviour.check_behaviours_and_impls( + Module.Behaviour.check_behaviours_and_impls( module, file, line, diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 87a89266218..7c49abb64d1 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -27,36 +27,11 @@ defmodule Module.Types.Of do # assumptions. @doc """ - Handles open maps (with dynamic => dynamic). + Handles open maps. """ def open_map(args, stack, context, of_fun) do - with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do - # If we match on a map such as %{"foo" => "bar"}, we cannot - # assert that %{binary() => binary()}, since we are matching - # only a single binary of infinite possible values. Therefore, - # the correct would be to match it to %{binary() => binary() | var}. - # - # We can skip this in two cases: - # - # 1. If the key is a singleton, then we know that it has no - # other value than the current one - # - # 2. If the value is a variable, then there is no benefit in - # creating another variable, so we can skip it - # - # For now, we skip generating the var itself and introduce - # :dynamic instead. - pairs = - for {key, value} <- pairs, not has_unbound_var?(key, context) do - if singleton?(key, context) or match?({:var, _}, value) do - {key, value} - else - {key, to_union([value, :dynamic], context)} - end - end - - triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}] - {:ok, {:map, triplets}, context} + with {:ok, _pairs, context} <- map_pairs(args, stack, context, of_fun) do + {:ok, :dynamic, context} end end @@ -64,8 +39,8 @@ defmodule Module.Types.Of do Handles closed maps (without dynamic => dynamic). """ def closed_map(args, stack, context, of_fun) do - with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do - {:ok, {:map, closed_to_unions(pairs, context)}, context} + with {:ok, _pairs, context} <- map_pairs(args, stack, context, of_fun) do + {:ok, :dynamic, context} end end @@ -77,62 +52,12 @@ defmodule Module.Types.Of do end) end - defp closed_to_unions([{key, value}], _context), do: [{:required, key, value}] - - defp closed_to_unions(pairs, context) do - case Enum.split_with(pairs, fn {key, _value} -> has_unbound_var?(key, context) end) do - {[], pairs} -> pairs_to_unions(pairs, [], context) - {[_ | _], pairs} -> pairs_to_unions([{:dynamic, :dynamic} | pairs], [], context) - end - end - - defp pairs_to_unions([{key, value} | ahead], behind, context) do - {matched_ahead, values} = find_matching_values(ahead, key, [], []) - - # In case nothing matches, use the original ahead - ahead = matched_ahead || ahead - - all_values = - [value | values] ++ - find_subtype_values(ahead, key, context) ++ - find_subtype_values(behind, key, context) - - pairs_to_unions(ahead, [{key, to_union(all_values, context)} | behind], context) - end - - defp pairs_to_unions([], acc, context) do - acc - |> Enum.sort(&subtype?(elem(&1, 0), elem(&2, 0), context)) - |> Enum.map(fn {key, value} -> {:required, key, value} end) - end - - defp find_subtype_values(pairs, key, context) do - for {pair_key, pair_value} <- pairs, subtype?(pair_key, key, context), do: pair_value - end - - defp find_matching_values([{key, value} | ahead], key, acc, values) do - find_matching_values(ahead, key, acc, [value | values]) - end - - defp find_matching_values([{_, _} = pair | ahead], key, acc, values) do - find_matching_values(ahead, key, [pair | acc], values) - end - - defp find_matching_values([], _key, acc, [_ | _] = values), do: {Enum.reverse(acc), values} - defp find_matching_values([], _key, _acc, []), do: {nil, []} - @doc """ Handles structs. """ def struct(struct, meta, context) do context = remote(struct, :__struct__, 0, meta, context) - - entries = - for key <- Map.keys(struct.__struct__()), key != :__struct__ do - {:required, {:atom, key}, :dynamic} - end - - {:ok, {:map, [{:required, {:atom, :__struct__}, {:atom, struct}} | entries]}, context} + {:ok, :dynamic, context} end ## Binary @@ -235,8 +160,6 @@ defmodule Module.Types.Of do Handles remote calls. """ def remote(module, fun, arity, meta, context) when is_atom(module) do - # TODO: In the future we may want to warn for modules defined - # in the local context if Keyword.get(meta, :context_module, false) do context else diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 36d2d25a1c2..412b9bf37f4 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -81,562 +81,14 @@ defmodule Module.Types.Pattern do of_shared(expr, stack, context, &of_pattern/3) end - ## GUARDS - - # TODO: Some guards can be changed to intersection types or higher order types - @boolean {:union, [{:atom, true}, {:atom, false}]} - @number {:union, [:integer, :float]} - @unary_number_fun [{[:integer], :integer}, {[@number], :float}] - @binary_number_fun [ - {[:integer, :integer], :integer}, - {[:float, @number], :float}, - {[@number, :float], :float} - ] - - @guard_functions %{ - {:is_atom, 1} => [{[:atom], @boolean}], - {:is_binary, 1} => [{[:binary], @boolean}], - {:is_bitstring, 1} => [{[:binary], @boolean}], - {:is_boolean, 1} => [{[@boolean], @boolean}], - {:is_float, 1} => [{[:float], @boolean}], - {:is_function, 1} => [{[:fun], @boolean}], - {:is_function, 2} => [{[:fun, :integer], @boolean}], - {:is_integer, 1} => [{[:integer], @boolean}], - {:is_list, 1} => [{[{:list, :dynamic}], @boolean}], - {:is_map, 1} => [{[{:map, [{:optional, :dynamic, :dynamic}]}], @boolean}], - {:is_map_key, 2} => [{[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic}], - {:is_number, 1} => [{[@number], @boolean}], - {:is_pid, 1} => [{[:pid], @boolean}], - {:is_port, 1} => [{[:port], @boolean}], - {:is_reference, 1} => [{[:reference], @boolean}], - {:is_tuple, 1} => [{[:tuple], @boolean}], - {:<, 2} => [{[:dynamic, :dynamic], @boolean}], - {:"=<", 2} => [{[:dynamic, :dynamic], @boolean}], - {:>, 2} => [{[:dynamic, :dynamic], @boolean}], - {:>=, 2} => [{[:dynamic, :dynamic], @boolean}], - {:"/=", 2} => [{[:dynamic, :dynamic], @boolean}], - {:"=/=", 2} => [{[:dynamic, :dynamic], @boolean}], - {:==, 2} => [{[:dynamic, :dynamic], @boolean}], - {:"=:=", 2} => [{[:dynamic, :dynamic], @boolean}], - {:*, 2} => @binary_number_fun, - {:+, 1} => @unary_number_fun, - {:+, 2} => @binary_number_fun, - {:-, 1} => @unary_number_fun, - {:-, 2} => @binary_number_fun, - {:/, 2} => @binary_number_fun, - {:abs, 1} => @unary_number_fun, - {:ceil, 1} => [{[@number], :integer}], - {:floor, 1} => [{[@number], :integer}], - {:round, 1} => [{[@number], :integer}], - {:trunc, 1} => [{[@number], :integer}], - {:element, 2} => [{[:integer, :tuple], :dynamic}], - {:hd, 1} => [{[{:list, :dynamic}], :dynamic}], - {:length, 1} => [{[{:list, :dynamic}], :integer}], - {:map_get, 2} => [{[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic}], - {:map_size, 1} => [{[{:map, [{:optional, :dynamic, :dynamic}]}], :integer}], - {:tl, 1} => [{[{:list, :dynamic}], :dynamic}], - {:tuple_size, 1} => [{[:tuple], :integer}], - {:node, 1} => [{[{:union, [:pid, :reference, :port]}], :atom}], - {:binary_part, 3} => [{[:binary, :integer, :integer], :binary}], - {:bit_size, 1} => [{[:binary], :integer}], - {:byte_size, 1} => [{[:binary], :integer}], - {:size, 1} => [{[{:union, [:binary, :tuple]}], @boolean}], - {:div, 2} => [{[:integer, :integer], :integer}], - {:rem, 2} => [{[:integer, :integer], :integer}], - {:node, 0} => [{[], :atom}], - {:self, 0} => [{[], :pid}], - {:bnot, 1} => [{[:integer], :integer}], - {:band, 2} => [{[:integer, :integer], :integer}], - {:bor, 2} => [{[:integer, :integer], :integer}], - {:bxor, 2} => [{[:integer, :integer], :integer}], - {:bsl, 2} => [{[:integer, :integer], :integer}], - {:bsr, 2} => [{[:integer, :integer], :integer}], - {:or, 2} => [{[@boolean, @boolean], @boolean}], - {:and, 2} => [{[@boolean, @boolean], @boolean}], - {:xor, 2} => [{[@boolean, @boolean], @boolean}], - {:not, 1} => [{[@boolean], @boolean}] - - # Following guards are matched explicitly to handle - # type guard functions such as is_atom/1 - # {:andalso, 2} => {[@boolean, @boolean], @boolean} - # {:orelse, 2} => {[@boolean, @boolean], @boolean} - } - - @type_guards [ - :is_atom, - :is_binary, - :is_bitstring, - :is_boolean, - :is_float, - :is_function, - :is_integer, - :is_list, - :is_map, - :is_number, - :is_pid, - :is_port, - :is_reference, - :is_tuple - ] - @doc """ Refines the type variables in the typing context using type check guards such as `is_integer/1`. """ - def of_guard(expr, expected, %{context: stack_context} = stack, context) - when stack_context != :pattern do - of_guard(expr, expected, %{stack | context: :pattern}, context) - end - - def of_guard({{:., _, [:erlang, :andalso]}, _, [left, right]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - - with {:ok, left_type, context} <- of_guard(left, @boolean, stack, context), - {:ok, _, context} <- unify(left_type, @boolean, stack, context), - {:ok, right_type, context} <- of_guard(right, :dynamic, keep_guarded(stack), context), - do: {:ok, to_union([@boolean, right_type], context), context} - end - - def of_guard({{:., _, [:erlang, :orelse]}, _, [left, right]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - left_indexes = collect_var_indexes_from_expr(left, context) - right_indexes = collect_var_indexes_from_expr(right, context) - - with {:ok, left_type, left_context} <- of_guard(left, @boolean, stack, context), - {:ok, _right_type, right_context} <- of_guard(right, :dynamic, stack, context), - context = - merge_context_or( - left_indexes, - right_indexes, - context, - stack, - left_context, - right_context - ), - {:ok, _, context} <- unify(left_type, @boolean, stack, context), - do: {:ok, @boolean, context} - end - - # The unary operators + and - are special cased to avoid common warnings until - # we add support for intersection types for the guard functions - # -integer / +integer - def of_guard({{:., _, [:erlang, guard]}, _, [integer]}, _expected, _stack, context) - when guard in [:+, :-] and is_integer(integer) do - {:ok, :integer, context} - end - - # -float / +float - def of_guard({{:., _, [:erlang, guard]}, _, [float]}, _expected, _stack, context) - when guard in [:+, :-] and is_float(float) do - {:ok, :float, context} - end - - # tuple_size(arg) == integer - def of_guard( - {{:., _, [:erlang, :==]}, _, [{{:., _, [:erlang, :tuple_size]}, _, [var]}, size]} = expr, - expected, - stack, - context - ) - when is_var(var) and is_integer(size) do - of_tuple_size(var, size, expr, expected, stack, context) - end - - # integer == tuple_size(arg) - def of_guard( - {{:., _, [:erlang, :==]}, _, [size, {{:., _, [:erlang, :tuple_size]}, _, [var]}]} = expr, - expected, - stack, - context - ) - when is_var(var) and is_integer(size) do - of_tuple_size(var, size, expr, expected, stack, context) - end - - # fun(args) - def of_guard({{:., _, [:erlang, guard]}, _, args} = expr, expected, stack, context) do - type_guard? = type_guard?(guard) - {consider_type_guards?, keep_guarded?} = stack.type_guards - signature = guard_signature(guard, length(args)) - - # Only check type guards in the context of and/or/not, - # a type guard in the context of is_tuple(x) > :foo - # should not affect the inference of x - if not type_guard? or consider_type_guards? do - stack = push_expr_stack(expr, stack) - expected_clauses = filter_clauses(signature, expected, stack, context) - param_unions = signature_to_param_unions(expected_clauses, context) - arg_stack = %{stack | type_guards: {false, keep_guarded?}} - mfa = {:erlang, guard, length(args)} - - with {:ok, arg_types, context} <- - map_reduce_ok(Enum.zip(args, param_unions), context, fn {arg, param}, context -> - of_guard(arg, param, arg_stack, context) - end), - {:ok, return_type, context} <- - unify_call( - arg_types, - expected_clauses, - expected, - mfa, - signature, - stack, - context, - type_guard? - ) do - guard_sources = guard_sources(arg_types, type_guard?, keep_guarded?, context) - {:ok, return_type, %{context | guard_sources: guard_sources}} - end - else - # Assume that type guards always return boolean - boolean = {:union, [atom: true, atom: false]} - [{_params, ^boolean}] = signature - {:ok, boolean, context} - end - end - - # map.field - def of_guard({{:., meta1, [map, field]}, meta2, []}, expected, stack, context) do - of_guard({{:., meta1, [:erlang, :map_get]}, meta2, [field, map]}, expected, stack, context) - end - - # var - def of_guard(var, _expected, _stack, context) when is_var(var) do - {:ok, get_var!(var, context), context} - end - - def of_guard(expr, _expected, stack, context) do - of_shared(expr, stack, context, &of_guard(&1, :dynamic, &2, &3)) - end - - defp of_tuple_size(var, size, expr, _expected, stack, context) do - {consider_type_guards?, _keep_guarded?} = stack.type_guards - - result = - if consider_type_guards? do - stack = push_expr_stack(expr, stack) - tuple_elems = Enum.map(1..size//1, fn _ -> :dynamic end) - - with {:ok, type, context} <- of_guard(var, :dynamic, stack, context), - {:ok, _type, context} <- unify({:tuple, size, tuple_elems}, type, stack, context), - do: {:ok, context} - else - {:ok, context} - end - - case result do - {:ok, context} -> - boolean = {:union, [atom: true, atom: false]} - {:ok, boolean, context} - - {:error, reason} -> - {:error, reason} - end - end - - defp signature_to_param_unions(signature, context) do - signature - |> Enum.map(fn {params, _return} -> params end) - |> zip_many() - |> Enum.map(&to_union(&1, context)) - end - - # Collect guard sources from argument types, see type context documentation - # for more information - defp guard_sources(arg_types, type_guard?, keep_guarded?, context) do - {arg_types, guard_sources} = - case arg_types do - [{:var, index} | rest_arg_types] when type_guard? -> - guard_sources = Map.put_new(context.guard_sources, index, :guarded) - {rest_arg_types, guard_sources} - - _ -> - {arg_types, context.guard_sources} - end - - Enum.reduce(arg_types, guard_sources, fn - {:var, index}, guard_sources -> - Map.update(guard_sources, index, :fail, &guarded_if_keep_guarded(&1, keep_guarded?)) - - _, guard_sources -> - guard_sources - end) - end - - defp collect_var_indexes_from_expr(expr, context) do - {_, vars} = - Macro.prewalk(expr, %{}, fn - {:"::", _, [left, right]}, acc -> - # Do not mistake binary modifiers as variables - {collect_exprs_from_modifiers(right, [left]), acc} - - var, acc when is_var(var) -> - var_name = var_name(var) - %{^var_name => type} = context.vars - {var, collect_var_indexes(type, context, acc)} - - other, acc -> - {other, acc} - end) - - Map.keys(vars) - end - - defp collect_exprs_from_modifiers({:-, _, [left, right]}, acc) do - collect_exprs_from_modifiers(left, collect_expr_from_modifier(right, acc)) - end - - defp collect_exprs_from_modifiers(modifier, acc) do - collect_expr_from_modifier(modifier, acc) - end - - defp collect_expr_from_modifier({:unit, _, [arg]}, acc), do: [arg | acc] - defp collect_expr_from_modifier({:size, _, [arg]}, acc), do: [arg | acc] - defp collect_expr_from_modifier({var, _, ctx}, acc) when is_atom(var) and is_atom(ctx), do: acc - - defp unify_call(args, clauses, _expected, _mfa, _signature, stack, context, true = _type_guard?) do - unify_type_guard_call(args, clauses, stack, context) - end - - defp unify_call(args, clauses, expected, mfa, signature, stack, context, false = _type_guard?) do - unify_call(args, clauses, expected, mfa, signature, stack, context) - end - - defp unify_call([], [{[], return}], _expected, _mfa, _signature, _stack, context) do - {:ok, return, context} - end - - defp unify_call(args, clauses, expected, mfa, signature, stack, context) do - # Given the arguments: - # foo | bar, {:ok, baz | bat} - - # Expand unions in arguments: - # foo | bar, {:ok, baz} | {:ok, bat} - - # Permute arguments: - # foo, {:ok, baz} - # foo, {:ok, bat} - # bar, {:ok, baz} - # bar, {:ok, bat} - - flatten_args = Enum.map(args, &flatten_union(&1, context)) - cartesian_args = cartesian_product(flatten_args) - - # Remove clauses that do not match the expected type - # Ignore type variables in parameters by changing them to dynamic - - clauses = - clauses - |> filter_clauses(expected, stack, context) - |> Enum.map(fn {params, return} -> - {Enum.map(params, &var_to_dynamic/1), return} - end) - - # For each permuted argument find the clauses they match - # All arguments must match at least one clause, but all clauses - # do not need to match - # Collect the return values from clauses that matched and collect - # the type contexts from unifying argument and parameter to - # infer type variables in arguments - result = - flat_map_ok(cartesian_args, fn cartesian_args -> - result = - Enum.flat_map(clauses, fn {params, return} -> - result = - map_ok(Enum.zip(cartesian_args, params), fn {arg, param} -> - case unify(arg, param, stack, context) do - {:ok, _type, context} -> {:ok, context} - {:error, reason} -> {:error, reason} - end - end) - - case result do - {:ok, contexts} -> [{return, contexts}] - {:error, _reason} -> [] - end - end) - - if result != [] do - {:ok, result} - else - {:error, args} - end - end) - - case result do - {:ok, returns_contexts} -> - {success_returns, contexts} = Enum.unzip(returns_contexts) - contexts = Enum.concat(contexts) - - indexes = - for types <- flatten_args, - type <- types, - index <- collect_var_indexes_from_type(type), - do: index, - uniq: true - - # Build unions from collected type contexts to unify with - # type variables from arguments - result = - map_reduce_ok(indexes, context, fn index, context -> - union = - contexts - |> Enum.map(&Map.fetch!(&1.types, index)) - |> Enum.reject(&(&1 == :unbound)) - - if union == [] do - {:ok, {:var, index}, context} - else - unify({:var, index}, to_union(union, context), stack, context) - end - end) - - case result do - {:ok, _types, context} -> {:ok, to_union(success_returns, context), context} - {:error, reason} -> {:error, reason} - end - - {:error, args} -> - error(:unable_apply, {mfa, args, expected, signature, stack}, context) - end - end - - defp unify_type_guard_call(args, [{params, return}], stack, context) do - result = - reduce_ok(Enum.zip(args, params), context, fn {arg, param}, context -> - case unify(arg, param, stack, context) do - {:ok, _, context} -> {:ok, context} - {:error, reason} -> {:error, reason} - end - end) - - case result do - {:ok, context} -> {:ok, return, context} - {:error, reason} -> {:error, reason} - end - end - - defp cartesian_product(lists) do - List.foldr(lists, [[]], fn list, acc -> - for elem_list <- list, - list_acc <- acc, - do: [elem_list | list_acc] - end) - end - - defp var_to_dynamic(type) do - {type, _acc} = - walk(type, :ok, fn - {:var, _index}, :ok -> - {:dynamic, :ok} - - other, :ok -> - {other, :ok} - end) - - type - end - - defp collect_var_indexes_from_type(type) do - {_type, indexes} = - walk(type, [], fn - {:var, index}, indexes -> - {{:var, index}, [index | indexes]} - - other, indexes -> - {other, indexes} - end) - - indexes - end - - defp merge_context_or(left_indexes, right_indexes, context, stack, left, right) do - left_different = filter_different_indexes(left_indexes, left, right) - right_different = filter_different_indexes(right_indexes, left, right) - - case {left_different, right_different} do - {[index], [index]} -> merge_context_or_equal(index, stack, left, right) - {_, _} -> merge_context_or_diff(left_different, context, left) - end - end - - defp filter_different_indexes(indexes, left, right) do - Enum.filter(indexes, fn index -> - %{^index => left_type} = left.types - %{^index => right_type} = right.types - left_type != right_type - end) - end - - defp merge_context_or_equal(index, stack, left, right) do - %{^index => left_type} = left.types - %{^index => right_type} = right.types - - cond do - left_type == :unbound -> - refine_var!(index, right_type, stack, left) - - right_type == :unbound -> - left - - true -> - # Only include right side if left side is from type guard such as is_list(x), - # do not refine in case of length(x) - if left.guard_sources[index] == :fail do - guard_sources = Map.put(left.guard_sources, index, :fail) - left = %{left | guard_sources: guard_sources} - refine_var!(index, left_type, stack, left) - else - guard_sources = merge_guard_sources([left.guard_sources, right.guard_sources]) - left = %{left | guard_sources: guard_sources} - refine_var!(index, to_union([left_type, right_type], left), stack, left) - end - end - end - - # If the variable failed, we can keep them from the left side as is. - # If they didn't fail, then we need to restore them to their original value. - defp merge_context_or_diff(indexes, old_context, new_context) do - Enum.reduce(indexes, new_context, fn index, context -> - if new_context.guard_sources[index] == :fail do - context - else - restore_var!(index, new_context, old_context) - end - end) - end - - defp merge_guard_sources(sources) do - Enum.reduce(sources, fn left, right -> - Map.merge(left, right, fn - _index, :guarded, :guarded -> :guarded - _index, _, _ -> :fail - end) - end) - end - - defp guarded_if_keep_guarded(:guarded, true), do: :guarded - defp guarded_if_keep_guarded(_, _), do: :fail - - defp keep_guarded(%{type_guards: {consider?, _}} = stack), - do: %{stack | type_guards: {consider?, true}} - - defp filter_clauses(signature, expected, stack, context) do - Enum.filter(signature, fn {_params, return} -> - match?({:ok, _type, _context}, unify(return, expected, stack, context)) - end) + def of_guard(_expr, _expected, _stack, context) do + {:ok, :dynamic, context} end - Enum.each(@guard_functions, fn {{name, arity}, signature} -> - defp guard_signature(unquote(name), unquote(arity)), do: unquote(Macro.escape(signature)) - end) - - Enum.each(@type_guards, fn name -> - defp type_guard?(unquote(name)), do: true - end) - - defp type_guard?(name) when is_atom(name), do: false - ## Shared # :atom diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 980aa9e0e86..3b712b00d7b 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -185,7 +185,7 @@ bootstrap_files() -> <<"list/chars.ex">>, <<"module/locals_tracker.ex">>, <<"module/parallel_checker.ex">>, - <<"module/types/behaviour.ex">>, + <<"module/behaviour.ex">>, <<"module/types/helpers.ex">>, <<"module/types/unify.ex">>, <<"module/types/of.ex">>, diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 2fc391c9081..3a7db18335e 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -211,30 +211,6 @@ defmodule CodeTest do end) =~ "an __ENV__ with outdated compilation information was given to eval" end - test "emits checker warnings" do - output = - ExUnit.CaptureIO.capture_io(:stderr, fn -> - Code.eval_string(File.read!(fixture_path("checker_warning.exs")), []) - end) - - assert output =~ "incompatible types" - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end - - test "captures checker diagnostics" do - {{{:module, _, _, _}, _}, diagnostics} = - Code.with_diagnostics(fn -> - Code.eval_string(File.read!(fixture_path("checker_warning.exs")), []) - end) - - assert [%{message: "incompatible types:" <> _}] = diagnostics - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end - test "formats diagnostic file paths as relatives" do {_, diagnostics} = Code.with_diagnostics(fn -> @@ -398,47 +374,6 @@ defmodule CodeTest do assert Code.compile_file(fixture_path("code_sample.exs")) == [] refute fixture_path("code_sample.exs") in Code.required_files() end - - test "emits checker warnings" do - output = - ExUnit.CaptureIO.capture_io(:stderr, fn -> - Code.compile_file(fixture_path("checker_warning.exs")) - end) - - assert output =~ "incompatible types" - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end - - test "captures checker diagnostics" do - {[{CodeTest.CheckerWarning, _}], diagnostics} = - Code.with_diagnostics(fn -> - Code.compile_file(fixture_path("checker_warning.exs")) - end) - - assert [%{message: "incompatible types:" <> _}] = diagnostics - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end - - test "captures checker diagnostics with logging" do - output = - ExUnit.CaptureIO.capture_io(:stderr, fn -> - {[{CodeTest.CheckerWarning, _}], diagnostics} = - Code.with_diagnostics([log: true], fn -> - Code.compile_file(fixture_path("checker_warning.exs")) - end) - - assert [%{message: "incompatible types:" <> _}] = diagnostics - end) - - assert output =~ "incompatible types" - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end end test "require_file/1" do @@ -522,18 +457,6 @@ defmodule CodeTest do :code.delete(CompileSimpleSample) end - test "emits checker warnings" do - output = - ExUnit.CaptureIO.capture_io(:stderr, fn -> - Code.compile_string(File.read!(fixture_path("checker_warning.exs"))) - end) - - assert output =~ "incompatible types" - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end - test "works across lexical scopes" do assert [{CompileCrossSample, _}] = Code.compile_string("CodeTest.genmodule CompileCrossSample") diff --git a/lib/elixir/test/elixir/fixtures/checker_warning.exs b/lib/elixir/test/elixir/fixtures/checker_warning.exs deleted file mode 100644 index a9c0cab9fcc..00000000000 --- a/lib/elixir/test/elixir/fixtures/checker_warning.exs +++ /dev/null @@ -1,3 +0,0 @@ -defmodule CodeTest.CheckerWarning do - def foo(x) when is_atom(x) and is_list(x), do: x -end diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index ee8cdca18b4..3c5944e18e2 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -521,20 +521,6 @@ defmodule Module.Types.IntegrationTest do end describe "regressions" do - test "handle missing location info from quoted" do - assert capture_io(:stderr, fn -> - quote do - defmodule X do - def f() do - x = %{} - %{x | key: :value} - end - end - end - |> Code.compile_quoted() - end) =~ "warning:" - end - test "do not parse binary segments as variables" do files = %{ "a.ex" => """ diff --git a/lib/elixir/test/elixir/module/types/map_test.exs b/lib/elixir/test/elixir/module/types/map_test.exs deleted file mode 100644 index da6818f2e54..00000000000 --- a/lib/elixir/test/elixir/module/types/map_test.exs +++ /dev/null @@ -1,377 +0,0 @@ -Code.require_file("type_helper.exs", __DIR__) - -defmodule Module.Types.MapTest do - # This file holds cases for maps and structs. - use ExUnit.Case, async: true - - import TypeHelper - - defmodule :"Elixir.Module.Types.MapTest.Struct" do - defstruct foo: :atom, bar: 123, baz: %{} - end - - test "map" do - assert quoted_expr(%{}) == {:ok, {:map, []}} - assert quoted_expr(%{a: :b}) == {:ok, {:map, [{:required, {:atom, :a}, {:atom, :b}}]}} - assert quoted_expr([a], %{123 => a}) == {:ok, {:map, [{:required, :integer, {:var, 0}}]}} - - assert quoted_expr(%{123 => :foo, 456 => :bar}) == - {:ok, {:map, [{:required, :integer, {:union, [{:atom, :foo}, {:atom, :bar}]}}]}} - end - - test "struct" do - assert quoted_expr(%:"Elixir.Module.Types.MapTest.Struct"{}) == - {:ok, - {:map, - [ - {:required, {:atom, :bar}, :integer}, - {:required, {:atom, :baz}, {:map, []}}, - {:required, {:atom, :foo}, {:atom, :atom}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct}} - ]}} - - assert quoted_expr(%:"Elixir.Module.Types.MapTest.Struct"{foo: 123, bar: :atom}) == - {:ok, - {:map, - [ - {:required, {:atom, :baz}, {:map, []}}, - {:required, {:atom, :foo}, :integer}, - {:required, {:atom, :bar}, {:atom, :atom}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct}} - ]}} - end - - test "map field" do - assert quoted_expr(%{foo: :bar}.foo) == {:ok, {:atom, :bar}} - - assert quoted_expr( - ( - map = %{foo: :bar} - map.foo - ) - ) == {:ok, {:atom, :bar}} - - assert quoted_expr( - [map], - ( - map.foo - map.bar - map - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :bar}, {:var, 0}}, - {:required, {:atom, :foo}, {:var, 1}}, - {:optional, :dynamic, :dynamic} - ]}} - - assert quoted_expr( - [map], - ( - :foo = map.foo - :bar = map.bar - map - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :bar}, {:atom, :bar}}, - {:required, {:atom, :foo}, {:atom, :foo}}, - {:optional, :dynamic, :dynamic} - ]}} - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :bar}, {:var, 1}}, {:optional, :dynamic, :dynamic}]}, - {:map, [{:required, {:atom, :foo}, {:atom, :foo}}]}, - _}}} = - quoted_expr( - ( - map = %{foo: :foo} - map.bar - ) - ) - end - - defmodule :"Elixir.Module.Types.MapTest.Struct2" do - defstruct [:field] - end - - test "map and struct fields" do - assert quoted_expr( - [map], - ( - %Module.Types.MapTest.Struct2{} = map - map.field - map - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :field}, {:var, 0}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} - ]}} - - assert quoted_expr( - [map], - ( - _ = map.field - %Module.Types.MapTest.Struct2{} = map - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :field}, {:var, 0}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} - ]}} - - assert {:error, {:unable_unify, {_, _, _}}} = - quoted_expr( - [map], - ( - %Module.Types.MapTest.Struct2{} = map - map.no_field - ) - ) - - assert {:error, {:unable_unify, {_, _, _}}} = - quoted_expr( - [map], - ( - _ = map.no_field - %Module.Types.MapTest.Struct2{} = map - ) - ) - end - - test "map pattern" do - assert quoted_expr(%{a: :b} = %{a: :b}) == - {:ok, {:map, [{:required, {:atom, :a}, {:atom, :b}}]}} - - assert quoted_expr( - ( - a = :a - %{^a => :b} = %{:a => :b} - ) - ) == {:ok, {:map, [{:required, {:atom, :a}, {:atom, :b}}]}} - - assert quoted_expr( - ( - a = :a - %{{^a, :b} => :c} = %{{:a, :b} => :c} - ) - ) == {:ok, {:map, [{:required, {:tuple, 2, [{:atom, :a}, {:atom, :b}]}, {:atom, :c}}]}} - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :c}, {:atom, :d}}]}, - {:map, [{:required, {:atom, :a}, {:atom, :b}}, {:optional, :dynamic, :dynamic}]}, - _}}} = quoted_expr(%{a: :b} = %{c: :d}) - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :b}, {:atom, :error}}]}, - {:map, [{:required, {:var, 0}, {:atom, :ok}}, {:optional, :dynamic, :dynamic}]}, - _}}} = - quoted_expr( - ( - a = :a - %{^a => :ok} = %{:b => :error} - ) - ) - end - - test "map update" do - assert quoted_expr( - ( - map = %{foo: :a} - %{map | foo: :b} - ) - ) == - {:ok, {:map, [{:required, {:atom, :foo}, {:atom, :b}}]}} - - assert quoted_expr([map], %{map | foo: :b}) == - {:ok, - {:map, [{:required, {:atom, :foo}, {:atom, :b}}, {:optional, :dynamic, :dynamic}]}} - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :foo}, {:atom, :a}}]}, - {:map, [{:required, {:atom, :bar}, :dynamic}, {:optional, :dynamic, :dynamic}]}, - _}}} = - quoted_expr( - ( - map = %{foo: :a} - %{map | bar: :b} - ) - ) - end - - test "struct update" do - assert quoted_expr( - ( - map = %Module.Types.MapTest.Struct2{field: :a} - %Module.Types.MapTest.Struct2{map | field: :b} - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :field}, {:atom, :b}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} - ]}} - - # TODO: improve error message to translate to MULTIPLE missing fields - assert {:error, - {:unable_unify, - {{:map, - [ - {:required, {:atom, :foo}, {:var, 1}}, - {:required, {:atom, :field}, {:atom, :b}}, - {:optional, :dynamic, :dynamic} - ]}, - {:map, - [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}}, - {:required, {:atom, :field}, :dynamic} - ]}, - _}}} = - quoted_expr( - [map], - ( - _ = map.foo - %Module.Types.MapTest.Struct2{map | field: :b} - ) - ) - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :field}, {:atom, :b}}]}, - {:map, - [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}}, - {:required, {:atom, :field}, :dynamic} - ]}, - _}}} = - quoted_expr( - ( - map = %{field: :a} - %Module.Types.MapTest.Struct2{map | field: :b} - ) - ) - - assert quoted_expr([map], %Module.Types.MapTest.Struct2{map | field: :b}) == - {:ok, - {:map, - [ - {:required, {:atom, :field}, {:atom, :b}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} - ]}} - - assert {:error, - {:unable_unify, - {{:map, - [ - {:required, {:atom, :field}, {:atom, nil}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} - ]}, - {:map, - [{:required, {:atom, :not_field}, :dynamic}, {:optional, :dynamic, :dynamic}]}, - _}}} = - quoted_expr( - ( - map = %Module.Types.MapTest.Struct2{} - %{map | not_field: :b} - ) - ) - end - - describe "in guards" do - test "not is_struct/2" do - assert quoted_expr([var], [not is_struct(var, URI)], var.name) == {:ok, {:var, 0}} - end - - test "map guards" do - assert quoted_expr([var], [is_map(var)], var.foo) == {:ok, {:var, 0}} - assert quoted_expr([var], [is_map_key(var, :bar)], var.foo) == {:ok, {:var, 0}} - assert quoted_expr([var], [:erlang.map_get(:bar, var)], var.foo) == {:ok, {:var, 0}} - assert quoted_expr([var], [map_size(var) == 1], var.foo) == {:ok, {:var, 0}} - end - end - - test "map creation with bound var keys" do - assert quoted_expr( - [atom, bool, true = var], - [is_atom(atom) and is_boolean(bool)], - %{atom => :atom, bool => :bool, var => true} - ) == - {:ok, - {:map, - [ - {:required, {:atom, true}, {:atom, true}}, - {:required, {:union, [atom: true, atom: false]}, - {:union, [{:atom, :bool}, {:atom, true}]}}, - {:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}} - ]}} - - assert quoted_expr( - [atom, bool, true = var], - [is_atom(atom) and is_boolean(bool)], - %{var => true, bool => :bool, atom => :atom} - ) == - {:ok, - {:map, - [ - {:required, {:atom, true}, {:atom, true}}, - {:required, {:union, [atom: true, atom: false]}, - {:union, [{:atom, :bool}, {:atom, true}]}}, - {:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}} - ]}} - - assert quoted_expr( - [atom, bool, true = var], - [is_atom(atom) and is_boolean(bool)], - %{var => true, atom => :atom, bool => :bool} - ) == - {:ok, - {:map, - [ - {:required, {:atom, true}, {:atom, true}}, - {:required, {:union, [atom: true, atom: false]}, - {:union, [{:atom, :bool}, {:atom, true}]}}, - {:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}} - ]}} - end - - test "map creation with unbound var keys" do - assert quoted_expr( - [var, struct], - ( - map = %{var => :foo} - %^var{} = struct - map - ) - ) == {:ok, {:map, [{:required, :atom, {:atom, :foo}}]}} - - # If we have multiple keys, the unbound key must become required(dynamic) => dynamic - assert quoted_expr( - [var, struct], - ( - map = %{var => :foo, :foo => :bar} - %^var{} = struct - map - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :foo}, {:atom, :bar}}, - {:required, :dynamic, :dynamic} - ]}} - end -end diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs deleted file mode 100644 index 70e7ca108d6..00000000000 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ /dev/null @@ -1,542 +0,0 @@ -Code.require_file("type_helper.exs", __DIR__) - -defmodule Module.Types.PatternTest do - use ExUnit.Case, async: true - - alias Module.Types - alias Module.Types.{Unify, Pattern} - - defmacrop quoted_pattern(patterns) do - quote do - {patterns, true} = unquote(Macro.escape(expand_head(patterns, true))) - - Pattern.of_pattern(patterns, new_stack(), new_context()) - |> lift_result() - end - end - - defmacrop quoted_pattern_with_diagnostics(patterns) do - {ast, diagnostics} = Code.with_diagnostics(fn -> expand_head(patterns, true) end) - - quote do - {patterns, true} = unquote(Macro.escape(ast)) - - result = - Pattern.of_pattern(patterns, new_stack(), new_context()) - |> lift_result() - - {result, unquote(Macro.escape(diagnostics))} - end - end - - defmacrop quoted_head(patterns, guards \\ []) do - quote do - {patterns, guards} = unquote(Macro.escape(expand_head(patterns, guards))) - - Pattern.of_head(patterns, guards, new_stack(), new_context()) - |> lift_result() - end - end - - defp expand_head(patterns, guards) do - fun = - quote do - fn unquote(patterns) when unquote(guards) -> :ok end - end - - fun = - Macro.prewalk(fun, fn - {var, meta, nil} -> {var, meta, __MODULE__} - other -> other - end) - - {ast, _, _} = :elixir_expand.expand(fun, :elixir_env.env_to_ex(__ENV__), __ENV__) - {:fn, _, [{:->, _, [[{:when, _, [patterns, guards]}], _]}]} = ast - {patterns, guards} - end - - defp new_context() do - Types.context("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache()) - end - - defp new_stack() do - %{ - Types.stack() - | last_expr: {:foo, [], nil} - } - end - - defp lift_result({:ok, types, context}) when is_list(types) do - {types, _context} = Unify.lift_types(types, context) - {:ok, types} - end - - defp lift_result({:ok, type, context}) do - {[type], _context} = Unify.lift_types([type], context) - {:ok, type} - end - - defp lift_result({:error, {type, reason, _context}}) do - {:error, {type, reason}} - end - - defmodule :"Elixir.Module.Types.PatternTest.Struct" do - defstruct foo: :atom, bar: 123, baz: %{} - end - - describe "patterns" do - test "literal" do - assert quoted_pattern(true) == {:ok, {:atom, true}} - assert quoted_pattern(false) == {:ok, {:atom, false}} - assert quoted_pattern(:foo) == {:ok, {:atom, :foo}} - assert quoted_pattern(0) == {:ok, :integer} - assert quoted_pattern(+0.0) == {:ok, :float} - assert quoted_pattern(-0.0) == {:ok, :float} - assert quoted_pattern("foo") == {:ok, :binary} - - assert {{:ok, :float}, [diagnostic]} = quoted_pattern_with_diagnostics(0.0) - - assert diagnostic.message =~ - "pattern matching on 0.0 is equivalent to matching only on +0.0" - end - - test "list" do - assert quoted_pattern([]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([_]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([123]) == {:ok, {:list, :integer}} - assert quoted_pattern([123, 456]) == {:ok, {:list, :integer}} - assert quoted_pattern([123, _]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([_, 456]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([123 | []]) == {:ok, {:list, :integer}} - assert quoted_pattern([123, "foo"]) == {:ok, {:list, {:union, [:integer, :binary]}}} - assert quoted_pattern([123 | ["foo"]]) == {:ok, {:list, {:union, [:integer, :binary]}}} - - # TODO: improper list? - assert quoted_pattern([123 | 456]) == {:ok, {:list, :integer}} - assert quoted_pattern([123, 456 | 789]) == {:ok, {:list, :integer}} - assert quoted_pattern([123 | "foo"]) == {:ok, {:list, {:union, [:integer, :binary]}}} - assert quoted_pattern([123 | _]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([_ | [456]]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([_ | _]) == {:ok, {:list, :dynamic}} - - assert quoted_pattern([] ++ []) == {:ok, {:list, :dynamic}} - assert quoted_pattern([_] ++ _) == {:ok, {:list, :dynamic}} - assert quoted_pattern([123] ++ [456]) == {:ok, {:list, :integer}} - assert quoted_pattern([123] ++ _) == {:ok, {:list, :dynamic}} - assert quoted_pattern([123] ++ ["foo"]) == {:ok, {:list, {:union, [:integer, :binary]}}} - end - - test "tuple" do - assert quoted_pattern({}) == {:ok, {:tuple, 0, []}} - assert quoted_pattern({:a}) == {:ok, {:tuple, 1, [{:atom, :a}]}} - assert quoted_pattern({:a, 123}) == {:ok, {:tuple, 2, [{:atom, :a}, :integer]}} - end - - test "map" do - assert quoted_pattern(%{}) == {:ok, {:map, [{:optional, :dynamic, :dynamic}]}} - - assert quoted_pattern(%{a: :b}) == - {:ok, - {:map, [{:required, {:atom, :a}, {:atom, :b}}, {:optional, :dynamic, :dynamic}]}} - - assert quoted_pattern(%{123 => a}) == - {:ok, - {:map, - [ - {:required, :integer, {:var, 0}}, - {:optional, :dynamic, :dynamic} - ]}} - - assert quoted_pattern(%{123 => :foo, 456 => :bar}) == - {:ok, - {:map, - [ - {:required, :integer, :dynamic}, - {:optional, :dynamic, :dynamic} - ]}} - - assert {:error, {:unable_unify, {:integer, {:atom, :foo}, _}}} = - quoted_pattern(%{a: a = 123, b: a = :foo}) - end - - test "struct" do - assert {:ok, {:map, fields}} = quoted_pattern(%:"Elixir.Module.Types.PatternTest.Struct"{}) - - assert Enum.sort(fields) == [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}}, - {:required, {:atom, :bar}, :dynamic}, - {:required, {:atom, :baz}, :dynamic}, - {:required, {:atom, :foo}, :dynamic} - ] - - assert {:ok, {:map, fields}} = - quoted_pattern(%:"Elixir.Module.Types.PatternTest.Struct"{foo: 123, bar: :atom}) - - assert Enum.sort(fields) == - [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}}, - {:required, {:atom, :bar}, {:atom, :atom}}, - {:required, {:atom, :baz}, :dynamic}, - {:required, {:atom, :foo}, :integer} - ] - end - - test "struct var" do - assert quoted_pattern(%var{}) == - {:ok, - {:map, - [{:required, {:atom, :__struct__}, :atom}, {:optional, :dynamic, :dynamic}]}} - - assert quoted_pattern(%var{foo: 123}) == - {:ok, - {:map, - [ - {:required, {:atom, :__struct__}, :atom}, - {:required, {:atom, :foo}, :integer}, - {:optional, :dynamic, :dynamic} - ]}} - - assert quoted_pattern(%var{foo: var}) == - {:ok, - {:map, - [ - {:required, {:atom, :__struct__}, :atom}, - {:required, {:atom, :foo}, :atom}, - {:optional, :dynamic, :dynamic} - ]}} - end - - defmacrop custom_type do - quote do: 1 * 8 - big - signed - integer - end - - test "binary" do - assert quoted_pattern(<<"foo"::binary>>) == {:ok, :binary} - assert quoted_pattern(<<123::integer>>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} - assert quoted_pattern(<<123::utf8>>) == {:ok, :binary} - assert quoted_pattern(<<"foo"::utf8>>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} - - assert quoted_pattern({<>, foo}) == {:ok, {:tuple, 2, [:binary, :integer]}} - assert quoted_pattern({<>, foo}) == {:ok, {:tuple, 2, [:binary, :binary]}} - assert quoted_pattern({<>, foo}) == {:ok, {:tuple, 2, [:binary, :integer]}} - - assert {:error, {:unable_unify, {:binary, :integer, _}}} = - quoted_pattern(<>) - end - - test "variables" do - assert quoted_pattern(foo) == {:ok, {:var, 0}} - assert quoted_pattern({foo}) == {:ok, {:tuple, 1, [{:var, 0}]}} - assert quoted_pattern({foo, bar}) == {:ok, {:tuple, 2, [{:var, 0}, {:var, 1}]}} - - assert quoted_pattern(_) == {:ok, :dynamic} - assert quoted_pattern({_ = 123, _}) == {:ok, {:tuple, 2, [:integer, :dynamic]}} - end - - test "assignment" do - assert quoted_pattern(x = y) == {:ok, {:var, 0}} - assert quoted_pattern(x = 123) == {:ok, :integer} - assert quoted_pattern({foo}) == {:ok, {:tuple, 1, [{:var, 0}]}} - assert quoted_pattern({x = y}) == {:ok, {:tuple, 1, [{:var, 0}]}} - - assert quoted_pattern(x = y = 123) == {:ok, :integer} - assert quoted_pattern(x = 123 = y) == {:ok, :integer} - - assert {:error, {:unable_unify, {{:tuple, 1, [var: 0]}, {:var, 0}, _}}} = - quoted_pattern({x} = x) - end - end - - describe "heads" do - test "variable" do - assert quoted_head([a]) == {:ok, [{:var, 0}]} - assert quoted_head([a, b]) == {:ok, [{:var, 0}, {:var, 1}]} - assert quoted_head([a, a]) == {:ok, [{:var, 0}, {:var, 0}]} - - assert {:ok, [{:var, 0}, {:var, 0}], _} = - Pattern.of_head( - [{:a, [version: 0], :foo}, {:a, [version: 0], :foo}], - [], - new_stack(), - new_context() - ) - - assert {:ok, [{:var, 0}, {:var, 1}], _} = - Pattern.of_head( - [{:a, [version: 0], :foo}, {:a, [version: 1], :foo}], - [], - new_stack(), - new_context() - ) - end - - test "assignment" do - assert quoted_head([x = y, x = y]) == {:ok, [{:var, 0}, {:var, 0}]} - assert quoted_head([x = y, y = x]) == {:ok, [{:var, 0}, {:var, 0}]} - - assert quoted_head([x = :foo, x = y, y = z]) == - {:ok, [{:atom, :foo}, {:atom, :foo}, {:atom, :foo}]} - - assert quoted_head([x = y, y = :foo, y = z]) == - {:ok, [{:atom, :foo}, {:atom, :foo}, {:atom, :foo}]} - - assert quoted_head([x = y, y = z, z = :foo]) == - {:ok, [{:atom, :foo}, {:atom, :foo}, {:atom, :foo}]} - - assert {:error, {:unable_unify, {{:tuple, 1, [var: 1]}, {:var, 0}, _}}} = - quoted_head([{x} = y, {y} = x]) - end - - test "guards" do - assert quoted_head([x], [is_binary(x)]) == {:ok, [:binary]} - - assert quoted_head([x, y], [is_binary(x) and is_atom(y)]) == - {:ok, [:binary, :atom]} - - assert quoted_head([x], [is_binary(x) or is_atom(x)]) == - {:ok, [{:union, [:binary, :atom]}]} - - assert quoted_head([x, x], [is_integer(x)]) == {:ok, [:integer, :integer]} - - assert quoted_head([x = 123], [is_integer(x)]) == {:ok, [:integer]} - - assert quoted_head([x], [is_boolean(x) or is_atom(x)]) == - {:ok, [:atom]} - - assert quoted_head([x], [is_atom(x) or is_boolean(x)]) == - {:ok, [:atom]} - - assert quoted_head([x], [is_tuple(x) or is_atom(x)]) == - {:ok, [{:union, [:tuple, :atom]}]} - - assert quoted_head([x], [is_boolean(x) and is_atom(x)]) == - {:ok, [{:union, [atom: true, atom: false]}]} - - assert quoted_head([x], [is_atom(x) > :foo]) == {:ok, [var: 0]} - - assert quoted_head([x, y], [is_atom(x) or is_integer(y)]) == - {:ok, [{:var, 0}, {:var, 1}]} - - assert quoted_head([x], [is_atom(x) or is_atom(x)]) == - {:ok, [:atom]} - - assert quoted_head([x, y], [(is_atom(x) and is_atom(y)) or (is_atom(x) and is_integer(y))]) == - {:ok, [:atom, union: [:atom, :integer]]} - - assert quoted_head([x, y], [is_atom(x) or is_integer(x)]) == - {:ok, [union: [:atom, :integer], var: 0]} - - assert quoted_head([x, y], [is_atom(y) or is_integer(y)]) == - {:ok, [{:var, 0}, {:union, [:atom, :integer]}]} - - assert quoted_head([x], [true == false or is_integer(x)]) == - {:ok, [var: 0]} - - assert {:error, {:unable_unify, {:binary, :integer, _}}} = - quoted_head([x], [is_binary(x) and is_integer(x)]) - - assert {:error, {:unable_unify, {:tuple, :atom, _}}} = - quoted_head([x], [is_tuple(x) and is_atom(x)]) - - assert {:error, {:unable_unify, {{:atom, true}, :tuple, _}}} = - quoted_head([x], [is_tuple(is_atom(x))]) - end - - test "guard downcast" do - assert {:error, _} = quoted_head([x], [is_atom(x) and is_boolean(x)]) - end - - test "guard and" do - assert quoted_head([], [(true and 1) > 0]) == {:ok, []} - - assert quoted_head( - [struct], - [is_map_key(struct, :map) and map_size(:erlang.map_get(:map, struct))] - ) == {:ok, [{:map, [{:optional, :dynamic, :dynamic}]}]} - end - - test "intersection functions" do - assert quoted_head([x], [+x]) == {:ok, [{:union, [:integer, :float]}]} - assert quoted_head([x], [x + 1]) == {:ok, [{:union, [:float, :integer]}]} - assert quoted_head([x], [x + 1.0]) == {:ok, [{:union, [:integer, :float]}]} - end - - test "nested calls with intersections in guards" do - assert quoted_head([x], [:erlang.rem(x, 2)]) == {:ok, [:integer]} - assert quoted_head([x], [:erlang.rem(x + x, 2)]) == {:ok, [:integer]} - - assert quoted_head([x], [:erlang.bnot(+x)]) == {:ok, [:integer]} - assert quoted_head([x], [:erlang.bnot(x + 1)]) == {:ok, [:integer]} - - assert quoted_head([x], [is_integer(1 + x - 1)]) == {:ok, [:integer]} - - assert quoted_head([x], [is_integer(1 + x - 1) and is_integer(1 + x - 1)]) == - {:ok, [:integer]} - - assert quoted_head([x], [1 - x >= 0]) == {:ok, [{:union, [:float, :integer]}]} - assert quoted_head([x], [1 - x >= 0 and 1 - x < 0]) == {:ok, [{:union, [:float, :integer]}]} - - assert {:error, - {:unable_apply, - {_, [{:var, 0}, :float], _, - [ - {[:integer, :integer], :integer}, - {[:float, {:union, [:integer, :float]}], :float}, - {[{:union, [:integer, :float]}, :float], :float} - ], _}}} = quoted_head([x], [:erlang.bnot(x + 1.0)]) - end - - test "erlang-only guards" do - assert quoted_head([x], [:erlang.size(x)]) == - {:ok, [{:union, [:binary, :tuple]}]} - end - - test "failing guard functions" do - assert quoted_head([x], [length([])]) == {:ok, [{:var, 0}]} - - assert {:error, - {:unable_apply, - {{:erlang, :length, 1}, [{:atom, :foo}], _, [{[{:list, :dynamic}], :integer}], _}}} = - quoted_head([x], [length(:foo)]) - - assert {:error, - {:unable_apply, - {_, [{:union, [{:atom, true}, {:atom, false}]}], _, - [{[{:list, :dynamic}], :integer}], _}}} = quoted_head([x], [length(is_tuple(x))]) - - assert {:error, - {:unable_apply, - {_, [:integer, {:union, [{:atom, true}, {:atom, false}]}], _, - [{[:integer, :tuple], :dynamic}], _}}} = quoted_head([x], [elem(is_tuple(x), 0)]) - - assert {:error, - {:unable_apply, - {_, [{:union, [{:atom, true}, {:atom, false}]}, :integer], _, - [ - {[:integer, :integer], :integer}, - {[:float, {:union, [:integer, :float]}], :float}, - {[{:union, [:integer, :float]}, :float], :float} - ], _}}} = quoted_head([x], [elem({}, is_tuple(x))]) - - assert quoted_head([x], [elem({}, 1)]) == {:ok, [var: 0]} - - assert quoted_head([x], [elem(x, 1) == :foo]) == {:ok, [:tuple]} - - assert quoted_head([x], [is_tuple(x) and elem(x, 1)]) == {:ok, [:tuple]} - - assert quoted_head([x], [length(x) == 0 or elem(x, 1)]) == {:ok, [{:list, :dynamic}]} - - assert quoted_head([x], [ - (is_list(x) and length(x) == 0) or (is_tuple(x) and elem(x, 1)) - ]) == - {:ok, [{:union, [{:list, :dynamic}, :tuple]}]} - - assert quoted_head([x], [ - (length(x) == 0 and is_list(x)) or (elem(x, 1) and is_tuple(x)) - ]) == {:ok, [{:list, :dynamic}]} - - assert quoted_head([x], [elem(x, 1) or is_atom(x)]) == {:ok, [:tuple]} - - assert quoted_head([x], [is_atom(x) or elem(x, 1)]) == {:ok, [{:union, [:atom, :tuple]}]} - - assert quoted_head([x, y], [elem(x, 1) and is_atom(y)]) == {:ok, [:tuple, :atom]} - - assert quoted_head([x, y], [elem(x, 1) or is_atom(y)]) == {:ok, [:tuple, {:var, 0}]} - - assert {:error, {:unable_unify, {:tuple, :atom, _}}} = - quoted_head([x], [elem(x, 1) and is_atom(x)]) - end - - test "map" do - assert quoted_head([%{true: false} = foo, %{} = foo]) == - {:ok, - [ - {:map, - [{:required, {:atom, true}, {:atom, false}}, {:optional, :dynamic, :dynamic}]}, - {:map, - [{:required, {:atom, true}, {:atom, false}}, {:optional, :dynamic, :dynamic}]} - ]} - - assert quoted_head([%{true: bool}], [is_boolean(bool)]) == - {:ok, - [ - {:map, - [ - {:required, {:atom, true}, {:union, [atom: true, atom: false]}}, - {:optional, :dynamic, :dynamic} - ]} - ]} - - assert quoted_head([%{true: true} = foo, %{false: false} = foo]) == - {:ok, - [ - {:map, - [ - {:required, {:atom, false}, {:atom, false}}, - {:required, {:atom, true}, {:atom, true}}, - {:optional, :dynamic, :dynamic} - ]}, - {:map, - [ - {:required, {:atom, false}, {:atom, false}}, - {:required, {:atom, true}, {:atom, true}}, - {:optional, :dynamic, :dynamic} - ]} - ]} - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, true}, {:atom, true}}]}, - {:map, [{:required, {:atom, true}, {:atom, false}}]}, - _}}} = quoted_head([%{true: false} = foo, %{true: true} = foo]) - end - - test "binary in guards" do - assert quoted_head([a, b], [byte_size(a <> b) > 0]) == - {:ok, [:binary, :binary]} - - assert quoted_head([map], [byte_size(map.a <> map.b) > 0]) == - {:ok, [map: [{:optional, :dynamic, :dynamic}]]} - end - - test "struct var guard" do - assert quoted_head([%var{}], [is_atom(var)]) == - {:ok, - [ - {:map, - [{:required, {:atom, :__struct__}, :atom}, {:optional, :dynamic, :dynamic}]} - ]} - - assert {:error, {:unable_unify, {:atom, :integer, _}}} = - quoted_head([%var{}], [is_integer(var)]) - end - - test "tuple_size/1" do - assert quoted_head([x], [tuple_size(x) == 0]) == {:ok, [{:tuple, 0, []}]} - assert quoted_head([x], [0 == tuple_size(x)]) == {:ok, [{:tuple, 0, []}]} - assert quoted_head([x], [tuple_size(x) == 2]) == {:ok, [{:tuple, 2, [:dynamic, :dynamic]}]} - assert quoted_head([x], [is_tuple(x) and tuple_size(x) == 0]) == {:ok, [{:tuple, 0, []}]} - assert quoted_head([x], [tuple_size(x) == 0 and is_tuple(x)]) == {:ok, [{:tuple, 0, []}]} - - assert quoted_head([x = {y}], [is_integer(y) and tuple_size(x) == 1]) == - {:ok, [{:tuple, 1, [:integer]}]} - - assert {:error, {:unable_unify, {{:tuple, 0, []}, :integer, _}}} = - quoted_head([x], [tuple_size(x) == 0 and is_integer(x)]) - - assert {:error, {:unable_unify, {{:tuple, 0, []}, :integer, _}}} = - quoted_head([x], [is_integer(x) and tuple_size(x) == 0]) - - assert {:error, {:unable_unify, {{:tuple, 0, []}, :integer, _}}} = - quoted_head([x], [is_tuple(x) and tuple_size(x) == 0 and is_integer(x)]) - - assert {:error, {:unable_unify, {{:tuple, 1, [:dynamic]}, {:tuple, 0, []}, _}}} = - quoted_head([x = {}], [tuple_size(x) == 1]) - end - end -end diff --git a/lib/elixir/test/elixir/module/types/types_test.exs b/lib/elixir/test/elixir/module/types/types_test.exs index f25f067dab1..bee4b5bc5cf 100644 --- a/lib/elixir/test/elixir/module/types/types_test.exs +++ b/lib/elixir/test/elixir/module/types/types_test.exs @@ -5,8 +5,6 @@ defmodule Module.Types.TypesTest do alias Module.Types alias Module.Types.{Pattern, Expr} - @hint :elixir_errors.prefix(:hint) - defmacro warning(patterns \\ [], guards \\ [], body) do min_line = min_line(patterns ++ guards ++ [body]) patterns = reset_line(patterns, min_line) @@ -92,630 +90,4 @@ defmodule Module.Types.TypesTest do assert warning([], try(do: :ok, after: URI.unknown("foo"))) == "URI.unknown/1 is undefined or private" end - - describe "function head warnings" do - test "warns on literals" do - string = warning([var = 123, var = "abc"], var) - - assert string == """ - incompatible types: - - integer() !~ binary() - - in expression: - - # types_test.ex:1 - var = "abc" - - where "var" was given the type integer() in: - - # types_test.ex:1 - var = 123 - - where "var" was given the type binary() in: - - # types_test.ex:1 - var = "abc" - """ - end - - test "warns on binary patterns" do - string = warning([<>], var) - - assert string == """ - incompatible types: - - integer() !~ binary() - - in expression: - - # types_test.ex:1 - <<..., var::binary>> - - where "var" was given the type integer() in: - - # types_test.ex:1 - <> - - where "var" was given the type binary() in: - - # types_test.ex:1 - <<..., var::binary>> - """ - end - - test "warns on recursive patterns" do - string = warning([{var} = var], var) - - assert string == """ - incompatible types: - - {var1} !~ var1 - - in expression: - - # types_test.ex:1 - {var} = var - - where "var" was given the type {var1} in: - - # types_test.ex:1 - {var} = var - """ - end - - test "warns on guards" do - string = warning([var], [is_integer(var) and is_binary(var)], var) - - assert string == """ - incompatible types: - - integer() !~ binary() - - in expression: - - # types_test.ex:1 - is_binary(var) - - where "var" was given the type integer() in: - - # types_test.ex:1 - is_integer(var) - - where "var" was given the type binary() in: - - # types_test.ex:1 - is_binary(var) - """ - end - - test "warns on guards from cases unless generated" do - string = - warning( - [var], - [is_integer(var)], - case var do - _ when is_binary(var) -> :ok - end - ) - - assert is_binary(string) - - string = - generated( - warning( - [var], - [is_integer(var)], - case var do - _ when is_binary(var) -> :ok - end - ) - ) - - assert string == :none - end - - test "check body" do - string = warning([x], [is_integer(x)], :foo = x) - - assert string == """ - incompatible types: - - integer() !~ :foo - - in expression: - - # types_test.ex:1 - :foo = x - - where "x" was given the type integer() in: - - # types_test.ex:1 - is_integer(x) - - where "x" was given the type :foo in: - - # types_test.ex:1 - :foo = x - """ - end - - test "check binary" do - string = warning([foo], [is_binary(foo)], <>) - - assert string == """ - incompatible types: - - binary() !~ integer() - - in expression: - - # types_test.ex:1 - <> - - where "foo" was given the type binary() in: - - # types_test.ex:1 - is_binary(foo) - - where "foo" was given the type integer() in: - - # types_test.ex:1 - <> - - #{@hint} all expressions given to binaries are assumed to be of type \ - integer() unless said otherwise. For example, <> assumes "expr" \ - is an integer. Pass a modifier, such as <> or <>, \ - to change the default behaviour. - """ - - string = warning([foo], [is_binary(foo)], <>) - - assert string == """ - incompatible types: - - binary() !~ integer() - - in expression: - - # types_test.ex:1 - <> - - where "foo" was given the type binary() in: - - # types_test.ex:1 - is_binary(foo) - - where "foo" was given the type integer() in: - - # types_test.ex:1 - <> - """ - end - - test "is_tuple warning" do - string = warning([foo], [is_tuple(foo)], {_} = foo) - - assert string == """ - incompatible types: - - tuple() !~ {dynamic()} - - in expression: - - # types_test.ex:1 - {_} = foo - - where "foo" was given the type tuple() in: - - # types_test.ex:1 - is_tuple(foo) - - where "foo" was given the type {dynamic()} in: - - # types_test.ex:1 - {_} = foo - - #{@hint} use pattern matching or "is_tuple(foo) and tuple_size(foo) == 1" to guard a sized tuple. - """ - end - - test "function call" do - string = warning([foo], [rem(foo, 2.0) == 0], foo) - - assert string == """ - expected Kernel.rem/2 to have signature: - - var1, float() -> dynamic() - - but it has signature: - - integer(), integer() -> integer() - - in expression: - - # types_test.ex:1 - rem(foo, 2.0) - """ - end - - test "operator call" do - string = warning([foo], [foo - :bar == 0], foo) - - assert string == """ - expected Kernel.-/2 to have signature: - - var1, :bar -> dynamic() - - but it has signature: - - integer(), integer() -> integer() - float(), integer() | float() -> float() - integer() | float(), float() -> float() - - in expression: - - # types_test.ex:1 - foo - :bar - """ - end - - test "rewrite call" do - string = warning([foo], [is_map_key(1, foo)], foo) - - assert string == """ - expected Kernel.is_map_key/2 to have signature: - - integer(), var1 -> dynamic() - - but it has signature: - - %{optional(dynamic()) => dynamic()}, dynamic() -> dynamic() - - in expression: - - # types_test.ex:1 - is_map_key(1, foo) - """ - end - end - - describe "map warnings" do - test "handling of non-singleton types in maps" do - string = - warning( - [], - ( - event = %{"type" => "order"} - %{"amount" => amount} = event - %{"user" => user} = event - %{"id" => user_id} = user - {:order, user_id, amount} - ) - ) - - assert string == """ - incompatible types: - - binary() !~ map() - - in expression: - - # types_test.ex:5 - %{"id" => user_id} = user - - where "amount" was given the type binary() in: - - # types_test.ex:3 - %{"amount" => amount} = event - - where "amount" was given the same type as "user" in: - - # types_test.ex:4 - %{"user" => user} = event - - where "user" was given the type binary() in: - - # types_test.ex:4 - %{"user" => user} = event - - where "user" was given the type map() in: - - # types_test.ex:5 - %{"id" => user_id} = user - """ - end - - test "show map() when comparing against non-map" do - string = - warning( - [foo], - ( - foo.bar - :atom = foo - ) - ) - - assert string == """ - incompatible types: - - map() !~ :atom - - in expression: - - # types_test.ex:4 - :atom = foo - - where "foo" was given the type map() (due to calling var.field) in: - - # types_test.ex:3 - foo.bar - - where "foo" was given the type :atom in: - - # types_test.ex:4 - :atom = foo - - #{@hint} "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - """ - end - - test "use module as map (without parentheses)" do - string = - warning( - [foo], - ( - %module{} = foo - module.__struct__ - ) - ) - - assert string == """ - incompatible types: - - map() !~ atom() - - in expression: - - # types_test.ex:4 - module.__struct__ - - where "module" was given the type atom() in: - - # types_test.ex:3 - %module{} - - where "module" was given the type map() (due to calling var.field) in: - - # types_test.ex:4 - module.__struct__ - - #{@hint} "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - """ - end - - test "use map as module (with parentheses)" do - string = warning([foo], [is_map(foo)], foo.__struct__()) - - assert string == """ - incompatible types: - - map() !~ atom() - - in expression: - - # types_test.ex:1 - foo.__struct__() - - where "foo" was given the type map() in: - - # types_test.ex:1 - is_map(foo) - - where "foo" was given the type atom() (due to calling var.fun()) in: - - # types_test.ex:1 - foo.__struct__() - - #{@hint} "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - """ - end - - test "non-existent map field warning" do - string = - warning( - ( - map = %{foo: 1} - map.bar - ) - ) - - assert string == """ - undefined field "bar" in expression: - - # types_test.ex:3 - map.bar - - expected one of the following fields: foo - - where "map" was given the type map() in: - - # types_test.ex:2 - map = %{foo: 1} - """ - end - - test "non-existent struct field warning" do - string = - warning( - [foo], - ( - %URI{} = foo - foo.bar - ) - ) - - assert string == """ - undefined field "bar" in expression: - - # types_test.ex:4 - foo.bar - - expected one of the following fields: __struct__, authority, fragment, host, path, port, query, scheme, userinfo - - where "foo" was given the type %URI{} in: - - # types_test.ex:3 - %URI{} = foo - """ - end - - test "expands type variables" do - string = - warning( - [%{foo: key} = event, other_key], - [is_integer(key) and is_atom(other_key)], - %{foo: ^other_key} = event - ) - - assert string == """ - incompatible types: - - %{foo: integer()} !~ %{foo: atom()} - - in expression: - - # types_test.ex:3 - %{foo: ^other_key} = event - - where "event" was given the type %{foo: integer(), optional(dynamic()) => dynamic()} in: - - # types_test.ex:1 - %{foo: key} = event - - where "event" was given the type %{foo: atom(), optional(dynamic()) => dynamic()} in: - - # types_test.ex:3 - %{foo: ^other_key} = event - """ - end - - test "expands map when maps are nested" do - string = - warning( - [map1, map2], - ( - [_var1, _var2] = [map1, map2] - %{} = map1 - %{} = map2.subkey - ) - ) - - assert string == """ - incompatible types: - - %{subkey: var1, optional(dynamic()) => dynamic()} !~ %{optional(dynamic()) => dynamic()} | %{optional(dynamic()) => dynamic()} - - in expression: - - # types_test.ex:5 - map2.subkey - - where "map2" was given the type %{optional(dynamic()) => dynamic()} | %{optional(dynamic()) => dynamic()} in: - - # types_test.ex:3 - [_var1, _var2] = [map1, map2] - - where "map2" was given the type %{subkey: var1, optional(dynamic()) => dynamic()} (due to calling var.field) in: - - # types_test.ex:5 - map2.subkey - - #{@hint} "var.field" (without parentheses) implies "var" is a map() while "var.fun()" (with parentheses) implies "var" is an atom() - """ - end - end - - describe "regressions" do - test "recursive map fields" do - assert warning( - [queried], - with( - true <- is_nil(queried.foo.bar), - _ = queried.foo - ) do - %{foo: %{other_id: _other_id} = foo} = queried - %{other_id: id} = foo - %{id: id} - end - ) == :none - end - - test "no-recursion on guards with map fields" do - assert warning( - [assigns], - ( - variable_enum = assigns.variable_enum - - case true do - _ when variable_enum != nil -> assigns.variable_enum - end - ) - ) == :none - end - - test "map patterns with pinned keys and field access" do - assert warning( - [x, y], - ( - key_var = y - %{^key_var => _value} = x - key_var2 = y - %{^key_var2 => _value2} = x - y.z - ) - ) == :none - end - - test "map patterns with pinned keys" do - assert warning( - [x, y], - ( - key_var = y - %{^key_var => _value} = x - key_var2 = y - %{^key_var2 => _value2} = x - key_var3 = y - %{^key_var3 => _value3} = x - ) - ) == :none - end - - test "map updates with var key" do - assert warning( - [state0, key0], - ( - state1 = %{state0 | key0 => true} - key1 = key0 - state2 = %{state1 | key1 => true} - state2 - ) - ) == :none - end - - test "nested map updates" do - assert warning( - [state], - ( - _foo = state.key.user_id - _bar = state.key.user_id - state = %{state | key: %{state.key | other_id: 1}} - _baz = state.key.user_id - ) - ) == :none - end - end end From 5b4ac29874bde601e9feca281f4867319bda19c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 5 Jan 2024 17:43:33 +0100 Subject: [PATCH 2/3] More clean up --- lib/elixir/lib/module/types.ex | 395 +------ lib/elixir/lib/module/types/descr.ex | 14 + lib/elixir/lib/module/types/expr.ex | 339 ++---- lib/elixir/lib/module/types/helpers.ex | 26 - lib/elixir/lib/module/types/of.ex | 63 +- lib/elixir/lib/module/types/pattern.ex | 152 +-- lib/elixir/lib/module/types/unify.ex | 992 ------------------ lib/elixir/src/elixir_compiler.erl | 2 +- .../test/elixir/module/types/expr_test.exs | 325 +----- .../test/elixir/module/types/type_helper.exs | 60 +- .../test/elixir/module/types/types_test.exs | 46 +- .../test/elixir/module/types/unify_test.exs | 770 -------------- 12 files changed, 283 insertions(+), 2901 deletions(-) create mode 100644 lib/elixir/lib/module/types/descr.ex delete mode 100644 lib/elixir/lib/module/types/unify.ex delete mode 100644 lib/elixir/test/elixir/module/types/unify_test.exs diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index c1c7efdd5e6..2e9fb55bf39 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -5,8 +5,7 @@ defmodule Module.Types do defexception [:message] end - import Module.Types.Helpers - alias Module.Types.{Expr, Pattern, Unify} + alias Module.Types.{Expr, Pattern} @doc false def warnings(module, file, defs, no_warn_undefined, cache) do @@ -16,10 +15,8 @@ defmodule Module.Types do context = context(with_file_meta(meta, file), module, function, no_warn_undefined, cache) Enum.flat_map(clauses, fn {_meta, args, guards, body} -> - def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args})]} - try do - warnings_from_clause(args, guards, body, def_expr, stack, context) + warnings_from_clause(args, guards, body, stack, context) rescue e -> def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]} @@ -58,15 +55,12 @@ defmodule Module.Types do guards_to_expr(guards, {:when, [], [left, guard]}) end - defp warnings_from_clause(args, guards, body, def_expr, stack, context) do - head_stack = Unify.push_expr_stack(def_expr, stack) - - with {:ok, _types, context} <- Pattern.of_head(args, guards, head_stack, context), - {:ok, _type, context} <- Expr.of_expr(body, :dynamic, stack, context) do + defp warnings_from_clause(args, guards, body, stack, context) do + with {:ok, _types, context} <- Pattern.of_head(args, guards, stack, context), + {:ok, _type, context} <- Expr.of_expr(body, stack, context) do context.warnings else - {:error, {type, error, context}} -> - [error_to_warning(type, error, context) | context.warnings] + {:error, context} -> context.warnings end end @@ -132,255 +126,6 @@ defmodule Module.Types do } end - ## ERROR TO WARNING - - # Collect relevant information from context and traces to report error - def error_to_warning(:unable_apply, {mfa, args, expected, signature, stack}, context) do - {fun, arity} = context.function - location = {context.file, get_position(stack), {context.module, fun, arity}} - - traces = type_traces(stack, context) - {[signature | args], traces} = lift_all_types([signature | args], traces, context) - error = {:unable_apply, mfa, args, expected, signature, {location, stack.last_expr, traces}} - {Module.Types, error, location} - end - - def error_to_warning(:unable_unify, {left, right, stack}, context) do - {fun, arity} = context.function - location = {context.file, get_position(stack), {context.module, fun, arity}} - - traces = type_traces(stack, context) - {[left, right], traces} = lift_all_types([left, right], traces, context) - error = {:unable_unify, left, right, {location, stack.last_expr, traces}} - {Module.Types, error, location} - end - - defp get_position(stack) do - get_meta(stack.last_expr) - end - - # Collect relevant traces from context.traces using stack.unify_stack - defp type_traces(stack, context) do - # TODO: Do we need the unify_stack or is enough to only get the last variable - # in the stack since we get related variables anyway? - stack = - stack.unify_stack - |> Enum.flat_map(&[&1 | related_variables(&1, context.types)]) - |> Enum.uniq() - - Enum.flat_map(stack, fn var_index -> - with %{^var_index => traces} <- context.traces, - %{^var_index => expr_var} <- context.types_to_vars do - Enum.map(traces, &tag_trace(expr_var, &1, context)) - else - _other -> [] - end - end) - end - - defp related_variables(var, types) do - Enum.flat_map(types, fn - {related_var, {:var, ^var}} -> - [related_var | related_variables(related_var, types)] - - _ -> - [] - end) - end - - # Tag if trace is for a concrete type or type variable - defp tag_trace(var, {type, expr, location}, context) do - with {:var, var_index} <- type, - %{^var_index => expr_var} <- context.types_to_vars do - {:var, var, expr_var, expr, location} - else - _ -> {:type, var, type, expr, location} - end - end - - defp lift_all_types(types, traces, context) do - trace_types = for({:type, _, type, _, _} <- traces, do: type) - {types, lift_context} = Unify.lift_types(types, context) - {trace_types, _lift_context} = Unify.lift_types(trace_types, lift_context) - - {traces, []} = - Enum.map_reduce(traces, trace_types, fn - {:type, var, _, expr, location}, [type | acc] -> {{:type, var, type, expr, location}, acc} - other, acc -> {other, acc} - end) - - {types, traces} - end - - ## FORMAT WARNINGS - - def format_warning({:unable_apply, mfa, args, expected, signature, {location, expr, traces}}) do - {original_module, original_function, arity} = mfa - {_, _, args} = mfa_or_fa = erl_to_ex(original_module, original_function, args, []) - {module, function, ^arity} = call_to_mfa(mfa_or_fa) - format_mfa = Exception.format_mfa(module, function, arity) - {traces, [] = _hints} = format_traces(traces, [], false) - - clauses = - Enum.map(signature, fn {ins, out} -> - {_, _, ins} = erl_to_ex(original_module, original_function, ins, []) - - {:fun, [{ins, out}]} - |> Unify.format_type(false) - |> IO.iodata_to_binary() - |> binary_slice(1..-2//1) - end) - - [ - "expected #{format_mfa} to have signature:\n\n ", - Enum.map_join(args, ", ", &Unify.format_type(&1, false)), - " -> #{Unify.format_type(expected, false)}", - "\n\nbut it has signature:\n\n ", - indent(Enum.join(clauses, "\n")), - "\n\n", - format_expr(expr, location), - traces, - "Conflict found at" - ] - end - - def format_warning({:unable_unify, left, right, {location, expr, traces}}) do - if map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) do - {:ok, atom, known_atoms} = missing_field(left, right) - - # Drop the last trace which is the expression map.foo - traces = Enum.drop(traces, 1) - {traces, hints} = format_traces(traces, [left, right], true) - - [ - "undefined field \"#{atom}\" ", - format_expr(expr, location), - "expected one of the following fields: ", - Enum.join(Enum.sort(known_atoms), ", "), - "\n\n", - traces, - format_message_hints(hints), - "Conflict found at" - ] - else - simplify_left? = simplify_type?(left, right) - simplify_right? = simplify_type?(right, left) - - {traces, hints} = format_traces(traces, [left, right], simplify_left? or simplify_right?) - - [ - "incompatible types:\n\n ", - Unify.format_type(left, simplify_left?), - " !~ ", - Unify.format_type(right, simplify_right?), - "\n\n", - format_expr(expr, location), - traces, - format_message_hints(hints), - "Conflict found at" - ] - end - end - - defp missing_field( - {:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]}, - {:map, fields} - ) do - matched_missing_field(fields, type, atom) - end - - defp missing_field( - {:map, fields}, - {:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]} - ) do - matched_missing_field(fields, type, atom) - end - - defp missing_field(_, _), do: :error - - defp matched_missing_field(fields, type, atom) do - if List.keymember?(fields, type, 1) do - :error - else - known_atoms = for {_, {:atom, atom}, _} <- fields, do: atom - {:ok, atom, known_atoms} - end - end - - defp format_traces([], _types, _simplify?) do - {[], []} - end - - defp format_traces(traces, types, simplify?) do - traces - |> Enum.uniq() - |> Enum.reverse() - |> Enum.map_reduce([], fn - {:type, var, type, expr, location}, hints -> - {hint, hints} = format_type_hint(type, types, expr, hints) - - trace = [ - "where \"", - Macro.to_string(var), - "\" was given the type ", - Unify.format_type(type, simplify?), - hint, - " in:\n\n # ", - format_location(location), - " ", - indent(expr_to_string(expr)), - "\n\n" - ] - - {trace, hints} - - {:var, var1, var2, expr, location}, hints -> - trace = [ - "where \"", - Macro.to_string(var1), - "\" was given the same type as \"", - Macro.to_string(var2), - "\" in:\n\n # ", - format_location(location), - " ", - indent(expr_to_string(expr)), - "\n\n" - ] - - {trace, hints} - end) - end - - defp format_location({file, position, _mfa}) do - format_location({file, position[:line]}) - end - - defp format_location({file, line}) do - file = Path.relative_to_cwd(file) - line = if line, do: [Integer.to_string(line)], else: [] - [file, ?:, line, ?\n] - end - - defp simplify_type?(type, other) do - map_like_type?(type) and not map_like_type?(other) - end - - ## EXPRESSION FORMATTING - - defp format_expr(nil, _location) do - [] - end - - defp format_expr(expr, location) do - [ - "in expression:\n\n # ", - format_location(location), - " ", - indent(expr_to_string(expr)), - "\n\n" - ] - end - @doc false def expr_to_string(expr) do expr @@ -401,132 +146,4 @@ defmodule Module.Types do {mod, fun, args} -> {{:., [], [mod, fun]}, meta, args} end end - - ## Hints - - defp format_message_hints(hints) do - hints - |> Enum.uniq() - |> Enum.reverse() - |> Enum.map(&[format_message_hint(&1), "\n"]) - end - - defp format_message_hint(:inferred_dot) do - """ - #{hint()} "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - """ - end - - defp format_message_hint(:inferred_bitstring_spec) do - """ - #{hint()} all expressions given to binaries are assumed to be of type \ - integer() unless said otherwise. For example, <> assumes "expr" \ - is an integer. Pass a modifier, such as <> or <>, \ - to change the default behaviour. - """ - end - - defp format_message_hint({:sized_and_unsize_tuples, {size, var}}) do - """ - #{hint()} use pattern matching or "is_tuple(#{Macro.to_string(var)}) and \ - tuple_size(#{Macro.to_string(var)}) == #{size}" to guard a sized tuple. - """ - end - - defp hint, do: :elixir_errors.prefix(:hint) - - defp format_type_hint(type, types, expr, hints) do - case format_type_hint(type, types, expr) do - {message, hint} -> {message, [hint | hints]} - :error -> {[], hints} - end - end - - defp format_type_hint(type, types, expr) do - cond do - dynamic_map_dot?(type, expr) -> - {" (due to calling var.field)", :inferred_dot} - - dynamic_remote_call?(type, expr) -> - {" (due to calling var.fun())", :inferred_dot} - - inferred_bitstring_spec?(type, expr) -> - {[], :inferred_bitstring_spec} - - message = sized_and_unsize_tuples(expr, types) -> - {[], {:sized_and_unsize_tuples, message}} - - true -> - :error - end - end - - defp dynamic_map_dot?(type, expr) do - with true <- map_type?(type), - {{:., _meta1, [_map, _field]}, meta2, []} <- expr, - true <- Keyword.get(meta2, :no_parens, false) do - true - else - _ -> false - end - end - - defp dynamic_remote_call?(type, expr) do - with true <- atom_type?(type), - {{:., _meta1, [_module, _field]}, meta2, []} <- expr, - false <- Keyword.get(meta2, :no_parens, false) do - true - else - _ -> false - end - end - - defp inferred_bitstring_spec?(type, expr) do - with true <- integer_type?(type), - {:<<>>, _, args} <- expr, - true <- Enum.any?(args, &match?({:"::", [{:inferred_bitstring_spec, true} | _], _}, &1)) do - true - else - _ -> false - end - end - - defp sized_and_unsize_tuples({{:., _, [:erlang, :is_tuple]}, _, [var]}, types) do - case Enum.find(types, &match?({:tuple, _, _}, &1)) do - {:tuple, size, _} -> - {size, var} - - nil -> - nil - end - end - - defp sized_and_unsize_tuples(_expr, _types) do - nil - end - - ## Formatting helpers - - defp indent(string) do - String.replace(string, "\n", "\n ") - end - - defp map_type?({:map, _}), do: true - defp map_type?(_other), do: false - - defp map_like_type?({:map, _}), do: true - defp map_like_type?({:union, union}), do: Enum.any?(union, &map_like_type?/1) - defp map_like_type?(_other), do: false - - defp atom_type?(:atom), do: true - defp atom_type?({:atom, _}), do: false - defp atom_type?({:union, union}), do: Enum.all?(union, &atom_type?/1) - defp atom_type?(_other), do: false - - defp integer_type?(:integer), do: true - defp integer_type?(_other), do: false - - defp call_to_mfa({{:., _, [mod, fun]}, _, args}), do: {mod, fun, length(args)} - defp call_to_mfa({fun, _, args}) when is_atom(fun), do: {Kernel, fun, length(args)} end diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex new file mode 100644 index 00000000000..15e02baee37 --- /dev/null +++ b/lib/elixir/lib/module/types/descr.ex @@ -0,0 +1,14 @@ +defmodule Module.Types.Descr do + @moduledoc false + def term(), do: :term + def atom(_atom), do: :atom + def dynamic(), do: :dynamic + def integer(), do: :integer + def float(), do: :float + def binary(), do: :binary + def pid(), do: :pid + def tuple(), do: :tuple + def empty_list(), do: :list + def non_empty_list(), do: :list + def map(), do: :map +end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 9931e2906bd..47b76206a52 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -2,68 +2,55 @@ defmodule Module.Types.Expr do @moduledoc false alias Module.Types.{Of, Pattern} - import Module.Types.{Helpers, Unify} + import Module.Types.{Helpers, Descr} - def of_expr(expr, expected, %{context: stack_context} = stack, context) - when stack_context != :expr do - of_expr(expr, expected, %{stack | context: :expr}, context) + def of_expr(ast, stack, context) do + of_expr(ast, term(), stack, context) end # :atom def of_expr(atom, _expected, _stack, context) when is_atom(atom) do - {:ok, {:atom, atom}, context} + {:ok, atom(atom), context} end # 12 def of_expr(literal, _expected, _stack, context) when is_integer(literal) do - {:ok, :integer, context} + {:ok, integer(), context} end # 1.2 def of_expr(literal, _expected, _stack, context) when is_float(literal) do - {:ok, :float, context} + {:ok, float(), context} end # "..." def of_expr(literal, _expected, _stack, context) when is_binary(literal) do - {:ok, :binary, context} + {:ok, binary(), context} end # #PID<...> def of_expr(literal, _expected, _stack, context) when is_pid(literal) do - {:ok, :dynamic, context} + {:ok, pid(), context} end # <<...>>> def of_expr({:<<>>, _meta, args}, _expected, stack, context) do - case Of.binary(args, stack, context, &of_expr/4) do - {:ok, context} -> {:ok, :binary, context} + case Of.binary(args, :expr, stack, context, &of_expr/3) do + {:ok, context} -> {:ok, binary(), context} {:error, reason} -> {:error, reason} end end # left | [] - def of_expr({:|, _meta, [left_expr, []]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - of_expr(left_expr, :dynamic, stack, context) + def of_expr({:|, _meta, [left_expr, []]}, expected, stack, context) do + of_expr(left_expr, expected, stack, context) end # left | right - def of_expr({:|, _meta, [left_expr, right_expr]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - - case of_expr(left_expr, :dynamic, stack, context) do - {:ok, left, context} -> - case of_expr(right_expr, :dynamic, stack, context) do - {:ok, {:list, right}, context} -> - {:ok, to_union([left, right], context), context} - - {:ok, right, context} -> - {:ok, to_union([left, right], context), context} - - {:error, reason} -> - {:error, reason} - end + def of_expr({:|, _meta, [left_expr, right_expr]}, _expected, stack, context) do + case of_expr(left_expr, stack, context) do + {:ok, _left, context} -> + of_expr(right_expr, stack, context) {:error, reason} -> {:error, reason} @@ -72,15 +59,13 @@ defmodule Module.Types.Expr do # [] def of_expr([], _expected, _stack, context) do - {:ok, {:list, :dynamic}, context} + {:ok, empty_list(), context} end # [expr, ...] def of_expr(exprs, _expected, stack, context) when is_list(exprs) do - stack = push_expr_stack(exprs, stack) - - case map_reduce_ok(exprs, context, &of_expr(&1, :dynamic, stack, &2)) do - {:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context} + case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do + {:ok, _types, context} -> {:ok, non_empty_list(), context} {:error, reason} -> {:error, reason} end end @@ -88,29 +73,18 @@ defmodule Module.Types.Expr do # __CALLER__ def of_expr({:__CALLER__, _meta, var_context}, _expected, _stack, context) when is_atom(var_context) do - struct_pair = {:required, {:atom, :__struct__}, {:atom, Macro.Env}} - - pairs = - Enum.map(Map.from_struct(Macro.Env.__struct__()), fn {key, _value} -> - {:required, {:atom, key}, :dynamic} - end) - - {:ok, {:map, [struct_pair | pairs]}, context} + {:ok, dynamic(), context} end # __STACKTRACE__ def of_expr({:__STACKTRACE__, _meta, var_context}, _expected, _stack, context) when is_atom(var_context) do - file = {:tuple, 2, [{:atom, :file}, {:list, :integer}]} - line = {:tuple, 2, [{:atom, :line}, :integer]} - file_line = {:list, {:union, [file, line]}} - type = {:list, {:tuple, 4, [:atom, :atom, :integer, file_line]}} - {:ok, type, context} + {:ok, dynamic(), context} end # var def of_expr(var, _expected, _stack, context) when is_var(var) do - {:ok, get_var!(var, context), context} + {:ok, dynamic(), context} end # {left, right} @@ -119,117 +93,84 @@ defmodule Module.Types.Expr do end # {...} - def of_expr({:{}, _meta, exprs} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - - case map_reduce_ok(exprs, context, &of_expr(&1, :dynamic, stack, &2)) do - {:ok, types, context} -> {:ok, {:tuple, length(types), types}, context} + def of_expr({:{}, _meta, exprs}, _expected, stack, context) do + case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do + {:ok, _types, context} -> {:ok, tuple(), context} {:error, reason} -> {:error, reason} end end # left = right - def of_expr({:=, _meta, [left_expr, right_expr]} = expr, _expected, stack, context) do - # TODO: We might want to bring the expected type forward in case the type of this - # pattern is not useful. For example: 1 = _ = expr - - stack = push_expr_stack(expr, stack) - + def of_expr({:=, _meta, [left_expr, right_expr]}, _expected, stack, context) do with {:ok, left_type, context} <- Pattern.of_pattern(left_expr, stack, context), - {:ok, right_type, context} <- of_expr(right_expr, left_type, stack, context), - do: unify(right_type, left_type, stack, context) + {:ok, right_type, context} <- of_expr(right_expr, left_type, stack, context) do + {:ok, right_type, context} + end end # %{map | ...} - def of_expr({:%{}, _, [{:|, _, [map, args]}]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - map_type = {:map, [{:optional, :dynamic, :dynamic}]} - - with {:ok, map_type, context} <- of_expr(map, map_type, stack, context), - {:ok, {:map, arg_pairs}, context} <- Of.closed_map(args, stack, context, &of_expr/4), - dynamic_value_pairs = - Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end), - args_type = {:map, dynamic_value_pairs ++ [{:optional, :dynamic, :dynamic}]}, - {:ok, type, context} <- unify(map_type, args_type, stack, context) do - # Retrieve map type and overwrite with the new value types from the map update - case resolve_var(type, context) do - {:map, pairs} -> - updated_pairs = - Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs -> - List.keyreplace(pairs, key, 1, {:required, key, value}) - end) - - {:ok, {:map, updated_pairs}, context} - - _ -> - {:ok, :dynamic, context} - end + def of_expr({:%{}, _, [{:|, _, [map, args]}]}, _expected, stack, context) do + with {:ok, _, context} <- of_expr(map, stack, context), + {:ok, _, context} <- Of.closed_map(args, stack, context, &of_expr/3) do + {:ok, map(), context} end end # %Struct{map | ...} def of_expr( - {:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]} = expr, + {:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]}, _expected, stack, context ) do - stack = push_expr_stack(expr, stack) - map_type = {:map, [{:optional, :dynamic, :dynamic}]} - - with {:ok, struct, context} <- Of.struct(module, meta, context), - {:ok, update, context} <- of_expr(update, map_type, stack, context) do - unify(update, struct, stack, context) + with {:ok, _, context} <- Of.struct(module, meta, context), + {:ok, _, context} <- of_expr(update, stack, context) do + {:ok, map(), context} end end # %{...} - def of_expr({:%{}, _meta, args} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - Of.closed_map(args, stack, context, &of_expr/4) + def of_expr({:%{}, _meta, args}, _expected, stack, context) do + Of.closed_map(args, stack, context, &of_expr/3) end # %Struct{...} - def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - - with {:ok, struct, context} <- Of.struct(module, meta1, context), - {:ok, map, context} <- Of.open_map(args, stack, context, &of_expr/4) do - unify(map, struct, stack, context) + def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]}, _expected, stack, context) do + with {:ok, _, context} <- Of.struct(module, meta1, context), + {:ok, _, context} <- Of.open_map(args, stack, context, &of_expr/3) do + {:ok, map(), context} end end # () def of_expr({:__block__, _meta, []}, _expected, _stack, context) do - {:ok, {:atom, nil}, context} + {:ok, atom(nil), context} end # (expr; expr) def of_expr({:__block__, _meta, exprs}, expected, stack, context) do - expected_types = List.duplicate(:dynamic, length(exprs) - 1) ++ [expected] + {pre, [post]} = Enum.split(exprs, -1) result = - map_reduce_ok(Enum.zip(exprs, expected_types), context, fn {expr, expected}, context -> - of_expr(expr, expected, stack, context) + map_reduce_ok(pre, context, fn expr, context -> + of_expr(expr, stack, context) end) case result do - {:ok, expr_types, context} -> {:ok, Enum.at(expr_types, -1), context} + {:ok, _, context} -> of_expr(post, expected, stack, context) {:error, reason} -> {:error, reason} end end # cond do pat -> expr end - def of_expr({:cond, _meta, [[{:do, clauses}]]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - + def of_expr({:cond, _meta, [[{:do, clauses}]]}, _expected, stack, context) do {result, context} = reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context = acc -> - case of_expr(head, :dynamic, stack, context) do + case of_expr(head, stack, context) do {:ok, _, context} -> - with {:ok, _expr_type, context} <- of_expr(body, :dynamic, stack, context) do - {:ok, keep_warnings(acc, context)} + with {:ok, _expr_type, context} <- of_expr(body, stack, context) do + {:ok, context} end error -> @@ -239,26 +180,22 @@ defmodule Module.Types.Expr do end) case result do - :ok -> {:ok, :dynamic, context} + :ok -> {:ok, dynamic(), context} :error -> {:error, context} end end # case expr do pat -> expr end - def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - - with {:ok, _expr_type, context} <- of_expr(case_expr, :dynamic, stack, context), + def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, _expected, stack, context) do + with {:ok, _expr_type, context} <- of_expr(case_expr, stack, context), {:ok, context} <- of_clauses(clauses, stack, context), - do: {:ok, :dynamic, context} + do: {:ok, dynamic(), context} end # fn pat -> expr end - def of_expr({:fn, _meta, clauses} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - + def of_expr({:fn, _meta, clauses}, _expected, stack, context) do case of_clauses(clauses, stack, context) do - {:ok, context} -> {:ok, :dynamic, context} + {:ok, context} -> {:ok, dynamic(), context} {:error, reason} -> {:error, reason} end end @@ -267,47 +204,35 @@ defmodule Module.Types.Expr do @try_clause_blocks [:catch, :else, :after] # try do expr end - def of_expr({:try, _meta, [blocks]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - + def of_expr({:try, _meta, [blocks]}, _expected, stack, context) do {result, context} = reduce_ok(blocks, context, fn {:rescue, clauses}, context -> reduce_ok(clauses, context, fn - {:->, _, [[{:in, _, [var, _exceptions]}], body]}, context = acc -> - {_type, context} = new_pattern_var(var, context) + {:->, _, [[{:in, _, [_var, _exceptions]}], body]}, context -> + # TODO: make sure var is defined in context + of_expr_context(body, stack, context) - with {:ok, context} <- of_expr_context(body, :dynamic, stack, context) do - {:ok, keep_warnings(acc, context)} - end - - {:->, _, [[var], body]}, context = acc -> - {_type, context} = new_pattern_var(var, context) - - with {:ok, context} <- of_expr_context(body, :dynamic, stack, context) do - {:ok, keep_warnings(acc, context)} - end + {:->, _, [[_var], body]}, context -> + # TODO: make sure var is defined in context + of_expr_context(body, stack, context) end) - {block, body}, context = acc when block in @try_blocks -> - with {:ok, context} <- of_expr_context(body, :dynamic, stack, context) do - {:ok, keep_warnings(acc, context)} - end + {block, body}, context when block in @try_blocks -> + of_expr_context(body, stack, context) {block, clauses}, context when block in @try_clause_blocks -> of_clauses(clauses, stack, context) end) case result do - :ok -> {:ok, :dynamic, context} + :ok -> {:ok, dynamic(), context} :error -> {:error, context} end end # receive do pat -> expr end - def of_expr({:receive, _meta, [blocks]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - + def of_expr({:receive, _meta, [blocks]}, _expected, stack, context) do {result, context} = reduce_ok(blocks, context, fn {:do, {:__block__, _, []}}, context -> @@ -316,92 +241,75 @@ defmodule Module.Types.Expr do {:do, clauses}, context -> of_clauses(clauses, stack, context) - {:after, [{:->, _meta, [head, body]}]}, context = acc -> - with {:ok, _type, context} <- of_expr(head, :dynamic, stack, context), - {:ok, _type, context} <- of_expr(body, :dynamic, stack, context), - do: {:ok, keep_warnings(acc, context)} + {:after, [{:->, _meta, [head, body]}]}, context -> + with {:ok, _type, context} <- of_expr(head, stack, context), + {:ok, _type, context} <- of_expr(body, stack, context), + do: {:ok, context} end) case result do - :ok -> {:ok, :dynamic, context} + :ok -> {:ok, dynamic(), context} :error -> {:error, context} end end # for pat <- expr do expr end - def of_expr({:for, _meta, [_ | _] = args} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) + def of_expr({:for, _meta, [_ | _] = args}, _expected, stack, context) do {clauses, [[{:do, block} | opts]]} = Enum.split(args, -1) with {:ok, context} <- reduce_ok(clauses, context, &for_clause(&1, stack, &2)), {:ok, context} <- reduce_ok(opts, context, &for_option(&1, stack, &2)) do if Keyword.has_key?(opts, :reduce) do with {:ok, context} <- of_clauses(block, stack, context) do - {:ok, :dynamic, context} + {:ok, dynamic(), context} end else - with {:ok, _type, context} <- of_expr(block, :dynamic, stack, context) do - {:ok, :dynamic, context} + with {:ok, _type, context} <- of_expr(block, stack, context) do + {:ok, dynamic(), context} end end end end # with pat <- expr do expr end - def of_expr({:with, _meta, [_ | _] = clauses} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - + def of_expr({:with, _meta, [_ | _] = clauses}, _expected, stack, context) do case reduce_ok(clauses, context, &with_clause(&1, stack, &2)) do - {:ok, context} -> {:ok, :dynamic, context} + {:ok, context} -> {:ok, dynamic(), context} {:error, reason} -> {:error, reason} end end # fun.(args) - def of_expr({{:., _meta1, [fun]}, _meta2, args} = expr, _expected, stack, context) do - # TODO: Use expected type to infer intersection return type - stack = push_expr_stack(expr, stack) - - with {:ok, _fun_type, context} <- of_expr(fun, :dynamic, stack, context), + def of_expr({{:., _meta1, [fun]}, _meta2, args}, _expected, stack, context) do + with {:ok, _fun_type, context} <- of_expr(fun, stack, context), {:ok, _arg_types, context} <- - map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do - {:ok, :dynamic, context} + map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do + {:ok, dynamic(), context} end end # expr.key_or_fun - def of_expr({{:., _meta1, [expr1, key_or_fun]}, meta2, []} = expr2, _expected, stack, context) + def of_expr({{:., _meta1, [expr1, _key_or_fun]}, meta2, []}, _expected, stack, context) when not is_atom(expr1) do - stack = push_expr_stack(expr2, stack) - if Keyword.get(meta2, :no_parens, false) do - with {:ok, expr_type, context} <- of_expr(expr1, :dynamic, stack, context), - {value_var, context} = add_var(context), - pair_type = {:required, {:atom, key_or_fun}, value_var}, - optional_type = {:optional, :dynamic, :dynamic}, - map_field_type = {:map, [pair_type, optional_type]}, - {:ok, _map_type, context} <- unify(map_field_type, expr_type, stack, context), - do: {:ok, value_var, context} + with {:ok, _, context} <- of_expr(expr1, stack, context) do + {:ok, dynamic(), context} + end else - # TODO: Use expected type to infer intersection return type - with {:ok, expr_type, context} <- of_expr(expr1, :dynamic, stack, context), - {:ok, _map_type, context} <- unify(expr_type, :atom, stack, context), - do: {:ok, :dynamic, context} + with {:ok, _, context} <- of_expr(expr1, stack, context) do + {:ok, dynamic(), context} + end end end # expr.fun(arg) - def of_expr({{:., _meta1, [expr1, fun]}, meta2, args} = expr2, _expected, stack, context) do - # TODO: Use expected type to infer intersection return type - + def of_expr({{:., _meta1, [expr1, fun]}, meta2, args}, _expected, stack, context) do context = Of.remote(expr1, fun, length(args), meta2, context) - stack = push_expr_stack(expr2, stack) - with {:ok, _expr_type, context} <- of_expr(expr1, :dynamic, stack, context), - {:ok, _fun_type, context} <- of_expr(fun, :dynamic, stack, context), + with {:ok, _expr_type, context} <- of_expr(expr1, stack, context), {:ok, _arg_types, context} <- - map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do - {:ok, :dynamic, context} + map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do + {:ok, dynamic(), context} end end @@ -414,26 +322,21 @@ defmodule Module.Types.Expr do ) when is_atom(module) and is_atom(fun) do context = Of.remote(module, fun, arity, meta, context) - {:ok, :dynamic, context} + {:ok, dynamic(), context} end # &foo/1 # & &1 def of_expr({:&, _meta, _arg}, _expected, _stack, context) do - # TODO: Function type - {:ok, :dynamic, context} + {:ok, dynamic(), context} end # fun(arg) - def of_expr({fun, _meta, args} = expr, _expected, stack, context) + def of_expr({fun, _meta, args}, _expected, stack, context) when is_atom(fun) and is_list(args) do - # TODO: Use expected type to infer intersection return type - - stack = push_expr_stack(expr, stack) - - case map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do - {:ok, _arg_types, context} -> {:ok, :dynamic, context} - {:error, reason} -> {:error, reason} + with {:ok, _arg_types, context} <- + map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do + {:ok, dynamic(), context} end end @@ -441,14 +344,14 @@ defmodule Module.Types.Expr do {pattern, guards} = extract_head([left]) with {:ok, _pattern_type, context} <- Pattern.of_head([pattern], guards, stack, context), - {:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context), + {:ok, _expr_type, context} <- of_expr(expr, stack, context), do: {:ok, context} end defp for_clause({:<<>>, _, [{:<-, _, [pattern, expr]}]}, stack, context) do # TODO: the compiler guarantees pattern is a binary but we need to check expr is a binary with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context), - {:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context), + {:ok, _expr_type, context} <- of_expr(expr, stack, context), do: {:ok, context} end @@ -457,15 +360,15 @@ defmodule Module.Types.Expr do end defp for_clause(expr, stack, context) do - of_expr_context(expr, :dynamic, stack, context) + of_expr_context(expr, stack, context) end defp for_option({:into, expr}, stack, context) do - of_expr_context(expr, :dynamic, stack, context) + of_expr_context(expr, stack, context) end defp for_option({:reduce, expr}, stack, context) do - of_expr_context(expr, :dynamic, stack, context) + of_expr_context(expr, stack, context) end defp for_option({:uniq, _}, _stack, context) do @@ -476,7 +379,7 @@ defmodule Module.Types.Expr do {pattern, guards} = extract_head([left]) with {:ok, _pattern_type, context} <- Pattern.of_head([pattern], guards, stack, context), - {:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context), + {:ok, _expr_type, context} <- of_expr(expr, stack, context), do: {:ok, context} end @@ -485,11 +388,11 @@ defmodule Module.Types.Expr do end defp with_clause(expr, stack, context) do - of_expr_context(expr, :dynamic, stack, context) + of_expr_context(expr, stack, context) end defp with_option({:do, body}, stack, context) do - of_expr_context(body, :dynamic, stack, context) + of_expr_context(body, stack, context) end defp with_option({:else, clauses}, stack, context) do @@ -497,26 +400,22 @@ defmodule Module.Types.Expr do end defp of_clauses(clauses, stack, context) do - reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context = acc -> + reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context -> {patterns, guards} = extract_head(head) case Pattern.of_head(patterns, guards, stack, context) do {:ok, _, context} -> - with {:ok, _expr_type, context} <- of_expr(body, :dynamic, stack, context) do - {:ok, keep_warnings(acc, context)} + with {:ok, _expr_type, context} <- of_expr(body, stack, context) do + {:ok, context} end error -> # Skip the clause if it the head has an error - if meta[:generated], do: {:ok, acc}, else: error + if meta[:generated], do: {:ok, context}, else: error end end) end - defp keep_warnings(context, %{warnings: warnings}) do - %{context | warnings: warnings} - end - defp extract_head([{:when, _meta, args}]) do case Enum.split(args, -1) do {patterns, [guards]} -> {patterns, flatten_when(guards)} @@ -536,18 +435,10 @@ defmodule Module.Types.Expr do [other] end - defp of_expr_context(expr, expected, stack, context) do - case of_expr(expr, expected, stack, context) do + defp of_expr_context(expr, stack, context) do + case of_expr(expr, stack, context) do {:ok, _type, context} -> {:ok, context} {:error, reason} -> {:error, reason} end end - - defp new_pattern_var({:_, _meta, var_context}, context) when is_atom(var_context) do - {:dynamic, context} - end - - defp new_pattern_var(var, context) do - new_var(var, context) - end end diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index d4203ed38dc..d5ddf0b7b4f 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -142,32 +142,6 @@ defmodule Module.Types.Helpers do defp do_flat_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(Enum.concat(list)), acc} - @doc """ - Given a list of `[{:ok, term()} | {:error, term()}]` it returns a list of - errors `{:error, [term()]}` in case of at least one error or `{:ok, [term()]}` - if there are no errors. - """ - def oks_or_errors(list) do - case Enum.split_with(list, &match?({:ok, _}, &1)) do - {oks, []} -> {:ok, Enum.map(oks, fn {:ok, ok} -> ok end)} - {_oks, errors} -> {:error, Enum.map(errors, fn {:error, error} -> error end)} - end - end - - @doc """ - Combines a list of guard expressions `when x when y when z` to an expression - combined with `or`, `x or y or z`. - """ - # TODO: Remove this and let multiple when be treated as multiple clauses, - # meaning they will be intersection types - def guards_to_or([]) do - [] - end - - def guards_to_or(guards) do - Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end) - end - @doc """ Like `Enum.zip/1` but will zip multiple lists together instead of only two. """ diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 7c49abb64d1..12af815032e 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -3,13 +3,11 @@ defmodule Module.Types.Of do # Generic AST and Enum helpers go to Module.Types.Helpers. @moduledoc false - @prefix quote(do: ...) - @suffix quote(do: ...) + # @prefix quote(do: ...) + # @suffix quote(do: ...) alias Module.ParallelChecker - - import Module.Types.Helpers - import Module.Types.Unify + import Module.Types.{Helpers, Descr} # There are important assumptions on how we work with maps. # @@ -31,7 +29,7 @@ defmodule Module.Types.Of do """ def open_map(args, stack, context, of_fun) do with {:ok, _pairs, context} <- map_pairs(args, stack, context, of_fun) do - {:ok, :dynamic, context} + {:ok, map(), context} end end @@ -40,14 +38,14 @@ defmodule Module.Types.Of do """ def closed_map(args, stack, context, of_fun) do with {:ok, _pairs, context} <- map_pairs(args, stack, context, of_fun) do - {:ok, :dynamic, context} + {:ok, map(), context} end end defp map_pairs(pairs, stack, context, of_fun) do map_reduce_ok(pairs, context, fn {key, value}, context -> - with {:ok, key_type, context} <- of_fun.(key, :dynamic, stack, context), - {:ok, value_type, context} <- of_fun.(value, :dynamic, stack, context), + with {:ok, key_type, context} <- of_fun.(key, stack, context), + {:ok, value_type, context} <- of_fun.(value, stack, context), do: {:ok, {key_type, value_type}, context} end) end @@ -57,7 +55,7 @@ defmodule Module.Types.Of do """ def struct(struct, meta, context) do context = remote(struct, :__struct__, 0, meta, context) - {:ok, :dynamic, context} + {:ok, map(), context} end ## Binary @@ -68,41 +66,43 @@ defmodule Module.Types.Of do In the stack, we add nodes such as <>, <<..., expr>>, etc, based on the position of the expression within the binary. """ - def binary([], _stack, context, _of_fun) do + def binary([], _type, _stack, context, _of_fun) do {:ok, context} end - def binary([head], stack, context, of_fun) do - head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack) - binary_segment(head, head_stack, context, of_fun) + def binary([head], type, stack, context, of_fun) do + # stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack) + binary_segment(head, type, stack, context, of_fun) end - def binary([head | tail], stack, context, of_fun) do - head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack) + def binary([head | tail], type, stack, context, of_fun) do + # stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack) - case binary_segment(head, head_stack, context, of_fun) do - {:ok, context} -> binary_many(tail, stack, context, of_fun) + case binary_segment(head, type, stack, context, of_fun) do + {:ok, context} -> binary_many(tail, type, stack, context, of_fun) {:error, reason} -> {:error, reason} end end - defp binary_many([last], stack, context, of_fun) do - last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack) - binary_segment(last, last_stack, context, of_fun) + defp binary_many([last], type, stack, context, of_fun) do + # stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack) + binary_segment(last, type, stack, context, of_fun) end - defp binary_many([head | tail], stack, context, of_fun) do - head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack) + defp binary_many([head | tail], type, stack, context, of_fun) do + # stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack) - case binary_segment(head, head_stack, context, of_fun) do - {:ok, context} -> binary_many(tail, stack, context, of_fun) + case binary_segment(head, type, stack, context, of_fun) do + {:ok, context} -> binary_many(tail, type, stack, context, of_fun) {:error, reason} -> {:error, reason} end end - defp binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, of_fun) do - expected_type = - collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer + defp binary_segment({:"::", _meta, [expr, specifiers]}, type, stack, context, of_fun) do + # TODO: handle size in specifiers + # TODO: unpack specifiers once + _expected_type = + collect_binary_specifier(specifiers, &binary_type(type, &1)) || :integer utf? = collect_binary_specifier(specifiers, &utf_type?/1) float? = collect_binary_specifier(specifiers, &float_type?/1) @@ -110,15 +110,14 @@ defmodule Module.Types.Of do # Special case utf and float specifiers because they can be two types as literals # but only a specific type as a variable in a pattern cond do - stack.context == :pattern and utf? and is_binary(expr) -> + type == :pattern and utf? and is_binary(expr) -> {:ok, context} - stack.context == :pattern and float? and is_integer(expr) -> + type == :pattern and float? and is_integer(expr) -> {:ok, context} true -> - with {:ok, type, context} <- of_fun.(expr, expected_type, stack, context), - {:ok, _type, context} <- unify(type, expected_type, stack, context), + with {:ok, _type, context} <- of_fun.(expr, stack, context), do: {:ok, context} end end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 412b9bf37f4..b96de05b061 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -2,7 +2,7 @@ defmodule Module.Types.Pattern do @moduledoc false alias Module.Types.Of - import Module.Types.{Helpers, Unify} + import Module.Types.{Helpers, Descr} @doc """ Handles patterns and guards at once. @@ -11,7 +11,7 @@ defmodule Module.Types.Pattern do with {:ok, types, context} <- map_reduce_ok(patterns, context, &of_pattern(&1, stack, &2)), # TODO: Check that of_guard/4 returns boolean() | :fail - {:ok, _, context} <- of_guard(guards_to_or(guards), :dynamic, stack, context), + {:ok, _, context} <- of_guards(guards, term(), stack, context), do: {:ok, types, context} end @@ -19,61 +19,55 @@ defmodule Module.Types.Pattern do Return the type and typing context of a pattern expression or an error in case of a typing conflict. """ - def of_pattern(pattern, %{context: stack_context} = stack, context) - when stack_context != :pattern do - of_pattern(pattern, %{stack | context: :pattern}, context) - end - # _ def of_pattern({:_, _meta, atom}, _stack, context) when is_atom(atom) do - {:ok, :dynamic, context} + {:ok, dynamic(), context} end # ^var - def of_pattern({:^, _meta, [var]}, _stack, context) do - {:ok, get_var!(var, context), context} + def of_pattern({:^, _meta, [_var]}, _stack, context) do + {:ok, dynamic(), context} end # var def of_pattern(var, _stack, context) when is_var(var) do - {type, context} = new_var(var, context) - {:ok, type, context} + {:ok, dynamic(), context} end # left = right - def of_pattern({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do - stack = push_expr_stack(expr, stack) - - with {:ok, left_type, context} <- of_pattern(left_expr, stack, context), - {:ok, right_type, context} <- of_pattern(right_expr, stack, context), - do: unify(left_type, right_type, stack, context) + def of_pattern({:=, _meta, [left_expr, right_expr]}, stack, context) do + with {:ok, _, context} <- of_pattern(left_expr, stack, context), + {:ok, _, context} <- of_pattern(right_expr, stack, context), + do: {:ok, dynamic(), context} end # %_{...} def of_pattern( - {:%, _meta1, [{:_, _meta2, var_context}, {:%{}, _meta3, args}]} = expr, + {:%, _meta1, [{:_, _meta2, var_context}, {:%{}, _meta3, args}]}, stack, context ) when is_atom(var_context) do - stack = push_expr_stack(expr, stack) - expected_fun = fn arg, _expected, stack, context -> of_pattern(arg, stack, context) end - - with {:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, expected_fun) do - {:ok, {:map, [{:required, {:atom, :__struct__}, :atom} | pairs]}, context} + with {:ok, _, context} <- Of.open_map(args, stack, context, &of_pattern/3) do + {:ok, map(), context} end end # %var{...} and %^var{...} - def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context) + def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]}, stack, context) when not is_atom(var) do - stack = push_expr_stack(expr, stack) - expected_fun = fn arg, _expected, stack, context -> of_pattern(arg, stack, context) end + # TODO: validate var is an atom + with {:ok, _, context} = of_pattern(var, stack, context), + {:ok, _, context} <- Of.open_map(args, stack, context, &of_pattern/3) do + {:ok, map(), context} + end + end - with {:ok, var_type, context} = of_pattern(var, stack, context), - {:ok, _, context} <- unify(var_type, :atom, stack, context), - {:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, expected_fun) do - {:ok, {:map, [{:required, {:atom, :__struct__}, var_type} | pairs]}, context} + # <<...>>> + def of_pattern({:<<>>, _meta, args}, stack, context) do + case Of.binary(args, :pattern, stack, context, &of_pattern/3) do + {:ok, context} -> {:ok, binary(), context} + {:error, reason} -> {:error, reason} end end @@ -85,64 +79,42 @@ defmodule Module.Types.Pattern do Refines the type variables in the typing context using type check guards such as `is_integer/1`. """ - def of_guard(_expr, _expected, _stack, context) do - {:ok, :dynamic, context} + def of_guards(_expr, _expected, _stack, context) do + {:ok, dynamic(), context} end ## Shared # :atom defp of_shared(atom, _stack, context, _fun) when is_atom(atom) do - {:ok, {:atom, atom}, context} + {:ok, atom(atom), context} end # 12 defp of_shared(literal, _stack, context, _fun) when is_integer(literal) do - {:ok, :integer, context} + {:ok, integer(), context} end # 1.2 defp of_shared(literal, _stack, context, _fun) when is_float(literal) do - {:ok, :float, context} + {:ok, float(), context} end # "..." defp of_shared(literal, _stack, context, _fun) when is_binary(literal) do - {:ok, :binary, context} - end - - # <<...>>> - defp of_shared({:<<>>, _meta, args}, stack, context, fun) do - expected_fun = fn arg, _expected, stack, context -> fun.(arg, stack, context) end - - case Of.binary(args, stack, context, expected_fun) do - {:ok, context} -> {:ok, :binary, context} - {:error, reason} -> {:error, reason} - end + {:ok, binary(), context} end # left | [] - defp of_shared({:|, _meta, [left_expr, []]} = expr, stack, context, fun) do - stack = push_expr_stack(expr, stack) + defp of_shared({:|, _meta, [left_expr, []]}, stack, context, fun) do fun.(left_expr, stack, context) end # left | right - defp of_shared({:|, _meta, [left_expr, right_expr]} = expr, stack, context, fun) do - stack = push_expr_stack(expr, stack) - + defp of_shared({:|, _meta, [left_expr, right_expr]}, stack, context, fun) do case fun.(left_expr, stack, context) do - {:ok, left, context} -> - case fun.(right_expr, stack, context) do - {:ok, {:list, right}, context} -> - {:ok, to_union([left, right], context), context} - - {:ok, right, context} -> - {:ok, to_union([left, right], context), context} - - {:error, reason} -> - {:error, reason} - end + {:ok, _, context} -> + fun.(right_expr, stack, context) {:error, reason} -> {:error, reason} @@ -151,43 +123,30 @@ defmodule Module.Types.Pattern do # [] defp of_shared([], _stack, context, _fun) do - {:ok, {:list, :dynamic}, context} + {:ok, empty_list(), context} end # [expr, ...] defp of_shared(exprs, stack, context, fun) when is_list(exprs) do - stack = push_expr_stack(exprs, stack) - case map_reduce_ok(exprs, context, &fun.(&1, stack, &2)) do - {:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context} + {:ok, _types, context} -> {:ok, non_empty_list(), context} {:error, reason} -> {:error, reason} end end # left ++ right defp of_shared( - {{:., _meta1, [:erlang, :++]}, _meta2, [left_expr, right_expr]} = expr, + {{:., _meta1, [:erlang, :++]}, _meta2, [left_expr, right_expr]}, stack, context, fun ) do - stack = push_expr_stack(expr, stack) - - case fun.(left_expr, stack, context) do - {:ok, {:list, left}, context} -> - case fun.(right_expr, stack, context) do - {:ok, {:list, right}, context} -> - {:ok, {:list, to_union([left, right], context)}, context} - - {:ok, right, context} -> - {:ok, {:list, to_union([left, right], context)}, context} - - {:error, reason} -> - {:error, reason} - end - - {:error, reason} -> - {:error, reason} + # The left side is always a list + with {:ok, _, context} <- fun.(left_expr, stack, context), + {:ok, _, context} <- fun.(right_expr, stack, context) do + # TODO: Both lists can be empty, so this may be an empty list, + # so we return dynamic() for now. + {:ok, dynamic(), context} end end @@ -197,31 +156,24 @@ defmodule Module.Types.Pattern do end # {...} - defp of_shared({:{}, _meta, exprs} = expr, stack, context, fun) do - stack = push_expr_stack(expr, stack) - + defp of_shared({:{}, _meta, exprs}, stack, context, fun) do case map_reduce_ok(exprs, context, &fun.(&1, stack, &2)) do - {:ok, types, context} -> {:ok, {:tuple, length(types), types}, context} + {:ok, _, context} -> {:ok, tuple(), context} {:error, reason} -> {:error, reason} end end # %{...} - defp of_shared({:%{}, _meta, args} = expr, stack, context, fun) do - stack = push_expr_stack(expr, stack) - expected_fun = fn arg, _expected, stack, context -> fun.(arg, stack, context) end - Of.open_map(args, stack, context, expected_fun) + defp of_shared({:%{}, _meta, args}, stack, context, fun) do + Of.open_map(args, stack, context, fun) end # %Struct{...} - defp of_shared({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context, fun) + defp of_shared({:%, meta1, [module, {:%{}, _meta2, args}]}, stack, context, fun) when is_atom(module) do - stack = push_expr_stack(expr, stack) - expected_fun = fn arg, _expected, stack, context -> fun.(arg, stack, context) end - - with {:ok, struct, context} <- Of.struct(module, meta1, context), - {:ok, map, context} <- Of.open_map(args, stack, context, expected_fun) do - unify(map, struct, stack, context) + with {:ok, _, context} <- Of.struct(module, meta1, context), + {:ok, _, context} <- Of.open_map(args, stack, context, fun) do + {:ok, map(), context} end end end diff --git a/lib/elixir/lib/module/types/unify.ex b/lib/elixir/lib/module/types/unify.ex deleted file mode 100644 index 3cb658d8f2a..00000000000 --- a/lib/elixir/lib/module/types/unify.ex +++ /dev/null @@ -1,992 +0,0 @@ -defmodule Module.Types.Unify do - @moduledoc false - - import Module.Types.Helpers - - # Those are the simple types known to the system: - # - # :dynamic - # {:var, var} - # {:atom, atom} < :atom - # :integer - # :float - # :binary - # :pid - # :port - # :reference - # - # Those are the composite types: - # - # {:list, type} - # {:tuple, size, [type]} < :tuple - # {:union, [type]} - # {:map, [{:required | :optional, key_type, value_type}]} - # {:fun, [{params, return}]} - # - # Once new types are added, they should be considered in: - # - # * unify (all) - # * format_type (all) - # * subtype? (subtypes only) - # * recursive_type? (composite only) - # * collect_var_indexes (composite only) - # * lift_types (composite only) - # * flatten_union (composite only) - # * walk (composite only) - # - - @doc """ - Unifies two types and returns the unified type and an updated typing context - or an error in case of a typing conflict. - """ - def unify(same, same, _stack, context) do - {:ok, same, context} - end - - def unify({:var, var}, type, stack, context) do - unify_var(var, type, stack, context, _var_source = true) - end - - def unify(type, {:var, var}, stack, context) do - unify_var(var, type, stack, context, _var_source = false) - end - - def unify({:tuple, n, sources}, {:tuple, n, targets}, stack, context) do - result = - map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context -> - unify(source, target, stack, context) - end) - - case result do - {:ok, types, context} -> {:ok, {:tuple, n, types}, context} - {:error, reason} -> {:error, reason} - end - end - - def unify({:list, source}, {:list, target}, stack, context) do - case unify(source, target, stack, context) do - {:ok, type, context} -> {:ok, {:list, type}, context} - {:error, reason} -> {:error, reason} - end - end - - def unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do - unify_maps(source_pairs, target_pairs, stack, context) - end - - def unify(source, :dynamic, _stack, context) do - {:ok, source, context} - end - - def unify(:dynamic, target, _stack, context) do - {:ok, target, context} - end - - def unify({:union, types}, target, stack, context) do - unify_result = - map_reduce_ok(types, context, fn type, context -> - unify(type, target, stack, context) - end) - - case unify_result do - {:ok, types, context} -> {:ok, to_union(types, context), context} - {:error, context} -> {:error, context} - end - end - - def unify(source, target, stack, context) do - cond do - # TODO: This condition exists to handle unions with unbound vars. - match?({:union, _}, target) and has_unbound_var?(target, context) -> - {:ok, source, context} - - subtype?(source, target, context) -> - {:ok, source, context} - - true -> - error(:unable_unify, {source, target, stack}, context) - end - end - - def unify_var(var, :dynamic, _stack, context, _var_source?) do - {:ok, {:var, var}, context} - end - - def unify_var(var, type, stack, context, var_source?) do - case context.types do - %{^var => :unbound} -> - context = refine_var!(var, type, stack, context) - stack = push_unify_stack(var, stack) - - if recursive_type?(type, [], context) do - if var_source? do - error(:unable_unify, {{:var, var}, type, stack}, context) - else - error(:unable_unify, {type, {:var, var}, stack}, context) - end - else - {:ok, {:var, var}, context} - end - - %{^var => {:var, _} = var_type} -> - # Do not recursively traverse type vars for now - # to avoid pathological cases related to performance. - {:ok, var_type, context} - - %{^var => var_type} -> - # Only add trace if the variable wasn't already "expanded" - context = - if variable_expanded?(var, stack, context) do - context - else - trace_var(var, type, stack, context) - end - - stack = push_unify_stack(var, stack) - - unify_result = - if var_source? do - unify(var_type, type, stack, context) - else - unify(type, var_type, stack, context) - end - - case unify_result do - {:ok, {:var, ^var}, context} -> - {:ok, {:var, var}, context} - - {:ok, res_type, context} -> - context = refine_var!(var, res_type, stack, context) - {:ok, {:var, var}, context} - - {:error, reason} -> - {:error, reason} - end - end - end - - # * All required keys on each side need to match to the other side. - # * All optional keys on each side that do not match must be discarded. - - def unify_maps(source_pairs, target_pairs, stack, context) do - {source_required, source_optional} = split_pairs(source_pairs) - {target_required, target_optional} = split_pairs(target_pairs) - - with {:ok, source_required_pairs, context} <- - unify_source_required(source_required, target_pairs, stack, context), - {:ok, target_required_pairs, context} <- - unify_target_required(target_required, source_pairs, stack, context), - {:ok, source_optional_pairs, context} <- - unify_source_optional(source_optional, target_optional, stack, context), - {:ok, target_optional_pairs, context} <- - unify_target_optional(target_optional, source_optional, stack, context) do - # Remove duplicate pairs from matching in both left and right directions - pairs = - Enum.uniq( - source_required_pairs ++ - target_required_pairs ++ - source_optional_pairs ++ - target_optional_pairs - ) - - {:ok, {:map, pairs}, context} - else - {:error, :unify} -> - error(:unable_unify, {{:map, source_pairs}, {:map, target_pairs}, stack}, context) - - {:error, context} -> - {:error, context} - end - end - - def unify_source_required(source_required, target_pairs, stack, context) do - map_reduce_ok(source_required, context, fn {source_key, source_value}, context -> - Enum.find_value(target_pairs, fn {target_kind, target_key, target_value} -> - with {:ok, key, context} <- unify(source_key, target_key, stack, context) do - case unify(source_value, target_value, stack, context) do - {:ok, value, context} -> - {:ok, {:required, key, value}, context} - - {:error, _reason} -> - source_map = {:map, [{:required, source_key, source_value}]} - target_map = {:map, [{target_kind, target_key, target_value}]} - error(:unable_unify, {source_map, target_map, stack}, context) - end - else - {:error, _reason} -> nil - end - end) || {:error, :unify} - end) - end - - def unify_target_required(target_required, source_pairs, stack, context) do - map_reduce_ok(target_required, context, fn {target_key, target_value}, context -> - Enum.find_value(source_pairs, fn {source_kind, source_key, source_value} -> - with {:ok, key, context} <- unify(source_key, target_key, stack, context) do - case unify(source_value, target_value, stack, context) do - {:ok, value, context} -> - {:ok, {:required, key, value}, context} - - {:error, _reason} -> - source_map = {:map, [{source_kind, source_key, source_value}]} - target_map = {:map, [{:required, target_key, target_value}]} - error(:unable_unify, {source_map, target_map, stack}, context) - end - else - {:error, _reason} -> nil - end - end) || {:error, :unify} - end) - end - - def unify_source_optional(source_optional, target_optional, stack, context) do - flat_map_reduce_ok(source_optional, context, fn {source_key, source_value}, context -> - Enum.find_value(target_optional, fn {target_key, target_value} -> - with {:ok, key, context} <- unify(source_key, target_key, stack, context) do - case unify(source_value, target_value, stack, context) do - {:ok, value, context} -> - {:ok, [{:optional, key, value}], context} - - {:error, _reason} -> - source_map = {:map, [{:optional, source_key, source_value}]} - target_map = {:map, [{:optional, target_key, target_value}]} - error(:unable_unify, {source_map, target_map, stack}, context) - end - else - _ -> nil - end - end) || {:ok, [], context} - end) - end - - def unify_target_optional(target_optional, source_optional, stack, context) do - flat_map_reduce_ok(target_optional, context, fn {target_key, target_value}, context -> - Enum.find_value(source_optional, fn {source_key, source_value} -> - with {:ok, key, context} <- unify(source_key, target_key, stack, context) do - case unify(source_value, target_value, stack, context) do - {:ok, value, context} -> - {:ok, [{:optional, key, value}], context} - - {:error, _reason} -> - source_map = {:map, [{:optional, source_key, source_value}]} - target_map = {:map, [{:optional, target_key, target_value}]} - error(:unable_unify, {source_map, target_map, stack}, context) - end - else - _ -> nil - end - end) || {:ok, [], context} - end) - end - - defp split_pairs(pairs) do - {required, optional} = - Enum.split_with(pairs, fn {kind, _key, _value} -> kind == :required end) - - required = Enum.map(required, fn {_kind, key, value} -> {key, value} end) - optional = Enum.map(optional, fn {_kind, key, value} -> {key, value} end) - {required, optional} - end - - def error(type, reason, context), do: {:error, {type, reason, context}} - - @doc """ - Push expression to stack. - - The expression stack is used to give the context where a type variable - was refined when show a type conflict error. - """ - def push_expr_stack(expr, stack) do - %{stack | last_expr: expr} - end - - @doc """ - Gets a variable. - """ - def get_var!(var, context) do - Map.fetch!(context.vars, var_name(var)) - end - - @doc """ - Adds a variable to the typing context and returns its type variable. - If the variable has already been added, return the existing type variable. - """ - def new_var(var, context) do - var_name = var_name(var) - - case context.vars do - %{^var_name => type} -> - {type, context} - - %{} -> - type = {:var, context.counter} - vars = Map.put(context.vars, var_name, type) - types_to_vars = Map.put(context.types_to_vars, context.counter, var) - types = Map.put(context.types, context.counter, :unbound) - traces = Map.put(context.traces, context.counter, []) - - context = %{ - context - | vars: vars, - types_to_vars: types_to_vars, - types: types, - traces: traces, - counter: context.counter + 1 - } - - {type, context} - end - end - - @doc """ - Adds an internal variable to the typing context and returns its type variable. - An internal variable is used to help unify complex expressions, - it does not belong to a specific AST expression. - """ - def add_var(context) do - type = {:var, context.counter} - types = Map.put(context.types, context.counter, :unbound) - traces = Map.put(context.traces, context.counter, []) - - context = %{ - context - | types: types, - traces: traces, - counter: context.counter + 1 - } - - {type, context} - end - - @doc """ - Maybe resolves a variable. - """ - def resolve_var({:var, var}, context) do - case context.types do - %{^var => :unbound} -> {:var, var} - %{^var => type} -> resolve_var(type, context) - end - end - - def resolve_var(other, _context), do: other - - # Check unify stack to see if variable was already expanded - defp variable_expanded?(var, stack, context) do - Enum.any?(stack.unify_stack, &variable_same?(var, &1, context)) - end - - defp variable_same?(left, right, context) do - case context.types do - %{^left => {:var, new_left}} -> - variable_same?(new_left, right, context) - - %{^right => {:var, new_right}} -> - variable_same?(left, new_right, context) - - %{} -> - false - end - end - - defp push_unify_stack(var, stack) do - %{stack | unify_stack: [var | stack.unify_stack]} - end - - @doc """ - Restores the variable information from the old context into new context. - """ - def restore_var!(var, new_context, old_context) do - %{^var => type} = old_context.types - %{^var => trace} = old_context.traces - types = Map.put(new_context.types, var, type) - traces = Map.put(new_context.traces, var, trace) - %{new_context | types: types, traces: traces} - end - - @doc """ - Set the type for a variable and add trace. - """ - def refine_var!(var, type, stack, context) do - types = Map.put(context.types, var, type) - context = %{context | types: types} - trace_var(var, type, stack, context) - end - - @doc """ - Remove type variable and all its traces. - """ - def remove_var(var, context) do - types = Map.delete(context.types, var) - traces = Map.delete(context.traces, var) - %{context | types: types, traces: traces} - end - - defp trace_var(var, type, %{trace: true, last_expr: last_expr} = _stack, context) do - line = get_meta(last_expr)[:line] - trace = {type, last_expr, {context.file, line}} - traces = Map.update!(context.traces, var, &[trace | &1]) - %{context | traces: traces} - end - - defp trace_var(_var, _type, %{trace: false} = _stack, context) do - context - end - - # Check if a variable is recursive and incompatible with itself - # Bad: `{var} = var` - # Good: `x = y; y = z; z = x` - defp recursive_type?({:var, var} = parent, parents, context) do - case context.types do - %{^var => :unbound} -> - false - - %{^var => type} -> - if type in parents do - not Enum.all?(parents, &match?({:var, _}, &1)) - else - recursive_type?(type, [parent | parents], context) - end - end - end - - defp recursive_type?({:list, type} = parent, parents, context) do - recursive_type?(type, [parent | parents], context) - end - - defp recursive_type?({:union, types} = parent, parents, context) do - Enum.any?(types, &recursive_type?(&1, [parent | parents], context)) - end - - defp recursive_type?({:tuple, _, types} = parent, parents, context) do - Enum.any?(types, &recursive_type?(&1, [parent | parents], context)) - end - - defp recursive_type?({:map, pairs} = parent, parents, context) do - Enum.any?(pairs, fn {_kind, key, value} -> - recursive_type?(key, [parent | parents], context) or - recursive_type?(value, [parent | parents], context) - end) - end - - defp recursive_type?({:fun, clauses}, parents, context) do - Enum.any?(clauses, fn {args, return} -> - Enum.any?([return | args], &recursive_type?(&1, [clauses | parents], context)) - end) - end - - defp recursive_type?(_other, _parents, _context) do - false - end - - @doc """ - Collects all type vars recursively. - """ - def collect_var_indexes(type, context, acc \\ %{}) do - {_type, indexes} = - walk(type, acc, fn - {:var, var}, acc -> - case acc do - %{^var => _} -> - {{:var, var}, acc} - - %{} -> - case context.types do - %{^var => :unbound} -> - {{:var, var}, Map.put(acc, var, true)} - - %{^var => type} -> - {{:var, var}, collect_var_indexes(type, context, Map.put(acc, var, true))} - end - end - - other, acc -> - {other, acc} - end) - - indexes - end - - @doc """ - Checks if the type has a type var. - """ - def has_unbound_var?(type, context) do - walk(type, :ok, fn - {:var, var}, acc -> - case context.types do - %{^var => :unbound} -> - throw(:has_unbound_var?) - - %{^var => type} -> - has_unbound_var?(type, context) - {{:var, var}, acc} - end - - other, acc -> - {other, acc} - end) - - false - catch - :throw, :has_unbound_var? -> true - end - - @doc """ - Returns `true` if it is a singleton type. - - Only atoms are singleton types. Unbound vars are not - considered singleton types. - """ - def singleton?({:var, var}, context) do - case context.types do - %{^var => :unbound} -> false - %{^var => type} -> singleton?(type, context) - end - end - - def singleton?({:atom, _}, _context), do: true - def singleton?(_type, _context), do: false - - @doc """ - Checks if the first argument is a subtype of the second argument. - - This function assumes that: - - * unbound variables are not subtype of anything - - * dynamic is not considered a subtype of all other types but the top type. - This allows this function can be used for ordering, in other cases, you - may need to check for both sides - - """ - def subtype?(type, type, _context), do: true - - def subtype?({:var, var}, other, context) do - case context.types do - %{^var => :unbound} -> false - %{^var => type} -> subtype?(type, other, context) - end - end - - def subtype?(other, {:var, var}, context) do - case context.types do - %{^var => :unbound} -> false - %{^var => type} -> subtype?(other, type, context) - end - end - - def subtype?(_, :dynamic, _context), do: true - def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true - - # Composite - - def subtype?({:tuple, _, _}, :tuple, _context), do: true - - def subtype?({:tuple, n, left_types}, {:tuple, n, right_types}, context) do - left_types - |> Enum.zip(right_types) - |> Enum.all?(fn {left, right} -> subtype?(left, right, context) end) - end - - def subtype?({:map, left_pairs}, {:map, right_pairs}, context) do - Enum.all?(left_pairs, fn - {:required, left_key, left_value} -> - Enum.any?(right_pairs, fn {_, right_key, right_value} -> - subtype?(left_key, right_key, context) and subtype?(left_value, right_value, context) - end) - - {:optional, _, _} -> - true - end) - end - - def subtype?({:list, left}, {:list, right}, context) do - subtype?(left, right, context) - end - - def subtype?({:union, left_types}, {:union, _} = right_union, context) do - Enum.all?(left_types, &subtype?(&1, right_union, context)) - end - - def subtype?(left, {:union, right_types}, context) do - Enum.any?(right_types, &subtype?(left, &1, context)) - end - - def subtype?({:union, left_types}, right, context) do - Enum.all?(left_types, &subtype?(&1, right, context)) - end - - def subtype?(_left, _right, _context), do: false - - @doc """ - Returns a "simplified" union using `subtype?/3` to remove redundant types. - - Due to limitations in `subtype?/3` some overlapping types may still be - included. For example unions with overlapping non-concrete types such as - `{boolean()} | {atom()}` will not be merged or types with variables that - are distinct but equivalent such as `a | b when a ~ b`. - """ - def to_union([type], _context), do: type - - def to_union(types, context) when types != [] do - case unique_super_types(unnest_unions(types), context) do - [type] -> type - types -> {:union, types} - end - end - - defp unnest_unions(types) do - Enum.flat_map(types, fn - {:union, types} -> unnest_unions(types) - type -> [type] - end) - end - - # Filter subtypes - # - # `boolean() | atom()` => `atom()` - # `:foo | atom()` => `atom()` - # - # Does not merge `true | false` => `boolean()` - defp unique_super_types([type | types], context) do - types = Enum.reject(types, &subtype?(&1, type, context)) - - if Enum.any?(types, &subtype?(type, &1, context)) do - unique_super_types(types, context) - else - [type | unique_super_types(types, context)] - end - end - - defp unique_super_types([], _context) do - [] - end - - ## Type lifting - - @doc """ - Lifts type variables to their inferred types from the context. - """ - def lift_types(types, %{lifted_types: _} = context) do - Enum.map_reduce(types, context, &lift_type/2) - end - - def lift_types(types, context) do - context = %{ - types: context.types, - lifted_types: %{}, - lifted_counter: 0 - } - - Enum.map_reduce(types, context, &lift_type/2) - end - - # Lift type variable to its inferred (hopefully concrete) types from the context - defp lift_type({:var, var}, context) do - case context.lifted_types do - %{^var => lifted_var} -> - {{:var, lifted_var}, context} - - %{} -> - case context.types do - %{^var => :unbound} -> - new_lifted_var(var, context) - - %{^var => type} -> - if recursive_type?(type, [], context) do - new_lifted_var(var, context) - else - # Remove visited types to avoid infinite loops - # then restore after we are done recursing on vars - types = context.types - context = put_in(context.types[var], :unbound) - {type, context} = lift_type(type, context) - {type, %{context | types: types}} - end - - %{} -> - new_lifted_var(var, context) - end - end - end - - defp lift_type({:union, types}, context) do - {types, context} = Enum.map_reduce(types, context, &lift_type/2) - {{:union, types}, context} - end - - defp lift_type({:tuple, n, types}, context) do - {types, context} = Enum.map_reduce(types, context, &lift_type/2) - {{:tuple, n, types}, context} - end - - defp lift_type({:map, pairs}, context) do - {pairs, context} = - Enum.map_reduce(pairs, context, fn {kind, key, value}, context -> - {key, context} = lift_type(key, context) - {value, context} = lift_type(value, context) - {{kind, key, value}, context} - end) - - {{:map, pairs}, context} - end - - defp lift_type({:list, type}, context) do - {type, context} = lift_type(type, context) - {{:list, type}, context} - end - - defp lift_type({:fun, clauses}, context) do - clauses = - Enum.map_reduce(clauses, context, fn {args, return}, context -> - {[return | args], context} = Enum.map_reduce([return | args], context, &lift_type/2) - {{args, return}, context} - end) - - {{:fun, clauses}, context} - end - - defp lift_type(other, context) do - {other, context} - end - - defp new_lifted_var(original_var, context) do - types = Map.put(context.lifted_types, original_var, context.lifted_counter) - counter = context.lifted_counter + 1 - - type = {:var, context.lifted_counter} - context = %{context | lifted_types: types, lifted_counter: counter} - {type, context} - end - - # TODO: Figure out function expansion - - @doc """ - Expand unions so that all unions are at the top level. - - {integer() | float()} => {integer()} | {float()} - """ - def flatten_union({:union, types}, context) do - Enum.flat_map(types, &flatten_union(&1, context)) - end - - def flatten_union(type, context) do - List.wrap(do_flatten_union(type, context)) - end - - def do_flatten_union({:tuple, num, types}, context) do - flatten_union_tuple(types, num, context, []) - end - - def do_flatten_union({:list, type}, context) do - case do_flatten_union(type, context) do - {:union, union_types} -> Enum.map(union_types, &{:list, &1}) - _type -> [{:list, type}] - end - end - - def do_flatten_union({:map, pairs}, context) do - flatten_union_map(pairs, context, []) - end - - def do_flatten_union({:var, var}, context) do - if looping_var?(var, context, []) do - {:var, var} - else - case context.types do - %{^var => :unbound} -> {:var, var} - %{^var => {:union, types}} -> Enum.map(types, &do_flatten_union(&1, context)) - %{^var => type} -> do_flatten_union(type, context) - end - end - end - - def do_flatten_union(type, _context) do - type - end - - defp flatten_union_tuple([type | types], num, context, acc) do - case do_flatten_union(type, context) do - {:union, union_types} -> - Enum.flat_map(union_types, &flatten_union_tuple(types, num, context, [&1 | acc])) - - type -> - flatten_union_tuple(types, num, context, [type | acc]) - end - end - - defp flatten_union_tuple([], num, _context, acc) do - [{:tuple, num, Enum.reverse(acc)}] - end - - defp flatten_union_map([{kind, key, value} | pairs], context, acc) do - case do_flatten_union(key, context) do - {:union, union_types} -> - Enum.flat_map(union_types, &flatten_union_map_value(kind, &1, value, pairs, context, acc)) - - type -> - flatten_union_map_value(kind, type, value, pairs, context, acc) - end - end - - defp flatten_union_map([], _context, acc) do - [{:map, Enum.reverse(acc)}] - end - - defp flatten_union_map_value(kind, key, value, pairs, context, acc) do - case do_flatten_union(value, context) do - {:union, union_types} -> - Enum.flat_map(union_types, &flatten_union_map(pairs, context, [{kind, key, &1} | acc])) - - value -> - flatten_union_map(pairs, context, [{kind, key, value} | acc]) - end - end - - defp looping_var?(var, context, parents) do - case context.types do - %{^var => :unbound} -> - false - - %{^var => {:var, type}} -> - if var in parents do - true - else - looping_var?(type, context, [var | parents]) - end - - %{^var => _type} -> - false - end - end - - @doc """ - Formats types. - - The second argument says when complex types such as maps and - structs should be simplified and not shown. - """ - def format_type({:map, pairs}, true) do - case List.keyfind(pairs, {:atom, :__struct__}, 1) do - {:required, {:atom, :__struct__}, {:atom, struct}} -> - ["%", inspect(struct), "{}"] - - _ -> - "map()" - end - end - - def format_type({:union, types}, simplify?) do - types - |> Enum.map(&format_type(&1, simplify?)) - |> Enum.intersperse(" | ") - end - - def format_type({:tuple, _, types}, simplify?) do - format = - types - |> Enum.map(&format_type(&1, simplify?)) - |> Enum.intersperse(", ") - - ["{", format, "}"] - end - - def format_type({:list, type}, simplify?) do - ["[", format_type(type, simplify?), "]"] - end - - def format_type({:map, pairs}, false) do - case List.keytake(pairs, {:atom, :__struct__}, 1) do - {{:required, {:atom, :__struct__}, {:atom, struct}}, pairs} -> - ["%", inspect(struct), "{", format_map_pairs(pairs), "}"] - - _ -> - ["%{", format_map_pairs(pairs), "}"] - end - end - - def format_type({:atom, literal}, _simplify?) do - inspect(literal) - end - - def format_type({:var, index}, _simplify?) do - ["var", Integer.to_string(index + 1)] - end - - def format_type({:fun, clauses}, simplify?) do - format = - Enum.map(clauses, fn {params, return} -> - params = Enum.intersperse(Enum.map(params, &format_type(&1, simplify?)), ", ") - params = if params == [], do: params, else: [params, " "] - return = format_type(return, simplify?) - [params, "-> ", return] - end) - - ["(", Enum.intersperse(format, "; "), ")"] - end - - def format_type(atom, _simplify?) when is_atom(atom) do - [Atom.to_string(atom), "()"] - end - - defp format_map_pairs(pairs) do - {atoms, others} = Enum.split_with(pairs, &match?({:required, {:atom, _}, _}, &1)) - {required, optional} = Enum.split_with(others, &match?({:required, _, _}, &1)) - - (atoms ++ required ++ optional) - |> Enum.map(fn - {:required, {:atom, atom}, right} -> - [Atom.to_string(atom), ": ", format_type(right, false)] - - {:required, left, right} -> - [format_type(left, false), " => ", format_type(right, false)] - - {:optional, left, right} -> - ["optional(", format_type(left, false), ") => ", format_type(right, false)] - end) - |> Enum.intersperse(", ") - end - - @doc """ - Performs a depth-first, pre-order traversal of the type tree using an accumulator. - """ - def walk({:map, pairs}, acc, fun) do - {pairs, acc} = - Enum.map_reduce(pairs, acc, fn {kind, key, value}, acc -> - {key, acc} = walk(key, acc, fun) - {value, acc} = walk(value, acc, fun) - {{kind, key, value}, acc} - end) - - fun.({:map, pairs}, acc) - end - - def walk({:union, types}, acc, fun) do - {types, acc} = Enum.map_reduce(types, acc, &walk(&1, &2, fun)) - fun.({:union, types}, acc) - end - - def walk({:tuple, num, types}, acc, fun) do - {types, acc} = Enum.map_reduce(types, acc, &walk(&1, &2, fun)) - fun.({:tuple, num, types}, acc) - end - - def walk({:list, type}, acc, fun) do - {type, acc} = walk(type, acc, fun) - fun.({:list, type}, acc) - end - - def walk({:fun, clauses}, acc, fun) do - {clauses, acc} = - Enum.map_reduce(clauses, acc, fn {params, return}, acc -> - {params, acc} = Enum.map_reduce(params, acc, &walk(&1, &2, fun)) - {return, acc} = walk(return, acc, fun) - {{params, return}, acc} - end) - - fun.({:fun, clauses}, acc) - end - - def walk(type, acc, fun) do - fun.(type, acc) - end -end diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 3b712b00d7b..fb4963a4ca7 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -187,7 +187,7 @@ bootstrap_files() -> <<"module/parallel_checker.ex">>, <<"module/behaviour.ex">>, <<"module/types/helpers.ex">>, - <<"module/types/unify.ex">>, + <<"module/types/descr.ex">>, <<"module/types/of.ex">>, <<"module/types/pattern.ex">>, <<"module/types/expr.ex">>, diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 8a32975b2e2..12c07639682 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -4,321 +4,18 @@ defmodule Module.Types.ExprTest do use ExUnit.Case, async: true import TypeHelper - - defmodule :"Elixir.Module.Types.ExprTest.Struct" do - defstruct foo: :atom, bar: 123, baz: %{} - end + import Module.Types.Descr test "literal" do - assert quoted_expr(true) == {:ok, {:atom, true}} - assert quoted_expr(false) == {:ok, {:atom, false}} - assert quoted_expr(:foo) == {:ok, {:atom, :foo}} - assert quoted_expr(0) == {:ok, :integer} - assert quoted_expr(0.0) == {:ok, :float} - assert quoted_expr("foo") == {:ok, :binary} - end - - describe "list" do - test "proper" do - assert quoted_expr([]) == {:ok, {:list, :dynamic}} - assert quoted_expr([123]) == {:ok, {:list, :integer}} - assert quoted_expr([123, 456]) == {:ok, {:list, :integer}} - assert quoted_expr([123 | []]) == {:ok, {:list, :integer}} - assert quoted_expr([123, "foo"]) == {:ok, {:list, {:union, [:integer, :binary]}}} - assert quoted_expr([123 | ["foo"]]) == {:ok, {:list, {:union, [:integer, :binary]}}} - end - - test "improper" do - assert quoted_expr([123 | 456]) == {:ok, {:list, :integer}} - assert quoted_expr([123, 456 | 789]) == {:ok, {:list, :integer}} - assert quoted_expr([123 | "foo"]) == {:ok, {:list, {:union, [:integer, :binary]}}} - end - - test "keyword" do - assert quoted_expr(a: 1, b: 2) == - {:ok, - {:list, - {:union, - [ - {:tuple, 2, [{:atom, :a}, :integer]}, - {:tuple, 2, [{:atom, :b}, :integer]} - ]}}} - end - end - - test "tuple" do - assert quoted_expr({}) == {:ok, {:tuple, 0, []}} - assert quoted_expr({:a}) == {:ok, {:tuple, 1, [{:atom, :a}]}} - assert quoted_expr({:a, 123}) == {:ok, {:tuple, 2, [{:atom, :a}, :integer]}} - end - - describe "binary" do - test "literal" do - assert quoted_expr(<<"foo"::binary>>) == {:ok, :binary} - assert quoted_expr(<<123::integer>>) == {:ok, :binary} - assert quoted_expr(<<123::utf8>>) == {:ok, :binary} - assert quoted_expr(<<"foo"::utf8>>) == {:ok, :binary} - end - - defmacrop custom_type do - quote do: 1 * 8 - big - signed - integer - end - - test "variable" do - assert quoted_expr([foo], <>) == {:ok, :binary} - assert quoted_expr([foo], <>) == {:ok, :binary} - assert quoted_expr([foo], <>) == {:ok, :binary} - assert quoted_expr([foo], <>) == {:ok, :binary} - assert quoted_expr([foo], <>) == {:ok, :binary} - end - - test "infer" do - assert quoted_expr( - ( - foo = 0.0 - <> - ) - ) == {:ok, :binary} - - assert quoted_expr( - ( - foo = 0 - <> - ) - ) == {:ok, :binary} - - assert quoted_expr([foo], {<>, foo}) == - {:ok, {:tuple, 2, [:binary, :integer]}} - - assert quoted_expr([foo], {<>, foo}) == {:ok, {:tuple, 2, [:binary, :binary]}} - - assert quoted_expr([foo], {<>, foo}) == - {:ok, {:tuple, 2, [:binary, {:union, [:integer, :binary]}]}} - - assert {:error, {:unable_unify, {:integer, :binary, _}}} = - quoted_expr( - ( - foo = 0 - <> - ) - ) - - assert {:error, {:unable_unify, {:binary, :integer, _}}} = - quoted_expr([foo], <>) - end - end - - test "variables" do - assert quoted_expr([foo], foo) == {:ok, {:var, 0}} - assert quoted_expr([foo], {foo}) == {:ok, {:tuple, 1, [{:var, 0}]}} - assert quoted_expr([foo, bar], {foo, bar}) == {:ok, {:tuple, 2, [{:var, 0}, {:var, 1}]}} - end - - test "pattern match" do - assert {:error, _} = quoted_expr(:foo = 1) - assert {:error, _} = quoted_expr(1 = :foo) - - assert quoted_expr(:foo = :foo) == {:ok, {:atom, :foo}} - assert quoted_expr(1 = 1) == {:ok, :integer} - end - - test "block" do - assert quoted_expr( - ( - a = 1 - a - ) - ) == {:ok, :integer} - - assert quoted_expr( - ( - a = :foo - a - ) - ) == {:ok, {:atom, :foo}} - - assert {:error, _} = - quoted_expr( - ( - a = 1 - :foo = a - ) - ) - end - - describe "case" do - test "infer pattern" do - assert quoted_expr( - [a], - case a do - :foo = b -> :foo = b - end - ) == {:ok, :dynamic} - - assert {:error, _} = - quoted_expr( - [a], - case a do - :foo = b -> :bar = b - end - ) - end - - test "do not leak pattern/guard inference between clauses" do - assert quoted_expr( - [a], - case a do - :foo = b -> b - :bar = b -> b - end - ) == {:ok, :dynamic} - - assert quoted_expr( - [a], - case a do - b when is_atom(b) -> b - b when is_integer(b) -> b - end - ) == {:ok, :dynamic} - - assert quoted_expr( - [a], - case a do - :foo = b -> :foo = b - :bar = b -> :bar = b - end - ) == {:ok, :dynamic} - end - - test "do not leak body inference between clauses" do - assert quoted_expr( - [a], - case a do - :foo -> - b = :foo - b - - :bar -> - b = :bar - b - end - ) == {:ok, :dynamic} - - assert quoted_expr( - [a, b], - case a do - :foo -> :foo = b - :bar -> :bar = b - end - ) == {:ok, :dynamic} - - assert quoted_expr( - [a, b], - case a do - :foo when is_binary(b) -> b <> "" - :foo when is_list(b) -> b - end - ) == {:ok, :dynamic} - end - end - - describe "cond" do - test "do not leak body inference between clauses" do - assert quoted_expr( - [], - cond do - 1 -> - b = :foo - b - - 2 -> - b = :bar - b - end - ) == {:ok, :dynamic} - - assert quoted_expr( - [b], - cond do - 1 -> :foo = b - 2 -> :bar = b - end - ) == {:ok, :dynamic} - end - end - - test "fn" do - assert quoted_expr(fn :foo = b -> :foo = b end) == {:ok, :dynamic} - - assert {:error, _} = quoted_expr(fn :foo = b -> :bar = b end) - end - - test "receive" do - assert quoted_expr( - receive do - after - 0 -> :ok - end - ) == {:ok, :dynamic} - end - - test "with" do - assert quoted_expr( - [a, b], - with( - :foo <- a, - :bar <- b, - c = :baz, - do: c - ) - ) == {:ok, :dynamic} - - assert quoted_expr( - [a], - ( - with(a = :baz, do: a) - a - ) - ) == {:ok, {:var, 0}} - end - - describe "for comprehension" do - test "with generators and filters" do - assert quoted_expr( - [list], - for( - foo <- list, - is_integer(foo), - do: foo == 123 - ) - ) == {:ok, :dynamic} - end - - test "with unused return" do - assert quoted_expr( - [list, bar], - ( - for( - foo <- list, - is_integer(bar), - do: foo == 123 - ) - - bar - ) - ) == {:ok, {:var, 0}} - end - - test "with reduce" do - assert quoted_expr( - [], - for(i <- [1, 2, 3], do: (acc -> i + acc), reduce: 0) - ) == {:ok, :dynamic} - - assert quoted_expr( - [], - for(i <- [1, 2, 3], do: (_ -> i), reduce: nil) - ) == {:ok, :dynamic} - end + assert typecheck!(true) == atom(true) + assert typecheck!(false) == atom(false) + assert typecheck!(:foo) == atom(:foo) + assert typecheck!(0) == integer() + assert typecheck!(0.0) == float() + assert typecheck!("foo") == binary() + assert typecheck!([]) == empty_list() + assert typecheck!([1, 2]) == non_empty_list() + assert typecheck!({1, 2}) == tuple() + assert typecheck!(%{}) == map() end end diff --git a/lib/elixir/test/elixir/module/types/type_helper.exs b/lib/elixir/test/elixir/module/types/type_helper.exs index 351461cdf18..89e0ad784a2 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -2,37 +2,54 @@ Code.require_file("../../test_helper.exs", __DIR__) defmodule TypeHelper do alias Module.Types - alias Module.Types.{Pattern, Expr, Unify} - - defmacro quoted_expr(patterns \\ [], guards \\ [], body) do - expr = expand_expr(patterns, guards, body, __CALLER__) + alias Module.Types.{Pattern, Expr} + @doc """ + Main helper for checking the given AST type checks without warnings. + """ + defmacro typecheck!(patterns \\ [], guards \\ [], body) do quote do - TypeHelper.__expr__(unquote(Macro.escape(expr))) + unquote(typecheck(patterns, guards, body, __CALLER__)) + |> TypeHelper.__typecheck__!() end end - def __expr__({patterns, guards, body}) do - with {:ok, _types, context} <- - Pattern.of_head(patterns, guards, new_stack(), new_context()), - {:ok, type, context} <- Expr.of_expr(body, :dynamic, new_stack(), context) do - {[type], _context} = Unify.lift_types([type], context) - {:ok, type} - else - {:error, {type, reason, _context}} -> - {:error, {type, reason}} - end - end + def __typecheck__!({:ok, type, %{warnings: []}}), do: type - def expand_expr(patterns, guards, expr, env) do + def __typecheck__!({:ok, _type, %{warnings: warnings}}), + do: raise("type checking ok but with warnings: #{inspect(warnings)}") + + def __typecheck__!({:error, %{warnings: warnings}}), + do: raise("type checking errored with warnings: #{inspect(warnings)}") + + @doc """ + Building block for typechecking a given AST. + """ + def typecheck(patterns, guards, body, env) do fun = quote do - fn unquote(patterns) when unquote(guards) -> unquote(expr) end + fn unquote(patterns) when unquote(guards) -> unquote(body) end end {ast, _, _} = :elixir_expand.expand(fun, :elixir_env.env_to_ex(env), env) {:fn, _, [{:->, _, [[{:when, _, [patterns, guards]}], body]}]} = ast - {patterns, guards, body} + + quote do + TypeHelper.__typecheck__( + unquote(Macro.escape(patterns)), + unquote(Macro.escape(guards)), + unquote(Macro.escape(body)) + ) + end + end + + def __typecheck__(patterns, guards, body) do + stack = new_stack() + + with {:ok, _types, context} <- Pattern.of_head(patterns, guards, stack, new_context()), + {:ok, type, context} <- Expr.of_expr(body, stack, context) do + {:ok, type, context} + end end def new_context() do @@ -40,9 +57,6 @@ defmodule TypeHelper do end def new_stack() do - %{ - Types.stack() - | last_expr: {:foo, [], nil} - } + Types.stack() end end diff --git a/lib/elixir/test/elixir/module/types/types_test.exs b/lib/elixir/test/elixir/module/types/types_test.exs index bee4b5bc5cf..dfebb6cb1b4 100644 --- a/lib/elixir/test/elixir/module/types/types_test.exs +++ b/lib/elixir/test/elixir/module/types/types_test.exs @@ -3,35 +3,30 @@ Code.require_file("type_helper.exs", __DIR__) defmodule Module.Types.TypesTest do use ExUnit.Case, async: true alias Module.Types - alias Module.Types.{Pattern, Expr} defmacro warning(patterns \\ [], guards \\ [], body) do min_line = min_line(patterns ++ guards ++ [body]) patterns = reset_line(patterns, min_line) guards = reset_line(guards, min_line) body = reset_line(body, min_line) - expr = TypeHelper.expand_expr(patterns, guards, body, __CALLER__) quote do - Module.Types.TypesTest.__expr__(unquote(Macro.escape(expr))) + unquote(TypeHelper.typecheck(patterns, guards, body, __CALLER__)) + |> Module.Types.TypesTest.__warning__() end end - defmacro generated(ast) do - Macro.prewalk(ast, fn node -> Macro.update_meta(node, &([generated: true] ++ &1)) end) - end - - def __expr__({patterns, guards, body}) do - with {:ok, _types, context} <- - Pattern.of_head(patterns, guards, TypeHelper.new_stack(), TypeHelper.new_context()), - {:ok, _type, context} <- Expr.of_expr(body, :dynamic, TypeHelper.new_stack(), context) do - case context.warnings do - [warning] -> to_message(:warning, warning) - _ -> :none + def __warning__(result) do + context = + case result do + {:ok, _, context} -> context + {:error, context} -> context end - else - {:error, {type, reason, context}} -> - to_message(:error, {type, reason, context}) + + case context.warnings do + [warning] -> to_message(warning) + [] -> raise "no warnings" + [_ | _] = warnings -> raise "too many warnings: #{inspect(warnings)}" end end @@ -53,21 +48,12 @@ defmodule Module.Types.TypesTest do min end - defp to_message(:warning, {module, warning, _location}) do + defp to_message({module, warning, _location}) do warning |> module.format_warning() |> IO.iodata_to_binary() end - defp to_message(:error, {type, reason, context}) do - {Module.Types, error, _location} = Module.Types.error_to_warning(type, reason, context) - - error - |> Module.Types.format_warning() - |> IO.iodata_to_binary() - |> String.trim_trailing("\nConflict found at") - end - test "expr_to_string/1" do assert Types.expr_to_string({1, 2}) == "{1, 2}" assert Types.expr_to_string(quote(do: Foo.bar(arg))) == "Foo.bar(arg)" @@ -81,13 +67,13 @@ defmodule Module.Types.TypesTest do end test "undefined function warnings" do - assert warning([], URI.unknown("foo")) == + assert warning(URI.unknown("foo")) == "URI.unknown/1 is undefined or private" - assert warning([], if(true, do: URI.unknown("foo"))) == + assert warning(if(true, do: URI.unknown("foo"))) == "URI.unknown/1 is undefined or private" - assert warning([], try(do: :ok, after: URI.unknown("foo"))) == + assert warning(try(do: :ok, after: URI.unknown("foo"))) == "URI.unknown/1 is undefined or private" end end diff --git a/lib/elixir/test/elixir/module/types/unify_test.exs b/lib/elixir/test/elixir/module/types/unify_test.exs deleted file mode 100644 index 672fe3892b0..00000000000 --- a/lib/elixir/test/elixir/module/types/unify_test.exs +++ /dev/null @@ -1,770 +0,0 @@ -Code.require_file("type_helper.exs", __DIR__) - -defmodule Module.Types.UnifyTest do - use ExUnit.Case, async: true - import Module.Types.Unify - alias Module.Types - - defp unify_lift(left, right, context \\ new_context()) do - unify(left, right, new_stack(), context) - |> lift_result() - end - - defp new_context() do - Types.context("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache()) - end - - defp new_stack() do - %{ - Types.stack() - | context: :pattern, - last_expr: {:foo, [], nil} - } - end - - defp unify(left, right, context) do - unify(left, right, new_stack(), context) - end - - defp lift_result({:ok, type, context}) do - {:ok, lift_type(type, context)} - end - - defp lift_result({:error, {type, reason, _context}}) do - {:error, {type, reason}} - end - - defp lift_type(type, context) do - {[type], _context} = lift_types([type], context) - type - end - - defp format_type_string(type, simplify?) do - IO.iodata_to_binary(format_type(type, simplify?)) - end - - describe "unify/3" do - test "literal" do - assert unify_lift({:atom, :foo}, {:atom, :foo}) == {:ok, {:atom, :foo}} - - assert {:error, {:unable_unify, {{:atom, :foo}, {:atom, :bar}, _}}} = - unify_lift({:atom, :foo}, {:atom, :bar}) - end - - test "type" do - assert unify_lift(:integer, :integer) == {:ok, :integer} - assert unify_lift(:binary, :binary) == {:ok, :binary} - assert unify_lift(:atom, :atom) == {:ok, :atom} - - assert {:error, {:unable_unify, {:integer, :atom, _}}} = unify_lift(:integer, :atom) - end - - test "atom subtype" do - assert unify_lift({:atom, true}, :atom) == {:ok, {:atom, true}} - assert {:error, _} = unify_lift(:atom, {:atom, true}) - end - - test "tuple" do - assert unify_lift({:tuple, 0, []}, {:tuple, 0, []}) == {:ok, {:tuple, 0, []}} - - assert unify_lift({:tuple, 1, [:integer]}, {:tuple, 1, [:integer]}) == - {:ok, {:tuple, 1, [:integer]}} - - assert unify_lift({:tuple, 1, [{:atom, :foo}]}, {:tuple, 1, [:atom]}) == - {:ok, {:tuple, 1, [{:atom, :foo}]}} - - assert {:error, {:unable_unify, {{:tuple, 1, [:integer]}, {:tuple, 0, []}, _}}} = - unify_lift({:tuple, 1, [:integer]}, {:tuple, 0, []}) - - assert {:error, {:unable_unify, {:integer, :atom, _}}} = - unify_lift({:tuple, 1, [:integer]}, {:tuple, 1, [:atom]}) - end - - test "list" do - assert unify_lift({:list, :integer}, {:list, :integer}) == {:ok, {:list, :integer}} - - assert {:error, {:unable_unify, {:atom, :integer, _}}} = - unify_lift({:list, :atom}, {:list, :integer}) - end - - test "map" do - assert unify_lift({:map, []}, {:map, []}) == {:ok, {:map, []}} - - assert unify_lift( - {:map, [{:required, :integer, :atom}]}, - {:map, [{:optional, :dynamic, :dynamic}]} - ) == - {:ok, {:map, [{:required, :integer, :atom}]}} - - assert unify_lift( - {:map, [{:optional, :dynamic, :dynamic}]}, - {:map, [{:required, :integer, :atom}]} - ) == - {:ok, {:map, [{:required, :integer, :atom}]}} - - assert unify_lift( - {:map, [{:optional, :dynamic, :dynamic}]}, - {:map, [{:required, :integer, :atom}, {:optional, :dynamic, :dynamic}]} - ) == - {:ok, {:map, [{:required, :integer, :atom}, {:optional, :dynamic, :dynamic}]}} - - assert unify_lift( - {:map, [{:required, :integer, :atom}, {:optional, :dynamic, :dynamic}]}, - {:map, [{:optional, :dynamic, :dynamic}]} - ) == - {:ok, {:map, [{:required, :integer, :atom}, {:optional, :dynamic, :dynamic}]}} - - assert unify_lift( - {:map, [{:required, :integer, :atom}]}, - {:map, [{:required, :integer, :atom}]} - ) == - {:ok, {:map, [{:required, :integer, :atom}]}} - - assert {:error, - {:unable_unify, - {{:map, [{:required, :integer, :atom}]}, {:map, [{:required, :atom, :integer}]}, _}}} = - unify_lift( - {:map, [{:required, :integer, :atom}]}, - {:map, [{:required, :atom, :integer}]} - ) - - assert {:error, {:unable_unify, {{:map, [{:required, :integer, :atom}]}, {:map, []}, _}}} = - unify_lift({:map, [{:required, :integer, :atom}]}, {:map, []}) - - assert {:error, {:unable_unify, {{:map, []}, {:map, [{:required, :integer, :atom}]}, _}}} = - unify_lift({:map, []}, {:map, [{:required, :integer, :atom}]}) - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:required, {:atom, :foo}, :atom}]}, - _}}} = - unify_lift( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:required, {:atom, :foo}, :atom}]} - ) - end - - test "map required/optional key" do - assert unify_lift( - {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}, - {:map, [{:required, {:atom, :foo}, :atom}]} - ) == - {:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]}, - {:map, [{:required, {:atom, :foo}, :atom}]} - ) == - {:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}} - - assert unify_lift( - {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}, - {:map, [{:optional, {:atom, :foo}, :atom}]} - ) == - {:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]}, - {:map, [{:optional, {:atom, :foo}, :atom}]} - ) == - {:ok, {:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]}} - end - - test "map with subtyped keys" do - assert unify_lift( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:required, :atom, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, :integer}]}, - {:map, [{:required, :atom, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:optional, :atom, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, :integer}]}, - {:map, [{:optional, :atom, :integer}]} - ) == {:ok, {:map, [{:optional, {:atom, :foo}, :integer}]}} - - assert {:error, - {:unable_unify, - {{:map, [{:required, :atom, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]}, - _}}} = - unify_lift( - {:map, [{:required, :atom, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]} - ) - - assert {:error, - {:unable_unify, - {{:map, [{:optional, :atom, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]}, - _}}} = - unify_lift( - {:map, [{:optional, :atom, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]} - ) - - assert {:error, - {:unable_unify, - {{:map, [{:required, :atom, :integer}]}, - {:map, [{:optional, {:atom, :foo}, :integer}]}, - _}}} = - unify_lift( - {:map, [{:required, :atom, :integer}]}, - {:map, [{:optional, {:atom, :foo}, :integer}]} - ) - - assert unify_lift( - {:map, [{:optional, :atom, :integer}]}, - {:map, [{:optional, {:atom, :foo}, :integer}]} - ) == {:ok, {:map, []}} - - assert unify_lift( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:optional, {:atom, :foo}, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, :integer}]}, - {:map, [{:optional, {:atom, :foo}, :integer}]} - ) == {:ok, {:map, [{:optional, {:atom, :foo}, :integer}]}} - end - - test "map with subtyped and multiple matching keys" do - assert {:error, _} = - unify_lift( - {:map, - [ - {:required, {:atom, :foo}, :integer}, - {:required, :atom, {:union, [:integer, :boolean]}} - ]}, - {:map, [{:required, :atom, :integer}]} - ) - - assert unify_lift( - {:map, - [ - {:required, {:atom, :foo}, :integer}, - {:required, :atom, {:union, [:integer, :boolean]}} - ]}, - {:map, [{:required, :atom, {:union, [:integer, :boolean]}}]} - ) == - {:ok, - {:map, - [ - {:required, {:atom, :foo}, :integer}, - {:required, :atom, {:union, [:integer, :boolean]}} - ]}} - - assert {:error, _} = - unify_lift( - {:map, [{:required, :atom, :integer}]}, - {:map, - [ - {:required, {:atom, :foo}, :integer}, - {:required, :atom, {:union, [:integer, :boolean]}} - ]} - ) - - assert {:error, _} = - unify_lift( - {:map, [{:required, :atom, {:union, [:integer, :boolean]}}]}, - {:map, - [ - {:required, {:atom, :foo}, :integer}, - {:required, :atom, {:union, [:integer, :boolean]}} - ]} - ) - - assert unify_lift( - {:map, [{:required, :atom, :integer}]}, - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]} - ) == {:ok, {:map, [{:required, :atom, :integer}]}} - - assert unify_lift( - {:map, [{:required, :atom, {:union, [:integer, :boolean]}}]}, - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]} - ) == {:ok, {:map, [{:required, :atom, {:union, [:integer, :boolean]}}]}} - - assert unify_lift( - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]}, - {:map, [{:required, :atom, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - # TODO: FIX ME - # assert unify_lift( - # {:map, - # [ - # {:optional, {:atom, :foo}, :integer}, - # {:optional, :atom, {:union, [:integer, :boolean]}} - # ]}, - # {:map, [{:required, :atom, {:union, [:integer, :boolean]}}]} - # ) == - # {:ok, - # {:map, - # [ - # {:required, {:atom, :foo}, :integer}, - # {:required, :atom, {:union, [:integer, :boolean]}} - # ]}} - - assert {:error, _} = - unify_lift( - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]}, - {:map, [{:optional, :atom, :integer}]} - ) - - assert unify_lift( - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]}, - {:map, [{:optional, :atom, {:union, [:integer, :boolean]}}]} - ) == - {:ok, - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]}} - - assert unify_lift( - {:map, [{:optional, :atom, :integer}]}, - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]} - ) == {:ok, {:map, [{:optional, :atom, :integer}]}} - - assert unify_lift( - {:map, [{:optional, :atom, {:union, [:integer, :boolean]}}]}, - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]} - ) == {:ok, {:map, [{:optional, :atom, {:union, [:integer, :boolean]}}]}} - end - - test "union" do - assert unify_lift({:union, []}, {:union, []}) == {:ok, {:union, []}} - assert unify_lift({:union, [:integer]}, {:union, [:integer]}) == {:ok, {:union, [:integer]}} - - assert unify_lift({:union, [:integer, :atom]}, {:union, [:integer, :atom]}) == - {:ok, {:union, [:integer, :atom]}} - - assert unify_lift({:union, [:integer, :atom]}, {:union, [:atom, :integer]}) == - {:ok, {:union, [:integer, :atom]}} - - assert unify_lift({:union, [{:atom, :bar}]}, {:union, [:atom]}) == - {:ok, {:atom, :bar}} - - assert {:error, {:unable_unify, {:integer, {:union, [:atom]}, _}}} = - unify_lift({:union, [:integer]}, {:union, [:atom]}) - end - - test "dynamic" do - assert unify_lift({:atom, :foo}, :dynamic) == {:ok, {:atom, :foo}} - assert unify_lift(:dynamic, {:atom, :foo}) == {:ok, {:atom, :foo}} - assert unify_lift(:integer, :dynamic) == {:ok, :integer} - assert unify_lift(:dynamic, :integer) == {:ok, :integer} - end - - test "vars" do - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - {{:var, 1}, var_context} = new_var({:bar, [version: 1], nil}, var_context) - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert lift_type({:var, 0}, context) == :integer - - assert {:ok, {:var, 0}, context} = unify(:integer, {:var, 0}, var_context) - assert lift_type({:var, 0}, context) == :integer - - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context) - assert {:var, _} = lift_type({:var, 0}, context) - assert {:var, _} = lift_type({:var, 1}, context) - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :integer, context) - assert {:ok, {:var, _}, _context} = unify({:var, 0}, {:var, 1}, context) - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :integer, context) - assert {:ok, {:var, _}, _context} = unify({:var, 1}, {:var, 0}, context) - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :binary, context) - - assert {:error, {:unable_unify, {:integer, :binary, _}}} = - unify_lift({:var, 0}, {:var, 1}, context) - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :binary, context) - - assert {:error, {:unable_unify, {:binary, :integer, _}}} = - unify_lift({:var, 1}, {:var, 0}, context) - end - - test "vars inside tuples" do - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - {{:var, 1}, var_context} = new_var({:bar, [version: 1], nil}, var_context) - - assert {:ok, {:tuple, 1, [{:var, 0}]}, context} = - unify({:tuple, 1, [{:var, 0}]}, {:tuple, 1, [:integer]}, var_context) - - assert lift_type({:var, 0}, context) == :integer - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :integer, context) - - assert {:ok, {:tuple, 1, [{:var, _}]}, _context} = - unify({:tuple, 1, [{:var, 0}]}, {:tuple, 1, [{:var, 1}]}, context) - - assert {:ok, {:var, 1}, context} = unify({:var, 1}, {:tuple, 1, [{:var, 0}]}, var_context) - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, context) - assert lift_type({:var, 1}, context) == {:tuple, 1, [:integer]} - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :binary, context) - - assert {:error, {:unable_unify, {:integer, :binary, _}}} = - unify_lift({:tuple, 1, [{:var, 0}]}, {:tuple, 1, [{:var, 1}]}, context) - end - - # TODO: Vars inside right unions - - test "vars inside left unions" do - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - - assert {:ok, {:var, 0}, context} = - unify({:union, [{:var, 0}, :integer]}, :integer, var_context) - - assert lift_type({:var, 0}, context) == :integer - - assert {:ok, {:var, 0}, context} = - unify({:union, [{:var, 0}, :integer]}, {:union, [:integer, :atom]}, var_context) - - assert lift_type({:var, 0}, context) == {:union, [:integer, :atom]} - - assert {:error, {:unable_unify, {:integer, {:union, [:binary, :atom]}, _}}} = - unify_lift( - {:union, [{:var, 0}, :integer]}, - {:union, [:binary, :atom]}, - var_context - ) - end - - test "recursive type" do - assert {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - assert {{:var, 1}, var_context} = new_var({:bar, [version: 1], nil}, var_context) - assert {{:var, 2}, var_context} = new_var({:baz, [version: 2], nil}, var_context) - - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context) - assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 0}, context) - assert context.types[0] == {:var, 1} - assert context.types[1] == {:var, 0} - - assert {:ok, {:var, _}, context} = unify({:var, 0}, :tuple, var_context) - assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 0}, context) - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, context) - assert context.types[0] == :tuple - assert context.types[1] == {:var, 0} - - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context) - assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 2}, context) - assert {:ok, {:var, _}, _context} = unify({:var, 2}, {:var, 0}, context) - assert context.types[0] == {:var, 1} - assert context.types[1] == {:var, 2} - assert context.types[2] == :unbound - - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context) - - assert {:error, {:unable_unify, {{:var, 1}, {:tuple, 1, [{:var, 0}]}, _}}} = - unify_lift({:var, 1}, {:tuple, 1, [{:var, 0}]}, context) - - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context) - assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 2}, context) - - assert {:error, {:unable_unify, {{:var, 2}, {:tuple, 1, [{:var, 0}]}, _}}} = - unify_lift({:var, 2}, {:tuple, 1, [{:var, 0}]}, context) - end - - test "error with internal variable" do - context = new_context() - {var_integer, context} = add_var(context) - {var_atom, context} = add_var(context) - - {:ok, _, context} = unify(var_integer, :integer, context) - {:ok, _, context} = unify(var_atom, :atom, context) - - assert {:error, _} = unify(var_integer, var_atom, context) - end - end - - describe "has_unbound_var?/2" do - setup do - context = new_context() - {unbound_var, context} = add_var(context) - {bound_var, context} = add_var(context) - {:ok, _, context} = unify(bound_var, :integer, context) - %{context: context, unbound_var: unbound_var, bound_var: bound_var} - end - - test "returns true when there are unbound vars", - %{context: context, unbound_var: unbound_var} do - assert has_unbound_var?(unbound_var, context) - assert has_unbound_var?({:union, [unbound_var]}, context) - assert has_unbound_var?({:tuple, 1, [unbound_var]}, context) - assert has_unbound_var?({:list, unbound_var}, context) - assert has_unbound_var?({:map, [{:required, unbound_var, :atom}]}, context) - assert has_unbound_var?({:map, [{:required, :atom, unbound_var}]}, context) - end - - test "returns false when there are no unbound vars", - %{context: context, bound_var: bound_var} do - refute has_unbound_var?(bound_var, context) - refute has_unbound_var?({:union, [bound_var]}, context) - refute has_unbound_var?({:tuple, 1, [bound_var]}, context) - refute has_unbound_var?(:integer, context) - refute has_unbound_var?({:list, bound_var}, context) - refute has_unbound_var?({:map, [{:required, :atom, :atom}]}, context) - refute has_unbound_var?({:map, [{:required, bound_var, :atom}]}, context) - refute has_unbound_var?({:map, [{:required, :atom, bound_var}]}, context) - end - end - - describe "subtype?/3" do - test "with simple types" do - assert subtype?({:atom, :foo}, :atom, new_context()) - assert subtype?({:atom, true}, :atom, new_context()) - - refute subtype?(:integer, :binary, new_context()) - refute subtype?(:atom, {:atom, :foo}, new_context()) - refute subtype?(:atom, {:atom, true}, new_context()) - end - - test "with composite types" do - assert subtype?({:list, {:atom, :foo}}, {:list, :atom}, new_context()) - assert subtype?({:tuple, 1, [{:atom, :foo}]}, {:tuple, 1, [:atom]}, new_context()) - - refute subtype?({:list, :atom}, {:list, {:atom, :foo}}, new_context()) - refute subtype?({:tuple, 1, [:atom]}, {:tuple, 1, [{:atom, :foo}]}, new_context()) - refute subtype?({:tuple, 1, [:atom]}, {:tuple, 2, [:atom, :atom]}, new_context()) - refute subtype?({:tuple, 2, [:atom, :atom]}, {:tuple, 1, [:atom]}, new_context()) - - refute subtype?( - {:tuple, 2, [{:atom, :a}, :integer]}, - {:tuple, 2, [{:atom, :b}, :integer]}, - new_context() - ) - end - - test "with maps" do - assert subtype?({:map, [{:optional, :atom, :integer}]}, {:map, []}, new_context()) - - assert subtype?( - {:map, [{:required, :atom, :integer}]}, - {:map, [{:required, :atom, :integer}]}, - new_context() - ) - - assert subtype?( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:required, :atom, :integer}]}, - new_context() - ) - - assert subtype?( - {:map, [{:required, :integer, {:atom, :foo}}]}, - {:map, [{:required, :integer, :atom}]}, - new_context() - ) - - refute subtype?({:map, [{:required, :atom, :integer}]}, {:map, []}, new_context()) - - refute subtype?( - {:map, [{:required, :atom, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]}, - new_context() - ) - - refute subtype?( - {:map, [{:required, :integer, :atom}]}, - {:map, [{:required, :integer, {:atom, :foo}}]}, - new_context() - ) - end - - test "with unions" do - assert subtype?({:union, [{:atom, :foo}]}, {:union, [:atom]}, new_context()) - assert subtype?({:union, [{:atom, :foo}, {:atom, :bar}]}, {:union, [:atom]}, new_context()) - assert subtype?({:union, [{:atom, :foo}]}, {:union, [:integer, :atom]}, new_context()) - - assert subtype?({:atom, :foo}, {:union, [:atom]}, new_context()) - assert subtype?({:atom, :foo}, {:union, [:integer, :atom]}, new_context()) - - assert subtype?({:union, [{:atom, :foo}]}, :atom, new_context()) - assert subtype?({:union, [{:atom, :foo}, {:atom, :bar}]}, :atom, new_context()) - - refute subtype?({:union, [:atom]}, {:union, [{:atom, :foo}]}, new_context()) - refute subtype?({:union, [:atom]}, {:union, [{:atom, :foo}, :integer]}, new_context()) - refute subtype?(:atom, {:union, [{:atom, :foo}, :integer]}, new_context()) - refute subtype?({:union, [:atom]}, {:atom, :foo}, new_context()) - end - end - - test "to_union/2" do - assert to_union([:atom], new_context()) == :atom - assert to_union([:integer, :integer], new_context()) == :integer - assert to_union([{:atom, :foo}, {:atom, :bar}, :atom], new_context()) == :atom - - assert to_union([:binary, :atom], new_context()) == {:union, [:binary, :atom]} - assert to_union([:atom, :binary, :atom], new_context()) == {:union, [:atom, :binary]} - - assert to_union([{:atom, :foo}, :binary, :atom], new_context()) == - {:union, [:binary, :atom]} - - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - assert to_union([{:var, 0}], var_context) == {:var, 0} - - assert to_union([{:tuple, 1, [:integer]}, {:tuple, 1, [:integer]}], new_context()) == - {:tuple, 1, [:integer]} - end - - test "flatten_union/1" do - context = new_context() - assert flatten_union(:binary, context) == [:binary] - assert flatten_union({:atom, :foo}, context) == [{:atom, :foo}] - assert flatten_union({:union, [:binary, {:atom, :foo}]}, context) == [:binary, {:atom, :foo}] - - assert flatten_union({:union, [{:union, [:integer, :binary]}, {:atom, :foo}]}, context) == [ - :integer, - :binary, - {:atom, :foo} - ] - - assert flatten_union({:tuple, 2, [:binary, {:atom, :foo}]}, context) == - [{:tuple, 2, [:binary, {:atom, :foo}]}] - - assert flatten_union({:tuple, 1, [{:union, [:binary, :integer]}]}, context) == - [{:tuple, 1, [:binary]}, {:tuple, 1, [:integer]}] - - assert flatten_union( - {:tuple, 2, [{:union, [:binary, :integer]}, {:union, [:binary, :integer]}]}, - context - ) == - [ - {:tuple, 2, [:binary, :binary]}, - {:tuple, 2, [:binary, :integer]}, - {:tuple, 2, [:integer, :binary]}, - {:tuple, 2, [:integer, :integer]} - ] - - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - assert flatten_union({:var, 0}, var_context) == [{:var, 0}] - - {:ok, {:var, 0}, var_context} = unify({:var, 0}, :integer, var_context) - assert flatten_union({:var, 0}, var_context) == [:integer] - - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - {:ok, {:var, 0}, var_context} = unify({:var, 0}, {:union, [:integer, :float]}, var_context) - assert flatten_union({:var, 0}, var_context) == [:integer, :float] - - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - {{:var, 1}, var_context} = new_var({:bar, [version: 1], nil}, var_context) - {:ok, {:var, 0}, var_context} = unify({:var, 0}, {:var, 1}, var_context) - assert flatten_union({:var, 0}, var_context) == [{:var, 1}] - assert flatten_union({:var, 1}, var_context) == [{:var, 1}] - end - - test "format_type/1" do - assert format_type_string(:binary, false) == "binary()" - assert format_type_string({:atom, true}, false) == "true" - assert format_type_string({:atom, :atom}, false) == ":atom" - assert format_type_string({:list, :binary}, false) == "[binary()]" - assert format_type_string({:tuple, 0, []}, false) == "{}" - assert format_type_string({:tuple, 1, [:integer]}, false) == "{integer()}" - - assert format_type_string({:map, []}, true) == "map()" - assert format_type_string({:map, [{:required, {:atom, :foo}, :atom}]}, true) == "map()" - - assert format_type_string({:map, []}, false) == - "%{}" - - assert format_type_string({:map, [{:required, {:atom, :foo}, :atom}]}, false) == - "%{foo: atom()}" - - assert format_type_string({:map, [{:required, :integer, :atom}]}, false) == - "%{integer() => atom()}" - - assert format_type_string({:map, [{:optional, :integer, :atom}]}, false) == - "%{optional(integer()) => atom()}" - - assert format_type_string({:map, [{:optional, {:atom, :foo}, :atom}]}, false) == - "%{optional(:foo) => atom()}" - - assert format_type_string({:map, [{:required, {:atom, :__struct__}, {:atom, Struct}}]}, false) == - "%Struct{}" - - assert format_type_string( - {:map, - [{:required, {:atom, :__struct__}, {:atom, Struct}}, {:required, :integer, :atom}]}, - false - ) == - "%Struct{integer() => atom()}" - - assert format_type_string({:fun, [{[], :dynamic}]}, false) == "(-> dynamic())" - - assert format_type_string({:fun, [{[:integer], :dynamic}]}, false) == - "(integer() -> dynamic())" - - assert format_type_string({:fun, [{[:integer, :float], :dynamic}]}, false) == - "(integer(), float() -> dynamic())" - - assert format_type_string({:fun, [{[:integer], :dynamic}, {[:integer], :dynamic}]}, false) == - "(integer() -> dynamic(); integer() -> dynamic())" - end - - test "walk/3" do - assert walk(:dynamic, :acc, fn :dynamic, :acc -> {:integer, :bar} end) == {:integer, :bar} - - assert walk({:list, {:tuple, [:integer, :binary]}}, 1, fn type, counter -> - {type, counter + 1} - end) == {{:list, {:tuple, [:integer, :binary]}}, 3} - end -end From faa02ea0ba9afaae5abb4f39570ed21e84de2e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 5 Jan 2024 20:24:34 +0100 Subject: [PATCH 3/3] Final clean up --- lib/elixir/lib/module/types.ex | 52 +++------------- lib/elixir/lib/module/types/expr.ex | 40 ++++-------- lib/elixir/lib/module/types/helpers.ex | 9 +++ lib/elixir/lib/module/types/of.ex | 62 +++++++++---------- lib/elixir/lib/module/types/pattern.ex | 2 +- .../test/elixir/module/types/type_helper.exs | 8 +-- 6 files changed, 64 insertions(+), 109 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 2e9fb55bf39..efbc0656195 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -9,10 +9,10 @@ defmodule Module.Types do @doc false def warnings(module, file, defs, no_warn_undefined, cache) do - stack = stack() + context = context() Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} -> - context = context(with_file_meta(meta, file), module, function, no_warn_undefined, cache) + stack = stack(with_file_meta(meta, file), module, function, no_warn_undefined, cache) Enum.flat_map(clauses, fn {_meta, args, guards, body} -> try do @@ -65,7 +65,7 @@ defmodule Module.Types do end @doc false - def context(file, module, function, no_warn_undefined, cache) do + def stack(file, module, function, no_warn_undefined, cache) do %{ # File of module file: file, @@ -76,53 +76,15 @@ defmodule Module.Types do # List of calls to not warn on as undefined no_warn_undefined: no_warn_undefined, # A list of cached modules received from the parallel compiler - cache: cache, - # Expression variable to type variable - vars: %{}, - # Type variable to expression variable - types_to_vars: %{}, - # Type variable to type - types: %{}, - # Trace of all variables that have been refined to a type, - # including the type they were refined to, why, and where - traces: %{}, - # Counter to give type variables unique names - counter: 0, - # Track if a variable was inferred from a type guard function such is_tuple/1 - # or a guard function that fails such as elem/2, possible values are: - # `:guarded` when `is_tuple(x)` - # `:guarded` when `is_tuple and elem(x, 0)` - # `:fail` when `elem(x, 0)` - guard_sources: %{}, - # A list with all warnings from the running the code - warnings: [] + cache: cache } end @doc false - def stack() do + def context() do %{ - # Stack of variables we have refined during unification, - # used for creating relevant traces - unify_stack: [], - # Last expression we have recursed through during inference, - # used for tracing - last_expr: nil, - # When false do not add a trace when a type variable is refined, - # useful when merging contexts where the variables already have traces - trace: true, - # There are two factors that control how we track guards. - # - # * consider_type_guards?: if type guards should be considered. - # This applies only at the root and root-based "and" and "or" nodes. - # - # * keep_guarded? - if a guarded clause should remain as guarded - # even on failure. Used on the right side of and. - # - type_guards: {_consider_type_guards? = true, _keep_guarded? = false}, - # Context used to determine if unification is bi-directional, :expr - # is directional, :pattern is bi-directional - context: nil + # A list of all warnings found so far + warnings: [] } end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 47b76206a52..ab06ab7bce5 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -124,7 +124,7 @@ defmodule Module.Types.Expr do stack, context ) do - with {:ok, _, context} <- Of.struct(module, meta, context), + with {:ok, _, context} <- Of.struct(module, meta, stack, context), {:ok, _, context} <- of_expr(update, stack, context) do {:ok, map(), context} end @@ -137,7 +137,7 @@ defmodule Module.Types.Expr do # %Struct{...} def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]}, _expected, stack, context) do - with {:ok, _, context} <- Of.struct(module, meta1, context), + with {:ok, _, context} <- Of.struct(module, meta1, stack, context), {:ok, _, context} <- Of.open_map(args, stack, context, &of_expr/3) do {:ok, map(), context} end @@ -166,17 +166,10 @@ defmodule Module.Types.Expr do # cond do pat -> expr end def of_expr({:cond, _meta, [[{:do, clauses}]]}, _expected, stack, context) do {result, context} = - reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context = acc -> - case of_expr(head, stack, context) do - {:ok, _, context} -> - with {:ok, _expr_type, context} <- of_expr(body, stack, context) do - {:ok, context} - end - - error -> - # Skip the clause if it the head has an error - if meta[:generated], do: {:ok, acc}, else: error - end + reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context -> + with {:ok, _, context} <- of_expr(head, stack, context), + {:ok, _, context} <- of_expr(body, stack, context), + do: {:ok, context} end) case result do @@ -304,7 +297,7 @@ defmodule Module.Types.Expr do # expr.fun(arg) def of_expr({{:., _meta1, [expr1, fun]}, meta2, args}, _expected, stack, context) do - context = Of.remote(expr1, fun, length(args), meta2, context) + context = Of.remote(expr1, fun, length(args), meta2, stack, context) with {:ok, _expr_type, context} <- of_expr(expr1, stack, context), {:ok, _arg_types, context} <- @@ -317,11 +310,11 @@ defmodule Module.Types.Expr do def of_expr( {:&, _, [{:/, _, [{{:., _, [module, fun]}, meta, []}, arity]}]}, _expected, - _stack, + stack, context ) when is_atom(module) and is_atom(fun) do - context = Of.remote(module, fun, arity, meta, context) + context = Of.remote(module, fun, arity, meta, stack, context) {:ok, dynamic(), context} end @@ -400,19 +393,12 @@ defmodule Module.Types.Expr do end defp of_clauses(clauses, stack, context) do - reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context -> + reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context -> {patterns, guards} = extract_head(head) - case Pattern.of_head(patterns, guards, stack, context) do - {:ok, _, context} -> - with {:ok, _expr_type, context} <- of_expr(body, stack, context) do - {:ok, context} - end - - error -> - # Skip the clause if it the head has an error - if meta[:generated], do: {:ok, context}, else: error - end + with {:ok, _, context} <- Pattern.of_head(patterns, guards, stack, context), + {:ok, _, context} <- of_expr(body, stack, context), + do: {:ok, context} end) end diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index d5ddf0b7b4f..eecb7a6b5ca 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -25,6 +25,15 @@ defmodule Module.Types.Helpers do def get_meta({_, meta, _}), do: meta def get_meta(_other), do: [] + @doc """ + Emits a warnings. + """ + def warn(module, warning, meta, stack, context) do + {fun, arity} = stack.function + location = {stack.file, meta, {stack.module, fun, arity}} + %{context | warnings: [{module, warning, location} | context.warnings]} + end + @doc """ Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}` and stops on `{:error, reason}`. diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 12af815032e..9a97d343c7d 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -53,8 +53,8 @@ defmodule Module.Types.Of do @doc """ Handles structs. """ - def struct(struct, meta, context) do - context = remote(struct, :__struct__, 0, meta, context) + def struct(struct, meta, stack, context) do + context = remote(struct, :__struct__, 0, meta, stack, context) {:ok, map(), context} end @@ -158,61 +158,61 @@ defmodule Module.Types.Of do @doc """ Handles remote calls. """ - def remote(module, fun, arity, meta, context) when is_atom(module) do + def remote(module, fun, arity, meta, stack, context) when is_atom(module) do if Keyword.get(meta, :context_module, false) do context else - ParallelChecker.preload_module(context.cache, module) - check_export(module, fun, arity, meta, context) + ParallelChecker.preload_module(stack.cache, module) + check_export(module, fun, arity, meta, stack, context) end end - def remote(_module, _fun, _arity, _meta, context), do: context + def remote(_module, _fun, _arity, _meta, _stack, context), do: context - defp check_export(module, fun, arity, meta, context) do - case ParallelChecker.fetch_export(context.cache, module, fun, arity) do + defp check_export(module, fun, arity, meta, stack, context) do + case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do {:ok, mode, :def, reason} -> - check_deprecated(mode, module, fun, arity, reason, meta, context) + check_deprecated(mode, module, fun, arity, reason, meta, stack, context) {:ok, mode, :defmacro, reason} -> - context = warn(meta, context, {:unrequired_module, module, fun, arity}) - check_deprecated(mode, module, fun, arity, reason, meta, context) + context = warn({:unrequired_module, module, fun, arity}, meta, stack, context) + check_deprecated(mode, module, fun, arity, reason, meta, stack, context) {:error, :module} -> - if warn_undefined?(module, fun, arity, context) do - warn(meta, context, {:undefined_module, module, fun, arity}) + if warn_undefined?(module, fun, arity, stack) do + warn({:undefined_module, module, fun, arity}, meta, stack, context) else context end {:error, :function} -> - if warn_undefined?(module, fun, arity, context) do - exports = ParallelChecker.all_exports(context.cache, module) - warn(meta, context, {:undefined_function, module, fun, arity, exports}) + if warn_undefined?(module, fun, arity, stack) do + exports = ParallelChecker.all_exports(stack.cache, module) + warn({:undefined_function, module, fun, arity, exports}, meta, stack, context) else context end end end - defp check_deprecated(:elixir, module, fun, arity, reason, meta, context) do + defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do if reason do - warn(meta, context, {:deprecated, module, fun, arity, reason}) + warn({:deprecated, module, fun, arity, reason}, meta, stack, context) else context end end - defp check_deprecated(:erlang, module, fun, arity, _reason, meta, context) do + defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do case :otp_internal.obsolete(module, fun, arity) do {:deprecated, string} when is_list(string) -> reason = string |> List.to_string() |> :string.titlecase() - warn(meta, context, {:deprecated, module, fun, arity, reason}) + warn({:deprecated, module, fun, arity, reason}, meta, stack, context) {:deprecated, string, removal} when is_list(string) and is_list(removal) -> reason = string |> List.to_string() |> :string.titlecase() reason = "It will be removed in #{removal}. #{reason}" - warn(meta, context, {:deprecated, module, fun, arity, reason}) + warn({:deprecated, module, fun, arity, reason}, meta, stack, context) _ -> context @@ -229,24 +229,22 @@ defmodule Module.Types.Of do # # But for protocols we don't want to traverse the protocol code anyway. # TODO: remove this clause once we no longer traverse the protocol code. - defp warn_undefined?(_module, :__impl__, 1, _context), do: false - defp warn_undefined?(_module, :module_info, 0, _context), do: false - defp warn_undefined?(_module, :module_info, 1, _context), do: false - defp warn_undefined?(:erlang, :orelse, 2, _context), do: false - defp warn_undefined?(:erlang, :andalso, 2, _context), do: false + defp warn_undefined?(_module, :__impl__, 1, _stack), do: false + defp warn_undefined?(_module, :module_info, 0, _stack), do: false + defp warn_undefined?(_module, :module_info, 1, _stack), do: false + defp warn_undefined?(:erlang, :orelse, 2, _stack), do: false + defp warn_undefined?(:erlang, :andalso, 2, _stack), do: false defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do false end - defp warn_undefined?(module, fun, arity, context) do - not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity})) + defp warn_undefined?(module, fun, arity, stack) do + not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity})) end - defp warn(meta, context, warning) do - {fun, arity} = context.function - location = {context.file, meta, {context.module, fun, arity}} - %{context | warnings: [{__MODULE__, warning, location} | context.warnings]} + defp warn(warning, meta, stack, context) do + warn(__MODULE__, warning, meta, stack, context) end ## Warning formatting diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index b96de05b061..ad1ebc07d5d 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -171,7 +171,7 @@ defmodule Module.Types.Pattern do # %Struct{...} defp of_shared({:%, meta1, [module, {:%{}, _meta2, args}]}, stack, context, fun) when is_atom(module) do - with {:ok, _, context} <- Of.struct(module, meta1, context), + with {:ok, _, context} <- Of.struct(module, meta1, stack, context), {:ok, _, context} <- Of.open_map(args, stack, context, fun) do {:ok, map(), context} end diff --git a/lib/elixir/test/elixir/module/types/type_helper.exs b/lib/elixir/test/elixir/module/types/type_helper.exs index 89e0ad784a2..ecd9c56f4da 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -52,11 +52,11 @@ defmodule TypeHelper do end end - def new_context() do - Types.context("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache()) + def new_stack() do + Types.stack("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache()) end - def new_stack() do - Types.stack() + def new_context() do + Types.context() end end