From 4ce7c069ea32bb8bc6f238594e0f9780e372fc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 2 Jan 2025 16:40:34 +0100 Subject: [PATCH 01/22] Infer expression types in basic operations --- lib/elixir/lib/module/types.ex | 2 +- lib/elixir/lib/module/types/descr.ex | 38 ++- lib/elixir/lib/module/types/expr.ex | 263 +++++++++++------- lib/elixir/lib/module/types/of.ex | 45 ++- lib/elixir/lib/module/types/pattern.ex | 6 +- .../references/gradual-set-theoretic-types.md | 2 +- .../test/elixir/module/types/descr_test.exs | 4 + .../test/elixir/module/types/expr_test.exs | 61 +++- .../test/elixir/module/types/pattern_test.exs | 20 ++ .../test/elixir/module/types/type_helper.exs | 4 +- lib/mix/lib/mix/tasks/local.hex.ex | 4 +- 11 files changed, 324 insertions(+), 125 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 4374e4c0e21..6080d67bcc7 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -285,7 +285,7 @@ defmodule Module.Types do Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context) {return_type, context} = - Expr.of_expr(body, stack, context) + Expr.of_expr(body, {Descr.term(), :ok}, stack, context) {type_index, inferred} = add_inferred(inferred, args_types, return_type, total - 1, []) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index e808be394a7..141b689e21d 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -382,7 +382,8 @@ defmodule Module.Types.Descr do {dynamic, descr} = case :maps.take(:dynamic, descr) do :error -> {[], descr} - {dynamic, descr} -> {to_quoted(:dynamic, dynamic, opts), descr} + {:term, descr} -> {to_quoted(:dynamic, :term, opts), descr} + {dynamic, descr} -> {to_quoted(:dynamic, difference(dynamic, descr), opts), descr} end # Merge empty list and list together if they both exist @@ -547,6 +548,41 @@ defmodule Module.Types.Descr do end end + @doc """ + Returns the intersection between two types + only if they are compatible. Otherwise returns `:error`. + + This finds the intersection between the arguments and the + domain of a function. It is used to refine dynamic types + as we traverse the program. + """ + def compatible_intersection(left, right) do + {left_dynamic, left_static} = + case left do + :term -> {:term, :term} + _ -> Map.pop(left, :dynamic, left) + end + + right_dynamic = + case right do + %{dynamic: dynamic} -> dynamic + _ -> right + end + + cond do + empty?(left_static) -> + dynamic = intersection_static(unfold(left_dynamic), unfold(right_dynamic)) + if empty?(dynamic), do: :error, else: {:ok, dynamic(dynamic)} + + subtype_static?(left_static, right_dynamic) -> + dynamic = intersection_static(unfold(left_dynamic), unfold(right_dynamic)) + {:ok, union(dynamic(dynamic), left_static)} + + true -> + :error + end + end + @doc """ Optimized version of `not empty?(term(), type)`. """ diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 3d23650d74a..78fec42e4a0 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -49,35 +49,39 @@ defmodule Module.Types.Expr do ) ) + @expected_expr {term(), :ok} + @term_expected {term(), :ok} + # :atom - def of_expr(atom, _stack, context) when is_atom(atom), + def of_expr(atom, _expected_expr, _stack, context) when is_atom(atom), do: {atom([atom]), context} # 12 - def of_expr(literal, _stack, context) when is_integer(literal), + def of_expr(literal, _expected_expr, _stack, context) when is_integer(literal), do: {integer(), context} # 1.2 - def of_expr(literal, _stack, context) when is_float(literal), + def of_expr(literal, _expected_expr, _stack, context) when is_float(literal), do: {float(), context} # "..." - def of_expr(literal, _stack, context) when is_binary(literal), + def of_expr(literal, _expected_expr, _stack, context) when is_binary(literal), do: {binary(), context} # #PID<...> - def of_expr(literal, _stack, context) when is_pid(literal), + def of_expr(literal, _expected_expr, _stack, context) when is_pid(literal), do: {pid(), context} # [] - def of_expr([], _stack, context), + def of_expr([], _expected_expr, _stack, context), do: {empty_list(), context} # [expr, ...] - def of_expr(list, stack, context) when is_list(list) do + # TODO: here + def of_expr(list, _expected_expr, stack, context) when is_list(list) do {prefix, suffix} = unpack_list(list, []) - {prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, stack, &2)) - {suffix, context} = of_expr(suffix, stack, context) + {prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, @expected_expr, stack, &2)) + {suffix, context} = of_expr(suffix, @expected_expr, stack, context) if stack.mode == :traversal do {dynamic(), context} @@ -87,9 +91,10 @@ defmodule Module.Types.Expr do end # {left, right} - def of_expr({left, right}, stack, context) do - {left, context} = of_expr(left, stack, context) - {right, context} = of_expr(right, stack, context) + # TODO: here + def of_expr({left, right}, _expected_expr, stack, context) do + {left, context} = of_expr(left, @expected_expr, stack, context) + {right, context} = of_expr(right, @expected_expr, stack, context) if stack.mode == :traversal do {dynamic(), context} @@ -99,8 +104,9 @@ defmodule Module.Types.Expr do end # {...} - def of_expr({:{}, _meta, exprs}, stack, context) do - {types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, stack, &2)) + # TODO: here + def of_expr({:{}, _meta, exprs}, _expected_expr, stack, context) do + {types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, @expected_expr, stack, &2)) if stack.mode == :traversal do {dynamic(), context} @@ -110,24 +116,27 @@ defmodule Module.Types.Expr do end # <<...>>> - def of_expr({:<<>>, _meta, args}, stack, context) do + # TODO: here (including tests) + def of_expr({:<<>>, _meta, args}, _expected_expr, stack, context) do context = Of.binary(args, :expr, stack, context) {binary(), context} end - def of_expr({:__CALLER__, _meta, var_context}, _stack, context) when is_atom(var_context) do + def of_expr({:__CALLER__, _meta, var_context}, _expected_expr, _stack, context) + when is_atom(var_context) do {@caller, context} end - def of_expr({:__STACKTRACE__, _meta, var_context}, _stack, context) + def of_expr({:__STACKTRACE__, _meta, var_context}, _expected_expr, _stack, context) when is_atom(var_context) do {@stacktrace, context} end # left = right - def of_expr({:=, _, [left_expr, right_expr]} = expr, stack, context) do + # TODO: here + def of_expr({:=, _, [left_expr, right_expr]} = expr, _expected_expr, stack, context) do {left_expr, right_expr} = repack_match(left_expr, right_expr) - {right_type, context} = of_expr(right_expr, stack, context) + {right_type, context} = of_expr(right_expr, @expected_expr, stack, context) # We do not raise on underscore in case someone writes _ = raise "omg" case left_expr do @@ -138,37 +147,39 @@ defmodule Module.Types.Expr do # %{map | ...} # TODO: Once we support typed structs, we need to type check them here. - def of_expr({:%{}, meta, [{:|, _, [map, args]}]} = expr, stack, context) do - {map_type, context} = of_expr(map, stack, context) - - Of.permutate_map(args, stack, context, &of_expr/3, fn fallback, keys, pairs -> - # If there is no fallback (i.e. it is closed), we can update the existing map, - # otherwise we only assert the existing keys. - keys = if fallback == none(), do: keys, else: Enum.map(pairs, &elem(&1, 0)) ++ keys - - # Assert the keys exist - Enum.each(keys, fn key -> - case map_fetch(map_type, key) do - {_, _} -> :ok - :badkey -> throw({:badkey, map_type, key, expr, context}) - :badmap -> throw({:badmap, map_type, expr, context}) - end - end) - - if fallback == none() do - Enum.reduce(pairs, map_type, fn {key, type}, acc -> - case map_fetch_and_put(acc, key, type) do - {_value, descr} -> descr + # TODO: here + def of_expr({:%{}, meta, [{:|, _, [map, args]}]} = expr, _expected_expr, stack, context) do + {map_type, context} = of_expr(map, @expected_expr, stack, context) + + Of.permutate_map(args, stack, context, &of_expr(&1, @expected_expr, &2, &3), fn + fallback, keys, pairs -> + # If there is no fallback (i.e. it is closed), we can update the existing map, + # otherwise we only assert the existing keys. + keys = if fallback == none(), do: keys, else: Enum.map(pairs, &elem(&1, 0)) ++ keys + + # Assert the keys exist + Enum.each(keys, fn key -> + case map_fetch(map_type, key) do + {_, _} -> :ok :badkey -> throw({:badkey, map_type, key, expr, context}) :badmap -> throw({:badmap, map_type, expr, context}) end end) - else - # TODO: Use the fallback type to actually indicate if open or closed. - # The fallback must be unioned with the result of map_values with all - # `keys` deleted. - open_map(pairs) - end + + if fallback == none() do + Enum.reduce(pairs, map_type, fn {key, type}, acc -> + case map_fetch_and_put(acc, key, type) do + {_value, descr} -> descr + :badkey -> throw({:badkey, map_type, key, expr, context}) + :badmap -> throw({:badmap, map_type, expr, context}) + end + end) + else + # TODO: Use the fallback type to actually indicate if open or closed. + # The fallback must be unioned with the result of map_values with all + # `keys` deleted. + open_map(pairs) + end end) catch error -> {error_type(), error(__MODULE__, error, meta, stack, context)} @@ -178,14 +189,16 @@ defmodule Module.Types.Expr do # Note this code, by definition, adds missing struct fields to `map` # because at runtime we do not check for them (only for __struct__ itself). # TODO: Once we support typed structs, we need to type check them here. + # TODO: here def of_expr( {:%, struct_meta, [module, {:%{}, _, [{:|, update_meta, [map, args]}]}]} = expr, + _expected_expr, stack, context ) do {info, context} = Of.struct_info(module, struct_meta, stack, context) struct_type = Of.struct_type(module, info) - {map_type, context} = of_expr(map, stack, context) + {map_type, context} = of_expr(map, @expected_expr, stack, context) if disjoint?(struct_type, map_type) do warning = {:badstruct, expr, struct_type, map_type, context} @@ -195,45 +208,47 @@ defmodule Module.Types.Expr do Enum.reduce(args, {map_type, context}, fn {key, value}, {map_type, context} when is_atom(key) -> - {value_type, context} = of_expr(value, stack, context) + {value_type, context} = of_expr(value, @expected_expr, stack, context) {map_put!(map_type, key, value_type), context} end) end end # %{...} - def of_expr({:%{}, _meta, args}, stack, context) do - Of.closed_map(args, stack, context, &of_expr/3) + # TODO: here + def of_expr({:%{}, _meta, args}, _expected_expr, stack, context) do + Of.closed_map(args, stack, context, &of_expr(&1, @expected_expr, &2, &3)) end # %Struct{} - def of_expr({:%, meta, [module, {:%{}, _, args}]}, stack, context) do - Of.struct_instance(module, args, meta, stack, context, &of_expr/3) + # TODO: here + def of_expr({:%, meta, [module, {:%{}, _, args}]}, _expected_expr, stack, context) do + Of.struct_instance(module, args, meta, stack, context, &of_expr(&1, @expected_expr, &2, &3)) end # () - def of_expr({:__block__, _meta, []}, _stack, context) do + def of_expr({:__block__, _meta, []}, _expected_expr, _stack, context) do {atom([nil]), context} end # (expr; expr) - def of_expr({:__block__, _meta, exprs}, stack, context) do + def of_expr({:__block__, _meta, exprs}, expected_expr, stack, context) do {pre, [post]} = Enum.split(exprs, -1) context = Enum.reduce(pre, context, fn expr, context -> - {_, context} = of_expr(expr, stack, context) + {_, context} = of_expr(expr, @term_expected, stack, context) context end) - of_expr(post, stack, context) + of_expr(post, expected_expr, stack, context) end - def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do + def of_expr({:cond, _meta, [[{:do, clauses}]]}, expected_expr, stack, context) do clauses |> reduce_non_empty({none(), context}, fn {:->, meta, [[head], body]}, {acc, context}, last? -> - {head_type, context} = of_expr(head, stack, context) + {head_type, context} = of_expr(head, @term_expected, stack, context) context = if stack.mode in [:infer, :traversal] do @@ -253,14 +268,15 @@ defmodule Module.Types.Expr do end end - {body_type, context} = of_expr(body, stack, context) + {body_type, context} = of_expr(body, expected_expr, stack, context) {union(body_type, acc), context} end) |> dynamic_unless_static(stack) end - def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, stack, context) do - {case_type, context} = of_expr(case_expr, stack, context) + # TODO: here + def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, _expected_expr, stack, context) do + {case_type, context} = of_expr(case_expr, @term_expected, stack, context) # If we are only type checking the expression and the expression is a literal, # let's mark it as generated, as it is most likely a macro code. However, if @@ -275,7 +291,8 @@ defmodule Module.Types.Expr do end # TODO: fn pat -> expr end - def of_expr({:fn, _meta, clauses}, stack, context) do + # TODO: here + def of_expr({:fn, _meta, clauses}, _expected_expr, stack, context) do [{:->, _, [head, _]} | _] = clauses {patterns, _guards} = extract_head(head) expected = Enum.map(patterns, fn _ -> dynamic() end) @@ -283,8 +300,9 @@ defmodule Module.Types.Expr do {fun(), context} end - def of_expr({:try, _meta, [[do: body] ++ blocks]}, stack, context) do - {body_type, context} = of_expr(body, stack, context) + # TODO: here + def of_expr({:try, _meta, [[do: body] ++ blocks]}, _expected_expr, stack, context) do + {body_type, context} = of_expr(body, @expected_expr, stack, context) initial = if Keyword.has_key?(blocks, :else), do: none(), else: body_type blocks @@ -302,7 +320,7 @@ defmodule Module.Types.Expr do end) {:after, body}, {acc, context} -> - {_type, context} = of_expr(body, stack, context) + {_type, context} = of_expr(body, @expected_expr, stack, context) {acc, context} {:catch, clauses}, acc_context -> @@ -314,7 +332,8 @@ defmodule Module.Types.Expr do |> dynamic_unless_static(stack) end - def of_expr({:receive, _meta, [blocks]}, stack, context) do + # TODO: here + def of_expr({:receive, _meta, [blocks]}, expected_expr, stack, context) do blocks |> Enum.reduce({none(), context}, fn {:do, {:__block__, _, []}}, acc_context -> @@ -323,9 +342,9 @@ defmodule Module.Types.Expr do {:do, clauses}, acc_context -> of_clauses(clauses, [dynamic()], :receive, stack, acc_context) - {:after, [{:->, meta, [[timeout], body]}]}, {acc, context} -> - {timeout_type, context} = of_expr(timeout, stack, context) - {body_type, context} = of_expr(body, stack, context) + {:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} -> + {timeout_type, context} = of_expr(timeout, {integer(), after_expr}, stack, context) + {body_type, context} = of_expr(body, expected_expr, stack, context) if integer_type?(timeout_type) do {union(body_type, acc), context} @@ -338,7 +357,8 @@ defmodule Module.Types.Expr do end # TODO: for pat <- expr do expr end - def of_expr({:for, meta, [_ | _] = args}, stack, context) do + # TODO: here + def of_expr({:for, meta, [_ | _] = args}, _expected_expr, stack, context) do {clauses, [[{:do, block} | opts]]} = Enum.split(args, -1) context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2)) @@ -346,14 +366,14 @@ defmodule Module.Types.Expr do # We handle reduce and into accordingly instead. if Keyword.has_key?(opts, :reduce) do reduce = Keyword.fetch!(opts, :reduce) - {reduce_type, context} = of_expr(reduce, stack, context) + {reduce_type, context} = of_expr(reduce, @expected_expr, stack, context) # TODO: We need to type check against dynamic() instead of using reduce_type # because this is recursive. We need to infer the block type first. of_clauses(block, [dynamic()], :for_reduce, stack, {reduce_type, context}) else into = Keyword.get(opts, :into, []) {into_wrapper, gradual?, context} = for_into(into, meta, stack, context) - {block_type, context} = of_expr(block, stack, context) + {block_type, context} = of_expr(block, @expected_expr, stack, context) for_type = for type <- into_wrapper do @@ -370,7 +390,8 @@ defmodule Module.Types.Expr do end # TODO: with pat <- expr do expr end - def of_expr({:with, _meta, [_ | _] = clauses}, stack, context) do + # TODO: here + def of_expr({:with, _meta, [_ | _] = clauses}, _expected_expr, stack, context) do {clauses, [options]} = Enum.split(clauses, -1) context = Enum.reduce(clauses, context, &with_clause(&1, stack, &2)) context = Enum.reduce(options, context, &with_option(&1, stack, &2)) @@ -378,9 +399,11 @@ defmodule Module.Types.Expr do end # TODO: fun.(args) - def of_expr({{:., meta, [fun]}, _meta, args} = call, stack, context) do - {fun_type, context} = of_expr(fun, stack, context) - {_args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2)) + def of_expr({{:., meta, [fun]}, _meta, args} = call, _expected_expr, stack, context) do + {fun_type, context} = of_expr(fun, {fun(), call}, stack, context) + + {_args_types, context} = + Enum.map_reduce(args, context, &of_expr(&1, @expected_expr, stack, &2)) case fun_fetch(fun_type, length(args)) do :ok -> @@ -392,22 +415,29 @@ defmodule Module.Types.Expr do end end - def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = expr, stack, context) + # TODO: here + def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = expr, _expected_expr, stack, context) when not is_atom(callee) and is_atom(key_or_fun) do - {type, context} = of_expr(callee, stack, context) - if Keyword.get(meta, :no_parens, false) do + {type, context} = of_expr(callee, {open_map([{key_or_fun, term()}]), expr}, stack, context) Of.map_fetch(expr, type, key_or_fun, stack, context) else + {type, context} = of_expr(callee, {atom(), expr}, stack, context) {mods, context} = Of.modules(type, key_or_fun, 0, [:dot], expr, meta, stack, context) apply_many(mods, key_or_fun, [], [], expr, stack, context) end end - def of_expr({{:., _, [remote, :apply]}, _meta, [mod, fun, args]} = expr, stack, context) + # TODO: here + def of_expr( + {{:., _, [remote, :apply]}, _meta, [mod, fun, args]} = expr, + _expected_expr, + stack, + context + ) when remote in [Kernel, :erlang] and is_list(args) do - {mod_type, context} = of_expr(mod, stack, context) - {fun_type, context} = of_expr(fun, stack, context) + {mod_type, context} = of_expr(mod, {atom(), expr}, stack, context) + {fun_type, context} = of_expr(fun, {atom(), expr}, stack, context) improper_list? = Enum.any?(args, &match?({:|, _, [_, _]}, &1)) case atom_fetch(fun_type) do @@ -418,7 +448,8 @@ defmodule Module.Types.Expr do _ -> [] end - {args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2)) + {args_types, context} = + Enum.map_reduce(args, context, &of_expr(&1, @expected_expr, stack, &2)) {types, context} = Enum.map_reduce(funs, context, fn fun, context -> @@ -428,15 +459,19 @@ defmodule Module.Types.Expr do {Enum.reduce(types, &union/2), context} _ -> - {args_type, context} = of_expr(args, stack, context) + {args_type, context} = of_expr(args, {list(term()), expr}, stack, context) args_types = [mod_type, fun_type, args_type] Apply.remote(:erlang, :apply, [mod, fun, args], args_types, expr, stack, context) end end - def of_expr({{:., _, [remote, name]}, meta, args} = expr, stack, context) do - {remote_type, context} = of_expr(remote, stack, context) - {args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2)) + # TODO: here + def of_expr({{:., _, [remote, name]}, meta, args} = expr, _expected_expr, stack, context) do + {remote_type, context} = of_expr(remote, {atom(), expr}, stack, context) + + {args_types, context} = + Enum.map_reduce(args, context, &of_expr(&1, @expected_expr, stack, &2)) + {mods, context} = Of.modules(remote_type, name, length(args), expr, meta, stack, context) apply_many(mods, name, args, args_types, expr, stack, context) end @@ -444,40 +479,49 @@ defmodule Module.Types.Expr do # TODO: &Foo.bar/1 def of_expr( {:&, _, [{:/, _, [{{:., _, [remote, name]}, meta, []}, arity]}]} = expr, + _expected_expr, stack, context ) when is_atom(name) and is_integer(arity) do - {remote_type, context} = of_expr(remote, stack, context) + {remote_type, context} = of_expr(remote, {atom(), expr}, stack, context) {mods, context} = Of.modules(remote_type, name, arity, expr, meta, stack, context) Apply.remote_capture(mods, name, arity, meta, stack, context) end # TODO: &foo/1 - def of_expr({:&, _meta, [{:/, _, [{fun, meta, _}, arity]}]}, stack, context) do + def of_expr({:&, _meta, [{:/, _, [{fun, meta, _}, arity]}]}, _expected_expr, stack, context) do Apply.local_capture(fun, arity, meta, stack, context) end # Super - def of_expr({:super, meta, args} = expr, stack, context) when is_list(args) do + # TODO: here + def of_expr({:super, meta, args} = expr, _expected_expr, stack, context) when is_list(args) do {_kind, fun} = Keyword.fetch!(meta, :super) - {args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2)) + + {args_types, context} = + Enum.map_reduce(args, context, &of_expr(&1, @expected_expr, stack, &2)) + Apply.local(fun, args_types, expr, stack, context) end # Local calls - def of_expr({fun, _meta, args} = expr, stack, context) + # TODO: here + def of_expr({fun, _meta, args} = expr, _expected_expr, stack, context) when is_atom(fun) and is_list(args) do - {args_types, context} = Enum.map_reduce(args, context, &of_expr(&1, stack, &2)) + {args_types, context} = + Enum.map_reduce(args, context, &of_expr(&1, @expected_expr, stack, &2)) + Apply.local(fun, args_types, expr, stack, context) end # var - def of_expr(var, stack, context) when is_var(var) do + # TODO: here + def of_expr(var, {expected, expr}, stack, context) when is_var(var) do if stack.mode == :traversal do {dynamic(), context} else - {Of.var(var, context), context} + Of.refine_existing_var(var, expected, expr, stack, context) end end @@ -513,7 +557,7 @@ defmodule Module.Types.Expr do context end - of_expr(body, stack, context) + of_expr(body, @expected_expr, stack, context) end ## Comprehensions @@ -521,7 +565,7 @@ defmodule Module.Types.Expr do defp for_clause({:<-, meta, [left, right]}, stack, context) do expr = {:<-, [type_check: :generator] ++ meta, [left, right]} {pattern, guards} = extract_head([left]) - {type, context} = of_expr(right, stack, context) + {type, context} = of_expr(right, @expected_expr, stack, context) {_type, context} = Pattern.of_match(pattern, guards, dynamic(), expr, :for, stack, context) @@ -533,7 +577,7 @@ defmodule Module.Types.Expr do end defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do - {right_type, context} = of_expr(right, stack, context) + {right_type, context} = of_expr(right, {binary(), expr}, stack, context) {_pattern_type, context} = Pattern.of_match(left, binary(), expr, :for, stack, context) @@ -547,7 +591,7 @@ defmodule Module.Types.Expr do end defp for_clause(expr, stack, context) do - {_type, context} = of_expr(expr, stack, context) + {_type, context} = of_expr(expr, @term_expected, stack, context) context end @@ -561,7 +605,7 @@ defmodule Module.Types.Expr do # TODO: Use the collectable protocol for the output defp for_into(into, meta, stack, context) do - {type, context} = of_expr(into, stack, context) + {type, context} = of_expr(into, @expected_expr, stack, context) if subtype?(type, @into_compile) do case {binary_type?(type), empty_list_type?(type)} do @@ -587,17 +631,17 @@ defmodule Module.Types.Expr do defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do {pattern, guards} = extract_head([left]) {_type, context} = Pattern.of_match(pattern, guards, dynamic(), expr, :with, stack, context) - {_, context} = of_expr(right, stack, context) + {_, context} = of_expr(right, @expected_expr, stack, context) context end defp with_clause(expr, stack, context) do - {_type, context} = of_expr(expr, stack, context) + {_type, context} = of_expr(expr, @expected_expr, stack, context) context end defp with_option({:do, body}, stack, context) do - {_type, context} = of_expr(body, stack, context) + {_type, context} = of_expr(body, @expected_expr, stack, context) context end @@ -635,13 +679,13 @@ defmodule Module.Types.Expr do defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context} defp of_clauses(clauses, expected, info, %{mode: mode} = stack, {acc, context}) do - %{failed: failed?} = context + %{failed: failed?, vars: vars} = context Enum.reduce(clauses, {acc, context}, fn {:->, meta, [head, body]}, {acc, context} -> - {failed?, context} = reset_failed(context, failed?) + {failed?, context} = reset_context(context, vars, failed?) {patterns, guards} = extract_head(head) {_types, context} = Pattern.of_head(patterns, guards, expected, info, meta, stack, context) - {body, context} = of_expr(body, stack, context) + {body, context} = of_expr(body, @expected_expr, stack, context) context = set_failed(context, failed?) if mode == :traversal do @@ -652,8 +696,11 @@ defmodule Module.Types.Expr do end) end - defp reset_failed(%{failed: true} = context, false), do: {true, %{context | failed: false}} - defp reset_failed(context, _), do: {false, context} + defp reset_context(%{failed: true} = context, vars, false), + do: {true, %{context | failed: false, vars: vars}} + + defp reset_context(context, vars, _), + do: {false, %{context | vars: vars}} defp set_failed(%{failed: false} = context, true), do: %{context | failed: true} defp set_failed(context, _bool), do: context diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index c356ecbfcd4..d5a407247f9 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -41,6 +41,39 @@ defmodule Module.Types.Of do put_in(context.vars[version], data) end + @doc """ + Refines a variable that already exists. + + This only happens if the var contains a gradual type, + or if we are doing a guard analysis or occurrence typing. + Returns `true` if there was a refinement, `false` otherwise. + """ + def refine_existing_var({_, meta, _}, type, expr, stack, context) do + version = Keyword.fetch!(meta, :version) + %{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context + + context = + if gradual?(old_type) and type not in [term(), dynamic()] do + case compatible_intersection(old_type, type) do + {:ok, new_type} when new_type != old_type -> + data = %{ + data + | type: new_type, + off_traces: new_trace(expr, new_type, :default, stack, off_traces) + } + + %{context | vars: %{vars | version => data}} + + _ -> + context + end + else + context + end + + {old_type, context} + end + @doc """ Refines the type of a variable. """ @@ -49,7 +82,7 @@ defmodule Module.Types.Of do version = Keyword.fetch!(meta, :version) case context.vars do - %{^version => %{type: old_type, off_traces: off_traces} = data} -> + %{^version => %{type: old_type, off_traces: off_traces} = data} = vars -> new_type = intersection(type, old_type) data = %{ @@ -58,7 +91,7 @@ defmodule Module.Types.Of do off_traces: new_trace(expr, type, formatter, stack, off_traces) } - context = put_in(context.vars[version], data) + context = %{context | vars: %{vars | version => data}} # We need to return error otherwise it leads to cascading errors if empty?(new_type) do @@ -68,7 +101,7 @@ defmodule Module.Types.Of do {:ok, new_type, context} end - %{} -> + %{} = vars -> data = %{ type: type, name: var_name, @@ -76,7 +109,7 @@ defmodule Module.Types.Of do off_traces: new_trace(expr, type, formatter, stack, []) } - context = put_in(context.vars[version], data) + context = %{context | vars: Map.put(vars, version, data)} {:ok, type, context} end end @@ -328,7 +361,7 @@ defmodule Module.Types.Of do :expr -> left = annotate_interpolation(left, right) - {actual, context} = Module.Types.Expr.of_expr(left, stack, context) + {actual, context} = Module.Types.Expr.of_expr(left, {type, expr}, stack, context) intersect(actual, type, expr, stack, context) end @@ -368,7 +401,7 @@ defmodule Module.Types.Of do defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context) when not is_integer(arg) do - {actual, context} = Module.Types.Expr.of_expr(arg, stack, context) + {actual, context} = Module.Types.Expr.of_expr(arg, {integer(), expr}, stack, context) {_, context} = intersect(actual, integer(), expr, stack, context) context end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 400599f358e..ee8ca34902c 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -324,7 +324,8 @@ defmodule Module.Types.Pattern do and binary patterns. """ def of_match_var({:^, _, [var]}, expected, expr, stack, context) do - Of.intersect(Of.var(var, context), expected, expr, stack, context) + {type, context} = Of.refine_existing_var(var, expected, expr, stack, context) + Of.intersect(type, expected, expr, stack, context) end def of_match_var({:_, _, _}, expected, _expr, _stack, context) do @@ -687,7 +688,8 @@ defmodule Module.Types.Pattern do # ^var def of_guard({:^, _meta, [var]}, expected, expr, stack, context) do # This is by definition a variable defined outside of this pattern, so we don't track it. - Of.intersect(Of.var(var, context), expected, expr, stack, context) + {type, context} = Of.refine_existing_var(var, expected, expr, stack, context) + Of.intersect(type, expected, expr, stack, context) end # {...} diff --git a/lib/elixir/pages/references/gradual-set-theoretic-types.md b/lib/elixir/pages/references/gradual-set-theoretic-types.md index ab8a0abcb95..f433031ea2b 100644 --- a/lib/elixir/pages/references/gradual-set-theoretic-types.md +++ b/lib/elixir/pages/references/gradual-set-theoretic-types.md @@ -96,7 +96,7 @@ On the other hand, type inference offers the benefit of enabling type checking f * Type inference of patterns (and guards in future releases) - the argument types of a function are automatically inferred based on patterns and guards, which capture and narrow types based on common Elixir constructs. - * Module-local inference of return types - the gradual return types of functions are computed considering all of the functions within the module itself. Any call to a function in another module is conservatively assumed to return `dynamic()`. + * Module-local inference of return types - the gradual return types of functions are computed considering all of the functions within the module itself. Any call to a function in another module is conservatively assumed to return `dynamic()` during inference. The last two items offer gradual reconstruction of type signatures. Our goal is to provide an efficient type reconstruction algorithm that can detect definite bugs in dynamic codebases, even in the absence of explicit type annotations. The gradual system focuses on proving cases where all combinations of a type *will* fail, rather than issuing warnings for cases where some combinations *might* error. diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 2d814672a13..90635b5071d 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -1198,6 +1198,10 @@ defmodule Module.Types.DescrTest do test "dynamic" do assert dynamic() |> to_quoted_string() == "dynamic()" + + assert dynamic(union(atom(), integer())) |> union(integer()) |> to_quoted_string() == + "dynamic(atom()) or integer()" + assert intersection(binary(), dynamic()) |> to_quoted_string() == "binary()" assert intersection(union(binary(), pid()), dynamic()) |> to_quoted_string() == diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 6b5ec1a8043..4b68c5b9583 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -115,6 +115,16 @@ defmodule Module.Types.ExprTest do end describe "funs" do + test "infers funs" do + assert typecheck!( + [x], + ( + x.(1, 2) + x + ) + ) == dynamic(fun()) + end + test "incompatible" do assert typeerror!([%x{}, a1, a2], x.(a1, a2)) == ~l""" expected a 2-arity function on call: @@ -139,6 +149,43 @@ defmodule Module.Types.ExprTest do assert typecheck!([%x{}], x.foo_bar()) == dynamic() end + test "infers atoms" do + assert typecheck!( + [x], + ( + x.foo_bar() + x + ) + ) == dynamic(atom()) + + assert typecheck!( + [x], + ( + x.foo_bar(123) + x + ) + ) == dynamic(atom()) + + assert typecheck!( + [x], + ( + &x.foo_bar/1 + x + ) + ) == dynamic(atom()) + end + + test "infers maps" do + assert typecheck!( + [x], + ( + x.foo_bar + x.baz_bat + x + ) + ) == dynamic(open_map(foo_bar: term(), baz_bat: term())) + end + test "undefined function warnings" do assert typewarn!(URI.unknown("foo")) == {dynamic(), "URI.unknown/1 is undefined or private"} @@ -215,7 +262,7 @@ defmodule Module.Types.ExprTest do """ assert typeerror!( - [<>, y = Atom, z], + [<>, y = SomeMod, z], ( mod = cond do @@ -233,7 +280,7 @@ defmodule Module.Types.ExprTest do where "mod" was given the type: - # type: dynamic(Atom or integer()) or integer() + # type: dynamic(SomeMod) or integer() # from: types_test.ex:LINE-9 mod = cond do @@ -1433,6 +1480,16 @@ defmodule Module.Types.ExprTest do """ end + test "infers binary generators" do + assert typecheck!( + [x], + ( + for <<_ <- x>>, do: :ok + x + ) + ) == dynamic(binary()) + end + test ":into" do assert typecheck!([binary], for(<>, do: x)) == list(integer()) assert typecheck!([binary], for(<>, do: x, into: [])) == list(integer()) diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index 28558fc4ae2..326c67dd729 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -287,6 +287,16 @@ defmodule Module.Types.PatternTest do """ end + test "pin inference" do + assert typecheck!( + [x, y], + ( + <<^x>> = y + x + ) + ) == dynamic(integer()) + end + test "size ok" do assert typecheck!([<>], :ok) == atom([:ok]) end @@ -313,5 +323,15 @@ defmodule Module.Types.PatternTest do <> """ end + + test "size pin inference" do + assert typecheck!( + [x, y], + ( + <<_::size(^x)>> = y + x + ) + ) == dynamic(integer()) + end 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 4ecd03ef87f..d05e3331e2d 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -116,12 +116,12 @@ defmodule TypeHelper do def __typecheck__(mode, patterns, guards, body) do stack = new_stack(mode) - expected = Enum.map(patterns, fn _ -> Module.Types.Descr.dynamic() end) + expected = Enum.map(patterns, fn _ -> Descr.dynamic() end) {_types, context} = Pattern.of_head(patterns, guards, expected, :default, [], stack, new_context()) - Expr.of_expr(body, stack, context) + Expr.of_expr(body, {Descr.term(), :ok}, stack, context) end defp expand_and_unpack(patterns, guards, body, env) do diff --git a/lib/mix/lib/mix/tasks/local.hex.ex b/lib/mix/lib/mix/tasks/local.hex.ex index 2d43db67554..82f4bd4ad36 100644 --- a/lib/mix/lib/mix/tasks/local.hex.ex +++ b/lib/mix/lib/mix/tasks/local.hex.ex @@ -37,6 +37,7 @@ defmodule Mix.Tasks.Local.Hex do used for fetching Hex, set the `HEX_BUILDS_URL` environment variable. """ @switches [if_missing: :boolean, force: :boolean] + @compile {:no_warn_undefined, {Hex, :version, 0}} @impl true def run(argv) do @@ -46,8 +47,7 @@ defmodule Mix.Tasks.Local.Hex do should_install? = if Keyword.get(opts, :if_missing, false) do if version do - not Code.ensure_loaded?(Hex) or - Version.compare(apply(Hex, :version, []), version) == :gt + not Code.ensure_loaded?(Hex) or Version.compare(Hex.version(), version) == :gt else not Code.ensure_loaded?(Hex) end From d961fefcaa5781ec5466e1db091d1a4fafe7f885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 2 Jan 2025 18:19:55 +0100 Subject: [PATCH 02/22] Store inferred expression types on byte code --- lib/elixir/lib/module/types.ex | 13 +- lib/elixir/lib/module/types/expr.ex | 140 ++++++++++-------- lib/elixir/lib/module/types/pattern.ex | 65 ++++---- .../test/elixir/module/types/expr_test.exs | 63 ++++++++ .../test/elixir/module/types/infer_test.exs | 16 ++ .../test/elixir/module/types/type_helper.exs | 2 +- 6 files changed, 207 insertions(+), 92 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 6080d67bcc7..e869ad35f68 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -281,12 +281,19 @@ defmodule Module.Types do context = fresh_context(context) try do - {args_types, context} = + {trees, context} = Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context) {return_type, context} = Expr.of_expr(body, {Descr.term(), :ok}, stack, context) + args_types = + if stack.mode == :traversal do + expected + else + Pattern.of_domain(trees, expected, context) + end + {type_index, inferred} = add_inferred(inferred, args_types, return_type, total - 1, []) @@ -302,7 +309,7 @@ defmodule Module.Types do end) inferred = {:infer, Enum.reverse(clauses_types)} - {inferred, mapping, restore_context(context, clauses_context)} + {inferred, mapping, restore_context(clauses_context, context)} end # We check for term equality of types as an optimization @@ -399,7 +406,7 @@ defmodule Module.Types do %{context | vars: %{}, failed: false} end - defp restore_context(%{vars: vars, failed: failed}, later_context) do + defp restore_context(later_context, %{vars: vars, failed: failed}) do %{later_context | vars: vars, failed: failed} end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 78fec42e4a0..72c1484d5b3 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -139,10 +139,13 @@ defmodule Module.Types.Expr do {right_type, context} = of_expr(right_expr, @expected_expr, stack, context) # We do not raise on underscore in case someone writes _ = raise "omg" - case left_expr do - {:_, _, ctx} when is_atom(ctx) -> {right_type, context} - _ -> Pattern.of_match(left_expr, right_type, expr, {:match, right_type}, stack, context) - end + context = + case left_expr do + {:_, _, ctx} when is_atom(ctx) -> context + _ -> Pattern.of_match(left_expr, right_type, expr, {:match, right_type}, stack, context) + end + + {right_type, context} end # %{map | ...} @@ -244,9 +247,9 @@ defmodule Module.Types.Expr do of_expr(post, expected_expr, stack, context) end - def of_expr({:cond, _meta, [[{:do, clauses}]]}, expected_expr, stack, context) do + def of_expr({:cond, _meta, [[{:do, clauses}]]}, expected_expr, stack, original) do clauses - |> reduce_non_empty({none(), context}, fn + |> reduce_non_empty({none(), original}, fn {:->, meta, [[head], body]}, {acc, context}, last? -> {head_type, context} = of_expr(head, @term_expected, stack, context) @@ -269,7 +272,7 @@ defmodule Module.Types.Expr do end {body_type, context} = of_expr(body, expected_expr, stack, context) - {union(body_type, acc), context} + {union(body_type, acc), reset_vars(context, original)} end) |> dynamic_unless_static(stack) end @@ -301,41 +304,52 @@ defmodule Module.Types.Expr do end # TODO: here - def of_expr({:try, _meta, [[do: body] ++ blocks]}, _expected_expr, stack, context) do - {body_type, context} = of_expr(body, @expected_expr, stack, context) - initial = if Keyword.has_key?(blocks, :else), do: none(), else: body_type - - blocks - |> Enum.reduce({initial, context}, fn - {:rescue, clauses}, acc_context -> - Enum.reduce(clauses, acc_context, fn - {:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} -> - {type, context} = of_rescue(var, exceptions, body, expr, [], meta, stack, context) - {union(type, acc), context} - - {:->, meta, [[var], body]}, {acc, context} -> - hint = [:anonymous_rescue] - {type, context} = of_rescue(var, [], body, var, hint, meta, stack, context) - {union(type, acc), context} - end) + def of_expr({:try, _meta, [[do: body] ++ blocks]}, _expected_expr, stack, original) do + {type, context} = of_expr(body, @expected_expr, stack, original) + {after_block, blocks} = Keyword.pop(blocks, :after) + {else_block, blocks} = Keyword.pop(blocks, :else) + + {type, context} = + if else_block do + of_clauses(else_block, [type], {:try_else, type}, stack, {none(), context}) + else + {type, context} + end - {:after, body}, {acc, context} -> - {_type, context} = of_expr(body, @expected_expr, stack, context) - {acc, context} + {type, context} = + blocks + |> Enum.reduce({type, reset_vars(context, original)}, fn + {:rescue, clauses}, acc_context -> + Enum.reduce(clauses, acc_context, fn + {:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} -> + {type, context} = of_rescue(var, exceptions, body, expr, [], meta, stack, context) + {union(type, acc), context} + + {:->, meta, [[var], body]}, {acc, context} -> + hint = [:anonymous_rescue] + {type, context} = of_rescue(var, [], body, var, hint, meta, stack, context) + {union(type, acc), context} + end) - {:catch, clauses}, acc_context -> - of_clauses(clauses, [@try_catch, dynamic()], :try_catch, stack, acc_context) + {:catch, clauses}, {acc, context} -> + of_clauses(clauses, [@try_catch, dynamic()], :try_catch, stack, {acc, context}) + end) + |> dynamic_unless_static(stack) - {:else, clauses}, acc_context -> - of_clauses(clauses, [body_type], {:try_else, body_type}, stack, acc_context) - end) - |> dynamic_unless_static(stack) + if after_block do + {_type, context} = of_expr(after_block, @expected_expr, stack, context) + {type, context} + else + {type, context} + end end + @timeout_type union(integer(), atom([:infinity])) + # TODO: here - def of_expr({:receive, _meta, [blocks]}, expected_expr, stack, context) do + def of_expr({:receive, _meta, [blocks]}, expected_expr, stack, original) do blocks - |> Enum.reduce({none(), context}, fn + |> Enum.reduce({none(), original}, fn {:do, {:__block__, _, []}}, acc_context -> acc_context @@ -343,11 +357,11 @@ defmodule Module.Types.Expr do of_clauses(clauses, [dynamic()], :receive, stack, acc_context) {:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} -> - {timeout_type, context} = of_expr(timeout, {integer(), after_expr}, stack, context) + {timeout_type, context} = of_expr(timeout, {@timeout_type, after_expr}, stack, context) {body_type, context} = of_expr(body, expected_expr, stack, context) - if integer_type?(timeout_type) do - {union(body_type, acc), context} + if compatible?(timeout_type, @timeout_type) do + {union(body_type, acc), reset_vars(context, original)} else error = {:badtimeout, timeout_type, timeout, context} {union(body_type, acc), error(__MODULE__, error, meta, stack, context)} @@ -391,10 +405,10 @@ defmodule Module.Types.Expr do # TODO: with pat <- expr do expr end # TODO: here - def of_expr({:with, _meta, [_ | _] = clauses}, _expected_expr, stack, context) do + def of_expr({:with, _meta, [_ | _] = clauses}, _expected_expr, stack, original) do {clauses, [options]} = Enum.split(clauses, -1) - context = Enum.reduce(clauses, context, &with_clause(&1, stack, &2)) - context = Enum.reduce(options, context, &with_option(&1, stack, &2)) + context = Enum.reduce(clauses, original, &with_clause(&1, stack, &2)) + context = Enum.reduce(options, context, &with_option(&1, stack, &2, original)) {dynamic(), context} end @@ -527,11 +541,11 @@ defmodule Module.Types.Expr do ## Try - defp of_rescue(var, exceptions, body, expr, hints, meta, stack, context) do + defp of_rescue(var, exceptions, body, expr, hints, meta, stack, original) do args = [__exception__: @atom_true] {structs, context} = - Enum.map_reduce(exceptions, context, fn exception, context -> + Enum.map_reduce(exceptions, original, fn exception, context -> # Exceptions are not validated in the compiler, # to avoid export dependencies. So we do it here. if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do @@ -557,7 +571,8 @@ defmodule Module.Types.Expr do context end - of_expr(body, @expected_expr, stack, context) + {type, context} = of_expr(body, @expected_expr, stack, context) + {type, reset_vars(context, original)} end ## Comprehensions @@ -567,8 +582,7 @@ defmodule Module.Types.Expr do {pattern, guards} = extract_head([left]) {type, context} = of_expr(right, @expected_expr, stack, context) - {_type, context} = - Pattern.of_match(pattern, guards, dynamic(), expr, :for, stack, context) + context = Pattern.of_match(pattern, guards, dynamic(), expr, :for, stack, context) {_type, context} = Apply.remote(Enumerable, :count, [right], [type], expr, stack, context) @@ -579,8 +593,7 @@ defmodule Module.Types.Expr do defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do {right_type, context} = of_expr(right, {binary(), expr}, stack, context) - {_pattern_type, context} = - Pattern.of_match(left, binary(), expr, :for, stack, context) + context = Pattern.of_match(left, binary(), expr, :for, stack, context) if binary_type?(right_type) do context @@ -630,7 +643,7 @@ defmodule Module.Types.Expr do defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do {pattern, guards} = extract_head([left]) - {_type, context} = Pattern.of_match(pattern, guards, dynamic(), expr, :with, stack, context) + context = Pattern.of_match(pattern, guards, dynamic(), expr, :with, stack, context) {_, context} = of_expr(right, @expected_expr, stack, context) context end @@ -640,12 +653,12 @@ defmodule Module.Types.Expr do context end - defp with_option({:do, body}, stack, context) do + defp with_option({:do, body}, stack, context, original) do {_type, context} = of_expr(body, @expected_expr, stack, context) - context + reset_vars(context, original) end - defp with_option({:else, clauses}, stack, context) do + defp with_option({:else, clauses}, stack, context, _original) do {_, context} = of_clauses(clauses, [dynamic()], :with_else, stack, {none(), context}) context end @@ -678,15 +691,17 @@ defmodule Module.Types.Expr do defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context} - defp of_clauses(clauses, expected, info, %{mode: mode} = stack, {acc, context}) do - %{failed: failed?, vars: vars} = context + defp of_clauses(clauses, expected, info, %{mode: mode} = stack, {acc, original}) do + %{failed: failed?} = original - Enum.reduce(clauses, {acc, context}, fn {:->, meta, [head, body]}, {acc, context} -> - {failed?, context} = reset_context(context, vars, failed?) + Enum.reduce(clauses, {acc, original}, fn {:->, meta, [head, body]}, {acc, context} -> + {failed?, context} = reset_failed(context, failed?) {patterns, guards} = extract_head(head) - {_types, context} = Pattern.of_head(patterns, guards, expected, info, meta, stack, context) + + {_trees, context} = Pattern.of_head(patterns, guards, expected, info, meta, stack, context) + {body, context} = of_expr(body, @expected_expr, stack, context) - context = set_failed(context, failed?) + context = context |> set_failed(failed?) |> reset_vars(original) if mode == :traversal do {dynamic(), context} @@ -696,15 +711,14 @@ defmodule Module.Types.Expr do end) end - defp reset_context(%{failed: true} = context, vars, false), - do: {true, %{context | failed: false, vars: vars}} - - defp reset_context(context, vars, _), - do: {false, %{context | vars: vars}} + defp reset_failed(%{failed: true} = context, false), do: {true, %{context | failed: false}} + defp reset_failed(context, _), do: {false, context} defp set_failed(%{failed: false} = context, true), do: %{context | failed: true} defp set_failed(context, _bool), do: context + defp reset_vars(context, %{vars: vars}), do: %{context | vars: vars} + defp extract_head([{:when, _meta, args}]) do case Enum.split(args, -1) do {patterns, [guards]} -> {patterns, flatten_when(guards)} diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index ee8ca34902c..3e022ff1752 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -25,32 +25,44 @@ defmodule Module.Types.Pattern do is refined, we restart at step 2. """ - def of_head(_patterns, _guards, expected, _tag, _meta, %{mode: :traversal}, context) do - {expected, context} + def of_head(patterns, _guards, _expected, _tag, _meta, %{mode: :traversal}, context) do + term = term() + {Enum.map(patterns, &{&1, term}), context} end def of_head(patterns, guards, expected, tag, meta, stack, context) do stack = %{stack | meta: meta} - {_trees, types, context} = of_pattern_args(patterns, expected, tag, stack, context) + {trees, context} = of_pattern_args(patterns, expected, tag, stack, context) {_, context} = Enum.map_reduce(guards, context, &of_guard(&1, @guard, &1, stack, &2)) - {types, context} + {trees, context} + end + + @doc """ + Computes the domain from the pattern tree and expected types. + """ + def of_domain([{_pattern, tree} | trees], [type | expected], context) do + [intersection(of_pattern_tree(tree, context), type) | of_domain(trees, expected, context)] + end + + def of_domain([], [], _context) do + [] end defp of_pattern_args([], [], _tag, _stack, context) do - {[], [], context} + {[], context} end defp of_pattern_args(patterns, expected, tag, stack, context) do context = init_pattern_info(context) {trees, context} = of_pattern_args_index(patterns, 0, [], stack, context) - {types, context} = + context = of_pattern_recur(expected, tag, stack, context, fn types, changed, context -> of_pattern_args_tree(trees, types, changed, 0, [], tag, stack, context) end) - {trees, types, context} + {trees, context} end defp of_pattern_args_index([pattern | tail], index, acc, stack, context) do @@ -105,15 +117,15 @@ defmodule Module.Types.Pattern do """ def of_match(pattern, guards \\ [], expected, expr, tag, stack, context) - def of_match(_pattern, _guards, expected, _expr, _tag, %{mode: :traversal}, context) do - {expected, context} + def of_match(_pattern, _guards, _expected, _expr, _tag, %{mode: :traversal}, context) do + context end def of_match(pattern, guards, expected, expr, tag, stack, context) do context = init_pattern_info(context) {tree, context} = of_pattern(pattern, [{:arg, 0, expr}], stack, context) - {[type], context} = + context = of_pattern_recur([expected], tag, stack, context, fn [type], [0], context -> with {:ok, type, context} <- of_pattern_intersect(tree, type, expr, 0, tag, stack, context) do @@ -122,7 +134,7 @@ defmodule Module.Types.Pattern do end) {_, context} = Enum.map_reduce(guards, context, &of_guard(&1, @guard, &1, stack, &2)) - {type, context} + context end defp all_single_path?(vars, info, index) do @@ -148,10 +160,10 @@ defmodule Module.Types.Pattern do of_pattern_recur(types, unchangeable, vars, info, tag, stack, context, callback) {:error, context} -> - {types, error_vars(vars, context)} + error_vars(vars, context) end catch - {types, context} -> {types, error_vars(vars, context)} + context -> error_vars(vars, context) end end @@ -173,12 +185,12 @@ defmodule Module.Types.Pattern do _ -> case Of.refine_var(var, type, expr, stack, context) do {:ok, _type, context} -> {var_changed? or reachable_var?, context} - {:error, _type, context} -> throw({types, context}) + {:error, _type, context} -> throw(context) end end :error -> - throw({types, badpattern_error(expr, index, tag, stack, context)}) + throw(badpattern_error(expr, index, tag, stack, context)) end end) @@ -194,19 +206,19 @@ defmodule Module.Types.Pattern do case :lists.usort(changed) -- unchangeable do [] -> - {types, context} + context changed -> case callback.(types, changed, context) do # A simple structural comparison for optimization {:ok, ^types, context} -> - {types, context} + context {:ok, types, context} -> of_pattern_recur(types, unchangeable, vars, info, tag, stack, context, callback) {:error, context} -> - {types, error_vars(vars, context)} + error_vars(vars, context) end end end @@ -283,36 +295,39 @@ defmodule Module.Types.Pattern do end end - defp of_pattern_tree(descr, _context) when is_descr(descr), + @doc """ + Receives the pattern tree and the context and returns a concrete type. + """ + def of_pattern_tree(descr, _context) when is_descr(descr), do: descr - defp of_pattern_tree({:tuple, entries}, context) do + def of_pattern_tree({:tuple, entries}, context) do tuple(Enum.map(entries, &of_pattern_tree(&1, context))) end - defp of_pattern_tree({:open_map, static, dynamic}, context) do + def of_pattern_tree({:open_map, static, dynamic}, context) do dynamic = Enum.map(dynamic, fn {key, value} -> {key, of_pattern_tree(value, context)} end) open_map(static ++ dynamic) end - defp of_pattern_tree({:closed_map, static, dynamic}, context) do + def of_pattern_tree({:closed_map, static, dynamic}, context) do dynamic = Enum.map(dynamic, fn {key, value} -> {key, of_pattern_tree(value, context)} end) closed_map(static ++ dynamic) end - defp of_pattern_tree({:non_empty_list, [head | tail], suffix}, context) do + def of_pattern_tree({:non_empty_list, [head | tail], suffix}, context) do tail |> Enum.reduce(of_pattern_tree(head, context), &union(of_pattern_tree(&1, context), &2)) |> non_empty_list(of_pattern_tree(suffix, context)) end - defp of_pattern_tree({:intersection, entries}, context) do + def of_pattern_tree({:intersection, entries}, context) do entries |> Enum.map(&of_pattern_tree(&1, context)) |> Enum.reduce(&intersection/2) end - defp of_pattern_tree({:var, version}, context) do + def of_pattern_tree({:var, version}, context) do case context do %{vars: %{^version => %{type: type}}} -> type _ -> term() diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 4b68c5b9583..7e32e2b22d8 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -1120,6 +1120,20 @@ defmodule Module.Types.ExprTest do ) == atom([:ok, :error]) end + test "resets branches" do + assert typecheck!( + [x], + ( + case :rand.uniform() do + y when y < 0.5 -> x.foo + y when y > 0.5 -> x.bar() + end + + x + ) + ) == dynamic() + end + test "returns unions of all clauses" do assert typecheck!( [x], @@ -1217,6 +1231,22 @@ defmodule Module.Types.ExprTest do ) == dynamic(atom([:ok, :error, :timeout])) end + test "resets branches" do + assert typecheck!( + [x, timeout = :infinity], + ( + receive do + y when y > 0.5 -> x.foo + _ -> x.bar() + after + timeout -> <<^x::integer>> = :crypto.strong_rand_bytes(1) + end + + x + ) + ) == dynamic() + end + test "errors on bad timeout" do assert typeerror!( [x = :timeout], @@ -1275,6 +1305,25 @@ defmodule Module.Types.ExprTest do ) == atom([:caught1, :caught2, :rescue, :else1, :else2]) end + test "resets branches (except after)" do + assert typecheck!( + [x], + ( + try do + <<^x::float>> = :crypto.strong_rand_bytes(8) + rescue + ArgumentError -> x.foo + catch + _, _ -> x.bar() + after + <<^x::integer>> = :crypto.strong_rand_bytes(8) + end + + x + ) + ) == dynamic(integer()) + end + test "reports error from clause that will never match" do assert typeerror!( [x], @@ -1456,6 +1505,20 @@ defmodule Module.Types.ExprTest do y = false """} end + + test "resets branches" do + assert typecheck!( + [x], + ( + cond do + :rand.uniform() > 0.5 -> x.foo + true -> x.bar() + end + + x + ) + ) == dynamic() + end end describe "comprehensions" do diff --git a/lib/elixir/test/elixir/module/types/infer_test.exs b/lib/elixir/test/elixir/module/types/infer_test.exs index 656ee747485..c83ec2439a8 100644 --- a/lib/elixir/test/elixir/module/types/infer_test.exs +++ b/lib/elixir/test/elixir/module/types/infer_test.exs @@ -48,6 +48,22 @@ defmodule Module.Types.InferTest do assert types[{:fun4, 4}] == {:infer, [{args, atom([:ok])}]} end + test "infer types from expressions", config do + types = + infer config do + def fun(x) do + x.foo + x.bar + end + end + + assert types[{:fun, 1}] == + {:infer, + [ + {[dynamic(open_map(foo: term(), bar: term()))], + dynamic(union(integer(), float()))} + ]} + end + test "infer with Elixir built-in", config do types = infer config do diff --git a/lib/elixir/test/elixir/module/types/type_helper.exs b/lib/elixir/test/elixir/module/types/type_helper.exs index d05e3331e2d..4826a00b807 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -118,7 +118,7 @@ defmodule TypeHelper do stack = new_stack(mode) expected = Enum.map(patterns, fn _ -> Descr.dynamic() end) - {_types, context} = + {_trees, context} = Pattern.of_head(patterns, guards, expected, :default, [], stack, new_context()) Expr.of_expr(body, {Descr.term(), :ok}, stack, context) From 63283d5385f59e1c1d490f8283fb0b6bc5287784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 2 Jan 2025 20:34:19 +0100 Subject: [PATCH 03/22] More tests --- lib/elixir/lib/module/types/expr.ex | 4 +++- lib/elixir/lib/module/types/pattern.ex | 2 +- lib/elixir/test/elixir/module/types/expr_test.exs | 10 ++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 72c1484d5b3..6d6e71041b6 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -360,7 +360,7 @@ defmodule Module.Types.Expr do {timeout_type, context} = of_expr(timeout, {@timeout_type, after_expr}, stack, context) {body_type, context} = of_expr(body, expected_expr, stack, context) - if compatible?(timeout_type, @timeout_type) do + if integer_type?(timeout_type) or atom_type?(timeout_type, :infinity) do {union(body_type, acc), reset_vars(context, original)} else error = {:badtimeout, timeout_type, timeout, context} @@ -620,6 +620,8 @@ defmodule Module.Types.Expr do defp for_into(into, meta, stack, context) do {type, context} = of_expr(into, @expected_expr, stack, context) + # We use subtype? instead of compatible because we want to handle + # only binary/list, even if a dynamic with something else is given. if subtype?(type, @into_compile) do case {binary_type?(type), empty_list_type?(type)} do {false, true} -> {[:list], gradual?(type), context} diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 3e022ff1752..9af9b6f16e0 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -177,7 +177,7 @@ defmodule Module.Types.Pattern do case of_pattern_var(path, actual, true, info, context) do {type, reachable_var?} -> - # If current type is already a subtype, there is nothing to refine. + # Optimization: if current type is already a subtype, there is nothing to refine. with %{^version => %{type: current_type}} <- context.vars, true <- subtype?(current_type, type) do {var_changed?, context} diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 7e32e2b22d8..6be962b234b 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -1231,6 +1231,16 @@ defmodule Module.Types.ExprTest do ) == dynamic(atom([:ok, :error, :timeout])) end + test "infers type for timeout" do + assert typecheck!( + [x], + receive do + after + x -> x + end + ) == dynamic(union(integer(), atom([:infinity]))) + end + test "resets branches" do assert typecheck!( [x, timeout = :infinity], From 3a783bda72edbcb3bedf878558f001248ef94396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jan 2025 11:01:30 +0100 Subject: [PATCH 04/22] Check for compatibility, not subtyping --- lib/elixir/lib/module/types/descr.ex | 23 --------- lib/elixir/lib/module/types/expr.ex | 5 +- lib/elixir/lib/module/types/pattern.ex | 50 +++++-------------- .../test/elixir/module/types/descr_test.exs | 14 ------ .../test/elixir/module/types/expr_test.exs | 18 +++++++ 5 files changed, 33 insertions(+), 77 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 141b689e21d..a67b70410b8 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -637,15 +637,6 @@ defmodule Module.Types.Descr do def number_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_number) != 0, do: true def number_type?(_), do: false - @doc """ - Optimized version of `not empty?(intersection(atom(), type))`. - """ - def atom_type?(:term), do: true - def atom_type?(%{dynamic: :term}), do: true - def atom_type?(%{dynamic: %{atom: _}}), do: true - def atom_type?(%{atom: _}), do: true - def atom_type?(_), do: false - ## Bitmaps defp bitmap_to_quoted(val) do @@ -749,20 +740,6 @@ defmodule Module.Types.Descr do end end - @doc """ - Optimized version of `not empty?(intersection(atom([atom]), type))`. - """ - def atom_type?(:term, _atom), do: true - - def atom_type?(%{} = descr, atom) do - case Map.get(descr, :dynamic, descr) do - :term -> true - %{atom: {:union, set}} -> :sets.is_element(atom, set) - %{atom: {:negation, set}} -> not :sets.is_element(atom, set) - %{} -> false - end - end - @doc """ Returns a set of all known atoms. diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 6d6e71041b6..810aa4afc77 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -360,7 +360,7 @@ defmodule Module.Types.Expr do {timeout_type, context} = of_expr(timeout, {@timeout_type, after_expr}, stack, context) {body_type, context} = of_expr(body, expected_expr, stack, context) - if integer_type?(timeout_type) or atom_type?(timeout_type, :infinity) do + if compatible?(timeout_type, @timeout_type) do {union(body_type, acc), reset_vars(context, original)} else error = {:badtimeout, timeout_type, timeout, context} @@ -592,10 +592,9 @@ defmodule Module.Types.Expr do defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do {right_type, context} = of_expr(right, {binary(), expr}, stack, context) - context = Pattern.of_match(left, binary(), expr, :for, stack, context) - if binary_type?(right_type) do + if compatible?(right_type, binary()) do context else error = {:badbinary, right_type, right, context} diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 9af9b6f16e0..dbfb561152c 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -618,48 +618,28 @@ defmodule Module.Types.Pattern do # This function is public as it is invoked from Of.binary/4. # :atom - def of_guard(atom, expected, expr, stack, context) when is_atom(atom) do - if atom_type?(expected, atom) do - {atom([atom]), context} - else - {error_type(), Of.incompatible_error(expr, expected, atom([atom]), stack, context)} - end + def of_guard(atom, _expected, _expr, _stack, context) when is_atom(atom) do + {atom([atom]), context} end # 12 - def of_guard(literal, expected, expr, stack, context) when is_integer(literal) do - if integer_type?(expected) do - {integer(), context} - else - {error_type(), Of.incompatible_error(expr, expected, integer(), stack, context)} - end + def of_guard(literal, _expected, _expr, _stack, context) when is_integer(literal) do + {integer(), context} end # 1.2 - def of_guard(literal, expected, expr, stack, context) when is_float(literal) do - if float_type?(expected) do - {float(), context} - else - {error_type(), Of.incompatible_error(expr, expected, float(), stack, context)} - end + def of_guard(literal, _expected, _expr, _stack, context) when is_float(literal) do + {float(), context} end # "..." - def of_guard(literal, expected, expr, stack, context) when is_binary(literal) do - if binary_type?(expected) do - {binary(), context} - else - {error_type(), Of.incompatible_error(expr, expected, binary(), stack, context)} - end + def of_guard(literal, _expected, _expr, _stack, context) when is_binary(literal) do + {binary(), context} end # [] - def of_guard([], expected, expr, stack, context) do - if empty_list_type?(expected) do - {empty_list(), context} - else - {error_type(), Of.incompatible_error(expr, expected, empty_list(), stack, context)} - end + def of_guard([], _expected, _expr, _stack, context) do + {empty_list(), context} end # [expr, ...] @@ -691,13 +671,9 @@ defmodule Module.Types.Pattern do end # <<>> - def of_guard({:<<>>, _meta, args}, expected, expr, stack, context) do - if binary_type?(expected) do - context = Of.binary(args, :guard, stack, context) - {binary(), context} - else - {error_type(), Of.incompatible_error(expr, expected, binary(), stack, context)} - end + def of_guard({:<<>>, _meta, args}, _expected, _expr, stack, context) do + context = Of.binary(args, :guard, stack, context) + {binary(), context} end # ^var diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 90635b5071d..2ffcb977869 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -579,20 +579,6 @@ defmodule Module.Types.DescrTest do end end - describe "queries" do - test "atom_type?" do - assert atom_type?(term(), :foo) - assert atom_type?(dynamic(), :foo) - - assert atom_type?(atom([:foo, :bar]), :foo) - refute atom_type?(atom([:foo, :bar]), :baz) - assert atom_type?(negation(atom([:foo, :bar])), :baz) - - refute atom_type?(union(atom([:foo, :bar]), integer()), :baz) - refute atom_type?(dynamic(union(atom([:foo, :bar]), integer())), :baz) - end - end - describe "projections" do test "fun_fetch" do assert fun_fetch(term(), 1) == :error diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 6be962b234b..d8a84340576 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -1279,6 +1279,17 @@ defmodule Module.Types.ExprTest do # from: types_test.ex:LINE-5 x = :timeout """ + + # Check for compatibility, not subtyping + assert typeerror!( + [<>], + receive do + after + if(:rand.uniform(), do: x, else: y) -> :ok + end + ) =~ "expected " + after + " timeout given to receive to be an integer" end end @@ -1551,6 +1562,13 @@ defmodule Module.Types.ExprTest do #{hints(:inferred_bitstring_spec)} """ + + # Check for compatibility, not subtyping + assert typeerror!( + [<>], + for(< 0.5, do: x, else: y)>>, do: i) + ) =~ + "expected the right side of <- in a binary generator to be a binary" end test "infers binary generators" do From 47dce5d7e72559b12742f66a3ee381dbf341e3a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jan 2025 11:38:01 +0100 Subject: [PATCH 05/22] Use compatibility checks as much as possible --- lib/elixir/lib/module/types/descr.ex | 1 + lib/elixir/lib/module/types/expr.ex | 7 +++- lib/elixir/lib/module/types/of.ex | 37 +++++++++++-------- lib/elixir/lib/module/types/pattern.ex | 24 +++++++----- lib/elixir/src/elixir_bitstring.erl | 37 ++++++++----------- lib/elixir/test/elixir/kernel/errors_test.exs | 2 +- .../test/elixir/kernel/expansion_test.exs | 19 +++++----- 7 files changed, 67 insertions(+), 60 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index a67b70410b8..2086a9ccf77 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1885,6 +1885,7 @@ defmodule Module.Types.Descr do end :open -> + fields = Map.to_list(fields) {:%{}, [], [{:..., [], nil} | map_fields_to_quoted(tag, Enum.sort(fields), opts)]} end end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 810aa4afc77..6ee75daeba3 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -535,7 +535,7 @@ defmodule Module.Types.Expr do if stack.mode == :traversal do {dynamic(), context} else - Of.refine_existing_var(var, expected, expr, stack, context) + Of.refine_body_var(var, expected, expr, stack, context) end end @@ -567,7 +567,10 @@ defmodule Module.Types.Expr do _ -> expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2) formatter = fn expr -> {"rescue #{expr_to_string(expr)} ->", hints} end - {_ok?, _type, context} = Of.refine_var(var, expected, expr, formatter, stack, context) + + {_ok?, _type, context} = + Of.refine_head_var(var, expected, expr, formatter, stack, context) + context end diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index d5a407247f9..6adb9675d6e 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -42,13 +42,13 @@ defmodule Module.Types.Of do end @doc """ - Refines a variable that already exists. + Refines a variable that already exists (in a body). This only happens if the var contains a gradual type, or if we are doing a guard analysis or occurrence typing. Returns `true` if there was a refinement, `false` otherwise. """ - def refine_existing_var({_, meta, _}, type, expr, stack, context) do + def refine_body_var({_, meta, _}, type, expr, stack, context) do version = Keyword.fetch!(meta, :version) %{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context @@ -76,8 +76,12 @@ defmodule Module.Types.Of do @doc """ Refines the type of a variable. + + Since this happens in a head, we use intersection + because we want to refine types. Otherwise we should + use compatibility. """ - def refine_var(var, type, expr, formatter \\ :default, stack, context) do + def refine_head_var(var, type, expr, formatter \\ :default, stack, context) do {var_name, meta, var_context} = var version = Keyword.fetch!(meta, :version) @@ -96,7 +100,7 @@ defmodule Module.Types.Of do # We need to return error otherwise it leads to cascading errors if empty?(new_type) do {:error, error_type(), - error({:refine_var, old_type, type, var, context}, meta, stack, context)} + error({:refine_head_var, old_type, type, var, context}, meta, stack, context)} else {:ok, new_type, context} end @@ -357,12 +361,13 @@ defmodule Module.Types.Of do Module.Types.Pattern.of_match_var(left, type, expr, stack, context) :guard -> - Module.Types.Pattern.of_guard(left, type, expr, stack, context) + {actual, context} = Module.Types.Pattern.of_guard(left, type, expr, stack, context) + compatible(actual, type, expr, stack, context) :expr -> left = annotate_interpolation(left, right) {actual, context} = Module.Types.Expr.of_expr(left, {type, expr}, stack, context) - intersect(actual, type, expr, stack, context) + compatible(actual, type, expr, stack, context) end specifier_size(kind, right, stack, context) @@ -402,13 +407,14 @@ defmodule Module.Types.Of do defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context) when not is_integer(arg) do {actual, context} = Module.Types.Expr.of_expr(arg, {integer(), expr}, stack, context) - {_, context} = intersect(actual, integer(), expr, stack, context) + {_, context} = compatible(actual, integer(), expr, stack, context) context end defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context) when not is_integer(arg) do - {_type, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context) + {actual, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context) + {_, context} = compatible(actual, integer(), expr, stack, context) context end @@ -437,15 +443,14 @@ defmodule Module.Types.Of do ## Warning helpers @doc """ - Intersects two types and emit an incompatible error if empty. + Checks if two types are compatible and emit an incompatible error if not. """ - def intersect(actual, expected, expr, stack, context) do - type = intersection(actual, expected) - - if empty?(type) do - {error_type(), incompatible_error(expr, expected, actual, stack, context)} + # TODO: Consider getting rid of this and emitting precise errors instead. + def compatible(actual, expected, expr, stack, context) do + if compatible?(actual, expected) do + {actual, context} else - {type, context} + {error_type(), incompatible_error(expr, expected, actual, stack, context)} end end @@ -468,7 +473,7 @@ defmodule Module.Types.Of do ## Warning formatting - def format_diagnostic({:refine_var, old_type, new_type, var, context}) do + def format_diagnostic({:refine_head_var, old_type, new_type, var, context}) do traces = collect_traces(var, context) %{ diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index dbfb561152c..ae9921d9649 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -183,7 +183,7 @@ defmodule Module.Types.Pattern do {var_changed?, context} else _ -> - case Of.refine_var(var, type, expr, stack, context) do + case Of.refine_head_var(var, type, expr, stack, context) do {:ok, _type, context} -> {var_changed? or reachable_var?, context} {:error, _type, context} -> throw(context) end @@ -337,10 +337,14 @@ defmodule Module.Types.Pattern do @doc """ Function used to assign a type to a variable. Used by %struct{} and binary patterns. + + Given those values are actually checked at compile-time, + except for the variables, that's the only scenario we need to handle. """ + # TODO: Perhaps merge this with guards def of_match_var({:^, _, [var]}, expected, expr, stack, context) do - {type, context} = Of.refine_existing_var(var, expected, expr, stack, context) - Of.intersect(type, expected, expr, stack, context) + {type, context} = Of.refine_body_var(var, expected, expr, stack, context) + Of.compatible(type, expected, expr, stack, context) end def of_match_var({:_, _, _}, expected, _expr, _stack, context) do @@ -348,12 +352,12 @@ defmodule Module.Types.Pattern do end def of_match_var(var, expected, expr, stack, context) when is_var(var) do - {_ok?, type, context} = Of.refine_var(var, expected, expr, stack, context) + {_ok?, type, context} = Of.refine_head_var(var, expected, expr, stack, context) {type, context} end - def of_match_var(ast, expected, expr, stack, context) do - of_match(ast, expected, expr, :default, stack, context) + def of_match_var(_ast, expected, _expr, _stack, context) do + {expected, context} end ## Patterns @@ -678,9 +682,9 @@ defmodule Module.Types.Pattern do # ^var def of_guard({:^, _meta, [var]}, expected, expr, stack, context) do - # This is by definition a variable defined outside of this pattern, so we don't track it. - {type, context} = Of.refine_existing_var(var, expected, expr, stack, context) - Of.intersect(type, expected, expr, stack, context) + # This is used by binary size, which behaves as a mixture of match and guard + {type, context} = Of.refine_body_var(var, expected, expr, stack, context) + Of.compatible(type, expected, expr, stack, context) end # {...} @@ -716,7 +720,7 @@ defmodule Module.Types.Pattern do # var def of_guard(var, expected, expr, stack, context) when is_var(var) do - Of.intersect(Of.var(var, context), expected, expr, stack, context) + Of.compatible(Of.var(var, context), expected, expr, stack, context) end ## Helpers diff --git a/lib/elixir/src/elixir_bitstring.erl b/lib/elixir/src/elixir_bitstring.erl index 4a92a9827a3..7210f24f1bb 100644 --- a/lib/elixir/src/elixir_bitstring.erl +++ b/lib/elixir/src/elixir_bitstring.erl @@ -1,6 +1,6 @@ -module(elixir_bitstring). -export([expand/5, format_error/1, validate_spec/2]). --import(elixir_errors, [function_error/4, file_error/4]). +-import(elixir_errors, [function_error/4]). -include("elixir.hrl"). expand_match(Expr, {S, OriginalS}, E) -> @@ -13,11 +13,6 @@ expand(Meta, Args, S, E, RequireSize) -> {EArgs, Alignment, {SA, _}, EA} = expand(Meta, fun expand_match/3, Args, [], {S, S}, E, 0, RequireSize), - case find_match(EArgs) of - false -> ok; - Match -> file_error(Meta, EA, ?MODULE, {nested_match, Match}) - end, - {{'<<>>', [{alignment, Alignment} | Meta], EArgs}, SA, EA}; _ -> PairS = {elixir_env:prepare_write(S), S}, @@ -32,6 +27,7 @@ expand(_BitstrMeta, _Fun, [], Acc, S, E, Alignment, _RequireSize) -> {lists:reverse(Acc), Alignment, S, E}; expand(BitstrMeta, Fun, [{'::', Meta, [Left, Right]} | T], Acc, S, E, Alignment, RequireSize) -> {ELeft, {SL, OriginalS}, EL} = expand_expr(Left, Fun, S, E), + validate_expr(ELeft, Meta, E), MatchOrRequireSize = RequireSize or is_match_size(T, EL), EType = expr_type(ELeft), @@ -47,6 +43,7 @@ expand(BitstrMeta, Fun, [{'::', Meta, [Left, Right]} | T], Acc, S, E, Alignment, expand(BitstrMeta, Fun, [H | T], Acc, S, E, Alignment, RequireSize) -> Meta = extract_meta(H, BitstrMeta), {ELeft, {SS, OriginalS}, ES} = expand_expr(H, Fun, S, E), + validate_expr(ELeft, Meta, E), MatchOrRequireSize = RequireSize or is_match_size(T, ES), EType = expr_type(ELeft), @@ -145,6 +142,17 @@ expand_expr({{'.', _, [Mod, to_string]}, _, [Arg]} = AST, Fun, S, #{context := C expand_expr(Component, Fun, S, E) -> Fun(Component, S, E). +validate_expr(Expr, Meta, #{context := match} = E) -> + case Expr of + {Var, _Meta, Ctx} when is_atom(Var), is_atom(Ctx) -> ok; + {'<<>>', _, _} -> ok; + {'^', _, _} -> ok; + _ when is_number(Expr); is_binary(Expr) -> ok; + _ -> function_error(extract_meta(Expr, Meta), E, ?MODULE, {unknown_match, Expr}) + end; +validate_expr(_Expr, _Meta, _E) -> + ok. + %% Expands and normalizes types of a bitstring. expand_specs(ExprType, Meta, Info, S, OriginalS, E, ExpectSize) -> @@ -353,18 +361,6 @@ valid_float_size(_) -> false. add_spec(default, Spec) -> Spec; add_spec(Key, Spec) -> [{Key, [], nil} | Spec]. -find_match([{'=', _, [_Left, _Right]} = Expr | _Rest]) -> - Expr; -find_match([{_, _, Args} | Rest]) when is_list(Args) -> - case find_match(Args) of - false -> find_match(Rest); - Match -> Match - end; -find_match([_Arg | Rest]) -> - find_match(Rest); -find_match([]) -> - false. - format_error({unaligned_binary, Expr}) -> Message = "expected ~ts to be a binary but its number of bits is not divisible by 8", io_lib:format(Message, ['Elixir.Macro':to_string(Expr)]); @@ -409,10 +405,9 @@ format_error({bittype_mismatch, Val1, Val2, Where}) -> format_error({bad_unit_argument, Unit}) -> io_lib:format("unit in bitstring expects an integer as argument, got: ~ts", ['Elixir.Macro':to_string(Unit)]); -format_error({nested_match, Expr}) -> +format_error({unknown_match, Expr}) -> Message = - "cannot pattern match inside a bitstring " - "that is already in match, got: ~ts", + "a bitstring only accepts binaries, numbers, and variables inside a match, got: ~ts", io_lib:format(Message, ['Elixir.Macro':to_string(Expr)]); format_error({undefined_var_in_spec, Var}) -> Message = diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index a328f3767e4..8b99ae21785 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -941,7 +941,7 @@ defmodule Kernel.ErrorsTest do test "failed remote call stacktrace includes file/line info" do try do - bad_remote_call(1) + bad_remote_call(Process.get(:unused, 1)) rescue ArgumentError -> assert [ diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 4e08384a5b0..aeaea9bc40e 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -2387,6 +2387,15 @@ defmodule Kernel.ExpansionTest do |> clean_bit_modifiers() end + test "invalid match" do + assert_compile_error( + "a bitstring only accepts binaries, numbers, and variables inside a match", + fn -> + expand(quote(do: <<%{}>> = foo())) + end + ) + end + test "nested match" do assert expand(quote(do: <>)) |> clean_meta([:alignment]) == quote(do: <>) |> clean_bit_modifiers() @@ -2395,16 +2404,6 @@ defmodule Kernel.ExpansionTest do |> clean_meta([:alignment]) == quote(do: <<45::integer, <<_::integer, _::binary>> = rest()::binary>>) |> clean_bit_modifiers() - - message = ~r"cannot pattern match inside a bitstring that is already in match" - - assert_compile_error(message, fn -> - expand(quote(do: <> = foo())) - end) - - assert_compile_error(message, fn -> - expand(quote(do: <> = rest::binary>> = foo())) - end) end test "inlines binaries inside interpolation" do From 6c51c8a7e62ee1ca5d2d5a6c4775007bce8f2891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jan 2025 18:10:17 +0100 Subject: [PATCH 06/22] Match of_guard and of_expr signatures --- lib/elixir/lib/module/types.ex | 2 +- lib/elixir/lib/module/types/expr.ex | 237 ++++++++++-------- lib/elixir/lib/module/types/of.ex | 4 +- .../test/elixir/module/types/type_helper.exs | 2 +- 4 files changed, 137 insertions(+), 108 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index e869ad35f68..2d5e5c12bed 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -285,7 +285,7 @@ defmodule Module.Types do Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context) {return_type, context} = - Expr.of_expr(body, {Descr.term(), :ok}, stack, context) + Expr.of_expr(body, Descr.term(), :ok, stack, context) args_types = if stack.mode == :traversal do diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 6ee75daeba3..f4db1b02353 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -49,39 +49,39 @@ defmodule Module.Types.Expr do ) ) - @expected_expr {term(), :ok} - @term_expected {term(), :ok} + @term term() + @pending term() # :atom - def of_expr(atom, _expected_expr, _stack, context) when is_atom(atom), + def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom), do: {atom([atom]), context} # 12 - def of_expr(literal, _expected_expr, _stack, context) when is_integer(literal), + def of_expr(literal, _expected, _expr, _stack, context) when is_integer(literal), do: {integer(), context} # 1.2 - def of_expr(literal, _expected_expr, _stack, context) when is_float(literal), + def of_expr(literal, _expected, _expr, _stack, context) when is_float(literal), do: {float(), context} # "..." - def of_expr(literal, _expected_expr, _stack, context) when is_binary(literal), + def of_expr(literal, _expected, _expr, _stack, context) when is_binary(literal), do: {binary(), context} # #PID<...> - def of_expr(literal, _expected_expr, _stack, context) when is_pid(literal), + def of_expr(literal, _expected, _expr, _stack, context) when is_pid(literal), do: {pid(), context} # [] - def of_expr([], _expected_expr, _stack, context), + def of_expr([], _expected, _expr, _stack, context), do: {empty_list(), context} # [expr, ...] # TODO: here - def of_expr(list, _expected_expr, stack, context) when is_list(list) do + def of_expr(list, _expected, expr, stack, context) when is_list(list) do {prefix, suffix} = unpack_list(list, []) - {prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, @expected_expr, stack, &2)) - {suffix, context} = of_expr(suffix, @expected_expr, stack, context) + {prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, @pending, expr, stack, &2)) + {suffix, context} = of_expr(suffix, @pending, expr, stack, context) if stack.mode == :traversal do {dynamic(), context} @@ -92,9 +92,9 @@ defmodule Module.Types.Expr do # {left, right} # TODO: here - def of_expr({left, right}, _expected_expr, stack, context) do - {left, context} = of_expr(left, @expected_expr, stack, context) - {right, context} = of_expr(right, @expected_expr, stack, context) + def of_expr({left, right}, _expected, expr, stack, context) do + {left, context} = of_expr(left, @pending, expr, stack, context) + {right, context} = of_expr(right, @pending, expr, stack, context) if stack.mode == :traversal do {dynamic(), context} @@ -105,8 +105,8 @@ defmodule Module.Types.Expr do # {...} # TODO: here - def of_expr({:{}, _meta, exprs}, _expected_expr, stack, context) do - {types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, @expected_expr, stack, &2)) + def of_expr({:{}, _meta, exprs}, _expected, expr, stack, context) do + {types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, @pending, expr, stack, &2)) if stack.mode == :traversal do {dynamic(), context} @@ -117,26 +117,26 @@ defmodule Module.Types.Expr do # <<...>>> # TODO: here (including tests) - def of_expr({:<<>>, _meta, args}, _expected_expr, stack, context) do + def of_expr({:<<>>, _meta, args}, _expected, _expr, stack, context) do context = Of.binary(args, :expr, stack, context) {binary(), context} end - def of_expr({:__CALLER__, _meta, var_context}, _expected_expr, _stack, context) + def of_expr({:__CALLER__, _meta, var_context}, _expected, _expr, _stack, context) when is_atom(var_context) do {@caller, context} end - def of_expr({:__STACKTRACE__, _meta, var_context}, _expected_expr, _stack, context) + def of_expr({:__STACKTRACE__, _meta, var_context}, _expected, _expr, _stack, context) when is_atom(var_context) do {@stacktrace, context} end # left = right # TODO: here - def of_expr({:=, _, [left_expr, right_expr]} = expr, _expected_expr, stack, context) do + def of_expr({:=, _, [left_expr, right_expr]} = expr, _expected, _expr, stack, context) do {left_expr, right_expr} = repack_match(left_expr, right_expr) - {right_type, context} = of_expr(right_expr, @expected_expr, stack, context) + {right_type, context} = of_expr(right_expr, @pending, expr, stack, context) # We do not raise on underscore in case someone writes _ = raise "omg" context = @@ -151,10 +151,10 @@ defmodule Module.Types.Expr do # %{map | ...} # TODO: Once we support typed structs, we need to type check them here. # TODO: here - def of_expr({:%{}, meta, [{:|, _, [map, args]}]} = expr, _expected_expr, stack, context) do - {map_type, context} = of_expr(map, @expected_expr, stack, context) + def of_expr({:%{}, meta, [{:|, _, [map, args]}]} = expr, _expected, _expr, stack, context) do + {map_type, context} = of_expr(map, @pending, expr, stack, context) - Of.permutate_map(args, stack, context, &of_expr(&1, @expected_expr, &2, &3), fn + Of.permutate_map(args, stack, context, &of_expr(&1, @pending, expr, &2, &3), fn fallback, keys, pairs -> # If there is no fallback (i.e. it is closed), we can update the existing map, # otherwise we only assert the existing keys. @@ -195,13 +195,14 @@ defmodule Module.Types.Expr do # TODO: here def of_expr( {:%, struct_meta, [module, {:%{}, _, [{:|, update_meta, [map, args]}]}]} = expr, - _expected_expr, + _expected, + _expr, stack, context ) do {info, context} = Of.struct_info(module, struct_meta, stack, context) struct_type = Of.struct_type(module, info) - {map_type, context} = of_expr(map, @expected_expr, stack, context) + {map_type, context} = of_expr(map, @pending, expr, stack, context) if disjoint?(struct_type, map_type) do warning = {:badstruct, expr, struct_type, map_type, context} @@ -211,7 +212,7 @@ defmodule Module.Types.Expr do Enum.reduce(args, {map_type, context}, fn {key, value}, {map_type, context} when is_atom(key) -> - {value_type, context} = of_expr(value, @expected_expr, stack, context) + {value_type, context} = of_expr(value, @pending, expr, stack, context) {map_put!(map_type, key, value_type), context} end) end @@ -219,39 +220,39 @@ defmodule Module.Types.Expr do # %{...} # TODO: here - def of_expr({:%{}, _meta, args}, _expected_expr, stack, context) do - Of.closed_map(args, stack, context, &of_expr(&1, @expected_expr, &2, &3)) + def of_expr({:%{}, _meta, args}, _expected, expr, stack, context) do + Of.closed_map(args, stack, context, &of_expr(&1, @pending, expr, &2, &3)) end # %Struct{} # TODO: here - def of_expr({:%, meta, [module, {:%{}, _, args}]}, _expected_expr, stack, context) do - Of.struct_instance(module, args, meta, stack, context, &of_expr(&1, @expected_expr, &2, &3)) + def of_expr({:%, meta, [module, {:%{}, _, args}]}, _expected, expr, stack, context) do + Of.struct_instance(module, args, meta, stack, context, &of_expr(&1, @pending, expr, &2, &3)) end # () - def of_expr({:__block__, _meta, []}, _expected_expr, _stack, context) do + def of_expr({:__block__, _meta, []}, _expected, _expr, _stack, context) do {atom([nil]), context} end # (expr; expr) - def of_expr({:__block__, _meta, exprs}, expected_expr, stack, context) do + def of_expr({:__block__, _meta, exprs}, expected, expr, stack, context) do {pre, [post]} = Enum.split(exprs, -1) context = Enum.reduce(pre, context, fn expr, context -> - {_, context} = of_expr(expr, @term_expected, stack, context) + {_, context} = of_expr(expr, @term, :ok, stack, context) context end) - of_expr(post, expected_expr, stack, context) + of_expr(post, expected, expr, stack, context) end - def of_expr({:cond, _meta, [[{:do, clauses}]]}, expected_expr, stack, original) do + def of_expr({:cond, _meta, [[{:do, clauses}]]}, expected, expr, stack, original) do clauses |> reduce_non_empty({none(), original}, fn {:->, meta, [[head], body]}, {acc, context}, last? -> - {head_type, context} = of_expr(head, @term_expected, stack, context) + {head_type, context} = of_expr(head, @pending, :ok, stack, context) context = if stack.mode in [:infer, :traversal] do @@ -271,15 +272,15 @@ defmodule Module.Types.Expr do end end - {body_type, context} = of_expr(body, expected_expr, stack, context) + {body_type, context} = of_expr(body, expected, expr, stack, context) {union(body_type, acc), reset_vars(context, original)} end) |> dynamic_unless_static(stack) end # TODO: here - def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, _expected_expr, stack, context) do - {case_type, context} = of_expr(case_expr, @term_expected, stack, context) + def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, expr, stack, context) do + {case_type, context} = of_expr(case_expr, @pending, :ok, stack, context) # If we are only type checking the expression and the expression is a literal, # let's mark it as generated, as it is most likely a macro code. However, if @@ -289,31 +290,47 @@ defmodule Module.Types.Expr do else clauses end - |> of_clauses([case_type], {:case, meta, case_type, case_expr}, stack, {none(), context}) + |> of_clauses( + [case_type], + expected, + expr, + {:case, meta, case_type, case_expr}, + stack, + {none(), context} + ) |> dynamic_unless_static(stack) end # TODO: fn pat -> expr end # TODO: here - def of_expr({:fn, _meta, clauses}, _expected_expr, stack, context) do + def of_expr({:fn, _meta, clauses}, _expected, _expr, stack, context) do [{:->, _, [head, _]} | _] = clauses {patterns, _guards} = extract_head(head) - expected = Enum.map(patterns, fn _ -> dynamic() end) - {_acc, context} = of_clauses(clauses, expected, :fn, stack, {none(), context}) + domain = Enum.map(patterns, fn _ -> dynamic() end) + {_acc, context} = of_clauses(clauses, domain, @pending, :ok, :fn, stack, {none(), context}) {fun(), context} end # TODO: here - def of_expr({:try, _meta, [[do: body] ++ blocks]}, _expected_expr, stack, original) do - {type, context} = of_expr(body, @expected_expr, stack, original) + def of_expr({:try, _meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do {after_block, blocks} = Keyword.pop(blocks, :after) {else_block, blocks} = Keyword.pop(blocks, :else) {type, context} = if else_block do - of_clauses(else_block, [type], {:try_else, type}, stack, {none(), context}) + {type, context} = of_expr(body, @pending, :ok, stack, original) + + of_clauses( + else_block, + [type], + expected, + expr, + {:try_else, type}, + stack, + {none(), context} + ) else - {type, context} + of_expr(body, expected, expr, stack, original) end {type, context} = @@ -332,12 +349,20 @@ defmodule Module.Types.Expr do end) {:catch, clauses}, {acc, context} -> - of_clauses(clauses, [@try_catch, dynamic()], :try_catch, stack, {acc, context}) + of_clauses( + clauses, + [@try_catch, dynamic()], + expected, + expr, + :try_catch, + stack, + {acc, context} + ) end) |> dynamic_unless_static(stack) if after_block do - {_type, context} = of_expr(after_block, @expected_expr, stack, context) + {_type, context} = of_expr(after_block, @term, :ok, stack, context) {type, context} else {type, context} @@ -347,18 +372,18 @@ defmodule Module.Types.Expr do @timeout_type union(integer(), atom([:infinity])) # TODO: here - def of_expr({:receive, _meta, [blocks]}, expected_expr, stack, original) do + def of_expr({:receive, _meta, [blocks]}, expected, expr, stack, original) do blocks |> Enum.reduce({none(), original}, fn {:do, {:__block__, _, []}}, acc_context -> acc_context {:do, clauses}, acc_context -> - of_clauses(clauses, [dynamic()], :receive, stack, acc_context) + of_clauses(clauses, [dynamic()], expected, expr, :receive, stack, acc_context) {:after, [{:->, meta, [[timeout], body]}] = after_expr}, {acc, context} -> - {timeout_type, context} = of_expr(timeout, {@timeout_type, after_expr}, stack, context) - {body_type, context} = of_expr(body, expected_expr, stack, context) + {timeout_type, context} = of_expr(timeout, @timeout_type, after_expr, stack, context) + {body_type, context} = of_expr(body, expected, expr, stack, context) if compatible?(timeout_type, @timeout_type) do {union(body_type, acc), reset_vars(context, original)} @@ -372,7 +397,7 @@ defmodule Module.Types.Expr do # TODO: for pat <- expr do expr end # TODO: here - def of_expr({:for, meta, [_ | _] = args}, _expected_expr, stack, context) do + def of_expr({:for, meta, [_ | _] = args}, expected, expr, stack, context) do {clauses, [[{:do, block} | opts]]} = Enum.split(args, -1) context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2)) @@ -380,14 +405,14 @@ defmodule Module.Types.Expr do # We handle reduce and into accordingly instead. if Keyword.has_key?(opts, :reduce) do reduce = Keyword.fetch!(opts, :reduce) - {reduce_type, context} = of_expr(reduce, @expected_expr, stack, context) + {reduce_type, context} = of_expr(reduce, expected, expr, stack, context) # TODO: We need to type check against dynamic() instead of using reduce_type # because this is recursive. We need to infer the block type first. - of_clauses(block, [dynamic()], :for_reduce, stack, {reduce_type, context}) + of_clauses(block, [dynamic()], expected, expr, :for_reduce, stack, {reduce_type, context}) else into = Keyword.get(opts, :into, []) {into_wrapper, gradual?, context} = for_into(into, meta, stack, context) - {block_type, context} = of_expr(block, @expected_expr, stack, context) + {block_type, context} = of_expr(block, @pending, :ok, stack, context) for_type = for type <- into_wrapper do @@ -405,7 +430,7 @@ defmodule Module.Types.Expr do # TODO: with pat <- expr do expr end # TODO: here - def of_expr({:with, _meta, [_ | _] = clauses}, _expected_expr, stack, original) do + def of_expr({:with, _meta, [_ | _] = clauses}, _expected, _expr, stack, original) do {clauses, [options]} = Enum.split(clauses, -1) context = Enum.reduce(clauses, original, &with_clause(&1, stack, &2)) context = Enum.reduce(options, context, &with_option(&1, stack, &2, original)) @@ -413,11 +438,12 @@ defmodule Module.Types.Expr do end # TODO: fun.(args) - def of_expr({{:., meta, [fun]}, _meta, args} = call, _expected_expr, stack, context) do - {fun_type, context} = of_expr(fun, {fun(), call}, stack, context) + # TODO: here + def of_expr({{:., meta, [fun]}, _meta, args} = call, _expected, _expr, stack, context) do + {fun_type, context} = of_expr(fun, fun(), call, stack, context) {_args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @expected_expr, stack, &2)) + Enum.map_reduce(args, context, &of_expr(&1, @pending, :ok, stack, &2)) case fun_fetch(fun_type, length(args)) do :ok -> @@ -430,28 +456,29 @@ defmodule Module.Types.Expr do end # TODO: here - def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = expr, _expected_expr, stack, context) + def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context) when not is_atom(callee) and is_atom(key_or_fun) do if Keyword.get(meta, :no_parens, false) do - {type, context} = of_expr(callee, {open_map([{key_or_fun, term()}]), expr}, stack, context) - Of.map_fetch(expr, type, key_or_fun, stack, context) + {type, context} = of_expr(callee, open_map([{key_or_fun, expected}]), expr, stack, context) + Of.map_fetch(call, type, key_or_fun, stack, context) else - {type, context} = of_expr(callee, {atom(), expr}, stack, context) - {mods, context} = Of.modules(type, key_or_fun, 0, [:dot], expr, meta, stack, context) - apply_many(mods, key_or_fun, [], [], expr, stack, context) + {type, context} = of_expr(callee, atom(), call, stack, context) + {mods, context} = Of.modules(type, key_or_fun, 0, [:dot], call, meta, stack, context) + apply_many(mods, key_or_fun, [], [], call, stack, context) end end # TODO: here def of_expr( - {{:., _, [remote, :apply]}, _meta, [mod, fun, args]} = expr, - _expected_expr, + {{:., _, [remote, :apply]}, _meta, [mod, fun, args]} = call, + _expected, + _expr, stack, context ) when remote in [Kernel, :erlang] and is_list(args) do - {mod_type, context} = of_expr(mod, {atom(), expr}, stack, context) - {fun_type, context} = of_expr(fun, {atom(), expr}, stack, context) + {mod_type, context} = of_expr(mod, atom(), call, stack, context) + {fun_type, context} = of_expr(fun, atom(), call, stack, context) improper_list? = Enum.any?(args, &match?({:|, _, [_, _]}, &1)) case atom_fetch(fun_type) do @@ -463,75 +490,76 @@ defmodule Module.Types.Expr do end {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @expected_expr, stack, &2)) + Enum.map_reduce(args, context, &of_expr(&1, @pending, :ok, stack, &2)) {types, context} = Enum.map_reduce(funs, context, fn fun, context -> - apply_many(mods, fun, args, args_types, expr, stack, context) + apply_many(mods, fun, args, args_types, call, stack, context) end) {Enum.reduce(types, &union/2), context} _ -> - {args_type, context} = of_expr(args, {list(term()), expr}, stack, context) + {args_type, context} = of_expr(args, list(term()), call, stack, context) args_types = [mod_type, fun_type, args_type] - Apply.remote(:erlang, :apply, [mod, fun, args], args_types, expr, stack, context) + Apply.remote(:erlang, :apply, [mod, fun, args], args_types, call, stack, context) end end # TODO: here - def of_expr({{:., _, [remote, name]}, meta, args} = expr, _expected_expr, stack, context) do - {remote_type, context} = of_expr(remote, {atom(), expr}, stack, context) + def of_expr({{:., _, [remote, name]}, meta, args} = call, _expected, _expr, stack, context) do + {remote_type, context} = of_expr(remote, atom(), call, stack, context) {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @expected_expr, stack, &2)) + Enum.map_reduce(args, context, &of_expr(&1, @pending, :ok, stack, &2)) - {mods, context} = Of.modules(remote_type, name, length(args), expr, meta, stack, context) - apply_many(mods, name, args, args_types, expr, stack, context) + {mods, context} = Of.modules(remote_type, name, length(args), call, meta, stack, context) + apply_many(mods, name, args, args_types, call, stack, context) end # TODO: &Foo.bar/1 def of_expr( - {:&, _, [{:/, _, [{{:., _, [remote, name]}, meta, []}, arity]}]} = expr, - _expected_expr, + {:&, _, [{:/, _, [{{:., _, [remote, name]}, meta, []}, arity]}]} = call, + _expected, + _expr, stack, context ) when is_atom(name) and is_integer(arity) do - {remote_type, context} = of_expr(remote, {atom(), expr}, stack, context) - {mods, context} = Of.modules(remote_type, name, arity, expr, meta, stack, context) + {remote_type, context} = of_expr(remote, atom(), call, stack, context) + {mods, context} = Of.modules(remote_type, name, arity, call, meta, stack, context) Apply.remote_capture(mods, name, arity, meta, stack, context) end # TODO: &foo/1 - def of_expr({:&, _meta, [{:/, _, [{fun, meta, _}, arity]}]}, _expected_expr, stack, context) do + def of_expr({:&, _meta, [{:/, _, [{fun, meta, _}, arity]}]}, _expected, _expr, stack, context) do Apply.local_capture(fun, arity, meta, stack, context) end # Super # TODO: here - def of_expr({:super, meta, args} = expr, _expected_expr, stack, context) when is_list(args) do + def of_expr({:super, meta, args} = expr, _expected, _expr, stack, context) when is_list(args) do {_kind, fun} = Keyword.fetch!(meta, :super) {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @expected_expr, stack, &2)) + Enum.map_reduce(args, context, &of_expr(&1, @pending, :ok, stack, &2)) Apply.local(fun, args_types, expr, stack, context) end # Local calls # TODO: here - def of_expr({fun, _meta, args} = expr, _expected_expr, stack, context) + def of_expr({fun, _meta, args} = expr, _expected, _expr, stack, context) when is_atom(fun) and is_list(args) do {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @expected_expr, stack, &2)) + Enum.map_reduce(args, context, &of_expr(&1, @pending, :ok, stack, &2)) Apply.local(fun, args_types, expr, stack, context) end # var # TODO: here - def of_expr(var, {expected, expr}, stack, context) when is_var(var) do + def of_expr(var, expected, expr, stack, context) when is_var(var) do if stack.mode == :traversal do {dynamic(), context} else @@ -574,7 +602,7 @@ defmodule Module.Types.Expr do context end - {type, context} = of_expr(body, @expected_expr, stack, context) + {type, context} = of_expr(body, @pending, :ok, stack, context) {type, reset_vars(context, original)} end @@ -583,7 +611,7 @@ defmodule Module.Types.Expr do defp for_clause({:<-, meta, [left, right]}, stack, context) do expr = {:<-, [type_check: :generator] ++ meta, [left, right]} {pattern, guards} = extract_head([left]) - {type, context} = of_expr(right, @expected_expr, stack, context) + {type, context} = of_expr(right, @pending, :ok, stack, context) context = Pattern.of_match(pattern, guards, dynamic(), expr, :for, stack, context) @@ -594,7 +622,7 @@ defmodule Module.Types.Expr do end defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do - {right_type, context} = of_expr(right, {binary(), expr}, stack, context) + {right_type, context} = of_expr(right, binary(), expr, stack, context) context = Pattern.of_match(left, binary(), expr, :for, stack, context) if compatible?(right_type, binary()) do @@ -606,7 +634,7 @@ defmodule Module.Types.Expr do end defp for_clause(expr, stack, context) do - {_type, context} = of_expr(expr, @term_expected, stack, context) + {_type, context} = of_expr(expr, @term, expr, stack, context) context end @@ -620,7 +648,7 @@ defmodule Module.Types.Expr do # TODO: Use the collectable protocol for the output defp for_into(into, meta, stack, context) do - {type, context} = of_expr(into, @expected_expr, stack, context) + {type, context} = of_expr(into, @pending, :ok, stack, context) # We use subtype? instead of compatible because we want to handle # only binary/list, even if a dynamic with something else is given. @@ -648,22 +676,24 @@ defmodule Module.Types.Expr do defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do {pattern, guards} = extract_head([left]) context = Pattern.of_match(pattern, guards, dynamic(), expr, :with, stack, context) - {_, context} = of_expr(right, @expected_expr, stack, context) + {_, context} = of_expr(right, @pending, :ok, stack, context) context end defp with_clause(expr, stack, context) do - {_type, context} = of_expr(expr, @expected_expr, stack, context) + {_type, context} = of_expr(expr, @pending, :ok, stack, context) context end defp with_option({:do, body}, stack, context, original) do - {_type, context} = of_expr(body, @expected_expr, stack, context) + {_type, context} = of_expr(body, @pending, :ok, stack, context) reset_vars(context, original) end defp with_option({:else, clauses}, stack, context, _original) do - {_, context} = of_clauses(clauses, [dynamic()], :with_else, stack, {none(), context}) + {_, context} = + of_clauses(clauses, [dynamic()], @pending, :ok, :with_else, stack, {none(), context}) + context end @@ -695,16 +725,15 @@ defmodule Module.Types.Expr do defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context} - defp of_clauses(clauses, expected, info, %{mode: mode} = stack, {acc, original}) do + defp of_clauses(clauses, domain, expected, expr, info, %{mode: mode} = stack, {acc, original}) do %{failed: failed?} = original Enum.reduce(clauses, {acc, original}, fn {:->, meta, [head, body]}, {acc, context} -> {failed?, context} = reset_failed(context, failed?) {patterns, guards} = extract_head(head) + {_trees, context} = Pattern.of_head(patterns, guards, domain, info, meta, stack, context) - {_trees, context} = Pattern.of_head(patterns, guards, expected, info, meta, stack, context) - - {body, context} = of_expr(body, @expected_expr, stack, context) + {body, context} = of_expr(body, expected, expr, stack, context) context = context |> set_failed(failed?) |> reset_vars(original) if mode == :traversal do diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 6adb9675d6e..76f6f8bbbdd 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -366,7 +366,7 @@ defmodule Module.Types.Of do :expr -> left = annotate_interpolation(left, right) - {actual, context} = Module.Types.Expr.of_expr(left, {type, expr}, stack, context) + {actual, context} = Module.Types.Expr.of_expr(left, type, expr, stack, context) compatible(actual, type, expr, stack, context) end @@ -406,7 +406,7 @@ defmodule Module.Types.Of do defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context) when not is_integer(arg) do - {actual, context} = Module.Types.Expr.of_expr(arg, {integer(), expr}, stack, context) + {actual, context} = Module.Types.Expr.of_expr(arg, integer(), expr, stack, context) {_, context} = compatible(actual, integer(), expr, stack, context) 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 4826a00b807..33a8a4c0bc9 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -121,7 +121,7 @@ defmodule TypeHelper do {_trees, context} = Pattern.of_head(patterns, guards, expected, :default, [], stack, new_context()) - Expr.of_expr(body, {Descr.term(), :ok}, stack, context) + Expr.of_expr(body, Descr.term(), :ok, stack, context) end defp expand_and_unpack(patterns, guards, body, env) do From 1a4907af13b5273d76f91ba0224d811748f1e53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jan 2025 19:26:07 +0100 Subject: [PATCH 07/22] Pass proper expression instead of bogus one --- lib/elixir/lib/module/types.ex | 2 +- lib/elixir/lib/module/types/expr.ex | 89 ++++++++++--------- lib/elixir/lib/module/types/pattern.ex | 79 +++++++++------- .../test/elixir/module/types/expr_test.exs | 6 +- .../test/elixir/module/types/pattern_test.exs | 10 +++ 5 files changed, 109 insertions(+), 77 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 2d5e5c12bed..2bb1400373f 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -285,7 +285,7 @@ defmodule Module.Types do Pattern.of_head(args, guards, expected, {:infer, expected}, meta, stack, context) {return_type, context} = - Expr.of_expr(body, Descr.term(), :ok, stack, context) + Expr.of_expr(body, Descr.term(), body, stack, context) args_types = if stack.mode == :traversal do diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index f4db1b02353..00c8e45db77 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -133,19 +133,29 @@ defmodule Module.Types.Expr do end # left = right - # TODO: here - def of_expr({:=, _, [left_expr, right_expr]} = expr, _expected, _expr, stack, context) do + def of_expr({:=, _, [left_expr, right_expr]} = match, expected, expr, stack, context) do {left_expr, right_expr} = repack_match(left_expr, right_expr) - {right_type, context} = of_expr(right_expr, @pending, expr, stack, context) - # We do not raise on underscore in case someone writes _ = raise "omg" - context = - case left_expr do - {:_, _, ctx} when is_atom(ctx) -> context - _ -> Pattern.of_match(left_expr, right_type, expr, {:match, right_type}, stack, context) - end + case left_expr do + # We do not raise on underscore in case someone writes _ = raise "omg" + {:_, _, ctx} when is_atom(ctx) -> + of_expr(right_expr, expected, expr, stack, context) + + _ -> + type_fun = fn pattern_type, context -> + # See if we can use the expected type to further refine the pattern type, + # if we cannot, use the pattern type as that will fail later on. + type = + case compatible_intersection(pattern_type, expected) do + {:ok, expected} -> expected + :error -> pattern_type + end + + of_expr(right_expr, type, expr, stack, context) + end - {right_type, context} + Pattern.of_match(left_expr, type_fun, match, stack, context) + end end # %{map | ...} @@ -236,23 +246,23 @@ defmodule Module.Types.Expr do end # (expr; expr) - def of_expr({:__block__, _meta, exprs}, expected, expr, stack, context) do + def of_expr({:__block__, _meta, exprs}, expected, _expr, stack, context) do {pre, [post]} = Enum.split(exprs, -1) context = Enum.reduce(pre, context, fn expr, context -> - {_, context} = of_expr(expr, @term, :ok, stack, context) + {_, context} = of_expr(expr, @term, expr, stack, context) context end) - of_expr(post, expected, expr, stack, context) + of_expr(post, expected, post, stack, context) end def of_expr({:cond, _meta, [[{:do, clauses}]]}, expected, expr, stack, original) do clauses |> reduce_non_empty({none(), original}, fn {:->, meta, [[head], body]}, {acc, context}, last? -> - {head_type, context} = of_expr(head, @pending, :ok, stack, context) + {head_type, context} = of_expr(head, @pending, head, stack, context) context = if stack.mode in [:infer, :traversal] do @@ -280,7 +290,7 @@ defmodule Module.Types.Expr do # TODO: here def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, expr, stack, context) do - {case_type, context} = of_expr(case_expr, @pending, :ok, stack, context) + {case_type, context} = of_expr(case_expr, @pending, case_expr, stack, context) # If we are only type checking the expression and the expression is a literal, # let's mark it as generated, as it is most likely a macro code. However, if @@ -307,7 +317,7 @@ defmodule Module.Types.Expr do [{:->, _, [head, _]} | _] = clauses {patterns, _guards} = extract_head(head) domain = Enum.map(patterns, fn _ -> dynamic() end) - {_acc, context} = of_clauses(clauses, domain, @pending, :ok, :fn, stack, {none(), context}) + {_acc, context} = of_clauses(clauses, domain, @pending, nil, :fn, stack, {none(), context}) {fun(), context} end @@ -318,7 +328,7 @@ defmodule Module.Types.Expr do {type, context} = if else_block do - {type, context} = of_expr(body, @pending, :ok, stack, original) + {type, context} = of_expr(body, @pending, body, stack, original) of_clauses( else_block, @@ -362,7 +372,7 @@ defmodule Module.Types.Expr do |> dynamic_unless_static(stack) if after_block do - {_type, context} = of_expr(after_block, @term, :ok, stack, context) + {_type, context} = of_expr(after_block, @term, after_block, stack, context) {type, context} else {type, context} @@ -412,7 +422,7 @@ defmodule Module.Types.Expr do else into = Keyword.get(opts, :into, []) {into_wrapper, gradual?, context} = for_into(into, meta, stack, context) - {block_type, context} = of_expr(block, @pending, :ok, stack, context) + {block_type, context} = of_expr(block, @pending, block, stack, context) for_type = for type <- into_wrapper do @@ -443,7 +453,7 @@ defmodule Module.Types.Expr do {fun_type, context} = of_expr(fun, fun(), call, stack, context) {_args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @pending, :ok, stack, &2)) + Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2)) case fun_fetch(fun_type, length(args)) do :ok -> @@ -455,7 +465,6 @@ defmodule Module.Types.Expr do end end - # TODO: here def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context) when not is_atom(callee) and is_atom(key_or_fun) do if Keyword.get(meta, :no_parens, false) do @@ -490,7 +499,7 @@ defmodule Module.Types.Expr do end {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @pending, :ok, stack, &2)) + Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2)) {types, context} = Enum.map_reduce(funs, context, fn fun, context -> @@ -511,7 +520,7 @@ defmodule Module.Types.Expr do {remote_type, context} = of_expr(remote, atom(), call, stack, context) {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @pending, :ok, stack, &2)) + Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2)) {mods, context} = Of.modules(remote_type, name, length(args), call, meta, stack, context) apply_many(mods, name, args, args_types, call, stack, context) @@ -542,7 +551,7 @@ defmodule Module.Types.Expr do {_kind, fun} = Keyword.fetch!(meta, :super) {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @pending, :ok, stack, &2)) + Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2)) Apply.local(fun, args_types, expr, stack, context) end @@ -552,7 +561,7 @@ defmodule Module.Types.Expr do def of_expr({fun, _meta, args} = expr, _expected, _expr, stack, context) when is_atom(fun) and is_list(args) do {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @pending, :ok, stack, &2)) + Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2)) Apply.local(fun, args_types, expr, stack, context) end @@ -602,7 +611,7 @@ defmodule Module.Types.Expr do context end - {type, context} = of_expr(body, @pending, :ok, stack, context) + {type, context} = of_expr(body, @pending, body, stack, context) {type, reset_vars(context, original)} end @@ -611,19 +620,14 @@ defmodule Module.Types.Expr do defp for_clause({:<-, meta, [left, right]}, stack, context) do expr = {:<-, [type_check: :generator] ++ meta, [left, right]} {pattern, guards} = extract_head([left]) - {type, context} = of_expr(right, @pending, :ok, stack, context) - - context = Pattern.of_match(pattern, guards, dynamic(), expr, :for, stack, context) - - {_type, context} = - Apply.remote(Enumerable, :count, [right], [type], expr, stack, context) - - context + {type, context} = of_expr(right, @pending, expr, stack, context) + {_type, context} = Apply.remote(Enumerable, :count, [right], [type], expr, stack, context) + Pattern.of_generator(pattern, guards, dynamic(), :for, expr, stack, context) end defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do {right_type, context} = of_expr(right, binary(), expr, stack, context) - context = Pattern.of_match(left, binary(), expr, :for, stack, context) + context = Pattern.of_generator(left, [], binary(), :for, expr, stack, context) if compatible?(right_type, binary()) do context @@ -648,7 +652,7 @@ defmodule Module.Types.Expr do # TODO: Use the collectable protocol for the output defp for_into(into, meta, stack, context) do - {type, context} = of_expr(into, @pending, :ok, stack, context) + {type, context} = of_expr(into, @pending, into, stack, context) # We use subtype? instead of compatible because we want to handle # only binary/list, even if a dynamic with something else is given. @@ -675,24 +679,23 @@ defmodule Module.Types.Expr do defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do {pattern, guards} = extract_head([left]) - context = Pattern.of_match(pattern, guards, dynamic(), expr, :with, stack, context) - {_, context} = of_expr(right, @pending, :ok, stack, context) - context + {_type, context} = of_expr(right, @pending, right, stack, context) + Pattern.of_generator(pattern, guards, dynamic(), :with, expr, stack, context) end defp with_clause(expr, stack, context) do - {_type, context} = of_expr(expr, @pending, :ok, stack, context) + {_type, context} = of_expr(expr, @pending, expr, stack, context) context end defp with_option({:do, body}, stack, context, original) do - {_type, context} = of_expr(body, @pending, :ok, stack, context) + {_type, context} = of_expr(body, @pending, body, stack, context) reset_vars(context, original) end defp with_option({:else, clauses}, stack, context, _original) do {_, context} = - of_clauses(clauses, [dynamic()], @pending, :ok, :with_else, stack, {none(), context}) + of_clauses(clauses, [dynamic()], @pending, nil, :with_else, stack, {none(), context}) context end @@ -733,7 +736,7 @@ defmodule Module.Types.Expr do {patterns, guards} = extract_head(head) {_trees, context} = Pattern.of_head(patterns, guards, domain, info, meta, stack, context) - {body, context} = of_expr(body, expected, expr, stack, context) + {body, context} = of_expr(body, expected, expr || body, stack, context) context = context |> set_failed(failed?) |> reset_vars(original) if mode == :traversal do diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index ae9921d9649..38979782284 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -56,9 +56,10 @@ defmodule Module.Types.Pattern do defp of_pattern_args(patterns, expected, tag, stack, context) do context = init_pattern_info(context) {trees, context} = of_pattern_args_index(patterns, 0, [], stack, context) + {pattern_info, context} = pop_pattern_info(context) - context = - of_pattern_recur(expected, tag, stack, context, fn types, changed, context -> + {_, context} = + of_pattern_recur(expected, tag, pattern_info, stack, context, fn types, changed, context -> of_pattern_args_tree(trees, types, changed, 0, [], tag, stack, context) end) @@ -110,48 +111,66 @@ defmodule Module.Types.Pattern do end @doc """ - A simplified version of `of_head` used by `=` and `<-`. - - This version tracks the whole expression in tracing, - instead of only the pattern. + Handles the match operator. """ - def of_match(pattern, guards \\ [], expected, expr, tag, stack, context) + def of_match(pattern, expected_fun, expr, stack, context) - def of_match(_pattern, _guards, _expected, _expr, _tag, %{mode: :traversal}, context) do - context + def of_match(_pattern, expected_fun, _expr, %{mode: :traversal}, context) do + expected_fun.(dynamic(), context) end - def of_match(pattern, guards, expected, expr, tag, stack, context) do + def of_match(pattern, expected_fun, expr, stack, context) do context = init_pattern_info(context) {tree, context} = of_pattern(pattern, [{:arg, 0, expr}], stack, context) + {pattern_info, context} = pop_pattern_info(context) + {expected, context} = expected_fun.(of_pattern_tree(tree, context), context) - context = - of_pattern_recur([expected], tag, stack, context, fn [type], [0], context -> - with {:ok, type, context} <- - of_pattern_intersect(tree, type, expr, 0, tag, stack, context) do - {:ok, [type], context} - end - end) + {[type], context} = + of_single_pattern_recur(expected, {:match, expected}, tree, pattern_info, expr, stack, context) + + {type, context} + end + + @doc """ + Handles matches in generators. + """ + def of_generator(pattern, guards, expected, tag, expr, stack, context) + def of_generator(_pattern, _guards, _expected, _tag, _expr, %{mode: :traversal}, context) do + context + end + + def of_generator(pattern, guards, expected, tag, expr, stack, context) do + context = init_pattern_info(context) + {tree, context} = of_pattern(pattern, [{:arg, 0, expr}], stack, context) + {pattern_info, context} = pop_pattern_info(context) + {_, context} = of_single_pattern_recur(expected, tag, tree, pattern_info, expr, stack, context) {_, context} = Enum.map_reduce(guards, context, &of_guard(&1, @guard, &1, stack, &2)) context end + defp of_single_pattern_recur(expected, tag, tree, pattern_info, expr, stack, context) do + of_pattern_recur([expected], tag, pattern_info, stack, context, fn [type], [0], context -> + with {:ok, type, context} <- + of_pattern_intersect(tree, type, expr, 0, tag, stack, context) do + {:ok, [type], context} + end + end) + end + defp all_single_path?(vars, info, index) do info |> Map.get(index, []) |> Enum.all?(fn version -> match?([_], Map.fetch!(vars, version)) end) end - defp of_pattern_recur(types, tag, stack, context, callback) do - %{pattern_info: {vars, info, _counter}} = context - context = nilify_pattern_info(context) + defp of_pattern_recur(types, tag, pattern_info, stack, context, callback) do + {vars, info, _counter} = pattern_info changed = :lists.seq(0, length(types) - 1) # If all variables in a given index have a single path, # then there are no changes to propagate unchangeable = for index <- changed, all_single_path?(vars, info, index), do: index - vars = Map.to_list(vars) try do @@ -160,10 +179,10 @@ defmodule Module.Types.Pattern do of_pattern_recur(types, unchangeable, vars, info, tag, stack, context, callback) {:error, context} -> - error_vars(vars, context) + {types, error_vars(vars, context)} end catch - context -> error_vars(vars, context) + {types, context} -> {types, error_vars(vars, context)} end end @@ -185,12 +204,12 @@ defmodule Module.Types.Pattern do _ -> case Of.refine_head_var(var, type, expr, stack, context) do {:ok, _type, context} -> {var_changed? or reachable_var?, context} - {:error, _type, context} -> throw(context) + {:error, _type, context} -> throw({types, context}) end end :error -> - throw(badpattern_error(expr, index, tag, stack, context)) + throw({types, badpattern_error(expr, index, tag, stack, context)}) end end) @@ -206,19 +225,19 @@ defmodule Module.Types.Pattern do case :lists.usort(changed) -- unchangeable do [] -> - context + {types, context} changed -> case callback.(types, changed, context) do # A simple structural comparison for optimization {:ok, ^types, context} -> - context + {types, context} {:ok, types, context} -> of_pattern_recur(types, unchangeable, vars, info, tag, stack, context, callback) {:error, context} -> - error_vars(vars, context) + {types, error_vars(vars, context)} end end end @@ -733,8 +752,8 @@ defmodule Module.Types.Pattern do %{context | pattern_info: {%{}, %{}, 1}} end - defp nilify_pattern_info(context) do - %{context | pattern_info: nil} + defp pop_pattern_info(%{pattern_info: pattern_info} = context) do + {pattern_info, %{context | pattern_info: nil}} end # $ type tag = head_pattern() or match_pattern() diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index d8a84340576..011694d6263 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -179,11 +179,11 @@ defmodule Module.Types.ExprTest do assert typecheck!( [x], ( - x.foo_bar - x.baz_bat + :foo = x.foo_bar + 123 = x.baz_bat x ) - ) == dynamic(open_map(foo_bar: term(), baz_bat: term())) + ) == dynamic(open_map(foo_bar: atom([:foo]), baz_bat: integer())) end test "undefined function warnings" do diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index 326c67dd729..2df170a6a8d 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -103,6 +103,16 @@ defmodule Module.Types.PatternTest do ) == uri_type end + test "refines types" do + assert typecheck!( + [x, foo = :foo, bar = 123], + ( + {^foo, ^bar} = x + x + ) + ) == dynamic(tuple([atom([:foo]), integer()])) + end + test "reports incompatible types" do assert typeerror!([x = {:ok, _}], [_ | _] = x) == ~l""" the following pattern will never match: From 4ebd0e4839591d5a65f644ad4ed48d422e3197f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Jan 2025 19:30:38 +0100 Subject: [PATCH 08/22] mix format --- lib/elixir/lib/module/types/pattern.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 38979782284..c00cdfefa8f 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -124,9 +124,10 @@ defmodule Module.Types.Pattern do {tree, context} = of_pattern(pattern, [{:arg, 0, expr}], stack, context) {pattern_info, context} = pop_pattern_info(context) {expected, context} = expected_fun.(of_pattern_tree(tree, context), context) + tag = {:match, expected} {[type], context} = - of_single_pattern_recur(expected, {:match, expected}, tree, pattern_info, expr, stack, context) + of_single_pattern_recur(expected, tag, tree, pattern_info, expr, stack, context) {type, context} end @@ -144,7 +145,10 @@ defmodule Module.Types.Pattern do context = init_pattern_info(context) {tree, context} = of_pattern(pattern, [{:arg, 0, expr}], stack, context) {pattern_info, context} = pop_pattern_info(context) - {_, context} = of_single_pattern_recur(expected, tag, tree, pattern_info, expr, stack, context) + + {_, context} = + of_single_pattern_recur(expected, tag, tree, pattern_info, expr, stack, context) + {_, context} = Enum.map_reduce(guards, context, &of_guard(&1, @guard, &1, stack, &2)) context end From 939a7c54120a6f3c8a15706053df4deddf2286cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 4 Jan 2025 13:18:18 +0100 Subject: [PATCH 09/22] Decouple remote domains from remote application --- lib/elixir/lib/module/parallel_checker.ex | 11 +- lib/elixir/lib/module/types.ex | 13 +- lib/elixir/lib/module/types/apply.ex | 464 +++++++++--------- lib/elixir/lib/module/types/descr.ex | 4 +- lib/elixir/lib/module/types/expr.ex | 92 ++-- lib/elixir/lib/module/types/helpers.ex | 15 + lib/elixir/lib/module/types/pattern.ex | 15 +- .../test/elixir/module/types/expr_test.exs | 111 ++--- .../test/elixir/module/types/infer_test.exs | 30 +- .../elixir/module/types/integration_test.exs | 4 +- .../test/elixir/module/types/pattern_test.exs | 6 +- lib/mix/lib/mix/compilers/elixir.ex | 2 +- 12 files changed, 417 insertions(+), 350 deletions(-) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 87d81c82200..dcf5fe9b6ed 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -208,7 +208,7 @@ defmodule Module.ParallelChecker do or if the function does not exist return `{:error, :function}`. """ @spec fetch_export(cache(), module(), atom(), arity()) :: - {:ok, mode(), binary() | nil, {:infer, [term()]} | :none} + {:ok, mode(), binary() | nil, {:infer, [term()] | nil, [term()]} | :none} | :badmodule | {:badfunction, mode()} def fetch_export({checker, table}, module, fun, arity) do @@ -451,10 +451,15 @@ defmodule Module.ParallelChecker do defp cache_chunk(table, module, exports) do Enum.each(exports, fn {{fun, arity}, info} -> - # TODO: Match on signature directly in Elixir v1.22+ + sig = + case info do + %{sig: {key, _, _} = sig} when key in [:infer, :strong] -> sig + _ -> :none + end + :ets.insert( table, - {{module, {fun, arity}}, Map.get(info, :deprecated), Map.get(info, :sig, :none)} + {{module, {fun, arity}}, Map.get(info, :deprecated), sig} ) end) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 2bb1400373f..1d1532d10f2 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -308,7 +308,18 @@ defmodule Module.Types do end end) - inferred = {:infer, Enum.reverse(clauses_types)} + domain = + case clauses_types do + [_] -> + nil + + _ -> + clauses_types + |> Enum.map(fn {args, _} -> args end) + |> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end) + end + + inferred = {:infer, domain, Enum.reverse(clauses_types)} {inferred, mapping, restore_context(clauses_context, context)} end diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 9ca5e96caf3..18c7c751733 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -261,193 +261,217 @@ defmodule Module.Types.Apply do defp signature(_mod, _fun, _arity), do: :none @doc """ - Applies a function in unknown modules. + Returns the domain of an unknown module. Used only by info functions. """ - def remote(_name, _args_types, _expr, %{mode: :traversal}, context) do - {dynamic(), context} + def remote_domain(_fun, args, _expected, %{mode: :traversal}) do + {:none, Enum.map(args, fn _ -> term() end)} end - def remote(name, args_types, expr, stack, context) do - arity = length(args_types) + def remote_domain(fun, args, _expected, _stack) do + arity = length(args) - case signature(name, arity) do - :none -> {dynamic(), context} - info -> apply_remote(nil, name, info, args_types, expr, stack, context) + case signature(fun, arity) do + :none -> {:none, Enum.map(args, fn _ -> term() end)} + {:strong, domain, clauses} = info -> {info, domain(domain, clauses)} end end @doc """ - Applies a function in a given module. + Returns the domain of a remote function with info to apply it. """ - def remote(_module, _fun, _args, _args_types, _expr, %{mode: :traversal}, context) do - {dynamic(), context} + def remote_domain(_module, _fun, args, _expected, _meta, %{mode: :traversal}, context) do + {:none, Enum.map(args, fn _ -> term() end), context} end - def remote(:erlang, :element, [index, _], [_, tuple], expr, stack, context) + def remote_domain(:erlang, :element, [index, _], expected, _meta, _stack, context) when is_integer(index) do - case tuple_fetch(tuple, index - 1) do - {_optional?, value_type} -> - {value_type, context} - - :badtuple -> - {error_type(), - badremote_error(:erlang, :element, expr, [integer(), tuple], stack, context)} - - :badindex -> - mfac = mfac(expr, :erlang, :element, 2) + tuple = open_tuple(List.duplicate(term(), max(index - 1, 0)) ++ [expected]) + {{:element, index}, [integer(), tuple], context} + end - {error_type(), - error({:badindex, mfac, expr, tuple, index - 1, context}, elem(expr, 1), stack, context)} - end + def remote_domain(:erlang, :insert_element, [index, _, _], _expected, _meta, _stack, context) + when is_integer(index) do + tuple = open_tuple(List.duplicate(term(), max(index - 1, 0))) + {{:insert_element, index}, [integer(), tuple, term()], context} end - def remote(:erlang, :insert_element, [index, _, _], [_, tuple, value], expr, stack, context) + def remote_domain(:erlang, :delete_element, [index, _], _expected, _meta, _stack, context) when is_integer(index) do - case tuple_insert_at(tuple, index - 1, value) do - value_type when is_descr(value_type) -> - {value_type, context} + tuple = open_tuple(List.duplicate(term(), max(index, 1))) + {{:delete_element, index}, [integer(), tuple], context} + end + + def remote_domain(:erlang, :make_tuple, [size, _elem], _expected, _meta, _stack, context) + when is_integer(size) and size >= 0 do + {{:make_tuple, size}, [integer(), term()], context} + end - :badtuple -> - args_types = [integer(), tuple, value] + def remote_domain(:erlang, :hd, [_list], expected, _meta, _stack, context) do + {:hd, [non_empty_list(expected, term())], context} + end - {error_type(), - badremote_error(:erlang, :insert_element, expr, args_types, stack, context)} + def remote_domain(:erlang, :tl, [_list], _expected, _meta, _stack, context) do + {:tl, [non_empty_list(term(), term())], context} + end - :badindex -> - mfac = mfac(expr, :erlang, :insert_element, 3) + def remote_domain(:erlang, name, [_left, _right], _expected, _meta, stack, context) + when name in [:>=, :"=<", :>, :<, :min, :max] do + skip? = stack.mode == :infer + {{:ordered_compare, name, skip?}, [term(), term()], context} + end - {error_type(), - error({:badindex, mfac, expr, tuple, index - 2, context}, elem(expr, 1), stack, context)} - end + def remote_domain(:erlang, name, [_left, _right] = args, _expected, _meta, stack, context) + when name in [:==, :"/=", :"=:=", :"=/="] do + skip? = stack.mode == :infer or Macro.quoted_literal?(args) + {{:compare, name, skip?}, [term(), term()], context} end - def remote(:erlang, :delete_element, [index, _], [_, tuple], expr, stack, context) - when is_integer(index) do - case tuple_delete_at(tuple, index - 1) do - value_type when is_descr(value_type) -> - {value_type, context} + def remote_domain(mod, fun, args, expected, meta, stack, context) do + arity = length(args) - :badtuple -> - args_types = [integer(), tuple] + case :elixir_rewrite.inline(mod, fun, arity) do + {new_mod, new_fun} -> + remote_domain(new_mod, new_fun, args, expected, meta, stack, context) - {error_type(), - badremote_error(:erlang, :delete_element, expr, args_types, stack, context)} + false -> + {info, context} = signature(mod, fun, arity, meta, stack, context) - :badindex -> - mfac = mfac(expr, :erlang, :delete_element, 2) + case info do + {type, domain, clauses} -> + domain = domain(domain, clauses) + {{type, domain, clauses}, domain, context} - {error_type(), - error({:badindex, mfac, expr, tuple, index - 1, context}, elem(expr, 1), stack, context)} + :none -> + {:none, List.duplicate(term(), arity), context} + end end end - def remote(:erlang, :make_tuple, [size, _], [_, elem], _expr, _stack, context) - when is_integer(size) and size >= 0 do - {tuple(List.duplicate(elem, size)), context} + @doc """ + Applies a previously collected domain from `remote_domain/7`. + """ + def remote_apply(info, mod, fun, args_types, expr, stack, context) do + case remote_apply(info, args_types, stack) do + {:ok, type} -> + {type, context} + + {:error, error} -> + mfac = mfac(expr, mod, fun, length(args_types)) + error = {error, args_types, mfac, expr, context} + {error_type(), error(error, elem(expr, 1), stack, context)} + end end - def remote(:erlang, :hd, _args, [list], expr, stack, context) do - case list_hd(list) do - {_, value_type} -> - {value_type, context} + defp remote_apply(:none, _args_types, _stack) do + {:ok, dynamic()} + end - :badnonemptylist -> - {error_type(), badremote_error(:erlang, :hd, expr, [list], stack, context)} + defp remote_apply({:infer, domain, clauses}, args_types, _stack) do + case apply_infer(domain, clauses, args_types) do + {_used, type} -> {:ok, type} + {:error, domain, clauses} -> {:error, {:badremote, domain, clauses}} end end - def remote(:erlang, :tl, _args, [list], expr, stack, context) do - case list_tl(list) do - {_, value_type} -> - {value_type, context} - - :badnonemptylist -> - {error_type(), badremote_error(:erlang, :tl, expr, [list], stack, context)} + defp remote_apply({:strong, domain, clauses}, args_types, stack) do + case apply_strong(domain, clauses, args_types, stack) do + {_used, type} -> {:ok, type} + {:error, domain, clauses} -> {:error, {:badremote, domain, clauses}} end end - def remote(:erlang, name, _args, [left, right], expr, stack, context) - when name in [:>=, :"=<", :>, :<, :min, :max] do - context = - cond do - stack.mode == :infer -> - context + defp remote_apply({:element, index}, [_index, tuple], _stack) do + case tuple_fetch(tuple, index - 1) do + {_optional?, value_type} -> {:ok, value_type} + :badtuple -> {:error, badremote(:erlang, :element, 2)} + :badindex -> {:error, {:badindex, index, tuple}} + end + end - match?({false, _}, map_fetch(left, :__struct__)) or - match?({false, _}, map_fetch(right, :__struct__)) -> - warning = {:struct_comparison, expr, name, left, right, context} - warn(__MODULE__, warning, elem(expr, 1), stack, context) + defp remote_apply({:insert_element, index}, [_index, tuple, value], _stack) do + case tuple_insert_at(tuple, index - 1, value) do + value_type when is_descr(value_type) -> {:ok, value_type} + :badtuple -> {:error, badremote(:erlang, :insert_element, 3)} + :badindex -> {:error, {:badindex, index - 1, tuple}} + end + end - number_type?(left) and number_type?(right) -> - context + defp remote_apply({:delete_element, index}, [_index, tuple], _stack) do + case tuple_delete_at(tuple, index - 1) do + value_type when is_descr(value_type) -> {:ok, value_type} + :badtuple -> {:error, badremote(:erlang, :delete_element, 2)} + :badindex -> {:error, {:badindex, index, tuple}} + end + end - disjoint?(left, right) -> - warning = {:mismatched_comparison, expr, name, left, right, context} - warn(__MODULE__, warning, elem(expr, 1), stack, context) + defp remote_apply({:make_tuple, size}, [_size, elem], _stack) do + {:ok, tuple(List.duplicate(elem, size))} + end - true -> - context - end + defp remote_apply(:hd, [list], _stack) do + case list_hd(list) do + {_, value_type} -> {:ok, value_type} + :badnonemptylist -> {:error, badremote(:erlang, :hd, 1)} + end + end - if name in [:min, :max] do - {union(left, right), context} - else - {return(boolean(), [left, right], stack), context} + defp remote_apply(:tl, [list], _stack) do + case list_tl(list) do + {_, value_type} -> {:ok, value_type} + :badnonemptylist -> {:error, badremote(:erlang, :tl, 1)} end end - def remote(:erlang, name, args, [left, right] = args_types, expr, stack, context) - when name in [:==, :"/=", :"=:=", :"=/="] do - context = - cond do - # We ignore quoted literals as they most likely come from generated code. - stack.mode == :infer or Macro.quoted_literal?(args) -> - context + defp remote_apply({:ordered_compare, name, skip?}, [left, right], stack) do + result = + if name in [:min, :max] do + union(left, right) + else + return(boolean(), [left, right], stack) + end - name in [:==, :"/="] and number_type?(left) and number_type?(right) -> - context + cond do + skip? -> + {:ok, result} - disjoint?(left, right) -> - warning = {:mismatched_comparison, expr, name, left, right, context} - warn(__MODULE__, warning, elem(expr, 1), stack, context) + match?({false, _}, map_fetch(left, :__struct__)) or + match?({false, _}, map_fetch(right, :__struct__)) -> + {:error, :struct_comparison} - true -> - context - end + number_type?(left) and number_type?(right) -> + {:ok, result} + + disjoint?(left, right) -> + {:error, :mismatched_comparison} - {return(boolean(), args_types, stack), context} + true -> + {:ok, result} + end end - def remote(mod, fun, args, args_types, expr, stack, context) do - arity = length(args_types) + defp remote_apply({:compare, name, skip?}, [left, right], stack) do + result = return(boolean(), [left, right], stack) - case :elixir_rewrite.inline(mod, fun, arity) do - {new_mod, new_fun} -> - expr = inline_meta(expr, mod, fun) - remote(new_mod, new_fun, args, args_types, expr, stack, context) + cond do + skip? -> + {:ok, result} - false -> - {info, context} = signature(mod, fun, arity, elem(expr, 1), stack, context) - apply_remote(mod, fun, info, args_types, expr, stack, context) - end - end + name in [:==, :"/="] and number_type?(left) and number_type?(right) -> + {:ok, result} - defp apply_remote(mod, fun, info, args_types, expr, stack, context) do - case apply_signature(info, args_types, stack) do - {:ok, _indexes, type} -> - {type, context} + disjoint?(left, right) -> + {:error, :mismatched_comparison} - {:error, domain, clauses} -> - mfac = mfac(expr, mod, fun, length(args_types)) - error = {:badremote, mfac, expr, args_types, domain, clauses, context} - {error_type(), error(error, elem(expr, 1), stack, context)} + true -> + {:ok, result} end end - defp inline_meta({node, meta, args}, mod, fun) do - {node, [inline: {mod, fun}] ++ meta, args} + defp badremote(mod, fun, arity) do + {_, domain, clauses} = signature(mod, fun, arity) + {:badremote, domain, clauses} end @doc """ @@ -478,7 +502,7 @@ defmodule Module.Types.Apply do * `:none` - no typing information found. - * `{:infer, clauses}` - clauses from inferences. You must check all + * `{:infer, domain or nil, clauses}` - clauses from inferences. You must check all all clauses and return the union between them. They are dynamic and they can only be converted into arrows by computing the union of all arguments. @@ -604,48 +628,63 @@ defmodule Module.Types.Apply do ## Local - @doc """ - Deal with local functions. - """ - def local(fun, args_types, {_, meta, _} = expr, stack, context) do - fun_arity = {fun, length(args_types)} + def local_domain(fun, args, meta, stack, context) do + arity = length(args) - case stack.local_handler.(meta, fun_arity, stack, context) do + case stack.local_handler.(meta, {fun, arity}, stack, context) do false -> - {dynamic(), context} - - {_kind, _info, context} when stack.mode == :traversal -> - {dynamic(), context} + {{false, :none}, List.duplicate(term(), arity), context} {kind, info, context} -> - case apply_signature(info, args_types, stack) do - {:ok, indexes, type} -> - context = - if stack.mode != :infer and kind == :defp do - update_in(context.local_used[fun_arity], fn current -> - if info == :none do - [] - else - (current || used_from_clauses(info)) -- indexes - end - end) + update_used? = stack.mode not in [:traversal, :infer] and kind == :defp + + case info do + _ when stack.mode == :traversal or info == :none -> + {{update_used?, :none}, List.duplicate(term(), arity), context} + + {_strong_or_infer, domain, clauses} -> + {{update_used?, info}, domain(domain, clauses), context} + end + end + end + + def local_apply({update_used?, info}, fun, args_types, expr, stack, context) do + case local_apply(info, args_types, stack) do + {indexes, type} -> + context = + if update_used? do + update_in(context.local_used[{fun, length(args_types)}], fn current -> + if info == :none do + [] else - context + (current || used_from_clauses(info)) -- indexes end + end) + else + context + end - {type, context} + {type, context} - {:error, domain, clauses} -> - error = {:badlocal, expr, args_types, domain, clauses, context} - {error_type(), error(error, with_span(meta, fun), stack, context)} - end + {:error, domain, clauses} -> + error = {:badlocal, domain, clauses, args_types, expr, context} + {error_type(), error(error, with_span(elem(expr, 1), fun), stack, context)} end end - defp used_from_clauses({:infer, clauses}), - do: Enum.with_index(clauses, fn _, i -> i end) + defp local_apply(:none, _args_types, _stack) do + {[], dynamic()} + end - defp used_from_clauses({:strong, _, clauses}), + defp local_apply({:infer, domain, clauses}, args_types, _stack) do + apply_infer(domain, clauses, args_types) + end + + defp local_apply({:strong, domain, clauses}, args_types, stack) do + apply_strong(domain, clauses, args_types, stack) + end + + defp used_from_clauses({_strong_or_infer, _domain, clauses}), do: Enum.with_index(clauses, fn _, i -> i end) @doc """ @@ -681,47 +720,41 @@ defmodule Module.Types.Apply do end end - defp apply_signature(:none, _args_types, _stack) do - {:ok, [], dynamic()} + defp domain(nil, [{domain, _}]), do: domain + defp domain(domain, _clauses), do: domain + + defp apply_infer(domain, clauses, args_types) do + case apply_clauses(clauses, args_types, 0, 0, [], []) do + {0, [], []} -> + {:error, domain, clauses} + + {count, used, _returns} when count > @max_clauses -> + {used, dynamic()} + + {_count, used, returns} -> + {used, returns |> Enum.reduce(&union/2) |> dynamic()} + end end - defp apply_signature({:strong, nil, [{expected, return}] = clauses}, args_types, stack) do + defp apply_strong(domain, [{expected, return}] = clauses, args_types, stack) do # Optimize single clauses as the domain is the single clause args. case zip_compatible?(args_types, expected) do - true -> {:ok, [0], return(return, args_types, stack)} - false -> {:error, expected, clauses} + true -> {[0], return(return, args_types, stack)} + false -> {:error, domain, clauses} end end - defp apply_signature({:strong, domain, clauses}, args_types, stack) do + defp apply_strong(domain, clauses, args_types, stack) do # If the type is only gradual, the compatibility check is the same # as a non disjoint check. So we skip checking compatibility twice. with true <- zip_compatible_or_only_gradual?(args_types, domain), {count, used, returns} when count > 0 <- apply_clauses(clauses, args_types, 0, 0, [], []) do - {:ok, used, returns |> Enum.reduce(&union/2) |> return(args_types, stack)} + {used, returns |> Enum.reduce(&union/2) |> return(args_types, stack)} else _ -> {:error, domain, clauses} end end - defp apply_signature({:infer, clauses}, args_types, _stack) do - case apply_clauses(clauses, args_types, 0, 0, [], []) do - {0, [], []} -> - domain = - clauses - |> Enum.map(fn {args, _} -> args end) - |> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end) - - {:error, domain, clauses} - - {count, used, _returns} when count > @max_clauses -> - {:ok, used, dynamic()} - - {_count, used, returns} -> - {:ok, used, returns |> Enum.reduce(&union/2) |> dynamic()} - end - end - defp apply_clauses([{expected, return} | clauses], args_types, index, count, used, returns) do if zip_not_disjoint?(args_types, expected) do apply_clauses(clauses, args_types, index + 1, count + 1, [index | used], [return | returns]) @@ -759,73 +792,66 @@ defmodule Module.Types.Apply do error(__MODULE__, warning, meta, stack, context) end - defp badremote_error(mod, fun, {_, meta, _} = expr, args_types, stack, context) do - arity = length(args_types) - mfac = mfac(expr, mod, fun, arity) - {_type, domain, [{args, _} | _] = clauses} = signature(mod, fun, arity) - domain = domain || args - tuple = {:badremote, mfac, expr, args_types, domain, clauses, context} - error(tuple, meta, stack, context) - end - ## Diagnostics - def format_diagnostic({:badindex, mfac, expr, type, index, context}) do + def format_diagnostic({:badlocal, domain, clauses, args_types, expr, context}) do + domain = domain(domain, clauses) traces = collect_traces(expr, context) - {mod, fun, arity, _converter} = mfac - mfa = Exception.format_mfa(mod, fun, arity) + converter = &Function.identity/1 + {fun, _, _} = expr + + explanation = + empty_arg_reason(args_types) || + """ + but expected one of: + #{clauses_args_to_quoted_string(clauses, converter, [])} + """ %{ details: %{typing_traces: traces}, message: IO.iodata_to_binary([ """ - expected a tuple with at least #{pluralize(index + 1, "element", "elements")} in #{mfa}: + incompatible types given to #{fun}/#{length(args_types)}: #{expr_to_string(expr) |> indent(4)} - the given type does not have the given index: + given types: + + #{args_to_quoted_string(args_types, domain, converter) |> indent(4)} - #{to_quoted_string(type) |> indent(4)} """, + explanation, format_traces(traces) ]) } end - def format_diagnostic({:badlocal, expr, args_types, domain, clauses, context}) do + def format_diagnostic({{:badindex, index, type}, _args_types, mfac, expr, context}) do traces = collect_traces(expr, context) - converter = &Function.identity/1 - {fun, _, _} = expr - - explanation = - empty_arg_reason(args_types) || - """ - but expected one of: - #{clauses_args_to_quoted_string(clauses, converter, [])} - """ + {mod, fun, arity, _converter} = mfac + mfa = Exception.format_mfa(mod, fun, arity) %{ details: %{typing_traces: traces}, message: IO.iodata_to_binary([ """ - incompatible types given to #{fun}/#{length(args_types)}: + expected a tuple with at least #{pluralize(index, "element", "elements")} in #{mfa}: #{expr_to_string(expr) |> indent(4)} - given types: - - #{args_to_quoted_string(args_types, domain, converter) |> indent(4)} + the given type does not have the given index: + #{to_quoted_string(type) |> indent(4)} """, - explanation, format_traces(traces) ]) } end - def format_diagnostic({:badremote, mfac, expr, args_types, domain, clauses, context}) do + def format_diagnostic({{:badremote, domain, clauses}, args_types, mfac, expr, context}) do + domain = domain(domain, clauses) {mod, fun, arity, converter} = mfac meta = elem(expr, 1) @@ -911,7 +937,8 @@ defmodule Module.Types.Apply do } end - def format_diagnostic({:mismatched_comparison, expr, name, left, right, context}) do + def format_diagnostic({:mismatched_comparison, [left, right], mfac, expr, context}) do + {_, name, _, _} = mfac traces = collect_traces(expr, context) %{ @@ -938,7 +965,8 @@ defmodule Module.Types.Apply do } end - def format_diagnostic({:struct_comparison, expr, name, left, right, context}) do + def format_diagnostic({:struct_comparison, [left, right], mfac, expr, context}) do + {_, name, _, _} = mfac traces = collect_traces(expr, context) %{ @@ -1033,10 +1061,6 @@ defmodule Module.Types.Apply do defp pluralize(1, singular, _), do: "1 #{singular}" defp pluralize(i, _, plural), do: "#{i} #{plural}" - defp mfac({_, [inline: {mod, fun}] ++ _, _}, _mod, _fun, arity) do - {mod, fun, arity, & &1} - end - defp mfac({{:., _, [mod, fun]}, _, args}, _mod, _fun, _arity) when is_atom(mod) and is_atom(fun) do {mod, fun, args, converter} = :elixir_rewrite.erl_to_ex(mod, fun, args) @@ -1053,7 +1077,7 @@ defmodule Module.Types.Apply do alias Inspect.Algebra, as: IA defp type_comparison_to_string(fun, left, right) do - {Kernel, fun, [left, right], _} = :elixir_rewrite.erl_to_ex(:erlang, fun, [left, right]) + {_, fun, _, _} = :elixir_rewrite.erl_to_ex(:erlang, fun, [left, right]) {fun, [], [to_quoted(left, collapse_structs: true), to_quoted(right, collapse_structs: true)]} |> Code.Formatter.to_algebra() diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 2086a9ccf77..04b3f1dee4a 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -572,14 +572,14 @@ defmodule Module.Types.Descr do cond do empty?(left_static) -> dynamic = intersection_static(unfold(left_dynamic), unfold(right_dynamic)) - if empty?(dynamic), do: :error, else: {:ok, dynamic(dynamic)} + if empty?(dynamic), do: {:error, left}, else: {:ok, dynamic(dynamic)} subtype_static?(left_static, right_dynamic) -> dynamic = intersection_static(unfold(left_dynamic), unfold(right_dynamic)) {:ok, union(dynamic(dynamic), left_static)} true -> - :error + {:error, left} end end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 00c8e45db77..7790801a467 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -145,12 +145,7 @@ defmodule Module.Types.Expr do type_fun = fn pattern_type, context -> # See if we can use the expected type to further refine the pattern type, # if we cannot, use the pattern type as that will fail later on. - type = - case compatible_intersection(pattern_type, expected) do - {:ok, expected} -> expected - :error -> pattern_type - end - + {_ok_or_error, type} = compatible_intersection(pattern_type, expected) of_expr(right_expr, type, expr, stack, context) end @@ -439,7 +434,6 @@ defmodule Module.Types.Expr do end # TODO: with pat <- expr do expr end - # TODO: here def of_expr({:with, _meta, [_ | _] = clauses}, _expected, _expr, stack, original) do {clauses, [options]} = Enum.split(clauses, -1) context = Enum.reduce(clauses, original, &with_clause(&1, stack, &2)) @@ -448,7 +442,6 @@ defmodule Module.Types.Expr do end # TODO: fun.(args) - # TODO: here def of_expr({{:., meta, [fun]}, _meta, args} = call, _expected, _expr, stack, context) do {fun_type, context} = of_expr(fun, fun(), call, stack, context) @@ -473,14 +466,14 @@ defmodule Module.Types.Expr do else {type, context} = of_expr(callee, atom(), call, stack, context) {mods, context} = Of.modules(type, key_or_fun, 0, [:dot], call, meta, stack, context) - apply_many(mods, key_or_fun, [], [], call, stack, context) + apply_many(mods, key_or_fun, [], expected, call, stack, context) end end # TODO: here def of_expr( {{:., _, [remote, :apply]}, _meta, [mod, fun, args]} = call, - _expected, + expected, _expr, stack, context @@ -498,32 +491,25 @@ defmodule Module.Types.Expr do _ -> [] end - {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2)) - {types, context} = Enum.map_reduce(funs, context, fn fun, context -> - apply_many(mods, fun, args, args_types, call, stack, context) + apply_many(mods, fun, args, expected, call, stack, context) end) {Enum.reduce(types, &union/2), context} _ -> - {args_type, context} = of_expr(args, list(term()), call, stack, context) - args_types = [mod_type, fun_type, args_type] - Apply.remote(:erlang, :apply, [mod, fun, args], args_types, call, stack, context) + # PENDING: Do not process args twice + apply_args = [mod, fun, args] + apply_one(:erlang, :apply, apply_args, expected, call, stack, context) end end # TODO: here - def of_expr({{:., _, [remote, name]}, meta, args} = call, _expected, _expr, stack, context) do + def of_expr({{:., _, [remote, name]}, meta, args} = call, expected, _expr, stack, context) do {remote_type, context} = of_expr(remote, atom(), call, stack, context) - - {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2)) - {mods, context} = Of.modules(remote_type, name, length(args), call, meta, stack, context) - apply_many(mods, name, args, args_types, call, stack, context) + apply_many(mods, name, args, expected, call, stack, context) end # TODO: &Foo.bar/1 @@ -549,21 +535,14 @@ defmodule Module.Types.Expr do # TODO: here def of_expr({:super, meta, args} = expr, _expected, _expr, stack, context) when is_list(args) do {_kind, fun} = Keyword.fetch!(meta, :super) - - {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2)) - - Apply.local(fun, args_types, expr, stack, context) + apply_local(fun, args, expr, stack, context) end # Local calls # TODO: here def of_expr({fun, _meta, args} = expr, _expected, _expr, stack, context) when is_atom(fun) and is_list(args) do - {args_types, context} = - Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2)) - - Apply.local(fun, args_types, expr, stack, context) + apply_local(fun, args, expr, stack, context) end # var @@ -620,8 +599,11 @@ defmodule Module.Types.Expr do defp for_clause({:<-, meta, [left, right]}, stack, context) do expr = {:<-, [type_check: :generator] ++ meta, [left, right]} {pattern, guards} = extract_head([left]) - {type, context} = of_expr(right, @pending, expr, stack, context) - {_type, context} = Apply.remote(Enumerable, :count, [right], [type], expr, stack, context) + + # PENDING: test this + {_type, context} = + apply_one(Enumerable, :count, [right], dynamic(), expr, stack, context) + Pattern.of_generator(pattern, guards, dynamic(), :for, expr, stack, context) end @@ -669,8 +651,12 @@ defmodule Module.Types.Expr do _ -> meta end + # PENDING: do not do this twice expr = {:__block__, [type_check: :into] ++ meta, [into]} - {_type, context} = Apply.remote(Collectable, :into, [into], [type], expr, stack, context) + + {_type, context} = + apply_one(Collectable, :into, [into], dynamic(), expr, stack, context) + {[:term], true, context} end end @@ -702,18 +688,42 @@ defmodule Module.Types.Expr do ## General helpers - defp apply_many([], function, _args, args_types, expr, stack, context) do - Apply.remote(function, args_types, expr, stack, context) + defp apply_local(fun, args, {_, meta, _} = expr, stack, context) do + {local_info, domain, context} = Apply.local_domain(fun, args, meta, stack, context) + + {args_types, context} = + zip_map_reduce(args, domain, context, &of_expr(&1, &2, expr, stack, &3)) + + Apply.local_apply(local_info, fun, args_types, expr, stack, context) + end + + defp apply_one(mod, fun, args, expected, expr, stack, context) do + {info, domain, context} = + Apply.remote_domain(mod, fun, args, expected, elem(expr, 1), stack, context) + + {args_types, context} = + zip_map_reduce(args, domain, context, &of_expr(&1, &2, expr, stack, &3)) + + Apply.remote_apply(info, mod, fun, args_types, expr, stack, context) + end + + defp apply_many([], fun, args, expected, expr, stack, context) do + {info, domain} = Apply.remote_domain(fun, args, expected, stack) + + {args_types, context} = + zip_map_reduce(args, domain, context, &of_expr(&1, &2, expr, stack, &3)) + + Apply.remote_apply(info, nil, fun, args_types, expr, stack, context) end - defp apply_many([mod], function, args, args_types, expr, stack, context) do - Apply.remote(mod, function, args, args_types, expr, stack, context) + defp apply_many([mod], fun, args, expected, expr, stack, context) do + apply_one(mod, fun, args, expected, expr, stack, context) end - defp apply_many(mods, function, args, args_types, expr, stack, context) do + defp apply_many(mods, fun, args, expected, expr, stack, context) do {returns, context} = Enum.map_reduce(mods, context, fn mod, context -> - Apply.remote(mod, function, args, args_types, expr, stack, context) + apply_one(mod, fun, args, expected, expr, stack, context) end) {Enum.reduce(returns, &union/2), context} diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index 7ae354bef50..e2d79777f39 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -333,4 +333,19 @@ defmodule Module.Types.Helpers do The type to return when there is an error. """ def error_type, do: Module.Types.Descr.dynamic() + + ## Enum helpers + + def zip_map_reduce(args1, args2, acc, fun) do + zip_map_reduce(args1, args2, [], acc, fun) + end + + defp zip_map_reduce([arg1 | args1], [arg2 | args2], list, acc, fun) do + {item, acc} = fun.(arg1, arg2, acc) + zip_map_reduce(args1, args2, [item | list], acc, fun) + end + + defp zip_map_reduce([], [], list, acc, _fun) do + {Enum.reverse(list), acc} + end end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index c00cdfefa8f..7976673e51e 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -1,7 +1,7 @@ defmodule Module.Types.Pattern do @moduledoc false - alias Module.Types.Of + alias Module.Types.{Apply, Of} import Module.Types.{Helpers, Descr} @guard atom([true, false, :fail]) @@ -733,12 +733,15 @@ defmodule Module.Types.Pattern do end # Remote - def of_guard({{:., _, [:erlang, function]}, _, args} = call, _expected, expr, stack, context) - when is_atom(function) do - {args_type, context} = - Enum.map_reduce(args, context, &of_guard(&1, dynamic(), expr, stack, &2)) + def of_guard({{:., _, [:erlang, fun]}, meta, args} = call, expected, _expr, stack, context) + when is_atom(fun) do + {info, domain, context} = + Apply.remote_domain(:erlang, fun, args, expected, meta, stack, context) + + {args_types, context} = + zip_map_reduce(args, domain, context, &of_guard(&1, &2, call, stack, &3)) - Module.Types.Apply.remote(:erlang, function, args, args_type, call, stack, context) + Apply.remote_apply(info, :erlang, fun, args_types, call, stack, context) end # var diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 011694d6263..849617eed57 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -922,93 +922,90 @@ defmodule Module.Types.ExprTest do end test "warns when comparison is constant" do - assert typewarn!([x = :foo, y = 321], min(x, y)) == - {dynamic(union(integer(), atom([:foo]))), - ~l""" - comparison between distinct types found: + assert typeerror!([x = :foo, y = 321], min(x, y)) == + ~l""" + comparison between distinct types found: - min(x, y) + min(x, y) - given types: + given types: - min(dynamic(:foo), integer()) + min(dynamic(:foo), integer()) - where "x" was given the type: + where "x" was given the type: - # type: dynamic(:foo) - # from: types_test.ex:LINE-2 - x = :foo + # type: dynamic(:foo) + # from: types_test.ex:LINE-1 + x = :foo - where "y" was given the type: + where "y" was given the type: - # type: integer() - # from: types_test.ex:LINE-2 - y = 321 + # type: integer() + # from: types_test.ex:LINE-1 + y = 321 - While Elixir can compare across all types, you are comparing across types \ - which are always disjoint, and the result is either always true or always false - """} + While Elixir can compare across all types, you are comparing across types \ + which are always disjoint, and the result is either always true or always false + """ - assert typewarn!([x = 123, y = 456.0], x === y) == - {boolean(), - ~l""" - comparison between distinct types found: + assert typeerror!([x = 123, y = 456.0], x === y) == + ~l""" + comparison between distinct types found: - x === y + x === y - given types: + given types: - integer() === float() + integer() === float() - where "x" was given the type: + where "x" was given the type: - # type: integer() - # from: types_test.ex:LINE-2 - x = 123 + # type: integer() + # from: types_test.ex:LINE-1 + x = 123 - where "y" was given the type: + where "y" was given the type: - # type: float() - # from: types_test.ex:LINE-2 - y = 456.0 + # type: float() + # from: types_test.ex:LINE-1 + y = 456.0 - While Elixir can compare across all types, you are comparing across types \ - which are always disjoint, and the result is either always true or always false - """} + While Elixir can compare across all types, you are comparing across types \ + which are always disjoint, and the result is either always true or always false + """ end test "warns on comparison with struct across dynamic call" do - assert typewarn!([x = :foo, y = %Point{}, mod = Kernel], mod.<=(x, y)) == - {boolean(), - ~l""" - comparison with structs found: + assert typeerror!([x = :foo, y = %Point{}, mod = Kernel], mod.<=(x, y)) == + ~l""" + comparison with structs found: - mod.<=(x, y) + mod.<=(x, y) - given types: + given types: - dynamic(:foo) <= dynamic(%Point{}) + dynamic(:foo) <= dynamic(%Point{}) - where "mod" was given the type: + where "mod" was given the type: - # type: dynamic(Kernel) - # from: types_test.ex:LINE-2 - mod = Kernel + # type: dynamic(Kernel) + # from: types_test.ex:LINE-1 + mod = Kernel - where "x" was given the type: + where "x" was given the type: - # type: dynamic(:foo) - # from: types_test.ex:LINE-2 - x = :foo + # type: dynamic(:foo) + # from: types_test.ex:LINE-1 + x = :foo - where "y" was given the type: + where "y" was given the type: - # type: dynamic(%Point{}) - # from: types_test.ex:LINE-2 - y = %Point{} + # type: dynamic(%Point{}) + # from: types_test.ex:LINE-1 + y = %Point{} - Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Structs that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison. - """} + Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Structs that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison. + """ end end diff --git a/lib/elixir/test/elixir/module/types/infer_test.exs b/lib/elixir/test/elixir/module/types/infer_test.exs index c83ec2439a8..6fcb821d83a 100644 --- a/lib/elixir/test/elixir/module/types/infer_test.exs +++ b/lib/elixir/test/elixir/module/types/infer_test.exs @@ -42,10 +42,10 @@ defmodule Module.Types.InferTest do dynamic(atom([Point])) ] - assert types[{:fun1, 4}] == {:infer, [{args, atom([:ok])}]} - assert types[{:fun2, 4}] == {:infer, [{args, atom([:ok])}]} - assert types[{:fun3, 4}] == {:infer, [{args, atom([:ok])}]} - assert types[{:fun4, 4}] == {:infer, [{args, atom([:ok])}]} + assert types[{:fun1, 4}] == {:infer, nil, [{args, atom([:ok])}]} + assert types[{:fun2, 4}] == {:infer, nil, [{args, atom([:ok])}]} + assert types[{:fun3, 4}] == {:infer, nil, [{args, atom([:ok])}]} + assert types[{:fun4, 4}] == {:infer, nil, [{args, atom([:ok])}]} end test "infer types from expressions", config do @@ -56,12 +56,10 @@ defmodule Module.Types.InferTest do end end + number = union(integer(), float()) + assert types[{:fun, 1}] == - {:infer, - [ - {[dynamic(open_map(foo: term(), bar: term()))], - dynamic(union(integer(), float()))} - ]} + {:infer, nil, [{[dynamic(open_map(foo: number, bar: number))], dynamic(number)}]} end test "infer with Elixir built-in", config do @@ -71,7 +69,8 @@ defmodule Module.Types.InferTest do end assert types[{:parse, 1}] == - {:infer, [{[dynamic()], dynamic(union(atom([:error]), tuple([integer(), term()])))}]} + {:infer, nil, + [{[dynamic()], dynamic(union(atom([:error]), tuple([integer(), term()])))}]} end test "merges patterns", config do @@ -85,7 +84,7 @@ defmodule Module.Types.InferTest do end assert types[{:fun, 1}] == - {:infer, + {:infer, [dynamic(union(atom([:ok, :error]), binary()))], [ {[dynamic(atom([:ok]))], atom([:one])}, {[dynamic(binary())], atom([:two, :three, :four])}, @@ -101,7 +100,9 @@ defmodule Module.Types.InferTest do defp priv(:error), do: :error end - assert types[{:pub, 1}] == {:infer, [{[dynamic()], dynamic(atom([:ok, :error]))}]} + assert types[{:pub, 1}] == + {:infer, nil, [{[dynamic(atom([:ok, :error]))], dynamic(atom([:ok, :error]))}]} + assert types[{:priv, 1}] == nil end @@ -114,7 +115,8 @@ defmodule Module.Types.InferTest do def pub(x), do: super(x) end - assert types[{:pub, 1}] == {:infer, [{[dynamic()], dynamic(atom([:ok, :error]))}]} + assert types[{:pub, 1}] == + {:infer, nil, [{[dynamic(atom([:ok, :error]))], dynamic(atom([:ok, :error]))}]} end test "infers return types even with loops", config do @@ -123,6 +125,6 @@ defmodule Module.Types.InferTest do def pub(x), do: pub(x) end - assert types[{:pub, 1}] == {:infer, [{[dynamic()], dynamic()}]} + assert types[{:pub, 1}] == {:infer, nil, [{[dynamic()], dynamic()}]} end end diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 0e89c5630e0..25dd062d214 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -68,7 +68,7 @@ defmodule Module.Types.IntegrationTest do assert [ {{:c, 0}, %{}}, - {{:e, 0}, %{deprecated: "oops", sig: {:infer, _}}} + {{:e, 0}, %{deprecated: "oops", sig: {:infer, _, _}}} ] = read_chunk(modules[A]).exports assert read_chunk(modules[B]).exports == [ @@ -119,7 +119,7 @@ defmodule Module.Types.IntegrationTest do refute stderr =~ "this_wont_warn" itself_arg = fn mod -> - {_, %{sig: {:infer, [{[value], value}]}}} = + {_, %{sig: {:infer, nil, [{[value], value}]}}} = List.keyfind(read_chunk(modules[mod]).exports, {:itself, 1}, 0) value diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index 2df170a6a8d..66d6f2e2ffd 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -42,9 +42,9 @@ defmodule Module.Types.PatternTest do where "name" was given the type: - # type: dynamic() - # from: types_test.ex - {name, arity} + # type: dynamic(atom()) + # from: types_test.ex:LINE-1 + Atom.to_charlist(name) """ end diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index e6634320687..362c50833b7 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -1,7 +1,7 @@ defmodule Mix.Compilers.Elixir do @moduledoc false - @manifest_vsn 27 + @manifest_vsn 28 @checkpoint_vsn 2 import Record From 79cfdc932b6357001303409aad2ffd0a024a3545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 5 Jan 2025 20:17:44 +0100 Subject: [PATCH 10/22] Address pending notes --- lib/elixir/lib/module/types/apply.ex | 10 +++---- lib/elixir/lib/module/types/expr.ex | 43 ++++++++++++++-------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 18c7c751733..d403658981e 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -72,16 +72,16 @@ defmodule Module.Types.Apply do domain = atom(Keyword.keys(clauses)) clauses = Enum.map(clauses, fn {key, return} -> {[atom([key])], return} end) - defp signature(unquote(name), 1) do + def signature(unquote(name), 1) do {:strong, [unquote(Macro.escape(domain))], unquote(Macro.escape(clauses))} end end - defp signature(:module_info, 0) do + def signature(:module_info, 0) do {:strong, nil, [{[], unquote(Macro.escape(kw.(module_info)))}]} end - defp signature(_, _), do: :none + def signature(_, _), do: :none # Remote for compiler functions @@ -254,11 +254,11 @@ defmodule Module.Types.Apply do {:strong, domain, clauses} end - defp signature(unquote(mod), unquote(fun), unquote(arity)), + def signature(unquote(mod), unquote(fun), unquote(arity)), do: unquote(Macro.escape(domain_clauses)) end - defp signature(_mod, _fun, _arity), do: :none + def signature(_mod, _fun, _arity), do: :none @doc """ Returns the domain of an unknown module. diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 7790801a467..a8313e42bc9 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -30,6 +30,8 @@ defmodule Module.Types.Expr do versioned_vars: open_map() ) + # This is used temporarily until reverse arrows are defined + @pending term() @atom_true atom([true]) @exception open_map(__struct__: atom(), __exception__: @atom_true) @@ -49,9 +51,6 @@ defmodule Module.Types.Expr do ) ) - @term term() - @pending term() - # :atom def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom), do: {atom([atom]), context} @@ -246,7 +245,7 @@ defmodule Module.Types.Expr do context = Enum.reduce(pre, context, fn expr, context -> - {_, context} = of_expr(expr, @term, expr, stack, context) + {_, context} = of_expr(expr, term(), expr, stack, context) context end) @@ -367,7 +366,7 @@ defmodule Module.Types.Expr do |> dynamic_unless_static(stack) if after_block do - {_type, context} = of_expr(after_block, @term, after_block, stack, context) + {_type, context} = of_expr(after_block, term(), after_block, stack, context) {type, context} else {type, context} @@ -499,9 +498,10 @@ defmodule Module.Types.Expr do {Enum.reduce(types, &union/2), context} _ -> - # PENDING: Do not process args twice - apply_args = [mod, fun, args] - apply_one(:erlang, :apply, apply_args, expected, call, stack, context) + info = Apply.signature(:erlang, :apply, 3) + {args_type, context} = of_expr(args, list(term()), call, stack, context) + apply_types = [mod_type, fun_type, args_type] + Apply.remote_apply(info, :erlang, :apply, apply_types, call, stack, context) end end @@ -600,7 +600,6 @@ defmodule Module.Types.Expr do expr = {:<-, [type_check: :generator] ++ meta, [left, right]} {pattern, guards} = extract_head([left]) - # PENDING: test this {_type, context} = apply_one(Enumerable, :count, [right], dynamic(), expr, stack, context) @@ -620,7 +619,7 @@ defmodule Module.Types.Expr do end defp for_clause(expr, stack, context) do - {_type, context} = of_expr(expr, @term, expr, stack, context) + {_type, context} = of_expr(expr, term(), expr, stack, context) context end @@ -634,7 +633,18 @@ defmodule Module.Types.Expr do # TODO: Use the collectable protocol for the output defp for_into(into, meta, stack, context) do - {type, context} = of_expr(into, @pending, into, stack, context) + meta = + case into do + {_, meta, _} -> meta + _ -> meta + end + + expr = {:__block__, [type_check: :into] ++ meta, [into]} + + {info, [domain], context} = + Apply.remote_domain(Collectable, :into, [into], term(), meta, stack, context) + + {type, context} = of_expr(into, domain, expr, stack, context) # We use subtype? instead of compatible because we want to handle # only binary/list, even if a dynamic with something else is given. @@ -645,17 +655,8 @@ defmodule Module.Types.Expr do {_, _} -> {[:binary, :list], gradual?(type), context} end else - meta = - case into do - {_, meta, _} -> meta - _ -> meta - end - - # PENDING: do not do this twice - expr = {:__block__, [type_check: :into] ++ meta, [into]} - {_type, context} = - apply_one(Collectable, :into, [into], dynamic(), expr, stack, context) + Apply.remote_apply(info, Collectable, :into, [type], expr, stack, context) {[:term], true, context} end From e6483895dd37d9601de918832fc936757e22fb7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 5 Jan 2025 21:15:44 +0100 Subject: [PATCH 11/22] Tackle more TODOs --- lib/elixir/lib/module/types/expr.ex | 21 +++++---------- .../test/elixir/module/types/expr_test.exs | 26 ++++++++++++++++++- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index a8313e42bc9..37e27b6f32f 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -115,7 +115,6 @@ defmodule Module.Types.Expr do end # <<...>>> - # TODO: here (including tests) def of_expr({:<<>>, _meta, args}, _expected, _expr, stack, context) do context = Of.binary(args, :expr, stack, context) {binary(), context} @@ -306,7 +305,6 @@ defmodule Module.Types.Expr do end # TODO: fn pat -> expr end - # TODO: here def of_expr({:fn, _meta, clauses}, _expected, _expr, stack, context) do [{:->, _, [head, _]} | _] = clauses {patterns, _guards} = extract_head(head) @@ -399,8 +397,6 @@ defmodule Module.Types.Expr do |> dynamic_unless_static(stack) end - # TODO: for pat <- expr do expr end - # TODO: here def of_expr({:for, meta, [_ | _] = args}, expected, expr, stack, context) do {clauses, [[{:do, block} | opts]]} = Enum.split(args, -1) context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2)) @@ -414,6 +410,7 @@ defmodule Module.Types.Expr do # because this is recursive. We need to infer the block type first. of_clauses(block, [dynamic()], expected, expr, :for_reduce, stack, {reduce_type, context}) else + # TODO: Use the collectable protocol for the output into = Keyword.get(opts, :into, []) {into_wrapper, gradual?, context} = for_into(into, meta, stack, context) {block_type, context} = of_expr(block, @pending, block, stack, context) @@ -469,7 +466,6 @@ defmodule Module.Types.Expr do end end - # TODO: here def of_expr( {{:., _, [remote, :apply]}, _meta, [mod, fun, args]} = call, expected, @@ -505,7 +501,6 @@ defmodule Module.Types.Expr do end end - # TODO: here def of_expr({{:., _, [remote, name]}, meta, args} = call, expected, _expr, stack, context) do {remote_type, context} = of_expr(remote, atom(), call, stack, context) {mods, context} = Of.modules(remote_type, name, length(args), call, meta, stack, context) @@ -532,21 +527,18 @@ defmodule Module.Types.Expr do end # Super - # TODO: here - def of_expr({:super, meta, args} = expr, _expected, _expr, stack, context) when is_list(args) do + def of_expr({:super, meta, args} = call, expected, _expr, stack, context) when is_list(args) do {_kind, fun} = Keyword.fetch!(meta, :super) - apply_local(fun, args, expr, stack, context) + apply_local(fun, args, expected, call, stack, context) end # Local calls - # TODO: here - def of_expr({fun, _meta, args} = expr, _expected, _expr, stack, context) + def of_expr({fun, _meta, args} = call, expected, _expr, stack, context) when is_atom(fun) and is_list(args) do - apply_local(fun, args, expr, stack, context) + apply_local(fun, args, expected, call, stack, context) end # var - # TODO: here def of_expr(var, expected, expr, stack, context) when is_var(var) do if stack.mode == :traversal do {dynamic(), context} @@ -631,7 +623,6 @@ defmodule Module.Types.Expr do defp for_into(binary, _meta, _stack, context) when is_binary(binary), do: {[:binary], false, context} - # TODO: Use the collectable protocol for the output defp for_into(into, meta, stack, context) do meta = case into do @@ -689,7 +680,7 @@ defmodule Module.Types.Expr do ## General helpers - defp apply_local(fun, args, {_, meta, _} = expr, stack, context) do + defp apply_local(fun, args, _expected, {_, meta, _} = expr, stack, context) do {local_info, domain, context} = Apply.local_domain(fun, args, meta, stack, context) {args_types, context} = diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 849617eed57..c304732cdc8 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -340,6 +340,16 @@ defmodule Module.Types.ExprTest do end describe "binaries" do + test "inference" do + assert typecheck!( + [x, y], + ( + <> + {x, y} + ) + ) == dynamic(tuple([union(float(), integer()), integer()])) + end + test "warnings" do assert typeerror!([<>], <>) == ~l""" @@ -1606,7 +1616,7 @@ defmodule Module.Types.ExprTest do ) == dynamic(union(binary(), list(float()))) end - test ":reduce" do + test ":reduce checks" do assert typecheck!( [list], for _ <- list, reduce: :ok do @@ -1615,6 +1625,20 @@ defmodule Module.Types.ExprTest do end ) == union(atom([:ok]), union(integer(), float())) end + + test ":reduce inference" do + assert typecheck!( + [list, x], + ( + 123 = + for _ <- list, reduce: x do + x -> x + end + + x + ) + ) == dynamic(integer()) + end end describe "apply" do From adffc641b68b3a3178ac500c555be26a77fe58be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 5 Jan 2025 21:32:01 +0100 Subject: [PATCH 12/22] Clarify docs --- .../pages/references/gradual-set-theoretic-types.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/elixir/pages/references/gradual-set-theoretic-types.md b/lib/elixir/pages/references/gradual-set-theoretic-types.md index f433031ea2b..21ca2fd868a 100644 --- a/lib/elixir/pages/references/gradual-set-theoretic-types.md +++ b/lib/elixir/pages/references/gradual-set-theoretic-types.md @@ -90,17 +90,9 @@ Inferring type signatures comes with a series of trade-offs: * Cascading errors - when a user accidentally makes type errors or the code has conflicting assumptions, type inference may lead to less clear error messages as the type system tries to reconcile diverging type assumptions across code paths. -On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir’s type system provides the following type reconstruction capabilities: +On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir performs module-local inference: the arguments, returns types, and all variables in a function are computed considering all of the function calls to the same module and to Elixir's standard library. Any call to a function in another module is conservatively assumed to return `dynamic()` during inference. Our goal is to provide an efficient type reconstruction algorithm that can detect definite bugs in dynamic codebases, even in the absence of explicit type annotations. - * Local type inference - the type system automatically infer the types of variables, at the place those variables are defined. - - * Type inference of patterns (and guards in future releases) - the argument types of a function are automatically inferred based on patterns and guards, which capture and narrow types based on common Elixir constructs. - - * Module-local inference of return types - the gradual return types of functions are computed considering all of the functions within the module itself. Any call to a function in another module is conservatively assumed to return `dynamic()` during inference. - -The last two items offer gradual reconstruction of type signatures. Our goal is to provide an efficient type reconstruction algorithm that can detect definite bugs in dynamic codebases, even in the absence of explicit type annotations. The gradual system focuses on proving cases where all combinations of a type *will* fail, rather than issuing warnings for cases where some combinations *might* error. - -Once Elixir introduces typed function signatures (see "Roadmap"), any function with an explicit type signature will be checked against the user-provided type, as in other statically typed languages, without performing type inference of the function signature. +The gradual system focuses on proving cases where all combinations of a type *will* fail, rather than issuing warnings for cases where some combinations *might* error. Once Elixir introduces typed function signatures (see "Roadmap"), any function with an explicit type signature will be checked against the user-provided type, as in other statically typed languages, without performing type inference. ## Roadmap From 6ef897922afc860e0c64fb36863e9953ac540291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 7 Jan 2025 11:48:03 +0100 Subject: [PATCH 13/22] Remove more pending TODOs --- lib/elixir/lib/module/types/expr.ex | 39 ++++++++--------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 37e27b6f32f..ce906bb9581 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -76,7 +76,7 @@ defmodule Module.Types.Expr do do: {empty_list(), context} # [expr, ...] - # TODO: here + # PENDING: here def of_expr(list, _expected, expr, stack, context) when is_list(list) do {prefix, suffix} = unpack_list(list, []) {prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, @pending, expr, stack, &2)) @@ -90,7 +90,7 @@ defmodule Module.Types.Expr do end # {left, right} - # TODO: here + # PENDING: here def of_expr({left, right}, _expected, expr, stack, context) do {left, context} = of_expr(left, @pending, expr, stack, context) {right, context} = of_expr(right, @pending, expr, stack, context) @@ -103,7 +103,7 @@ defmodule Module.Types.Expr do end # {...} - # TODO: here + # PENDING: here def of_expr({:{}, _meta, exprs}, _expected, expr, stack, context) do {types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, @pending, expr, stack, &2)) @@ -153,7 +153,7 @@ defmodule Module.Types.Expr do # %{map | ...} # TODO: Once we support typed structs, we need to type check them here. - # TODO: here + # PENDING: here def of_expr({:%{}, meta, [{:|, _, [map, args]}]} = expr, _expected, _expr, stack, context) do {map_type, context} = of_expr(map, @pending, expr, stack, context) @@ -195,7 +195,7 @@ defmodule Module.Types.Expr do # Note this code, by definition, adds missing struct fields to `map` # because at runtime we do not check for them (only for __struct__ itself). # TODO: Once we support typed structs, we need to type check them here. - # TODO: here + # PENDING: here def of_expr( {:%, struct_meta, [module, {:%{}, _, [{:|, update_meta, [map, args]}]}]} = expr, _expected, @@ -222,13 +222,13 @@ defmodule Module.Types.Expr do end # %{...} - # TODO: here + # PENDING: here def of_expr({:%{}, _meta, args}, _expected, expr, stack, context) do Of.closed_map(args, stack, context, &of_expr(&1, @pending, expr, &2, &3)) end # %Struct{} - # TODO: here + # PENDING: here def of_expr({:%, meta, [module, {:%{}, _, args}]}, _expected, expr, stack, context) do Of.struct_instance(module, args, meta, stack, context, &of_expr(&1, @pending, expr, &2, &3)) end @@ -281,9 +281,9 @@ defmodule Module.Types.Expr do |> dynamic_unless_static(stack) end - # TODO: here def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, expr, stack, context) do {case_type, context} = of_expr(case_expr, @pending, case_expr, stack, context) + info = {:case, meta, case_type, case_expr} # If we are only type checking the expression and the expression is a literal, # let's mark it as generated, as it is most likely a macro code. However, if @@ -293,14 +293,7 @@ defmodule Module.Types.Expr do else clauses end - |> of_clauses( - [case_type], - expected, - expr, - {:case, meta, case_type, case_expr}, - stack, - {none(), context} - ) + |> of_clauses([case_type], expected, expr, info, stack, {none(), context}) |> dynamic_unless_static(stack) end @@ -313,7 +306,6 @@ defmodule Module.Types.Expr do {fun(), context} end - # TODO: here def of_expr({:try, _meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do {after_block, blocks} = Keyword.pop(blocks, :after) {else_block, blocks} = Keyword.pop(blocks, :else) @@ -321,16 +313,8 @@ defmodule Module.Types.Expr do {type, context} = if else_block do {type, context} = of_expr(body, @pending, body, stack, original) - - of_clauses( - else_block, - [type], - expected, - expr, - {:try_else, type}, - stack, - {none(), context} - ) + info = {:try_else, type} + of_clauses(else_block, [type], expected, expr, info, stack, {none(), context}) else of_expr(body, expected, expr, stack, original) end @@ -373,7 +357,6 @@ defmodule Module.Types.Expr do @timeout_type union(integer(), atom([:infinity])) - # TODO: here def of_expr({:receive, _meta, [blocks]}, expected, expr, stack, original) do blocks |> Enum.reduce({none(), original}, fn From bc54e5f7b986a04c8a955313aa994756d58d044d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 7 Jan 2025 12:51:43 +0100 Subject: [PATCH 14/22] Convert more expressions back into their original calls --- lib/elixir/lib/module/types/expr.ex | 21 +-- lib/elixir/lib/module/types/helpers.ex | 101 ++++++++++--- lib/elixir/lib/module/types/of.ex | 14 +- .../test/elixir/module/types/expr_test.exs | 138 +++++++++++++----- 4 files changed, 200 insertions(+), 74 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index ce906bb9581..891895b3e1b 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -325,12 +325,15 @@ defmodule Module.Types.Expr do {:rescue, clauses}, acc_context -> Enum.reduce(clauses, acc_context, fn {:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} -> - {type, context} = of_rescue(var, exceptions, body, expr, [], meta, stack, context) + {type, context} = + of_rescue(var, exceptions, body, expr, :rescue, meta, stack, context) + {union(type, acc), context} {:->, meta, [[var], body]}, {acc, context} -> - hint = [:anonymous_rescue] - {type, context} = of_rescue(var, [], body, var, hint, meta, stack, context) + {type, context} = + of_rescue(var, [], body, var, :anonymous_rescue, meta, stack, context) + {union(type, acc), context} end) @@ -532,7 +535,7 @@ defmodule Module.Types.Expr do ## Try - defp of_rescue(var, exceptions, body, expr, hints, meta, stack, original) do + defp of_rescue(var, exceptions, body, expr, info, meta, stack, original) do args = [__exception__: @atom_true] {structs, context} = @@ -557,11 +560,8 @@ defmodule Module.Types.Expr do _ -> expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2) - formatter = fn expr -> {"rescue #{expr_to_string(expr)} ->", hints} end - - {_ok?, _type, context} = - Of.refine_head_var(var, expected, expr, formatter, stack, context) - + expr = {:__block__, [type_check: info], [expr]} + {_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context) context end @@ -695,9 +695,10 @@ defmodule Module.Types.Expr do apply_one(mod, fun, args, expected, expr, stack, context) end - defp apply_many(mods, fun, args, expected, expr, stack, context) do + defp apply_many(mods, fun, args, expected, {remote, meta, args}, stack, context) do {returns, context} = Enum.map_reduce(mods, context, fn mod, context -> + expr = {remote, [type_check: {:invoked_as, mod, fun, length(args)}] ++ meta, args} apply_one(mod, fun, args, expected, expr, stack, context) end) diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index e2d79777f39..61230400d33 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -137,7 +137,7 @@ defmodule Module.Types.Helpers do type: :variable, name: name, context: context, - traces: collect_var_traces(off_traces) + traces: collect_var_traces(expr, off_traces) })} _ -> @@ -153,33 +153,27 @@ defmodule Module.Types.Helpers do |> Enum.sort_by(& &1.name) end - defp collect_var_traces(traces) do + defp collect_var_traces(parent_expr, traces) do traces - |> Enum.reject(fn {_expr, _file, type, _formatter} -> + |> Enum.reject(fn {expr, _file, type} -> # As an otimization do not care about dynamic terms - type == %{dynamic: :term} + type == %{dynamic: :term} or expr == parent_expr end) |> case do [] -> traces filtered -> filtered end |> Enum.reverse() - |> Enum.map(fn {expr, file, type, formatter} -> + |> Enum.map(fn {expr, file, type} -> meta = get_meta(expr) - {formatted_expr, formatter_hints} = - case formatter do - :default -> {expr_to_string(expr), []} - formatter -> formatter.(expr) - end - # This information is exposed to language servers and # therefore must remain backwards compatible. %{ file: file, meta: meta, - formatted_expr: formatted_expr, - formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)), + formatted_expr: expr_to_string(expr), + formatted_hints: format_hints(expr_hints(expr)), formatted_type: Module.Types.Descr.to_quoted_string(type, collapse_structs: true) } end) @@ -187,6 +181,22 @@ defmodule Module.Types.Helpers do |> Enum.dedup() end + defp expr_hints(expr) do + case expr do + {:<<>>, [inferred_bitstring_spec: true] ++ _meta, _} -> + [:inferred_bitstring_spec] + + {_, meta, _} -> + case meta[:type_check] do + :anonymous_rescue -> [:anonymous_rescue] + _ -> [] + end + + _ -> + [] + end + end + @doc """ Format previously collected traces. """ @@ -230,11 +240,6 @@ defmodule Module.Types.Helpers do defp pluralize([_], singular, _plural), do: singular defp pluralize(_, _singular, plural), do: plural - defp expr_hints({:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}), - do: [:inferred_bitstring_spec] - - defp expr_hints(_), do: [] - @doc """ Converts the given expression to a string, translating inlined Erlang calls back to Elixir. @@ -242,6 +247,30 @@ defmodule Module.Types.Helpers do We also undo some macro expressions done by the Kernel module. """ def expr_to_string(expr) do + string = prewalk_expr_to_string(expr) + + case expr do + {_, meta, _} -> + case meta[:type_check] do + :anonymous_rescue -> + "rescue " <> string + + :rescue -> + "rescue " <> string + + {:invoked_as, mod, fun, arity} -> + string <> "\n#=> invoked as " <> Exception.format_mfa(mod, fun, arity) + + _ -> + string + end + + _ -> + string + end + end + + defp prewalk_expr_to_string(expr) do expr |> Macro.prewalk(fn {:%, _, [Range, {:%{}, _, fields}]} = node -> @@ -264,6 +293,42 @@ defmodule Module.Types.Helpers do {{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta) + {:case, meta, [expr, [do: clauses]]} = case -> + if meta[:type_check] == :expr do + case clauses do + [ + {:->, _, + [ + [ + {:when, _, + [ + {var, _, Kernel}, + {{:., _, [:erlang, :orelse]}, _, + [ + {{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, false]}, + {{:., _, [:erlang, :"=:="]}, _, [{var, _, Kernel}, nil]} + ]} + ]} + ], + else_block + ]}, + {:->, _, [[{:_, _, Kernel}], do_block]} + ] -> + {:if, meta, [expr, [do: do_block, else: else_block]]} + + [ + {:->, _, [[false], else_block]}, + {:->, _, [[true], do_block]} + ] -> + {:if, meta, [expr, [do: do_block, else: else_block]]} + + _ -> + case + end + else + case + end + other -> other end) diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 76f6f8bbbdd..9f241e61734 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -59,7 +59,7 @@ defmodule Module.Types.Of do data = %{ data | type: new_type, - off_traces: new_trace(expr, new_type, :default, stack, off_traces) + off_traces: new_trace(expr, new_type, stack, off_traces) } %{context | vars: %{vars | version => data}} @@ -81,7 +81,7 @@ defmodule Module.Types.Of do because we want to refine types. Otherwise we should use compatibility. """ - def refine_head_var(var, type, expr, formatter \\ :default, stack, context) do + def refine_head_var(var, type, expr, stack, context) do {var_name, meta, var_context} = var version = Keyword.fetch!(meta, :version) @@ -92,7 +92,7 @@ defmodule Module.Types.Of do data = %{ data | type: new_type, - off_traces: new_trace(expr, type, formatter, stack, off_traces) + off_traces: new_trace(expr, type, stack, off_traces) } context = %{context | vars: %{vars | version => data}} @@ -110,7 +110,7 @@ defmodule Module.Types.Of do type: type, name: var_name, context: var_context, - off_traces: new_trace(expr, type, formatter, stack, []) + off_traces: new_trace(expr, type, stack, []) } context = %{context | vars: Map.put(vars, version, data)} @@ -118,11 +118,11 @@ defmodule Module.Types.Of do end end - defp new_trace(nil, _type, _formatter, _stack, traces), + defp new_trace(nil, _type, _stack, traces), do: traces - defp new_trace(expr, type, formatter, stack, traces), - do: [{expr, stack.file, type, formatter} | traces] + defp new_trace(expr, type, stack, traces), + do: [{expr, stack.file, type} | traces] ## Implementations diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index c304732cdc8..f877eb3f024 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -337,6 +337,48 @@ defmodule Module.Types.ExprTest do <> """ end + + test "requires all combinations to be compatible" do + assert typeerror!( + [condition, string], + ( + mod = if condition, do: String, else: List + mod.to_integer(string) + ) + ) + |> strip_ansi() == ~l""" + incompatible types given to String.to_integer/1: + + mod.to_integer(string) + #=> invoked as String.to_integer/1 + + given types: + + dynamic(non_empty_list(integer())) + + but expected one of: + + binary() + + where "mod" was given the type: + + # type: List or String + # from: types_test.ex:LINE-4 + mod = + if condition do + String + else + List + end + + where "string" was given the type: + + # type: dynamic(non_empty_list(integer())) + # from: types_test.ex:LINE-3 + mod.to_integer(string) + #=> invoked as List.to_integer/1 + """ + end end describe "binaries" do @@ -1422,6 +1464,38 @@ defmodule Module.Types.ExprTest do ) end + test "generates custom traces" do + assert typeerror!( + try do + raise "oops" + rescue + e -> + Integer.to_string(e) + end + ) + |> strip_ansi() == ~l""" + incompatible types given to Integer.to_string/1: + + Integer.to_string(e) + + given types: + + %{..., __exception__: true, __struct__: atom()} + + but expected one of: + + integer() + + where "e" was given the type: + + # type: %{..., __exception__: true, __struct__: atom()} + # from: types_test.ex + rescue e + + hint: when you rescue without specifying exception names, the variable is assigned a type of a struct but all of its fields are unknown. If you are trying to access an exception's :message key, either specify the exception names or use `Exception.message/1`. + """ + end + test "defines an open map of two fields in anonymous rescue" do assert typecheck!( try do @@ -1575,7 +1649,31 @@ defmodule Module.Types.ExprTest do [<>], for(< 0.5, do: x, else: y)>>, do: i) ) =~ - "expected the right side of <- in a binary generator to be a binary" + ~l""" + expected the right side of <- in a binary generator to be a binary: + + if :rand.uniform() > 0.5 do + x + else + y + end + + but got type: + + binary() or integer() + + where "x" was given the type: + + # type: integer() + # from: types_test.ex:LINE-3 + <> + + where "y" was given the type: + + # type: binary() + # from: types_test.ex:LINE-3 + <<..., y::binary>> + """ end test "infers binary generators" do @@ -1641,44 +1739,6 @@ defmodule Module.Types.ExprTest do end end - describe "apply" do - test "handles conditional modules and functions" do - assert typecheck!([fun], apply(String, fun, ["foo", "bar", "baz"])) == dynamic() - - assert typecheck!( - [condition, string], - ( - fun = if condition, do: :to_integer, else: :to_float - apply(String, fun, [string]) - ) - ) == union(integer(), float()) - - assert typecheck!( - [condition, string], - ( - mod = if condition, do: String, else: List - fun = if condition, do: :to_integer, else: :to_float - apply(mod, fun, [string]) - ) - ) == union(integer(), float()) - - assert typeerror!( - [condition, string], - ( - mod = if condition, do: String, else: List - fun = if condition, do: :to_integer, else: :to_float - :erlang.apply(mod, fun, [string | "tail"]) - ) - ) =~ - """ - incompatible types given to Kernel.apply/3: - - apply(mod, fun, [string | "tail"]) - - """ - end - end - describe "info" do test "__info__/1" do assert typecheck!(GenServer.__info__(:functions)) == list(tuple([atom(), integer()])) From c7581098da0948f2795e875468826c14cb8e4178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 7 Jan 2025 14:09:52 +0100 Subject: [PATCH 15/22] Tuple handling --- lib/elixir/lib/module/types/expr.ex | 51 +++++++++++-------- .../test/elixir/module/types/expr_test.exs | 12 ++++- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 891895b3e1b..7fbc13b2846 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -30,7 +30,7 @@ defmodule Module.Types.Expr do versioned_vars: open_map() ) - # This is used temporarily until reverse arrows are defined + # An annotation for terms where the reverse arrow is not yet fully defined @pending term() @atom_true atom([true]) @exception open_map(__struct__: atom(), __exception__: @atom_true) @@ -90,28 +90,13 @@ defmodule Module.Types.Expr do end # {left, right} - # PENDING: here - def of_expr({left, right}, _expected, expr, stack, context) do - {left, context} = of_expr(left, @pending, expr, stack, context) - {right, context} = of_expr(right, @pending, expr, stack, context) - - if stack.mode == :traversal do - {dynamic(), context} - else - {tuple([left, right]), context} - end + def of_expr({left, right}, expected, expr, stack, context) do + of_tuple([left, right], expected, expr, stack, context) end # {...} - # PENDING: here - def of_expr({:{}, _meta, exprs}, _expected, expr, stack, context) do - {types, context} = Enum.map_reduce(exprs, context, &of_expr(&1, @pending, expr, stack, &2)) - - if stack.mode == :traversal do - {dynamic(), context} - else - {tuple(types), context} - end + def of_expr({:{}, _meta, exprs}, expected, expr, stack, context) do + of_tuple(exprs, expected, expr, stack, context) end # <<...>>> @@ -533,6 +518,32 @@ defmodule Module.Types.Expr do end end + ## Tuples + + defp of_tuple(elems, _expected, expr, %{mode: :traversal} = stack, context) do + {_types, context} = Enum.map_reduce(elems, context, &of_expr(&1, term(), expr, stack, &2)) + {dynamic(), context} + end + + defp of_tuple(elems, expected, expr, stack, context) do + of_tuple(elems, 0, [], expected, expr, stack, context) + end + + defp of_tuple([elem | elems], index, acc, expected, expr, stack, context) do + expr_expected = + case tuple_fetch(expected, index) do + {_, type} -> type + _ -> term() + end + + {type, context} = of_expr(elem, expr_expected, expr, stack, context) + of_tuple(elems, index + 1, [type | acc], expected, expr, stack, context) + end + + defp of_tuple([], _index, acc, _expected, _expr, _stack, context) do + {tuple(Enum.reverse(acc)), context} + end + ## Try defp of_rescue(var, exceptions, body, expr, info, meta, stack, original) do diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index f877eb3f024..fc33e3c461d 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -495,6 +495,16 @@ defmodule Module.Types.ExprTest do assert typecheck!([x], {:ok, x}) == dynamic(tuple([atom([:ok]), term()])) end + test "inference" do + assert typecheck!( + [x, y], + ( + {:ok, :error} = {x, y} + {x, y} + ) + ) == dynamic(tuple([atom([:ok]), atom([:error])])) + end + test "elem/2" do assert typecheck!(elem({:ok, 123}, 0)) == atom([:ok]) assert typecheck!(elem({:ok, 123}, 1)) == integer() @@ -618,7 +628,7 @@ defmodule Module.Types.ExprTest do """ end - test "duplicate/2" do + test "Tuple.duplicate/2" do assert typecheck!(Tuple.duplicate(123, 0)) == tuple([]) assert typecheck!(Tuple.duplicate(123, 1)) == tuple([integer()]) assert typecheck!(Tuple.duplicate(123, 2)) == tuple([integer(), integer()]) From 67103348edf98415b2b08874c56793e88eadcc7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 7 Jan 2025 16:15:02 +0100 Subject: [PATCH 16/22] Inference of lists --- lib/elixir/lib/module/types/expr.ex | 28 ++++++++++++++++--- .../test/elixir/module/types/expr_test.exs | 10 +++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 7fbc13b2846..4fb7eaed2f3 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -76,15 +76,35 @@ defmodule Module.Types.Expr do do: {empty_list(), context} # [expr, ...] - # PENDING: here - def of_expr(list, _expected, expr, stack, context) when is_list(list) do + def of_expr(list, expected, expr, stack, context) when is_list(list) do {prefix, suffix} = unpack_list(list, []) - {prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, @pending, expr, stack, &2)) - {suffix, context} = of_expr(suffix, @pending, expr, stack, context) if stack.mode == :traversal do + {_, context} = Enum.map_reduce(prefix, context, &of_expr(&1, term(), expr, stack, &2)) + {_, context} = of_expr(suffix, term(), expr, stack, context) {dynamic(), context} else + hd_type = + case list_hd(expected) do + {_, type} -> type + _ -> term() + end + + {prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, hd_type, expr, stack, &2)) + + {suffix, context} = + if suffix == [] do + {empty_list(), context} + else + tl_type = + case list_tl(expected) do + {_, type} -> type + _ -> term() + end + + of_expr(suffix, tl_type, expr, stack, context) + end + {non_empty_list(Enum.reduce(prefix, &union/2), suffix), context} end end diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index fc33e3c461d..28318a10f56 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -41,6 +41,16 @@ defmodule Module.Types.ExprTest do assert typecheck!([x], [:ok | x]) == dynamic(non_empty_list(term(), term())) end + test "inference" do + assert typecheck!( + [x, y, z], + ( + List.to_integer([x, y | z]) + {x, y, z} + ) + ) == dynamic(tuple([integer(), integer(), list(integer())])) + end + test "hd" do assert typecheck!([x = [123, :foo]], hd(x)) == dynamic(union(atom([:foo]), integer())) assert typecheck!([x = [123 | :foo]], hd(x)) == dynamic(integer()) From 9a55c5678a6dec88833a4156afc13ce4854b611c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 15 Jan 2025 13:51:20 +0100 Subject: [PATCH 17/22] Reverse arrow for maps, tests and function application pending --- lib/elixir/lib/module/types/apply.ex | 4 +- lib/elixir/lib/module/types/expr.ex | 139 +++++++++------ lib/elixir/lib/module/types/of.ex | 158 +++++++++++------- lib/elixir/lib/module/types/pattern.ex | 10 +- .../test/elixir/module/types/expr_test.exs | 53 +++++- .../elixir/module/types/integration_test.exs | 20 ++- 6 files changed, 257 insertions(+), 127 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index d403658981e..fee69e0c905 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -265,6 +265,7 @@ defmodule Module.Types.Apply do Used only by info functions. """ + # PENDING: expected def remote_domain(_fun, args, _expected, %{mode: :traversal}) do {:none, Enum.map(args, fn _ -> term() end)} end @@ -628,7 +629,8 @@ defmodule Module.Types.Apply do ## Local - def local_domain(fun, args, meta, stack, context) do + # PENDING: expected + def local_domain(fun, args, _expected, meta, stack, context) do arity = length(args) case stack.local_handler.(meta, {fun, arity}, stack, context) do diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 4fb7eaed2f3..b9a17ce0f48 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -158,31 +158,56 @@ defmodule Module.Types.Expr do # %{map | ...} # TODO: Once we support typed structs, we need to type check them here. - # PENDING: here - def of_expr({:%{}, meta, [{:|, _, [map, args]}]} = expr, _expected, _expr, stack, context) do - {map_type, context} = of_expr(map, @pending, expr, stack, context) - - Of.permutate_map(args, stack, context, &of_expr(&1, @pending, expr, &2, &3), fn - fallback, keys, pairs -> - # If there is no fallback (i.e. it is closed), we can update the existing map, - # otherwise we only assert the existing keys. - keys = if fallback == none(), do: keys, else: Enum.map(pairs, &elem(&1, 0)) ++ keys - - # Assert the keys exist - Enum.each(keys, fn key -> + def of_expr({:%{}, meta, [{:|, _, [map, args]}]} = update, expected, expr, stack, context) do + # Theoretically we cannot process entries out of order but, + # because all variables are versioned, and Elixir does not + # allow variables defined on the left side of | to be available + # on the right side, this is safe. + {pairs_types, context} = + Of.pairs(args, expected, stack, context, &of_expr(&1, &2, expr, &3, &4)) + + expected = + if stack.mode == :traversal do + expected + else + # TODO: Once we introduce domain keys, if we ever find a domain + # that overlaps atoms, we can only assume optional(atom()) => term(), + # which is what the `open_map()` below falls back into anyway. + Enum.reduce_while(pairs_types, expected, fn + {_, [key], _}, acc -> + case map_fetch_and_put(acc, key, term()) do + {_value, acc} -> {:cont, acc} + _ -> {:halt, open_map()} + end + + _, _ -> + {:halt, open_map()} + end) + end + + {map_type, context} = of_expr(map, expected, expr, stack, context) + + try do + Of.permutate_map(pairs_types, stack, fn fallback, keys_to_assert, pairs -> + # Ensure all keys to assert and all type pairs exist in map + keys_to_assert = Enum.map(pairs, &elem(&1, 0)) ++ keys_to_assert + + Enum.each(Enum.map(pairs, &elem(&1, 0)) ++ keys_to_assert, fn key -> case map_fetch(map_type, key) do {_, _} -> :ok - :badkey -> throw({:badkey, map_type, key, expr, context}) - :badmap -> throw({:badmap, map_type, expr, context}) + :badkey -> throw({:badkey, map_type, key, update, context}) + :badmap -> throw({:badmap, map_type, update, context}) end end) + # If all keys are known is no fallback (i.e. we know all keys being updated), + # we can update the existing map. if fallback == none() do Enum.reduce(pairs, map_type, fn {key, type}, acc -> case map_fetch_and_put(acc, key, type) do {_value, descr} -> descr - :badkey -> throw({:badkey, map_type, key, expr, context}) - :badmap -> throw({:badmap, map_type, expr, context}) + :badkey -> throw({:badkey, map_type, key, update, context}) + :badmap -> throw({:badmap, map_type, update, context}) end end) else @@ -191,51 +216,68 @@ defmodule Module.Types.Expr do # `keys` deleted. open_map(pairs) end - end) - catch - error -> {error_type(), error(__MODULE__, error, meta, stack, context)} + end) + catch + error -> {error_type(), error(__MODULE__, error, meta, stack, context)} + else + map -> {map, context} + end end # %Struct{map | ...} - # Note this code, by definition, adds missing struct fields to `map` - # because at runtime we do not check for them (only for __struct__ itself). - # TODO: Once we support typed structs, we need to type check them here. - # PENDING: here def of_expr( - {:%, struct_meta, [module, {:%{}, _, [{:|, update_meta, [map, args]}]}]} = expr, - _expected, - _expr, + {:%, struct_meta, [module, {:%{}, _, [{:|, update_meta, [map, args]}]}]} = struct, + expected, + expr, stack, context ) do - {info, context} = Of.struct_info(module, struct_meta, stack, context) - struct_type = Of.struct_type(module, info) - {map_type, context} = of_expr(map, @pending, expr, stack, context) + if stack.mode == :traversal do + {_, context} = of_expr(map, term(), struct, stack, context) - if disjoint?(struct_type, map_type) do - warning = {:badstruct, expr, struct_type, map_type, context} - {error_type(), error(__MODULE__, warning, update_meta, stack, context)} - else - map_type = map_put!(map_type, :__struct__, atom([module])) + context = + Enum.reduce(args, context, fn {key, value}, context when is_atom(key) -> + {_, context} = of_expr(value, term(), expr, stack, context) + context + end) - Enum.reduce(args, {map_type, context}, fn - {key, value}, {map_type, context} when is_atom(key) -> - {value_type, context} = of_expr(value, @pending, expr, stack, context) - {map_put!(map_type, key, value_type), context} - end) + {dynamic(), context} + else + {info, context} = Of.struct_info(module, struct_meta, stack, context) + struct_type = Of.struct_type(module, info) + {map_type, context} = of_expr(map, struct_type, struct, stack, context) + + if compatible?(map_type, struct_type) do + map_type = map_put!(map_type, :__struct__, atom([module])) + + Enum.reduce(args, {map_type, context}, fn + {key, value}, {map_type, context} when is_atom(key) -> + # TODO: Once we support typed structs, we need to type check them here. + expected_value_type = + case map_fetch(expected, key) do + {_, expected_value_type} -> expected_value_type + _ -> term() + end + + {value_type, context} = of_expr(value, expected_value_type, expr, stack, context) + {map_put!(map_type, key, value_type), context} + end) + else + warning = {:badstruct, struct, struct_type, map_type, context} + {error_type(), error(__MODULE__, warning, update_meta, stack, context)} + end end end # %{...} - # PENDING: here - def of_expr({:%{}, _meta, args}, _expected, expr, stack, context) do - Of.closed_map(args, stack, context, &of_expr(&1, @pending, expr, &2, &3)) + def of_expr({:%{}, _meta, args}, expected, expr, stack, context) do + Of.closed_map(args, expected, stack, context, &of_expr(&1, &2, expr, &3, &4)) end # %Struct{} - # PENDING: here - def of_expr({:%, meta, [module, {:%{}, _, args}]}, _expected, expr, stack, context) do - Of.struct_instance(module, args, meta, stack, context, &of_expr(&1, @pending, expr, &2, &3)) + def of_expr({:%, meta, [module, {:%{}, _, args}]}, expected, expr, stack, context) do + fun = &of_expr(&1, &2, expr, &3, &4) + Of.struct_instance(module, args, expected, meta, stack, context, fun) end # () @@ -575,8 +617,7 @@ defmodule Module.Types.Expr do # to avoid export dependencies. So we do it here. if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do {info, context} = Of.struct_info(exception, meta, stack, context) - # TODO: For properly defined structs, this should not be dynamic - {dynamic(Of.struct_type(exception, info, args)), context} + {Of.struct_type(exception, info, args), context} else # If the exception cannot be found or is invalid, fetch the signature to emit warnings. {_, context} = Apply.signature(exception, :__struct__, 0, meta, stack, context) @@ -694,8 +735,8 @@ defmodule Module.Types.Expr do ## General helpers - defp apply_local(fun, args, _expected, {_, meta, _} = expr, stack, context) do - {local_info, domain, context} = Apply.local_domain(fun, args, meta, stack, context) + defp apply_local(fun, args, expected, {_, meta, _} = expr, stack, context) do + {local_info, domain, context} = Apply.local_domain(fun, args, expected, meta, stack, context) {args_types, context} = zip_map_reduce(args, domain, context, &of_expr(&1, &2, expr, stack, &3)) diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 9f241e61734..3d1820fcf8f 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -52,26 +52,23 @@ defmodule Module.Types.Of do version = Keyword.fetch!(meta, :version) %{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context - context = - if gradual?(old_type) and type not in [term(), dynamic()] do - case compatible_intersection(old_type, type) do - {:ok, new_type} when new_type != old_type -> - data = %{ - data - | type: new_type, - off_traces: new_trace(expr, new_type, stack, off_traces) - } - - %{context | vars: %{vars | version => data}} - - _ -> - context - end - else - context + if gradual?(old_type) and type not in [term(), dynamic()] do + case compatible_intersection(old_type, type) do + {:ok, new_type} when new_type != old_type -> + data = %{ + data + | type: new_type, + off_traces: new_trace(expr, new_type, stack, off_traces) + } + + {new_type, %{context | vars: %{vars | version => data}}} + + _ -> + {old_type, context} end - - {old_type, context} + else + {old_type, context} + end end @doc """ @@ -173,34 +170,70 @@ defmodule Module.Types.Of do @doc """ Builds a closed map. """ - def closed_map(pairs, stack, context, of_fun) do - permutate_map(pairs, stack, context, of_fun, fn fallback, _keys, pairs -> - # TODO: Use the fallback type to actually indicate if open or closed. - if fallback == none(), do: closed_map(pairs), else: open_map(pairs) - end) + def closed_map(pairs, expected, stack, context, of_fun) do + {pairs_types, context} = pairs(pairs, expected, stack, context, of_fun) + + map = + permutate_map(pairs_types, stack, fn fallback, _keys, pairs -> + # TODO: Use the fallback type to actually indicate if open or closed. + if fallback == none(), do: closed_map(pairs), else: open_map(pairs) + end) + + {map, context} end @doc """ - Builds permutation of maps according to the given keys. + Computes the types of key-value pairs. """ - def permutate_map(pairs, %{mode: :traversal} = stack, context, of_fun, _of_map) do - context = - Enum.reduce(pairs, context, fn {key, value}, context -> - {_, context} = of_fun.(key, stack, context) - {_, context} = of_fun.(value, stack, context) - context - end) + def pairs(pairs, _expected, %{mode: :traversal} = stack, context, of_fun) do + Enum.map_reduce(pairs, context, fn {key, value}, context -> + {_key_type, context} = of_fun.(key, term(), stack, context) + {value_type, context} = of_fun.(value, term(), stack, context) + {{true, :none, value_type}, context} + end) + end + + def pairs(pairs, expected, stack, context, of_fun) do + Enum.map_reduce(pairs, context, fn {key, value}, context -> + {dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun) + + expected_value_type = + with [key] <- keys, {_, expected_value_type} <- map_fetch(expected, key) do + expected_value_type + else + _ -> term() + end + + {value_type, context} = of_fun.(value, expected_value_type, stack, context) + {{dynamic_key? or gradual?(value_type), keys, value_type}, context} + end) + end + + defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do + {false, [key], context} + end + + defp finite_key_type(key, stack, context, of_fun) do + {key_type, context} = of_fun.(key, term(), stack, context) + + case atom_fetch(key_type) do + {:finite, list} -> {gradual?(key_type), list, context} + _ -> {gradual?(key_type), :none, context} + end + end - {dynamic(), context} + @doc """ + Builds permutation of maps according to the given pairs types. + """ + def permutate_map(_pairs_types, %{mode: :traversal}, _of_map) do + dynamic() end - def permutate_map(pairs, stack, context, of_fun, of_map) do - {dynamic?, fallback, single, multiple, assert, context} = - Enum.reduce(pairs, {false, none(), [], [], [], context}, fn - {key, value}, {dynamic?, fallback, single, multiple, assert, context} -> - {dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun) - {value_type, context} = of_fun.(value, stack, context) - dynamic? = dynamic? or dynamic_key? or gradual?(value_type) + def permutate_map(pairs_types, _stack, of_map) do + {dynamic?, fallback, single, multiple, assert} = + Enum.reduce(pairs_types, {false, none(), [], [], []}, fn + {dynamic_pair?, keys, value_type}, {dynamic?, fallback, single, multiple, assert} -> + dynamic? = dynamic? or dynamic_pair? case keys do :none -> @@ -216,13 +249,15 @@ defmodule Module.Types.Of do {union(fallback, type), keys ++ assert} end) - {dynamic?, fallback, [], [], assert, context} + {dynamic?, fallback, [], [], assert} + # Because a multiple key may override single keys, we can only + # collect single keys while there are no multiples. [key] when multiple == [] -> - {dynamic?, fallback, [{key, value_type} | single], multiple, assert, context} + {dynamic?, fallback, [{key, value_type} | single], multiple, assert} keys -> - {dynamic?, fallback, single, [{keys, value_type} | multiple], assert, context} + {dynamic?, fallback, single, [{keys, value_type} | multiple], assert} end end) @@ -238,20 +273,7 @@ defmodule Module.Types.Of do |> Enum.reduce(&union/2) end - if dynamic?, do: {dynamic(map), context}, else: {map, context} - end - - defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do - {false, [key], context} - end - - defp finite_key_type(key, stack, context, of_fun) do - {key_type, context} = of_fun.(key, stack, context) - - case atom_fetch(key_type) do - {:finite, list} -> {gradual?(key_type), list, context} - _ -> {gradual?(key_type), :none, context} - end + if dynamic?, do: dynamic(map), else: map end defp cartesian_map(lists) do @@ -267,17 +289,27 @@ defmodule Module.Types.Of do @doc """ Handles instantiation of a new struct. """ - def struct_instance(struct, args, meta, stack, context, of_fun) + # TODO: Type check the fields match the struct + def struct_instance(struct, args, expected, meta, %{mode: mode} = stack, context, of_fun) when is_atom(struct) do + {_info, context} = struct_info(struct, meta, stack, context) + # The compiler has already checked the keys are atoms and which ones are required. {args_types, context} = Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) -> - {type, context} = of_fun.(value, stack, context) + value_type = + with true <- mode != :traversal, + {_, expected_value_type} <- map_fetch(expected, key) do + expected_value_type + else + _ -> term() + end + + {type, context} = of_fun.(value, value_type, stack, context) {{key, type}, context} end) - {info, context} = struct_info(struct, meta, stack, context) - {struct_type(struct, info, args_types), context} + {closed_map([{:__struct__, atom([struct])} | args_types]), context} end @doc """ @@ -306,8 +338,10 @@ defmodule Module.Types.Of do @doc """ Builds a type from the struct info. """ + # TODO: This function shuold not receive args_types once + # we introduce typed structs. They are only used by exceptions. def struct_type(struct, info, args_types \\ []) do - term = term() + term = dynamic() pairs = for %{field: field} <- info, do: {field, term} pairs = [{:__struct__, atom([struct])} | pairs] pairs = if args_types == [], do: pairs, else: pairs ++ args_types diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 7976673e51e..214c2bd9267 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -686,15 +686,15 @@ defmodule Module.Types.Pattern do end # %Struct{...} - def of_guard({:%, meta, [module, {:%{}, _, args}]} = struct, _expected, _expr, stack, context) + def of_guard({:%, meta, [module, {:%{}, _, args}]} = struct, expected, _expr, stack, context) when is_atom(module) do - fun = &of_guard(&1, dynamic(), struct, &2, &3) - Of.struct_instance(module, args, meta, stack, context, fun) + fun = &of_guard(&1, &2, struct, &3, &4) + Of.struct_instance(module, args, expected, meta, stack, context, fun) end # %{...} - def of_guard({:%{}, _meta, args}, _expected, expr, stack, context) do - Of.closed_map(args, stack, context, &of_guard(&1, dynamic(), expr, &2, &3)) + def of_guard({:%{}, _meta, args}, expected, expr, stack, context) do + Of.closed_map(args, expected, stack, context, &of_guard(&1, &2, expr, &3, &4)) end # <<>> diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 28318a10f56..58d3bd8aa61 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -867,10 +867,19 @@ defmodule Module.Types.ExprTest do test "updating structs" do assert typecheck!([x], %Point{x | x: :zero}) == - dynamic(open_map(__struct__: atom([Point]), x: atom([:zero]))) + dynamic( + closed_map(__struct__: atom([Point]), x: atom([:zero]), y: term(), z: term()) + ) assert typecheck!([x], %Point{%Point{x | x: :zero} | y: :one}) == - dynamic(open_map(__struct__: atom([Point]), x: atom([:zero]), y: atom([:one]))) + dynamic( + closed_map( + __struct__: atom([Point]), + x: atom([:zero]), + y: atom([:one]), + z: term() + ) + ) assert typeerror!( ( @@ -885,7 +894,7 @@ defmodule Module.Types.ExprTest do expected type: - %Point{x: term(), y: term(), z: term()} + dynamic(%Point{x: term(), y: term(), z: term()}) but got type: @@ -899,6 +908,44 @@ defmodule Module.Types.ExprTest do """ end + test "inference on struct update" do + assert typecheck!( + [x], + ( + %Point{x | x: :zero} + x + ) + ) == + dynamic(closed_map(__struct__: atom([Point]), x: term(), y: term(), z: term())) + + assert typeerror!( + [x], + ( + x.w + %Point{x | x: :zero} + ) + ) == + ~l""" + incompatible types in struct update: + + %Point{x | x: :zero} + + expected type: + + dynamic(%Point{x: term(), y: term(), z: term()}) + + but got type: + + dynamic(%{..., w: term()}) + + where "x" was given the type: + + # type: dynamic(%{..., w: term()}) + # from: types_test.ex:LINE-4 + x.w + """ + end + test "nested map" do assert typecheck!([x = %{}], x.foo.bar) == dynamic() end diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 25dd062d214..6744fc3826f 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -396,8 +396,10 @@ defmodule Module.Types.IntegrationTest do but expected a type that implements the String.Chars protocol, it must be one of: - %Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or - %Version.Requirement{} or atom() or binary() or float() or integer() or list(term()) + dynamic( + %Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or + %Version.Requirement{} + ) or atom() or binary() or float() or integer() or list(term()) where "data" was given the type: @@ -418,8 +420,10 @@ defmodule Module.Types.IntegrationTest do but expected a type that implements the String.Chars protocol, it must be one of: - %Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or - %Version.Requirement{} or atom() or binary() or float() or integer() or list(term()) + dynamic( + %Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or + %Version.Requirement{} + ) or atom() or binary() or float() or integer() or list(term()) where "data" was given the type: @@ -455,8 +459,10 @@ defmodule Module.Types.IntegrationTest do but expected a type that implements the Enumerable protocol, it must be one of: - %Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or - %IO.Stream{} or %MapSet{} or %Range{} or %Stream{} or fun() or list(term()) or non_struct_map() + dynamic( + %Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or + %IO.Stream{} or %MapSet{} or %Range{} or %Stream{} + ) or fun() or list(term()) or non_struct_map() where "date" was given the type: @@ -484,7 +490,7 @@ defmodule Module.Types.IntegrationTest do but expected a type that implements the Collectable protocol, it must be one of: - %File.Stream{} or %HashDict{} or %HashSet{} or %IO.Stream{} or %MapSet{} or binary() or + dynamic(%File.Stream{} or %HashDict{} or %HashSet{} or %IO.Stream{} or %MapSet{}) or binary() or list(term()) or non_struct_map() hint: the :into option in for-comprehensions use the Collectable protocol to build its result. Either pass a valid data type or implement the protocol accordingly From de2462f77682cc0ba697c9213645548619d0a3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 15 Jan 2025 14:21:50 +0100 Subject: [PATCH 18/22] More dynamic() -> term() --- lib/elixir/lib/module/types/pattern.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 214c2bd9267..de1967cc2ee 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -674,9 +674,9 @@ defmodule Module.Types.Pattern do {prefix, suffix} = unpack_list(list, []) {prefix, context} = - Enum.map_reduce(prefix, context, &of_guard(&1, dynamic(), expr, stack, &2)) + Enum.map_reduce(prefix, context, &of_guard(&1, term(), expr, stack, &2)) - {suffix, context} = of_guard(suffix, dynamic(), expr, stack, context) + {suffix, context} = of_guard(suffix, term(), expr, stack, context) {non_empty_list(Enum.reduce(prefix, &union/2), suffix), context} end @@ -712,14 +712,14 @@ defmodule Module.Types.Pattern do # {...} def of_guard({:{}, _meta, args}, _expected, expr, stack, context) do - {types, context} = Enum.map_reduce(args, context, &of_guard(&1, dynamic(), expr, stack, &2)) + {types, context} = Enum.map_reduce(args, context, &of_guard(&1, term(), expr, stack, &2)) {tuple(types), context} end # var.field def of_guard({{:., _, [callee, key]}, _, []} = map_fetch, _expected, expr, stack, context) when not is_atom(callee) do - {type, context} = of_guard(callee, dynamic(), expr, stack, context) + {type, context} = of_guard(callee, term(), expr, stack, context) Of.map_fetch(map_fetch, type, key, stack, context) end @@ -727,7 +727,7 @@ defmodule Module.Types.Pattern do def of_guard({{:., _, [:erlang, function]}, _, args}, _expected, expr, stack, context) when function in [:==, :"/=", :"=:=", :"=/="] do {_args_type, context} = - Enum.map_reduce(args, context, &of_guard(&1, dynamic(), expr, stack, &2)) + Enum.map_reduce(args, context, &of_guard(&1, term(), expr, stack, &2)) {boolean(), context} end From 1d47983b576e3a223163031cc159caf673835c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 15 Jan 2025 17:40:31 +0100 Subject: [PATCH 19/22] Tests --- lib/elixir/lib/module/types/expr.ex | 2 +- .../elixir/module/types/integration_test.exs | 71 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index b9a17ce0f48..1f82245d81d 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -148,7 +148,7 @@ defmodule Module.Types.Expr do type_fun = fn pattern_type, context -> # See if we can use the expected type to further refine the pattern type, # if we cannot, use the pattern type as that will fail later on. - {_ok_or_error, type} = compatible_intersection(pattern_type, expected) + {_ok_or_error, type} = compatible_intersection(dynamic(pattern_type), expected) of_expr(right_expr, type, expr, stack, context) end diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 6744fc3826f..9b40094cde3 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -80,6 +80,77 @@ defmodule Module.Types.IntegrationTest do ] end + test "writes exports with inferred map types" do + files = %{ + "a.ex" => """ + defmodule A do + defstruct [:x, :y, :z] + + def struct_create_with_atom_keys(x) do + infer(y = %A{x: x}) + {x, y} + end + + def map_create_with_atom_keys(x) do + infer(%{__struct__: A, x: x, y: nil, z: nil}) + x + end + + def map_update_with_atom_keys(x) do + infer(%{x | y: nil}) + x + end + + def map_update_with_unknown_keys(x, y) do + infer(%{x | y => 123}) + x + end + + defp infer(%A{x: <<_::binary>>, y: nil}) do + :ok + end + end + """ + } + + modules = compile_modules(files) + exports = read_chunk(modules[A]).exports |> Map.new() + + return = fn name, arity -> + pair = {name, arity} + %{^pair => %{sig: {:infer, nil, [{_, return}]}}} = exports + return + end + + assert return.(:struct_create_with_atom_keys, 1) == + dynamic( + tuple([ + binary(), + closed_map( + __struct__: atom([A]), + x: binary(), + y: atom([nil]), + z: atom([nil]) + ) + ]) + ) + + assert return.(:map_create_with_atom_keys, 1) == dynamic(binary()) + + assert return.(:map_update_with_atom_keys, 1) == + dynamic( + closed_map( + __struct__: atom([A]), + x: binary(), + y: term(), + z: term() + ) + ) + + assert return.(:map_update_with_unknown_keys, 2) == + dynamic(open_map()) + end + test "writes exports for implementations" do files = %{ "pi.ex" => """ From 1763865312f8ad4c51c3a69450ad9e8d86921960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 15 Jan 2025 20:25:41 +0100 Subject: [PATCH 20/22] 1000!!!! --- lib/elixir/lib/module/types/apply.ex | 114 ++++++++++-------- .../test/elixir/module/types/expr_test.exs | 10 ++ .../test/elixir/module/types/pattern_test.exs | 6 +- 3 files changed, 77 insertions(+), 53 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index fee69e0c905..626e76ab5f0 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -270,13 +270,10 @@ defmodule Module.Types.Apply do {:none, Enum.map(args, fn _ -> term() end)} end - def remote_domain(fun, args, _expected, _stack) do + def remote_domain(fun, args, expected, _stack) do arity = length(args) - - case signature(fun, arity) do - :none -> {:none, Enum.map(args, fn _ -> term() end)} - {:strong, domain, clauses} = info -> {info, domain(domain, clauses)} - end + info = signature(fun, arity) + {info, filter_domain(info, expected, arity)} end @doc """ @@ -338,15 +335,7 @@ defmodule Module.Types.Apply do false -> {info, context} = signature(mod, fun, arity, meta, stack, context) - - case info do - {type, domain, clauses} -> - domain = domain(domain, clauses) - {{type, domain, clauses}, domain, context} - - :none -> - {:none, List.duplicate(term(), arity), context} - end + {info, filter_domain(info, expected, arity), context} end end @@ -369,17 +358,17 @@ defmodule Module.Types.Apply do {:ok, dynamic()} end - defp remote_apply({:infer, domain, clauses}, args_types, _stack) do - case apply_infer(domain, clauses, args_types) do + defp remote_apply({:infer, _domain, clauses} = sig, args_types, _stack) do + case apply_infer(clauses, args_types) do {_used, type} -> {:ok, type} - {:error, domain, clauses} -> {:error, {:badremote, domain, clauses}} + :error -> {:error, {:badremote, sig}} end end - defp remote_apply({:strong, domain, clauses}, args_types, stack) do + defp remote_apply({:strong, domain, clauses} = sig, args_types, stack) do case apply_strong(domain, clauses, args_types, stack) do {_used, type} -> {:ok, type} - {:error, domain, clauses} -> {:error, {:badremote, domain, clauses}} + :error -> {:error, {:badremote, sig}} end end @@ -471,8 +460,7 @@ defmodule Module.Types.Apply do end defp badremote(mod, fun, arity) do - {_, domain, clauses} = signature(mod, fun, arity) - {:badremote, domain, clauses} + {:badremote, signature(mod, fun, arity)} end @doc """ @@ -629,8 +617,7 @@ defmodule Module.Types.Apply do ## Local - # PENDING: expected - def local_domain(fun, args, _expected, meta, stack, context) do + def local_domain(fun, args, expected, meta, stack, context) do arity = length(args) case stack.local_handler.(meta, {fun, arity}, stack, context) do @@ -640,27 +627,29 @@ defmodule Module.Types.Apply do {kind, info, context} -> update_used? = stack.mode not in [:traversal, :infer] and kind == :defp - case info do - _ when stack.mode == :traversal or info == :none -> - {{update_used?, :none}, List.duplicate(term(), arity), context} - - {_strong_or_infer, domain, clauses} -> - {{update_used?, info}, domain(domain, clauses), context} + if stack.mode == :traversal or info == :none do + {{update_used?, :none}, List.duplicate(term(), arity), context} + else + {{update_used?, info}, filter_domain(info, expected, arity), context} end end end + def local_apply({update_used?, :none}, fun, args_types, _expr, _stack, context) do + if update_used? do + {dynamic(), put_in(context.local_used[{fun, length(args_types)}], [])} + else + {dynamic(), context} + end + end + def local_apply({update_used?, info}, fun, args_types, expr, stack, context) do case local_apply(info, args_types, stack) do {indexes, type} -> context = if update_used? do update_in(context.local_used[{fun, length(args_types)}], fn current -> - if info == :none do - [] - else - (current || used_from_clauses(info)) -- indexes - end + (current || used_from_clauses(info)) -- indexes end) else context @@ -668,18 +657,14 @@ defmodule Module.Types.Apply do {type, context} - {:error, domain, clauses} -> - error = {:badlocal, domain, clauses, args_types, expr, context} + :error -> + error = {:badlocal, info, args_types, expr, context} {error_type(), error(error, with_span(elem(expr, 1), fun), stack, context)} end end - defp local_apply(:none, _args_types, _stack) do - {[], dynamic()} - end - - defp local_apply({:infer, domain, clauses}, args_types, _stack) do - apply_infer(domain, clauses, args_types) + defp local_apply({:infer, _domain, clauses}, args_types, _stack) do + apply_infer(clauses, args_types) end defp local_apply({:strong, domain, clauses}, args_types, stack) do @@ -725,10 +710,39 @@ defmodule Module.Types.Apply do defp domain(nil, [{domain, _}]), do: domain defp domain(domain, _clauses), do: domain - defp apply_infer(domain, clauses, args_types) do + @term_or_dynamic [term(), dynamic()] + + defp filter_domain(:none, _expected, arity) do + List.duplicate(term(), arity) + end + + defp filter_domain({_, domain, clauses}, expected, _arity) when expected in @term_or_dynamic do + domain(domain, clauses) + end + + defp filter_domain({_type, domain, clauses}, expected, arity) do + case filter_domain(clauses, expected, [], true) do + :none -> List.duplicate(term(), arity) + :all -> domain(domain, clauses) + args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &union/2) end) + end + end + + defp filter_domain([{args, return} | clauses], expected, acc, all_compatible?) do + case compatible?(return, expected) do + true -> filter_domain(clauses, expected, [args | acc], all_compatible?) + false -> filter_domain(clauses, expected, acc, false) + end + end + + defp filter_domain([], _expected, [], _all_compatible?), do: :none + defp filter_domain([], _expected, _acc, true), do: :all + defp filter_domain([], _expected, acc, false), do: acc + + defp apply_infer(clauses, args_types) do case apply_clauses(clauses, args_types, 0, 0, [], []) do {0, [], []} -> - {:error, domain, clauses} + :error {count, used, _returns} when count > @max_clauses -> {used, dynamic()} @@ -738,11 +752,11 @@ defmodule Module.Types.Apply do end end - defp apply_strong(domain, [{expected, return}] = clauses, args_types, stack) do + defp apply_strong(_domain, [{expected, return}], args_types, stack) do # Optimize single clauses as the domain is the single clause args. case zip_compatible?(args_types, expected) do true -> {[0], return(return, args_types, stack)} - false -> {:error, domain, clauses} + false -> :error end end @@ -753,7 +767,7 @@ defmodule Module.Types.Apply do {count, used, returns} when count > 0 <- apply_clauses(clauses, args_types, 0, 0, [], []) do {used, returns |> Enum.reduce(&union/2) |> return(args_types, stack)} else - _ -> {:error, domain, clauses} + _ -> :error end end @@ -796,7 +810,7 @@ defmodule Module.Types.Apply do ## Diagnostics - def format_diagnostic({:badlocal, domain, clauses, args_types, expr, context}) do + def format_diagnostic({:badlocal, {_, domain, clauses}, args_types, expr, context}) do domain = domain(domain, clauses) traces = collect_traces(expr, context) converter = &Function.identity/1 @@ -852,7 +866,7 @@ defmodule Module.Types.Apply do } end - def format_diagnostic({{:badremote, domain, clauses}, args_types, mfac, expr, context}) do + def format_diagnostic({{:badremote, {_, domain, clauses}}, args_types, mfac, expr, context}) do domain = domain(domain, clauses) {mod, fun, arity, converter} = mfac meta = elem(expr, 1) diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 58d3bd8aa61..f86b07b3b17 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -196,6 +196,16 @@ defmodule Module.Types.ExprTest do ) == dynamic(open_map(foo_bar: atom([:foo]), baz_bat: integer())) end + test "infers args" do + assert typecheck!( + [x, y], + ( + z = Integer.to_string(x + y) + {x, y, z} + ) + ) == dynamic(tuple([integer(), integer(), binary()])) + end + test "undefined function warnings" do assert typewarn!(URI.unknown("foo")) == {dynamic(), "URI.unknown/1 is undefined or private"} diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index 66d6f2e2ffd..2df170a6a8d 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -42,9 +42,9 @@ defmodule Module.Types.PatternTest do where "name" was given the type: - # type: dynamic(atom()) - # from: types_test.ex:LINE-1 - Atom.to_charlist(name) + # type: dynamic() + # from: types_test.ex + {name, arity} """ end From b3da7b1b4cdf87577f570f10d91c6c21082aceaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 15 Jan 2025 20:28:49 +0100 Subject: [PATCH 21/22] Remove apply specific behaviour --- lib/elixir/lib/module/types/apply.ex | 1 + lib/elixir/lib/module/types/expr.ex | 35 ---------------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 626e76ab5f0..50406bef6d9 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -145,6 +145,7 @@ defmodule Module.Types.Apply do {:erlang, :>, [{[term(), term()], boolean()}]}, {:erlang, :>=, [{[term(), term()], boolean()}]}, {:erlang, :abs, [{[integer()], integer()}, {[float()], float()}]}, + # TODO: Decide if it returns dynamic() or term() {:erlang, :apply, [{[fun(), list(term())], dynamic()}]}, {:erlang, :apply, [{[atom(), atom(), list(term())], dynamic()}]}, {:erlang, :and, and_signature}, diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 1f82245d81d..b2e7504ff11 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -499,41 +499,6 @@ defmodule Module.Types.Expr do end end - def of_expr( - {{:., _, [remote, :apply]}, _meta, [mod, fun, args]} = call, - expected, - _expr, - stack, - context - ) - when remote in [Kernel, :erlang] and is_list(args) do - {mod_type, context} = of_expr(mod, atom(), call, stack, context) - {fun_type, context} = of_expr(fun, atom(), call, stack, context) - improper_list? = Enum.any?(args, &match?({:|, _, [_, _]}, &1)) - - case atom_fetch(fun_type) do - {_, [_ | _] = funs} when not improper_list? -> - mods = - case atom_fetch(mod_type) do - {_, mods} -> mods - _ -> [] - end - - {types, context} = - Enum.map_reduce(funs, context, fn fun, context -> - apply_many(mods, fun, args, expected, call, stack, context) - end) - - {Enum.reduce(types, &union/2), context} - - _ -> - info = Apply.signature(:erlang, :apply, 3) - {args_type, context} = of_expr(args, list(term()), call, stack, context) - apply_types = [mod_type, fun_type, args_type] - Apply.remote_apply(info, :erlang, :apply, apply_types, call, stack, context) - end - end - def of_expr({{:., _, [remote, name]}, meta, args} = call, expected, _expr, stack, context) do {remote_type, context} = of_expr(remote, atom(), call, stack, context) {mods, context} = Of.modules(remote_type, name, length(args), call, meta, stack, context) From 3ef530a24ccdc45b858890b4d948684b9ed7fe3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 15 Jan 2025 20:33:44 +0100 Subject: [PATCH 22/22] Fix warnings --- lib/elixir/lib/string.ex | 7 ++++--- lib/elixir/test/elixir/calendar/iso_test.exs | 4 ++-- lib/elixir/test/elixir/inspect_test.exs | 5 ++--- lib/elixir/test/elixir/integer_test.exs | 5 ----- lib/elixir/test/elixir/kernel/comprehension_test.exs | 4 ++-- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index 0b95ba4a139..34c2269b524 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -1825,6 +1825,10 @@ defmodule String do from a validation perspective (they will always produce the same output), but `:fast_ascii` can yield significant performance benefits in specific scenarios. + If anything else but a string is given as argument, it raises. + + ## Fast ASCII + If all of the following conditions are true, you may want to experiment with the `:fast_ascii` algorithm to see if it yields performance benefits in your specific scenario: @@ -1858,9 +1862,6 @@ defmodule String do iex> String.valid?("a", :fast_ascii) true - iex> String.valid?(4) - ** (FunctionClauseError) no function clause matching in String.valid?/2 - """ @spec valid?(t, :default | :fast_ascii) :: boolean def valid?(string, algorithm \\ :default) diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index a3ce35c25b2..d7e30d1a730 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -53,11 +53,11 @@ defmodule Calendar.ISOTest do describe "naive_datetime_to_iso_days/7" do test "raises with invalid dates" do assert_raise ArgumentError, "invalid date: 2018-02-30", fn -> - Calendar.ISO.naive_datetime_to_iso_days(2018, 2, 30, 0, 0, 0, 0) + Calendar.ISO.naive_datetime_to_iso_days(2018, 2, 30, 0, 0, 0, {0, 0}) end assert_raise ArgumentError, "invalid date: 2017-11--03", fn -> - Calendar.ISO.naive_datetime_to_iso_days(2017, 11, -3, 0, 0, 0, 0) + Calendar.ISO.naive_datetime_to_iso_days(2017, 11, -3, 0, 0, 0, {0, 0}) end end end diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index 4f273360da1..dde7ef0c1f4 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -595,10 +595,9 @@ defmodule Inspect.MapTest do {my_argument_error, stacktrace} = try do - atom_to_string(failing.name) + atom_to_string(Process.get(:unused, failing.name)) rescue - e -> - {e, __STACKTRACE__} + e -> {e, __STACKTRACE__} end inspected = diff --git a/lib/elixir/test/elixir/integer_test.exs b/lib/elixir/test/elixir/integer_test.exs index a968d8884dd..03754beb4a1 100644 --- a/lib/elixir/test/elixir/integer_test.exs +++ b/lib/elixir/test/elixir/integer_test.exs @@ -51,11 +51,6 @@ defmodule IntegerTest do assert_raise ArithmeticError, fn -> Integer.mod(-50, 0) end end - test "mod/2 raises ArithmeticError when non-integers used as arguments" do - assert_raise ArithmeticError, fn -> Integer.mod(3.0, 2) end - assert_raise ArithmeticError, fn -> Integer.mod(20, 1.2) end - end - test "floor_div/2" do assert Integer.floor_div(3, 2) == 1 assert Integer.floor_div(0, 10) == 0 diff --git a/lib/elixir/test/elixir/kernel/comprehension_test.exs b/lib/elixir/test/elixir/kernel/comprehension_test.exs index ca304c1b3bf..036a0c3f3ce 100644 --- a/lib/elixir/test/elixir/kernel/comprehension_test.exs +++ b/lib/elixir/test/elixir/kernel/comprehension_test.exs @@ -147,7 +147,7 @@ defmodule Kernel.ComprehensionTest do test "for comprehensions with errors on filters" do assert_raise ArgumentError, fn -> - for x <- 1..3, hd(x), do: x * 2 + for x <- 1..3, hd(x), do: :ok end end @@ -338,7 +338,7 @@ defmodule Kernel.ComprehensionTest do test "list for comprehensions with errors on filters" do assert_raise ArgumentError, fn -> - for x <- [1, 2, 3], hd(x), do: x * 2 + for x <- [1, 2, 3], hd(Process.get(:unused, x)), do: x * 2 end end