From 28a66b5ff124c6e405b69ce9afaade5e98d179b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 6 Apr 2026 19:06:46 +0200 Subject: [PATCH 1/8] Add version to case --- lib/elixir/lib/module/types.ex | 8 +++++-- lib/elixir/lib/module/types/expr.ex | 1 + lib/elixir/src/elixir.hrl | 8 ++++--- lib/elixir/src/elixir_clauses.erl | 11 ++++++---- lib/elixir/src/elixir_def.erl | 18 +++++++-------- lib/elixir/src/elixir_env.erl | 16 +++++++------- lib/elixir/src/elixir_erl_var.erl | 2 +- lib/elixir/src/elixir_expand.erl | 22 ++++++++++--------- lib/elixir/src/elixir_module.erl | 2 +- .../test/elixir/kernel/expansion_test.exs | 2 +- 10 files changed, 51 insertions(+), 39 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 8fc89d14e9e..7877926bd3d 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -437,7 +437,9 @@ defmodule Module.Types do # The mode to be used, see the @modes attribute mode: mode, # The function for handling local calls - local_handler: handler + local_handler: handler, + # Reverse arrow handling (nil | :cache | :use) + reverse_arrow: nil } end @@ -459,7 +461,9 @@ defmodule Module.Types do # Local signatures used by local handler local_sigs: %{}, # Track which clauses have been used across private local calls - local_used: %{} + local_used: %{}, + # Cached reverse arrows + reverse_arrows: %{} } end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 9d23423e48f..f274d687fea 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -312,6 +312,7 @@ defmodule Module.Types.Expr do end def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, _expr, stack, context) do + _ = Keyword.fetch!(meta, :version) {case_type, context} = of_expr(case_expr, @pending, case_expr, stack, context) info = {:case, meta, case_expr, case_type} diff --git a/lib/elixir/src/elixir.hrl b/lib/elixir/src/elixir.hrl index 75e46cd5938..f89e905878c 100644 --- a/lib/elixir/src/elixir.hrl +++ b/lib/elixir/src/elixir.hrl @@ -31,8 +31,8 @@ prematch=none, %% Stores if __STACKTRACE__ is allowed stacktrace=false, - %% A map of unused vars and a version counter for vars - unused={#{}, 0}, + %% A map of unused vars + unused=#{}, %% A list of modules defined in functions (runtime) runtime_modules=[], %% A tuple with maps of read and optional write current vars. @@ -46,7 +46,9 @@ %% if you write foo(a = 123), the value of `a` cannot be %% read in the following argument, only after the call %% - vars={#{}, false} + vars={#{}, false}, + %% Stores expression version counter + version=0 }). -record(elixir_erl, { diff --git a/lib/elixir/src/elixir_clauses.erl b/lib/elixir/src/elixir_clauses.erl index b252ae8b08a..22395f4bdfe 100644 --- a/lib/elixir/src/elixir_clauses.erl +++ b/lib/elixir/src/elixir_clauses.erl @@ -155,13 +155,14 @@ recur_cycles(Cycles, Current, Source, Seen, SkipList, Meta, Expr, E) -> %% Match match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) -> - #elixir_ex{vars=Current, unused={_, Counter} = Unused} = AfterS, + #elixir_ex{vars=Current, unused=Unused, version=Counter} = AfterS, #elixir_ex{vars={Read, _}, prematch=Prematch} = BeforeS, CallS = BeforeS#elixir_ex{ prematch={Read, {#{}, []}, Counter}, unused=Unused, - vars=Current + vars=Current, + version=Counter }, CallE = E#{context := match}, @@ -170,7 +171,8 @@ match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) -> #elixir_ex{ vars=NewCurrent, unused=NewUnused, - prematch={_, Cycles, _} + prematch={_, Cycles, _}, + version=NewCounter } = SE, validate_cycles(Cycles, Meta, {match, Expr}, E), @@ -178,7 +180,8 @@ match(Fun, Meta, Expr, AfterS, BeforeS, #{context := nil} = E) -> EndS = AfterS#elixir_ex{ prematch=Prematch, unused=NewUnused, - vars=NewCurrent + vars=NewCurrent, + version=NewCounter }, EndE = EE#{context := ?key(E, context)}, diff --git a/lib/elixir/src/elixir_def.erl b/lib/elixir/src/elixir_def.erl index 6ad980319bf..24f79e17cbd 100644 --- a/lib/elixir/src/elixir_def.erl +++ b/lib/elixir/src/elixir_def.erl @@ -295,17 +295,17 @@ store_definition(CheckClauses, Kind, Meta, Name, Arity, File, Module, Defaults, %% Handling of defaults unpack_defaults(Kind, Meta, Name, Args, S, E) -> - {Expanded, #elixir_ex{unused={_, VersionOffset}}} = expand_defaults(Args, S, E#{context := nil}, []), - unpack_expanded(Kind, Meta, Name, Expanded, VersionOffset, [], []). + {Expanded, #elixir_ex{version=Counter}} = expand_defaults(Args, S, E#{context := nil}, []), + unpack_expanded(Kind, Meta, Name, Expanded, Counter, [], []). -unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, VersionOffset, Acc, Clauses) -> - Base = match_defaults(Acc, length(Acc) + VersionOffset, []), - {Args, Invoke} = extract_defaults(List, length(Base) + VersionOffset, [], []), +unpack_expanded(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, Counter, Acc, Clauses) -> + Base = match_defaults(Acc, length(Acc) + Counter, []), + {Args, Invoke} = extract_defaults(List, length(Base) + Counter, [], []), Clause = {Meta, Base ++ Args, [], {super, [{super, {Kind, Name}}, {default, true} | DefaultMeta], Base ++ Invoke}}, - unpack_expanded(Kind, Meta, Name, T, VersionOffset, [Expr | Acc], [Clause | Clauses]); -unpack_expanded(Kind, Meta, Name, [H | T], VersionOffset, Acc, Clauses) -> - unpack_expanded(Kind, Meta, Name, T, VersionOffset, [H | Acc], Clauses); -unpack_expanded(_Kind, _Meta, _Name, [], _VersionOffset, Acc, Clauses) -> + unpack_expanded(Kind, Meta, Name, T, Counter, [Expr | Acc], [Clause | Clauses]); +unpack_expanded(Kind, Meta, Name, [H | T], Counter, Acc, Clauses) -> + unpack_expanded(Kind, Meta, Name, T, Counter, [H | Acc], Clauses); +unpack_expanded(_Kind, _Meta, _Name, [], _Counter, Acc, Clauses) -> {lists:reverse(Acc), lists:reverse(Clauses)}. expand_defaults([{'\\\\', Meta, [Expr, Default]} | Args], S, E, Acc) -> diff --git a/lib/elixir/src/elixir_env.erl b/lib/elixir/src/elixir_env.erl index ec6df8f6366..93519b9a3dc 100644 --- a/lib/elixir/src/elixir_env.erl +++ b/lib/elixir/src/elixir_env.erl @@ -56,12 +56,12 @@ env_to_ex(#{context := match, versioned_vars := Vars}) -> #elixir_ex{ prematch={Vars, {#{}, []}, Counter}, vars={Vars, false}, - unused={#{}, Counter} + version=Counter }; env_to_ex(#{versioned_vars := Vars}) -> #elixir_ex{ vars={Vars, false}, - unused={#{}, map_size(Vars)} + version=map_size(Vars) }. %% VAR HANDLING @@ -94,10 +94,10 @@ merge_vars(V1, V2) -> %% UNUSED VARS -reset_unused_vars(#elixir_ex{unused={_Unused, Version}} = S) -> - S#elixir_ex{unused={#{}, Version}}. +reset_unused_vars(#elixir_ex{} = S) -> + S#elixir_ex{unused=#{}}. -check_unused_vars(#elixir_ex{unused={Unused, _Version}}, E) -> +check_unused_vars(#elixir_ex{unused=Unused}, E) -> [elixir_errors:file_warn(calculate_span(Meta, Name), E, ?MODULE, {unused_var, Name, Overridden}) || {{{Name, _Kind}, _Count}, {Meta, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)], E. @@ -111,10 +111,10 @@ calculate_span(Meta, Name) -> Meta end. -merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused={Unused, _Version}}, E) -> - #elixir_ex{unused={ClauseUnused, Version}} = S, +merge_and_check_unused_vars(S, #elixir_ex{vars={Read, Write}, unused=Unused}, E) -> + #elixir_ex{unused=ClauseUnused} = S, NewUnused = merge_and_check_unused_vars(Read, Unused, ClauseUnused, E), - S#elixir_ex{unused={NewUnused, Version}, vars={Read, Write}}. + S#elixir_ex{unused=NewUnused, vars={Read, Write}}. merge_and_check_unused_vars(Current, Unused, ClauseUnused, E) -> maps:fold(fun diff --git a/lib/elixir/src/elixir_erl_var.erl b/lib/elixir/src/elixir_erl_var.erl index 337ff489fe0..c68a2edac52 100644 --- a/lib/elixir/src/elixir_erl_var.erl +++ b/lib/elixir/src/elixir_erl_var.erl @@ -102,7 +102,7 @@ load_pair({Pair, Value}) -> {Pair, Value}. dump_binding(Binding, ErlS, ExS, PruneBefore) -> #elixir_erl{var_names=ErlVars} = ErlS, - #elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS, + #elixir_ex{vars={ExVars, _}, unused=Unused} = ExS, maps:fold(fun ({Var, Kind} = Pair, Version, {B, V}) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 23e13ca6848..027d94edaa4 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -397,8 +397,8 @@ expand({'_', Meta, Kind} = Var, S, #{context := Context} = E) when is_atom(Kind) expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_atom(Kind) -> #elixir_ex{ prematch={_, _, PrematchVersion}, - unused={Unused, Version}, - vars={Read, Write} + vars={Read, Write}, + unused=Unused } = S, Pair = {Name, elixir_utils:var_context(Meta, Kind)}, @@ -408,29 +408,31 @@ expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_at #{Pair := VarVersion} when VarVersion >= PrematchVersion -> maybe_warn_underscored_var_repeat(Meta, Name, Kind, E), NewUnused = var_used(Pair, Meta, VarVersion, Unused), - NewWrite = (Write /= false) andalso Write#{Pair => Version}, + NewWrite = (Write /= false) andalso Write#{Pair => VarVersion}, Var = {Name, [{version, VarVersion} | Meta], Kind}, - {Var, S#elixir_ex{vars={Read, NewWrite}, unused={NewUnused, Version}}, E}; + {Var, S#elixir_ex{vars={Read, NewWrite}, unused=NewUnused}, E}; %% Variable is being overridden now #{Pair := _} -> + Version = S#elixir_ex.version, NewUnused = var_unused(Pair, Meta, Version, Unused, true), NewRead = Read#{Pair => Version}, NewWrite = (Write /= false) andalso Write#{Pair => Version}, Var = {Name, [{version, Version} | Meta], Kind}, - {Var, S#elixir_ex{vars={NewRead, NewWrite}, unused={NewUnused, Version + 1}}, E}; + {Var, S#elixir_ex{vars={NewRead, NewWrite}, unused=NewUnused, version=Version + 1}, E}; %% Variable defined for the first time _ -> + Version = S#elixir_ex.version, NewUnused = var_unused(Pair, Meta, Version, Unused, false), NewRead = Read#{Pair => Version}, NewWrite = (Write /= false) andalso Write#{Pair => Version}, Var = {Name, [{version, Version} | Meta], Kind}, - {Var, S#elixir_ex{vars={NewRead, NewWrite}, unused={NewUnused, Version + 1}}, E} + {Var, S#elixir_ex{vars={NewRead, NewWrite}, unused=NewUnused, version=Version + 1}, E} end; expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) -> - #elixir_ex{vars={Read, _Write}, unused={Unused, Version}, prematch=Prematch} = S, + #elixir_ex{vars={Read, _Write}, unused=Unused, prematch=Prematch} = S, Pair = {Name, elixir_utils:var_context(Meta, Kind)}, Result = @@ -471,7 +473,7 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) -> {ok, PairVersion} -> maybe_warn_underscored_var_access(Meta, Name, Kind, E), Var = {Name, [{version, PairVersion} | Meta], Kind}, - {Var, S#elixir_ex{unused={var_used(Pair, Meta, PairVersion, Unused), Version}}, E}; + {Var, S#elixir_ex{unused=var_used(Pair, Meta, PairVersion, Unused)}, E}; Error -> case lists:keyfind(if_undefined, 1, Meta) of @@ -795,8 +797,8 @@ expand_case(Meta, Expr, Opts, S, E) -> false -> Opts end, - {EOpts, SO, EO} = elixir_clauses:'case'(Meta, ROpts, SE, EE), - {{'case', Meta, [EExpr, EOpts]}, SO, EO}. + {EOpts, #elixir_ex{version=Counter} = SO, EO} = elixir_clauses:'case'(Meta, ROpts, SE, EE), + {{'case', [{version, Counter} | Meta], [EExpr, EOpts]}, SO#elixir_ex{version = Counter + 1}, EO}. rewrite_case_clauses([{do, [ {'->', FalseMeta, [ diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 6466af5e12d..387bb32e694 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -494,7 +494,7 @@ maybe_prune_versioned_vars(false, _Vars, _Exs, E) -> E; maybe_prune_versioned_vars(true, Vars, ExS, E) -> PruneBefore = length(Vars), - #elixir_ex{vars={ExVars, _}, unused={Unused, _}} = ExS, + #elixir_ex{vars={ExVars, _}, unused=Unused} = ExS, VersionedVars = maps:filter(fun diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index fd3ffbd9bcc..16e63d4aa32 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -420,7 +420,7 @@ defmodule Kernel.ExpansionTest do {:=, _, [var_ver(:x, 0), 0]}, {:case, _, [:foo, [do: [{:->, _, [[var_ver(:x, 1)], var_ver(:x, 1)]}]]]}, {:=, _, [_, var_ver(:x, 0)]}, - {:=, _, [var_ver(:x, 2), 2]} + {:=, _, [var_ver(:x, 3), 2]} ]} = expand_with_version( quote do From c7698ad065e0d01f8607eabe0172a9fccc9d4d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Apr 2026 11:34:11 +0200 Subject: [PATCH 2/8] Reverse arrows for case, precision for _ --- lib/elixir/lib/module/types/descr.ex | 3 +- lib/elixir/lib/module/types/expr.ex | 103 ++++++++++++++---- lib/elixir/lib/module/types/pattern.ex | 7 +- lib/elixir/lib/protocol.ex | 2 +- lib/elixir/src/elixir_expand.erl | 10 +- .../test/elixir/kernel/expansion_test.exs | 8 +- .../test/elixir/module/types/expr_test.exs | 16 +++ .../elixir/module/types/integration_test.exs | 2 +- 8 files changed, 110 insertions(+), 41 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 72c0d091ebc..a6670936c78 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1810,7 +1810,7 @@ defmodule Module.Types.Descr do cache = Map.put(cache, cache_key, false) {false, cache} else - {_index, result2, cache} = + {_index, result, cache} = Enum.reduce_while(arguments, {0, true, cache}, fn type, {index, acc_result, acc_cache} -> {new_result, new_cache} = @@ -1825,7 +1825,6 @@ defmodule Module.Types.Descr do end end) - result = result1 and result2 cache = Map.put(cache, cache_key, result) {result, cache} end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index f274d687fea..901f3d5ccc0 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -137,6 +137,8 @@ defmodule Module.Types.Expr do {@stacktrace, context} end + @dynamic_or_term_list [dynamic(), term()] + # left = right def of_expr({:=, _, [left_expr, right_expr]} = match, expected, expr, stack, context) do {left_expr, right_expr} = repack_match(left_expr, right_expr) @@ -147,12 +149,22 @@ defmodule Module.Types.Expr do 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. - {_ok_or_error, type} = compatible_intersection(dynamic(pattern_type), expected) - of_expr(right_expr, type, expr, stack, context) - end + type_fun = + fn pattern_type, context -> + if expected in @dynamic_or_term_list do + of_expr(right_expr, pattern_type, expr, stack, context) + else + # 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(dynamic(pattern_type), expected) + {result, context} = of_expr(right_expr, type, expr, stack, context) + + # The function may still return a too broad type, so we refine once again + # to assign the most appropriate one for reverse arrows. + {_ok_or_error, result} = compatible_intersection(result, expected) + {result, context} + end + end Pattern.of_match(left_expr, type_fun, match, stack, context) end @@ -311,9 +323,20 @@ defmodule Module.Types.Expr do |> dynamic_unless_static(stack) end - def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, _expr, stack, context) do - _ = Keyword.fetch!(meta, :version) - {case_type, context} = of_expr(case_expr, @pending, case_expr, stack, context) + def of_expr({:case, meta, [_case_expr, [{:do, _clauses}]]}, _expected, _expr, stack, context) + when stack.reverse_arrow == :use do + version = Keyword.fetch!(meta, :version) + clauses = Map.fetch!(context.reverse_arrows, version) + result = Enum.reduce(clauses, none(), &union(elem(&1, 1), &2)) + dynamic_unless_static({result, context}, stack) + end + + def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, _expr, stack, base_context) do + version = Keyword.fetch!(meta, :version) + + {case_type, context} = + of_expr(case_expr, @pending, case_expr, %{stack | reverse_arrow: :cache}, base_context) + info = {:case, meta, case_expr, case_type} added_meta = @@ -326,13 +349,35 @@ defmodule Module.Types.Expr do # If the expression is generated or the construct is a literal, # it is most likely a macro code. However, if no clause is matched, # we should still check for that. - if added_meta != [] do - for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args} - else - clauses + clauses = + if added_meta != [] do + for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args} + else + clauses + end + + of_body = fn trees, body, context -> + [arg_type] = Pattern.of_domain(trees, stack, context) + + {_, context} = + of_expr(case_expr, arg_type, case_expr, %{stack | reverse_arrow: :use}, context) + + of_expr(body, expected, body, stack, context) end - |> of_clauses([case_type], expected, info, stack, context, none()) - |> dynamic_unless_static(stack) + + result_context = + cache_arrows(version, stack, fn -> + of_clauses_fun(clauses, [case_type], info, stack, context, of_body, [], fn + trees, body_type, context, acc -> + [arg_type] = Pattern.of_domain(trees, stack, context) + [{arg_type, body_type} | acc] + end) + end) || + of_clauses_fun(clauses, [case_type], info, stack, context, of_body, none(), fn + _trees, body_type, _context, acc -> union(acc, body_type) + end) + + dynamic_unless_static(result_context, stack) end # fn pat -> expr end @@ -341,11 +386,13 @@ defmodule Module.Types.Expr do {patterns, _guards} = extract_head(head) domain = Enum.map(patterns, fn _ -> dynamic() end) + of_body = fn _args_types, body, context -> of_expr(body, @pending, body, stack, context) end + {acc, context} = - of_clauses_fun(clauses, domain, @pending, :fn, stack, context, [], fn - trees, body, context, acc -> + of_clauses_fun(clauses, domain, :fn, stack, context, of_body, [], fn + trees, body_type, context, acc -> args_types = Pattern.of_domain(trees, stack, context) - add_inferred(acc, args_types, body) + add_inferred(acc, args_types, body_type) end) {fun_from_inferred_clauses(acc), context} @@ -725,12 +772,22 @@ 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 cache_arrows(_version, %{reverse_arrow: nil}, _fun), do: nil + + defp cache_arrows(version, %{reverse_arrow: :cache}, fun) do + {clauses, context} = fun.() + context = put_in(context.reverse_arrows[version], clauses) + result = Enum.reduce(clauses, none(), &union(elem(&1, 1), &2)) + {result, context} + end + defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do - fun = fn _args_types, result, _context, acc -> union(result, acc) end - of_clauses_fun(clauses, domain, expected, base_info, stack, context, acc, fun) + of_body = fn _args_types, body, context -> of_expr(body, expected, body, stack, context) end + of_acc = fn _args_types, body_type, _context, acc -> union(acc, body_type) end + of_clauses_fun(clauses, domain, base_info, stack, context, of_body, acc, of_acc) end - defp of_clauses_fun(clauses, domain, expected, base_info, stack, original, acc, fun) do + defp of_clauses_fun(clauses, domain, base_info, stack, original, of_body, acc, of_acc) do %{failed: failed?} = original {result, _previous, context} = @@ -743,9 +800,9 @@ defmodule Module.Types.Expr do {trees, previous, context} = Pattern.of_head(patterns, guards, domain, previous, info, meta, stack, context) - {result, context} = of_expr(body, expected, body, stack, context) + {result, context} = of_body.(trees, body, context) - {fun.(trees, result, context, acc), previous, + {of_acc.(trees, result, context, acc), previous, context |> set_failed(failed?) |> Of.reset_vars(original)} end) diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 005384d2b02..46e2b6fd67b 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -805,12 +805,7 @@ defmodule Module.Types.Pattern do end end - # _ - defp of_pattern({:_, _meta, _var_context}, _path, _stack, context) do - {term(), true, context} - end - - # var + # var (includes underscores) defp of_pattern({name, meta, ctx} = var, path, _stack, context) when is_atom(name) and is_atom(ctx) do version = Keyword.fetch!(meta, :version) diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index 0c84c949dce..b1536c5a4ba 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -786,7 +786,7 @@ defmodule Protocol do end defp fallback_clause_for(value, _protocol, meta) do - {meta, [quote(do: _)], [], value} + {meta, [{:_, [version: -1], __MODULE__}], [], value} end # Finally compile the module and emit its bytecode. diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 027d94edaa4..b3a13810e65 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -385,13 +385,15 @@ expand({'^', Meta, [Arg]}, S, E) -> function_error(Meta, E, ?MODULE, {pin_outside_of_match, Arg}), {{'^', Meta, [Arg]}, S#elixir_ex{tainted_function=true}, E}; -expand({'_', Meta, Kind} = Var, S, #{context := Context} = E) when is_atom(Kind) -> +expand({'_', Meta, Kind}, #elixir_ex{version=Counter} = S, #{context := Context} = E) when is_atom(Kind) -> + NewVar = {'_', [{version, Counter} | Meta], Kind}, + case Context of match -> - {Var, S, E}; + {NewVar, S#elixir_ex{version=Counter+1}, E}; _ -> function_error(Meta, E, ?MODULE, unbound_underscore), - {Var, S#elixir_ex{tainted_function=true}, E} + {NewVar, S#elixir_ex{tainted_function=true, version=Counter+1}, E} end; expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_atom(Kind) -> @@ -798,7 +800,7 @@ expand_case(Meta, Expr, Opts, S, E) -> end, {EOpts, #elixir_ex{version=Counter} = SO, EO} = elixir_clauses:'case'(Meta, ROpts, SE, EE), - {{'case', [{version, Counter} | Meta], [EExpr, EOpts]}, SO#elixir_ex{version = Counter + 1}, EO}. + {{'case', [{version, Counter} | Meta], [EExpr, EOpts]}, SO#elixir_ex{version=Counter+1}, EO}. rewrite_case_clauses([{do, [ {'->', FalseMeta, [ diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 16e63d4aa32..423f740329f 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -387,8 +387,8 @@ defmodule Kernel.ExpansionTest do [ {:=, _, [var_ver(:x, 0), 0]}, {:=, _, [_, var_ver(:x, 0)]}, - {:=, _, [var_ver(:x, 1), 1]}, - {:=, _, [_, var_ver(:x, 1)]} + {:=, _, [var_ver(:x, 2), 1]}, + {:=, _, [_, var_ver(:x, 2)]} ]} = expand_with_version( quote do @@ -404,7 +404,7 @@ defmodule Kernel.ExpansionTest do {:=, _, [var_ver(:x, 0), 0]}, {:fn, _, [{:->, _, [[var_ver(:x, 1)], {:=, _, [var_ver(:x, 2), 2]}]}]}, {:=, _, [_, var_ver(:x, 0)]}, - {:=, _, [var_ver(:x, 3), 3]} + {:=, _, [var_ver(:x, 4), 3]} ]} = expand_with_version( quote do @@ -420,7 +420,7 @@ defmodule Kernel.ExpansionTest do {:=, _, [var_ver(:x, 0), 0]}, {:case, _, [:foo, [do: [{:->, _, [[var_ver(:x, 1)], var_ver(:x, 1)]}]]]}, {:=, _, [_, var_ver(:x, 0)]}, - {:=, _, [var_ver(:x, 3), 2]} + {:=, _, [var_ver(:x, 4), 2]} ]} = expand_with_version( quote do diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index bcdb69053ff..a3186be7b93 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -1915,6 +1915,22 @@ defmodule Module.Types.ExprTest do ) == atom([:ok, nil]) end + test "refines expression type" do + assert typecheck!( + if x = System.get_env("HELLO") do + {:ok, x} + else + {:error, x} + end + ) == + dynamic( + union( + tuple([atom([:ok]), binary()]), + tuple([atom([:error]), atom([nil])]) + ) + ) + end + test "and/or does not report on literals" do assert typecheck!(false and true) == boolean() assert typecheck!(false or true) == atom([true]) diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 118ee5df09c..0c7e11bae61 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -338,7 +338,7 @@ defmodule Module.Types.IntegrationTest do previous clauses have already matched on the following types: - term(), integer() + not integer(), integer() integer(), term() │ From 9cc6405e7abf82403d769d614b26ef2cd5aa0010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Apr 2026 11:48:27 +0200 Subject: [PATCH 3/8] Update annotations --- lib/elixir/lib/module/types/descr.ex | 2 ++ lib/elixir/lib/module/types/expr.ex | 29 ++++++++++++---------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index a6670936c78..3d660d5b6d1 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -971,6 +971,8 @@ defmodule Module.Types.Descr do domain of a function. It is used to refine dynamic types as we traverse the program. """ + def compatible_intersection(other, :term), do: {:ok, remove_optional(other)} + def compatible_intersection(left, right) do {left_dynamic, left_static} = pop_dynamic(left) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 901f3d5ccc0..66cde53b2b2 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -137,8 +137,6 @@ defmodule Module.Types.Expr do {@stacktrace, context} end - @dynamic_or_term_list [dynamic(), term()] - # left = right def of_expr({:=, _, [left_expr, right_expr]} = match, expected, expr, stack, context) do {left_expr, right_expr} = repack_match(left_expr, right_expr) @@ -151,19 +149,15 @@ defmodule Module.Types.Expr do _ -> type_fun = fn pattern_type, context -> - if expected in @dynamic_or_term_list do - of_expr(right_expr, pattern_type, expr, stack, context) - else - # 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(dynamic(pattern_type), expected) - {result, context} = of_expr(right_expr, type, expr, stack, context) - - # The function may still return a too broad type, so we refine once again - # to assign the most appropriate one for reverse arrows. - {_ok_or_error, result} = compatible_intersection(result, expected) - {result, context} - end + # 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(dynamic(pattern_type), expected) + {result, context} = of_expr(right_expr, type, expr, stack, context) + + # The function may still return a too broad type, so we refine once again + # to assign the most appropriate one for reverse arrows. + {_ok_or_error, result} = compatible_intersection(result, expected) + {result, context} end Pattern.of_match(left_expr, type_fun, match, stack, context) @@ -335,7 +329,7 @@ defmodule Module.Types.Expr do version = Keyword.fetch!(meta, :version) {case_type, context} = - of_expr(case_expr, @pending, case_expr, %{stack | reverse_arrow: :cache}, base_context) + of_expr(case_expr, term(), case_expr, %{stack | reverse_arrow: :cache}, base_context) info = {:case, meta, case_expr, case_type} @@ -386,7 +380,7 @@ defmodule Module.Types.Expr do {patterns, _guards} = extract_head(head) domain = Enum.map(patterns, fn _ -> dynamic() end) - of_body = fn _args_types, body, context -> of_expr(body, @pending, body, stack, context) end + of_body = fn _args_types, body, context -> of_expr(body, term(), body, stack, context) end {acc, context} = of_clauses_fun(clauses, domain, :fn, stack, context, of_body, [], fn @@ -483,6 +477,7 @@ defmodule Module.Types.Expr do of_clauses(block, args, expected, :for_reduce, stack, context, reduce_type) else # TODO: Use the collectable protocol for the output + # TODO: Use the expected type for the block output into = Keyword.get(opts, :into, []) {into_type, into_kind, context} = for_into(into, meta, stack, context) {block_type, context} = of_expr(block, @pending, block, stack, context) From 15e8da4d4cdd8b6b656f41e2154649ea97cf55e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Apr 2026 12:27:23 +0200 Subject: [PATCH 4/8] Handle cons in :lists.member/2 types --- lib/elixir/lib/module/types/apply.ex | 76 +++++++++++++++------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 61d539af9ad..2a516d0db6e 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -497,50 +497,54 @@ defmodule Module.Types.Apply do end defp do_remote(:lists, :member, [arg, list] = args, expected, expr, stack, context, of_fun) - when is_list(list) and list != [] do - case booleaness(expected) do - {polarity, _maybe_or_always} -> - {return, acc} = - case polarity do - true -> {@atom_true, none()} - false -> {@atom_false, term()} - end + when is_list(list) do + if list == [] or Enum.any?(list, &match?({:|, _, [_, _]}, &1)) do + remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context) + else + case booleaness(expected) do + {polarity, _maybe_or_always} -> + {return, acc} = + case polarity do + true -> {@atom_true, none()} + false -> {@atom_false, term()} + end - {expected, singleton?, context} = - Enum.reduce(list, {acc, true, context}, fn literal, {acc, all_singleton?, context} -> - {type, context} = of_fun.(literal, term(), expr, stack, context) + {expected, singleton?, context} = + Enum.reduce(list, {acc, true, context}, fn literal, {acc, all_singleton?, context} -> + {type, context} = of_fun.(literal, term(), expr, stack, context) - if singleton?(type) do - acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type)) - {acc, all_singleton?, context} - else - acc = if polarity, do: union(acc, type), else: acc - {acc, false, context} - end - end) + if singleton?(type) do + acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type)) + {acc, all_singleton?, context} + else + acc = if polarity, do: union(acc, type), else: acc + {acc, false, context} + end + end) - {arg_type, context} = of_fun.(arg, expected, expr, stack, context) + {arg_type, context} = of_fun.(arg, expected, expr, stack, context) - cond do - # Return a precise result - singleton? and subtype?(arg_type, expected) -> - {return(return, [arg_type, expected], stack), context} + cond do + # Return a precise result + singleton? and subtype?(arg_type, expected) -> + {return(return, [arg_type, expected], stack), context} - # Singleton types with reverse polarity are negated, so we don't check for disjoint - (singleton? and not polarity) or not is_warning(stack) -> - {return(boolean(), [arg_type, expected], stack), context} + # Singleton types with reverse polarity are negated, so we don't check for disjoint + (singleton? and not polarity) or not is_warning(stack) -> + {return(boolean(), [arg_type, expected], stack), context} - # Nothing in common between left and right, emit a warning - disjoint?(arg_type, expected) -> - error = {:mismatched_comparison, arg_type, list(expected)} - remote_error(error, :lists, :member, 2, expr, stack, context) + # Nothing in common between left and right, emit a warning + disjoint?(arg_type, expected) -> + error = {:mismatched_comparison, arg_type, list(expected)} + remote_error(error, :lists, :member, 2, expr, stack, context) - true -> - {return(boolean(), [arg_type, expected], stack), context} - end + true -> + {return(boolean(), [arg_type, expected], stack), context} + end - _ -> - remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context) + _ -> + remote_domain(:lists, :member, args, expected, elem(expr, 1), stack, context) + end end end From fff0060a216209583dbc9634efb8dcabb6e2b9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Apr 2026 13:06:43 +0200 Subject: [PATCH 5/8] Discard warnings from refinemenets --- lib/elixir/lib/module/types/expr.ex | 6 ++++-- lib/elixir/test/elixir/module/types/expr_test.exs | 11 +++++++++++ lib/mix/lib/mix/compilers/elixir.ex | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 66cde53b2b2..c89366a257e 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -353,10 +353,10 @@ defmodule Module.Types.Expr do of_body = fn trees, body, context -> [arg_type] = Pattern.of_domain(trees, stack, context) - {_, context} = + {_, refined_context} = of_expr(case_expr, arg_type, case_expr, %{stack | reverse_arrow: :use}, context) - of_expr(body, expected, body, stack, context) + of_expr(body, expected, body, stack, reset_warnings(refined_context, context)) end result_context = @@ -804,6 +804,8 @@ defmodule Module.Types.Expr do {result, context} end + defp reset_warnings(context, %{warnings: warnings}), do: %{context | warnings: warnings} + defp reset_failed(%{failed: true} = context, false), do: {true, %{context | failed: false}} defp reset_failed(context, _), do: {false, context} diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index a3186be7b93..f9c4c433300 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -1931,6 +1931,17 @@ defmodule Module.Types.ExprTest do ) end + test "discards warnings from refinements" do + assert {_, [_]} = + typediag!( + if x = System.unknown_function_get_env("HELLO") do + {:ok, x} + else + {:error, x} + end + ) + end + test "and/or does not report on literals" do assert typecheck!(false and true) == boolean() assert typecheck!(false or true) == atom([true]) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index bf8a03ef005..0feb5085c28 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -5,7 +5,7 @@ defmodule Mix.Compilers.Elixir do @moduledoc false - @manifest_vsn 33 + @manifest_vsn 34 @checkpoint_vsn 4 import Record From 3d5cd3ccbc460984165b70d680334ed098a12709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Apr 2026 13:39:31 +0200 Subject: [PATCH 6/8] Nested handling in case --- lib/elixir/lib/module/types/expr.ex | 25 +++++++++++++------ .../test/elixir/module/types/expr_test.exs | 19 ++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index c89366a257e..8442cadf6c2 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -317,17 +317,25 @@ defmodule Module.Types.Expr do |> dynamic_unless_static(stack) end - def of_expr({:case, meta, [_case_expr, [{:do, _clauses}]]}, _expected, _expr, stack, context) + def of_expr({:case, meta, [case_expr, [{:do, _clauses}]]}, expected, _expr, stack, context) when stack.reverse_arrow == :use do version = Keyword.fetch!(meta, :version) clauses = Map.fetch!(context.reverse_arrows, version) - result = Enum.reduce(clauses, none(), &union(elem(&1, 1), &2)) - dynamic_unless_static({result, context}, stack) + + case_expected = + Enum.reduce(clauses, none(), fn {arg_type, body_type}, acc -> + if disjoint?(body_type, expected) do + acc + else + union(arg_type, acc) + end + end) + + {_, context} = of_expr(case_expr, case_expected, case_expr, stack, context) + {expected, context} end def of_expr({:case, meta, [case_expr, [{:do, clauses}]]}, expected, _expr, stack, base_context) do - version = Keyword.fetch!(meta, :version) - {case_type, context} = of_expr(case_expr, term(), case_expr, %{stack | reverse_arrow: :cache}, base_context) @@ -360,7 +368,7 @@ defmodule Module.Types.Expr do end result_context = - cache_arrows(version, stack, fn -> + cache_arrows(meta, stack, fn -> of_clauses_fun(clauses, [case_type], info, stack, context, of_body, [], fn trees, body_type, context, acc -> [arg_type] = Pattern.of_domain(trees, stack, context) @@ -767,10 +775,11 @@ 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 cache_arrows(_version, %{reverse_arrow: nil}, _fun), do: nil + defp cache_arrows(_meta, %{reverse_arrow: nil}, _fun), do: nil - defp cache_arrows(version, %{reverse_arrow: :cache}, fun) do + defp cache_arrows(meta, %{reverse_arrow: :cache}, fun) do {clauses, context} = fun.() + version = Keyword.fetch!(meta, :version) context = put_in(context.reverse_arrows[version], clauses) result = Enum.reduce(clauses, none(), &union(elem(&1, 1), &2)) {result, context} diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index f9c4c433300..cb31de167da 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -1931,6 +1931,25 @@ defmodule Module.Types.ExprTest do ) end + test "refines nested expression type" do + assert typecheck!( + case (if x = System.get_env("HELLO") do + :do + else + :else + end) do + :do -> {:ok, x} + :else -> {:error, x} + end + ) == + dynamic( + union( + tuple([atom([:ok]), binary()]), + tuple([atom([:error]), atom([nil])]) + ) + ) + end + test "discards warnings from refinements" do assert {_, [_]} = typediag!( From b5f5e04fe064ac02eb7cdf935badd68735c0183b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Apr 2026 13:52:03 +0200 Subject: [PATCH 7/8] Add counters to other block constructs --- lib/elixir/src/elixir_clauses.erl | 4 +++- lib/elixir/src/elixir_expand.erl | 15 +++++++++------ lib/elixir/src/elixir_fn.erl | 3 ++- lib/elixir/test/elixir/kernel/expansion_test.exs | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/elixir/src/elixir_clauses.erl b/lib/elixir/src/elixir_clauses.erl index 22395f4bdfe..20400728346 100644 --- a/lib/elixir/src/elixir_clauses.erl +++ b/lib/elixir/src/elixir_clauses.erl @@ -322,7 +322,9 @@ with(Meta, Args, S, E) -> ok end, - {{with, Meta, EExprs ++ [[{do, EDo} | EOpts]]}, S3, E}. + #elixir_ex{version=Counter} = S3, + {{with, [{version, Counter} | Meta], EExprs ++ [[{do, EDo} | EOpts]]}, + S3#elixir_ex{version=Counter+1}, E}. expand_with({'<-', Meta, [Left, Right]}, {S, E, HasMatch}) -> {ERight, SR, ER} = elixir_expand:expand(Right, S, E), diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index b3a13810e65..5e6e2e8a618 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -331,7 +331,8 @@ expand({'cond', Meta, [Opts]}, S, E) -> assert_no_match_or_guard_scope(Meta, "cond", S, E), assert_no_underscore_clause_in_cond(Opts, E), {EClauses, SC, EC} = elixir_clauses:'cond'(Meta, Opts, S, E), - {{'cond', Meta, [EClauses]}, SC, EC}; + #elixir_ex{version=Counter} = SC, + {{'cond', [{version, Counter} | Meta], [EClauses]}, SC#elixir_ex{version=Counter+1}, EC}; expand({'case', Meta, [Expr, Options]}, S, E) -> assert_no_match_or_guard_scope(Meta, "case", S, E), @@ -340,12 +341,14 @@ expand({'case', Meta, [Expr, Options]}, S, E) -> expand({'receive', Meta, [Opts]}, S, E) -> assert_no_match_or_guard_scope(Meta, "receive", S, E), {EClauses, SC, EC} = elixir_clauses:'receive'(Meta, Opts, S, E), - {{'receive', Meta, [EClauses]}, SC, EC}; + #elixir_ex{version=Counter} = SC, + {{'receive', [{version, Counter} | Meta], [EClauses]}, SC#elixir_ex{version=Counter+1}, EC}; expand({'try', Meta, [Opts]}, S, E) -> assert_no_match_or_guard_scope(Meta, "try", S, E), {EClauses, SC, EC} = elixir_clauses:'try'(Meta, Opts, S, E), - {{'try', Meta, [EClauses]}, SC, EC}; + #elixir_ex{version=Counter} = SC, + {{'try', [{version, Counter} | Meta], [EClauses]}, SC#elixir_ex{version=Counter+1}, EC}; %% Comprehensions @@ -857,9 +860,9 @@ expand_for({for, Meta, [_ | _] = Args}, S, E, Return) -> {error, Error} -> {file_error(Meta, E, ?MODULE, Error), EOpts} end, - {{for, Meta, ECases ++ [[{do, EExpr} | NormalizedOpts]]}, - elixir_env:merge_and_check_unused_vars(SE, S, EE), - E}. + #elixir_ex{version=Counter} = SF = elixir_env:merge_and_check_unused_vars(SE, S, EE), + {{for, [{version, Counter} | Meta], ECases ++ [[{do, EExpr} | NormalizedOpts]]}, + SF#elixir_ex{version=Counter+1}, E}. validate_for_options([{into, _} = Pair | Opts], _Into, Uniq, Reduce, Return, Meta, E, Acc) -> validate_for_options(Opts, Pair, Uniq, Reduce, Return, Meta, E, [Pair | Acc]); diff --git a/lib/elixir/src/elixir_fn.erl b/lib/elixir/src/elixir_fn.erl index 9a2a436d67f..b14bcd77677 100644 --- a/lib/elixir/src/elixir_fn.erl +++ b/lib/elixir/src/elixir_fn.erl @@ -29,7 +29,8 @@ expand(Meta, Clauses, S, E) when is_list(Clauses) -> case lists:usort(EArities) of [_] -> - {{fn, Meta, EClauses}, SE, E}; + #elixir_ex{version=Counter} = SE, + {{fn, [{version, Counter} | Meta], EClauses}, SE#elixir_ex{version=Counter+1}, E}; _ -> file_error(Meta, E, ?MODULE, clauses_with_different_arities) end. diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 423f740329f..ac7ae775619 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -404,7 +404,7 @@ defmodule Kernel.ExpansionTest do {:=, _, [var_ver(:x, 0), 0]}, {:fn, _, [{:->, _, [[var_ver(:x, 1)], {:=, _, [var_ver(:x, 2), 2]}]}]}, {:=, _, [_, var_ver(:x, 0)]}, - {:=, _, [var_ver(:x, 4), 3]} + {:=, _, [var_ver(:x, 5), 3]} ]} = expand_with_version( quote do From cbb87a5e47c2841becc8ed6b91b91950c309b925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Apr 2026 14:21:11 +0200 Subject: [PATCH 8/8] Cache results across --- lib/elixir/lib/module/types.ex | 12 +- lib/elixir/lib/module/types/expr.ex | 291 +++++++++++++++------------- lib/elixir/src/elixir_clauses.erl | 2 +- 3 files changed, 169 insertions(+), 136 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 7877926bd3d..8a6d8b220b3 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -468,15 +468,19 @@ defmodule Module.Types do end defp fresh_stack(stack, mode, function) when mode in @modes do - %{stack | mode: mode, function: function} + %{stack | mode: mode, function: function, reverse_arrow: nil} end defp fresh_context(context) do - %{context | vars: %{}, failed: false} + %{context | vars: %{}, failed: false, reverse_arrows: %{}} end - defp restore_context(later_context, %{vars: vars, failed: failed}) do - %{later_context | vars: vars, failed: failed} + defp restore_context(later_context, %{ + vars: vars, + failed: failed, + reverse_arrows: reverse_arrows + }) do + %{later_context | vars: vars, failed: failed, reverse_arrows: reverse_arrows} end ## Diagnostics diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 8442cadf6c2..4b573ced370 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -287,34 +287,36 @@ defmodule Module.Types.Expr do 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, head, stack, context) - - context = - if is_warning(stack) do - case truthiness(head_type) do - :always_true when not last? -> - warning = {:badcond, "always match", head_type, head, context} - warn(__MODULE__, warning, meta, stack, context) - - :always_false -> - warning = {:badcond, "never match", head_type, head, context} - warn(__MODULE__, warning, meta, stack, context) - - _ -> - context + def of_expr({:cond, meta, [[{:do, clauses}]]}, expected, expr, stack, original) do + cache_result(meta, stack, original, fn -> + clauses + |> reduce_non_empty({none(), original}, fn + {:->, meta, [[head], body]}, {acc, context}, last? -> + {head_type, context} = of_expr(head, @pending, head, stack, context) + + context = + if is_warning(stack) do + case truthiness(head_type) do + :always_true when not last? -> + warning = {:badcond, "always match", head_type, head, context} + warn(__MODULE__, warning, meta, stack, context) + + :always_false -> + warning = {:badcond, "never match", head_type, head, context} + warn(__MODULE__, warning, meta, stack, context) + + _ -> + context + end + else + context end - else - context - end - {body_type, context} = of_expr(body, expected, expr, stack, context) - {union(body_type, acc), Of.reset_vars(context, original)} + {body_type, context} = of_expr(body, expected, expr, stack, context) + {union(body_type, acc), Of.reset_vars(context, original)} + end) + |> dynamic_unless_static(stack) end) - |> dynamic_unless_static(stack) end def of_expr({:case, meta, [case_expr, [{:do, _clauses}]]}, expected, _expr, stack, context) @@ -383,139 +385,149 @@ defmodule Module.Types.Expr do end # fn pat -> expr end - def of_expr({:fn, _meta, clauses}, _expected, _expr, stack, context) do - [{:->, _, [head, _]} | _] = clauses - {patterns, _guards} = extract_head(head) - domain = Enum.map(patterns, fn _ -> dynamic() end) - - of_body = fn _args_types, body, context -> of_expr(body, term(), body, stack, context) end - - {acc, context} = - of_clauses_fun(clauses, domain, :fn, stack, context, of_body, [], fn - trees, body_type, context, acc -> - args_types = Pattern.of_domain(trees, stack, context) - add_inferred(acc, args_types, body_type) - end) + def of_expr({:fn, meta, clauses}, _expected, _expr, stack, context) do + cache_result(meta, stack, context, fn -> + [{:->, _, [head, _]} | _] = clauses + {patterns, _guards} = extract_head(head) + domain = Enum.map(patterns, fn _ -> dynamic() end) - {fun_from_inferred_clauses(acc), context} + of_body = fn _args_types, body, context -> of_expr(body, term(), body, stack, context) end + + {acc, context} = + of_clauses_fun(clauses, domain, :fn, stack, context, of_body, [], fn + trees, body_type, context, acc -> + args_types = Pattern.of_domain(trees, stack, context) + add_inferred(acc, args_types, body_type) + end) + + {fun_from_inferred_clauses(acc), context} + end) end 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 - {type, context} = of_expr(body, @pending, body, stack, original) - info = {:try_else, meta, body, type} - of_clauses(else_block, [type], expected, info, stack, context, none()) - else - of_expr(body, expected, expr, stack, original) - end + cache_result(meta, stack, original, fn -> + {after_block, blocks} = Keyword.pop(blocks, :after) + {else_block, blocks} = Keyword.pop(blocks, :else) + + {type, context} = + if else_block do + {type, context} = of_expr(body, @pending, body, stack, original) + info = {:try_else, meta, body, type} + of_clauses(else_block, [type], expected, info, stack, context, none()) + else + of_expr(body, expected, expr, stack, original) + end - {type, context} = - blocks - |> Enum.reduce({type, Of.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, :rescue, meta, stack, context) + {type, context} = + blocks + |> Enum.reduce({type, Of.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, :rescue, meta, stack, context) - {union(type, acc), context} + {union(type, acc), context} - {:->, meta, [[var], body]}, {acc, context} -> - {type, context} = - of_rescue(var, [], body, var, :anonymous_rescue, meta, stack, context) + {:->, meta, [[var], body]}, {acc, context} -> + {type, context} = + of_rescue(var, [], body, var, :anonymous_rescue, meta, stack, context) - {union(type, acc), context} - end) + {union(type, acc), context} + end) - {:catch, clauses}, {acc, context} -> - args = [@try_catch, dynamic()] - of_clauses(clauses, args, expected, :try_catch, stack, context, acc) - end) - |> dynamic_unless_static(stack) + {:catch, clauses}, {acc, context} -> + args = [@try_catch, dynamic()] + of_clauses(clauses, args, expected, :try_catch, stack, context, acc) + end) + |> dynamic_unless_static(stack) - if after_block do - {_type, context} = of_expr(after_block, term(), after_block, stack, context) - {type, context} - else - {type, context} - end + if after_block do + {_type, context} = of_expr(after_block, term(), after_block, stack, context) + {type, context} + else + {type, context} + end + end) end @timeout_type union(integer(), atom([:infinity])) - def of_expr({:receive, _meta, [blocks]}, expected, expr, stack, original) do - blocks - |> Enum.reduce({none(), original}, fn - {:do, {:__block__, _, []}}, acc_context -> - acc_context + def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do + cache_result(meta, stack, original, fn -> + blocks + |> Enum.reduce({none(), original}, fn + {:do, {:__block__, _, []}}, acc_context -> + acc_context - {:do, clauses}, {acc, context} -> - of_clauses(clauses, [dynamic()], expected, :receive, stack, context, acc) + {:do, clauses}, {acc, context} -> + of_clauses(clauses, [dynamic()], expected, :receive, stack, context, acc) - {: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) + {: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) - if compatible?(timeout_type, @timeout_type) do - {union(body_type, acc), Of.reset_vars(context, original)} - else - error = {:badtimeout, timeout_type, timeout, context} - {union(body_type, acc), error(__MODULE__, error, meta, stack, context)} - end + if compatible?(timeout_type, @timeout_type) do + {union(body_type, acc), Of.reset_vars(context, original)} + else + error = {:badtimeout, timeout_type, timeout, context} + {union(body_type, acc), error(__MODULE__, error, meta, stack, context)} + end + end) + |> dynamic_unless_static(stack) end) - |> dynamic_unless_static(stack) end 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)) - - # We don't need to type check uniq, as it is a compile-time boolean. - # 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) - # 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. - args = [dynamic()] - of_clauses(block, args, expected, :for_reduce, stack, context, reduce_type) - else - # TODO: Use the collectable protocol for the output - # TODO: Use the expected type for the block output - into = Keyword.get(opts, :into, []) - {into_type, into_kind, context} = for_into(into, meta, stack, context) - {block_type, context} = of_expr(block, @pending, block, stack, context) - - case into_kind do - :bitstring -> - case compatible_intersection(block_type, bitstring()) do - {:ok, intersection} -> - {return_union(into_type, intersection, stack), context} - - {:error, _} -> - error = {:badbitbody, block_type, block, context} - {error_type(), error(__MODULE__, error, meta, stack, context)} - end + cache_result(meta, stack, context, fn -> + {clauses, [[{:do, block} | opts]]} = Enum.split(args, -1) + context = Enum.reduce(clauses, context, &for_clause(&1, stack, &2)) + + # We don't need to type check uniq, as it is a compile-time boolean. + # 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) + # 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. + args = [dynamic()] + of_clauses(block, args, expected, :for_reduce, stack, context, reduce_type) + else + # TODO: Use the collectable protocol for the output + # TODO: Use the expected type for the block output + into = Keyword.get(opts, :into, []) + {into_type, into_kind, context} = for_into(into, meta, stack, context) + {block_type, context} = of_expr(block, @pending, block, stack, context) + + case into_kind do + :bitstring -> + case compatible_intersection(block_type, bitstring()) do + {:ok, intersection} -> + {return_union(into_type, intersection, stack), context} + + {:error, _} -> + error = {:badbitbody, block_type, block, context} + {error_type(), error(__MODULE__, error, meta, stack, context)} + end - :non_empty_list -> - {return_union(into_type, non_empty_list(block_type), stack), context} + :non_empty_list -> + {return_union(into_type, non_empty_list(block_type), stack), context} - :none -> - {into_type, context} + :none -> + {into_type, context} + end end - end + end) end # TODO: with pat <- expr do expr end - 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)) - {dynamic(), context} + def of_expr({:with, meta, [_ | _] = clauses}, _expected, _expr, stack, original) do + cache_result(meta, stack, original, fn -> + {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)) + {dynamic(), context} + end) end def of_expr({{:., _, [fun]}, _, args} = call, _expected, _expr, stack, context) do @@ -775,6 +787,23 @@ 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 cache_result(meta, %{reverse_arrow: reverse_arrow}, context, fun) do + case reverse_arrow do + nil -> + fun.() + + :cache -> + {result, context} = fun.() + version = Keyword.fetch!(meta, :version) + context = put_in(context.reverse_arrows[version], result) + {result, context} + + :use -> + version = Keyword.fetch!(meta, :version) + {Map.fetch!(context.reverse_arrows, version), context} + end + end + defp cache_arrows(_meta, %{reverse_arrow: nil}, _fun), do: nil defp cache_arrows(meta, %{reverse_arrow: :cache}, fun) do diff --git a/lib/elixir/src/elixir_clauses.erl b/lib/elixir/src/elixir_clauses.erl index 20400728346..aa774f8145c 100644 --- a/lib/elixir/src/elixir_clauses.erl +++ b/lib/elixir/src/elixir_clauses.erl @@ -322,7 +322,7 @@ with(Meta, Args, S, E) -> ok end, - #elixir_ex{version=Counter} = S3, + #elixir_ex{version=Counter} = S3, {{with, [{version, Counter} | Meta], EExprs ++ [[{do, EDo} | EOpts]]}, S3#elixir_ex{version=Counter+1}, E}.