From d50147a4cd22ae7e9c7bc5915e975265f56bd30c Mon Sep 17 00:00:00 2001 From: Guillaume Duboc <27832828+gldubc@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:58:26 +0200 Subject: [PATCH 1/6] Hash-cons BDD descriptors --- lib/elixir/lib/module/types/descr.ex | 352 +++++++++++------- lib/elixir/src/elixir_erl.erl | 2 +- .../test/elixir/module/types/descr_test.exs | 72 +++- .../test/elixir/module/types/expr_test.exs | 2 +- .../elixir/module/types/integration_test.exs | 2 +- .../elixir/protocol/consolidation_test.exs | 2 +- 6 files changed, 291 insertions(+), 141 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 3f786dc7796..92c14510518 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -35,8 +35,6 @@ defmodule Module.Types.Descr do @bit_bitstring @bit_binary ||| @bit_bitstring_no_binary @bit_number @bit_integer ||| @bit_float - defmacro bdd_leaf(arg1, arg2), do: {arg1, arg2} - # Map fields and domains are stored as orddicts (sorted key-value lists). @fields_new [] defguardp is_fields_empty(fields) when fields == [] @@ -50,10 +48,16 @@ defmodule Module.Types.Descr do # Remark: those are explicit BDD constructors. The functional constructors are `bdd_new/1` and `bdd_new/3`. @fun_top {:negation, %{}} @atom_top {:negation, :sets.new(version: 2)} - @map_top {:open, @fields_new} - @non_empty_list_top {:term, :term} - @tuple_top {:open, []} - @map_empty {:closed, @fields_new} + @map_top {:erlang.phash2({:open, @fields_new}), :open, @fields_new} + @non_empty_list_top {:erlang.phash2({:term, :term}), :term, :term} + @tuple_top {:erlang.phash2({:open, []}), :open, []} + @map_empty {:erlang.phash2({:closed, @fields_new}), :closed, @fields_new} + + defmacrop bdd_leaf(arg1, arg2) do + quote do + {_, unquote(arg1), unquote(arg2)} + end + end # The top BDD for each arity. @fun_bdd_top :bdd_top @@ -839,10 +843,10 @@ defmodule Module.Types.Descr do defp print_as_negated_bdd(bdd_leaf(_, _), _top), do: 0 defp print_as_negated_bdd(bdd, top), do: if(negated_bdd?(bdd, top), do: 1, else: -100) - defp negated_bdd?({top, bdd, :bdd_bot, :bdd_bot}, top), + defp negated_bdd?({_, top, bdd, :bdd_bot, :bdd_bot}, top), do: negated_bdd?(bdd, top) - defp negated_bdd?({_, :bdd_bot, :bdd_bot, bdd}, top), + defp negated_bdd?({_, _, :bdd_bot, :bdd_bot, bdd}, top), do: bdd in [:bdd_top, top] or negated_bdd?(bdd, top) defp negated_bdd?(_, _), do: false @@ -1320,7 +1324,7 @@ defmodule Module.Types.Descr do # Note: Function domains are expressed as tuple types. We use separate representations # rather than unary functions with tuple domains to handle special cases like representing # functions of a specific arity (e.g., (none,none->term) for arity 2). - defp fun_new(arity, inputs, output), do: {:union, %{arity => bdd_leaf(inputs, output)}} + defp fun_new(arity, inputs, output), do: {:union, %{arity => bdd_leaf_new(inputs, output)}} # Creates a function type from a list of inputs and an output # where the inputs and/or output may be dynamic. @@ -1647,7 +1651,7 @@ defmodule Module.Types.Descr do defp apply_disjoint(input_arguments, arrows) do type_input = args_to_domain(input_arguments) - Enum.reduce(arrows, none(), fn {args, ret}, acc_return -> + Enum.reduce(arrows, none(), fn bdd_leaf(args, ret), acc_return -> if empty?(intersection(args_to_domain(args), type_input)), do: acc_return, else: union(acc_return, ret) @@ -1682,7 +1686,7 @@ defmodule Module.Types.Descr do if subtype?(rets_reached, result), do: result, else: union(result, rets_reached) end - defp aux_apply(result, input, returns_reached, [{args, ret} | arrow_intersections]) do + defp aux_apply(result, input, returns_reached, [bdd_leaf(args, ret) | arrow_intersections]) do # Calculate the part of the input not covered by this arrow's domain dom_subtract = difference(input, args_to_domain(args)) @@ -1743,7 +1747,7 @@ defmodule Module.Types.Descr do # * (none() -> atom()) # * (integer() -> term()) # * (integer() -> atom()) - Enum.any?(negatives, fn {neg_arguments, neg_return} -> + Enum.any?(negatives, fn bdd_leaf(neg_arguments, neg_return) -> # Check if the negative function's domain is a supertype of the positive # domain and if the phi function determines emptiness. subtype?(args_to_domain(neg_arguments), fetch_domain(positives)) and @@ -1753,7 +1757,9 @@ defmodule Module.Types.Descr do # Returns the union of all domains of the arrows in the intersection of positives. defp fetch_domain(positives) do - Enum.reduce(positives, none(), fn {args, _}, acc -> union(acc, args_to_domain(args)) end) + Enum.reduce(positives, none(), fn bdd_leaf(args, _), acc -> + union(acc, args_to_domain(args)) + end) end # Implements the Φ (phi) function for determining function subtyping relationships. @@ -1796,9 +1802,9 @@ defmodule Module.Types.Descr do {result, Map.put(cache, {args, {b, t}, []}, result)} end - defp phi(args, {b, ret}, [{arguments, return} | rest_positive], cache) do + defp phi(args, {b, ret}, [bdd_leaf(arguments, return) = positive | rest_positive], cache) do # Create cache key from function arguments - cache_key = {args, {b, ret}, [{arguments, return} | rest_positive]} + cache_key = {args, {b, ret}, [positive | rest_positive]} case cache do %{^cache_key => value} -> @@ -1833,9 +1839,12 @@ defmodule Module.Types.Descr do end end - defp disjoint_non_empty_domains?({arguments, return}, positives) do + defp disjoint_non_empty_domains?({arguments, _return}, positives) do b1 = all_disjoint_arguments?(positives) - b2 = all_non_empty_arguments?([{arguments, return} | positives]) + + b2 = + Enum.all?(arguments, fn arg -> not empty?(arg) end) and + all_non_empty_arguments?(positives) cond do b1 and b2 -> :disjoint_non_empty @@ -1845,7 +1854,7 @@ defmodule Module.Types.Descr do end defp all_non_empty_arguments?(positives) do - Enum.all?(positives, fn {args, _ret} -> + Enum.all?(positives, fn bdd_leaf(args, _ret) -> Enum.all?(args, fn arg -> not empty?(arg) end) end) end @@ -1857,8 +1866,8 @@ defmodule Module.Types.Descr do defp all_disjoint_arguments?([]), do: true - defp all_disjoint_arguments?([{args, _} | rest]) do - Enum.all?(rest, fn {args_rest, _} -> disjoint_arguments?(args, args_rest) end) and + defp all_disjoint_arguments?([bdd_leaf(args, _) | rest]) do + Enum.all?(rest, fn bdd_leaf(args_rest, _) -> disjoint_arguments?(args, args_rest) end) and all_disjoint_arguments?(rest) end @@ -1997,23 +2006,27 @@ defmodule Module.Types.Descr do end end - defp fun_denormalize_intersections([{static_args, static_return} | statics], dynamics, acc) do + defp fun_denormalize_intersections( + [bdd_leaf(static_args, static_return) | statics], + dynamics, + acc + ) do dynamics - |> Enum.split_while(fn {dynamic_args, dynamic_return} -> + |> Enum.split_while(fn bdd_leaf(dynamic_args, dynamic_return) -> not arrow_subtype?(static_args, static_return, dynamic_args, dynamic_return) end) |> case do {_dynamics, []} -> :error - {pre, [{dynamic_args, dynamic_return} | post]} -> + {pre, [bdd_leaf(dynamic_args, dynamic_return) | post]} -> args = Enum.zip_with(static_args, dynamic_args, fn static_arg, dynamic_arg -> union(dynamic(static_arg), dynamic_arg) end) return = union(dynamic(dynamic_return), static_return) - fun_denormalize_intersections(statics, pre ++ post, [{args, return} | acc]) + fun_denormalize_intersections(statics, pre ++ post, [bdd_leaf_new(args, return) | acc]) end end @@ -2060,7 +2073,7 @@ defmodule Module.Types.Descr do defp fun_bdd_to_pos_dnf(arity, :bdd_top) do args = List.duplicate(none(), arity) ret = term() - [[{args, ret}]] + [[bdd_leaf_new(args, ret)]] end defp fun_bdd_to_pos_dnf(_arity, bdd) do @@ -2071,10 +2084,10 @@ defmodule Module.Types.Descr do defp fun_eliminate_unions([], acc), do: acc - defp fun_eliminate_unions([[{args, return}] | tail], acc) do + defp fun_eliminate_unions([[bdd_leaf(args, return) = current] | tail], acc) do # If another arrow is a superset of the current one, we skip it superset = fn - [{other_args, other_return}] -> + [bdd_leaf(other_args, other_return)] -> arrow_subtype?(args, return, other_args, other_return) _ -> @@ -2084,7 +2097,7 @@ defmodule Module.Types.Descr do if Enum.any?(tail, superset) or Enum.any?(acc, superset) do fun_eliminate_unions(tail, acc) else - fun_eliminate_unions(tail, [[{args, return}] | acc]) + fun_eliminate_unions(tail, [[current] | acc]) end end @@ -2104,7 +2117,7 @@ defmodule Module.Types.Descr do defp fun_intersection_to_quoted(intersection, opts) do intersection |> Enum.sort() - |> Enum.map(fn {args, ret} -> + |> Enum.map(fn bdd_leaf(args, ret) -> {:__block__, [], [[{:->, [], [Enum.map(args, &to_quoted(&1, opts)), to_quoted(ret, opts)]}]]} end) @@ -2172,11 +2185,12 @@ defmodule Module.Types.Descr do end end - defp list_new(list_type, last_type), do: bdd_leaf(list_type, last_type) + defp list_new(list_type, last_type), do: bdd_leaf_new(list_type, last_type) defp non_empty_list_literals_intersection(list_literals) do try do - Enum.reduce(list_literals, {:term, :term}, fn {next_list, next_last}, {list, last} -> + Enum.reduce(list_literals, {:term, :term}, fn bdd_leaf(next_list, next_last), + {list, last} -> {non_empty_intersection!(list, next_list), non_empty_intersection!(last, next_last)} end) catch @@ -2200,12 +2214,13 @@ defmodule Module.Types.Descr do acc {list, last} -> - Enum.reduce_while(negs, {last, []}, fn {neg_type, neg_last}, {acc_last, acc_negs} -> + Enum.reduce_while(negs, {last, []}, fn bdd_leaf(neg_type, neg_last) = neg, + {acc_last, acc_negs} -> if subtype?(list, neg_type) do difference = difference(acc_last, neg_last) if empty?(difference), do: {:halt, nil}, else: {:cont, {difference, acc_negs}} else - {:cont, {acc_last, [{neg_type, neg_last} | acc_negs]}} + {:cont, {acc_last, [neg | acc_negs]}} end end) |> case do @@ -2354,7 +2369,7 @@ defmodule Module.Types.Descr do try do list = non_empty_intersection!(list1, list2) last = non_empty_intersection!(last1, last2) - bdd_leaf(list, last) + bdd_leaf_new(list, last) catch :empty -> :bdd_bot end @@ -2379,7 +2394,7 @@ defmodule Module.Types.Descr do if subtype?(list1, list2) do if subtype?(last1, last2), do: :bdd_bot, - else: bdd_leaf(list1, difference(last1, last2)) + else: bdd_leaf_new(list1, difference(last1, last2)) else bdd_difference(bdd1, bdd2, &list_leaf_difference/3) end @@ -2416,7 +2431,7 @@ defmodule Module.Types.Descr do # a. either completely covers the type, if its last type is a supertype of the positive one, # b. or it removes part of the last type. empty?(list_type) or empty?(last_type) or - Enum.reduce_while(negs, last_type, fn {neg_type, neg_last}, acc_last_type -> + Enum.reduce_while(negs, last_type, fn bdd_leaf(neg_type, neg_last), acc_last_type -> if subtype?(list_type, neg_type) do d = difference(acc_last_type, neg_last) if empty?(d), do: {:halt, nil}, else: {:cont, d} @@ -2545,7 +2560,7 @@ defmodule Module.Types.Descr do [{name, [], arguments} | acc] else negs - |> non_empty_map_or(fn {ty, lst} -> + |> non_empty_map_or(fn bdd_leaf(ty, lst) -> args = if subtype?(lst, @empty_list) do [to_quoted(ty, opts)] @@ -2582,7 +2597,7 @@ defmodule Module.Types.Descr do # Prune negations from those with empty intersections. negs = Enum.uniq(negs) - |> Enum.filter(fn {nlist, nlast} -> + |> Enum.filter(fn bdd_leaf(nlist, nlast) -> not empty?(intersection(list, nlist)) and not empty?(intersection(last, nlast)) end) @@ -2604,7 +2619,7 @@ defmodule Module.Types.Descr do # Case 3: when a list with negations is united with one of its negations defp add_to_list_normalize([{t, l, n} = cur | rest], list, last, []) do - case pop_elem(n, {list, last}, []) do + case pop_elem(n, bdd_leaf_new(list, last), []) do {true, n1} -> [{t, l, n1} | rest] {false, _} -> [cur | add_to_list_normalize(rest, list, last, n)] end @@ -2859,7 +2874,7 @@ defmodule Module.Types.Descr do defguardp is_optional_static(map) when is_map(map) and is_map_key(map, :optional) - defp map_new(tag, fields), do: bdd_leaf(tag, fields) + defp map_new(tag, fields), do: bdd_leaf_new(tag, fields) defp map_only?(descr), do: empty?(Map.delete(descr, :map)) @@ -2878,8 +2893,8 @@ defmodule Module.Types.Descr do defp map_union(bdd_leaf(tag1, fields1), bdd_leaf(tag2, fields2)) do case maybe_optimize_map_union(tag1, fields1, tag2, fields2) do - {tag, fields} -> bdd_leaf(tag, fields) - nil -> bdd_union(bdd_leaf(tag1, fields1), bdd_leaf(tag2, fields2)) + {tag, fields} -> bdd_leaf_new(tag, fields) + nil -> bdd_union(bdd_leaf_new(tag1, fields1), bdd_leaf_new(tag2, fields2)) end end @@ -3044,7 +3059,7 @@ defmodule Module.Types.Descr do defp map_leaf_intersection(bdd_leaf(tag1, fields1), bdd_leaf(tag2, fields2)) do try do {tag, fields} = map_literal_intersection(tag1, fields1, tag2, fields2) - bdd_leaf(tag, fields) + bdd_leaf_new(tag, fields) catch :empty -> :bdd_bot end @@ -3053,7 +3068,7 @@ defmodule Module.Types.Descr do defp map_difference(_, bdd_leaf(:open, [])), do: :bdd_bot - defp map_difference(bdd_leaf(:open, []), {_, _, _, _} = bdd2), + defp map_difference(bdd_leaf(:open, []), {_, _, _, _, _} = bdd2), do: bdd_negation(bdd2) defp map_difference(bdd1, bdd2), @@ -3110,7 +3125,7 @@ defmodule Module.Types.Descr do if empty?(v_diff) do :subtype else - a_diff = bdd_leaf(tag, fields_store(key, v_diff, fields)) + a_diff = bdd_leaf_new(tag, fields_store(key, v_diff, fields)) a_type = case type do @@ -3118,14 +3133,14 @@ defmodule Module.Types.Descr do :bdd_bot :union -> - bdd_leaf(tag, fields_store(key, union(v1, v2), fields)) + bdd_leaf_new(tag, fields_store(key, union(v1, v2), fields)) :intersection -> v_int = intersection(v1, v2) if empty?(v_int), do: :bdd_bot, - else: bdd_leaf(tag, fields_store(key, v_int, fields)) + else: bdd_leaf_new(tag, fields_store(key, v_int, fields)) end {:one_key_difference, a_diff, a_type} @@ -3405,10 +3420,10 @@ defmodule Module.Types.Descr do # {t, s} \ {t₁, s₁} = {t \ t₁, s} ∪ {t ∩ t₁, s \ s₁} defp map_split_negative(negs, value, bdd, take_fun) do Enum.reduce(negs, [{value, bdd}], fn - {:open, empty}, _acc when is_fields_empty(empty) -> + bdd_leaf(:open, empty), _acc when is_fields_empty(empty) -> throw(:empty) - {neg_tag, neg_fields}, acc -> + bdd_leaf(neg_tag, neg_fields), acc -> {found?, neg_value, neg_bdd} = take_fun.(neg_tag, neg_fields) if not found? and neg_tag == :open do @@ -3528,7 +3543,7 @@ defmodule Module.Types.Descr do defp has_empty_map?(dnf) do Enum.any?(dnf, fn {_, fields, negs} -> Enum.all?(fields_to_list(fields), fn {_key, value} -> is_optional_static(value) end) and - Enum.all?(negs, fn {_, fields} -> + Enum.all?(negs, fn bdd_leaf(_, fields) -> not Enum.all?(fields_to_list(fields), fn {_key, value} -> is_optional_static(value) end) end) end) @@ -4256,7 +4271,7 @@ defmodule Module.Types.Descr do defp non_empty_map_literals_intersection(maps) do try do - Enum.reduce(maps, {:open, @fields_new}, fn {next_tag, next_fields}, {tag, fields} -> + Enum.reduce(maps, {:open, []}, fn bdd_leaf(next_tag, next_fields), {tag, fields} -> map_literal_intersection(tag, fields, next_tag, next_fields) end) catch @@ -4292,10 +4307,14 @@ defmodule Module.Types.Descr do # an intersection or difference is computed, its emptiness is checked again. # So they are all necessarily non-empty. defp map_line_empty?(_, _pos, []), do: false - defp map_line_empty?(_, _, [{:open, neg_fields} | _]) when is_fields_empty(neg_fields), do: true - defp map_line_empty?(:open, fs, [{:closed, _} | negs]), do: map_line_empty?(:open, fs, negs) - defp map_line_empty?(tag, fields, [{neg_tag, neg_fields} | negs]) do + defp map_line_empty?(_, _, [bdd_leaf(:open, neg_fields) | _]) when is_fields_empty(neg_fields), + do: true + + defp map_line_empty?(:open, fs, [bdd_leaf(:closed, _) | negs]), + do: map_line_empty?(:open, fs, negs) + + defp map_line_empty?(tag, fields, [bdd_leaf(neg_tag, neg_fields) | negs]) do if map_check_domain_keys?(tag, neg_tag) do if tag == :closed or neg_tag == :open do # This implements the same map line check as tuples @@ -4478,7 +4497,8 @@ defmodule Module.Types.Descr do end defp maybe_eliminate_map_negations(tag, fields, negs) do - Enum.reduce(negs, {fields, []}, fn neg = {neg_tag, neg_fields}, {acc_fields, acc_negs} -> + Enum.reduce(negs, {fields, []}, fn neg = bdd_leaf(neg_tag, neg_fields), + {acc_fields, acc_negs} -> # If the intersection with the negative is empty, we can remove the negative. empty_intersection? = try do @@ -4686,6 +4706,10 @@ defmodule Module.Types.Descr do {:map, [], []} end + defp map_literal_to_quoted({hash, tag, fields}, opts) when is_integer(hash) do + map_literal_to_quoted({tag, fields}, opts) + end + defp map_literal_to_quoted({domains, empty}, _opts) when is_fields_empty(domains) and is_fields_empty(empty) do {:empty_map, [], []} @@ -4915,7 +4939,7 @@ defmodule Module.Types.Descr do {acc, dynamic?} end - defp tuple_new(tag, elements), do: bdd_leaf(tag, elements) + defp tuple_new(tag, elements), do: bdd_leaf_new(tag, elements) defp tuple_intersection(bdd_leaf(:open, []), bdd), do: bdd defp tuple_intersection(bdd, bdd_leaf(:open, [])), do: bdd @@ -4926,7 +4950,7 @@ defmodule Module.Types.Descr do defp tuple_leaf_intersection(bdd_leaf(tag1, elements1), bdd_leaf(tag2, elements2)) do case tuple_literal_intersection(tag1, elements1, tag2, elements2) do - {tag, elements} -> bdd_leaf(tag, elements) + {tag, elements} -> bdd_leaf_new(tag, elements) :empty -> :bdd_bot end end @@ -4978,7 +5002,7 @@ defmodule Module.Types.Descr do defp tuple_difference(_, bdd_leaf(:open, [])), do: :bdd_bot - defp tuple_difference(bdd_leaf(:open, []), {_, _, _, _} = bdd2), + defp tuple_difference(bdd_leaf(:open, []), {_, _, _, _, _} = bdd2), do: bdd_negation(bdd2) defp tuple_difference(bdd1, bdd2), @@ -5005,7 +5029,7 @@ defmodule Module.Types.Descr do defp non_empty_tuple_literals_intersection(tuples) do try do - Enum.reduce(tuples, {:open, []}, fn {next_tag, next_elements}, {tag, elements} -> + Enum.reduce(tuples, {:open, []}, fn bdd_leaf(next_tag, next_elements), {tag, elements} -> case tuple_literal_intersection(tag, elements, next_tag, next_elements) do :empty -> throw(:empty) next -> next @@ -5032,11 +5056,11 @@ defmodule Module.Types.Descr do # Otherwise, tuple_empty?(_, elements, []) would be Enum.any?(elements, &empty?/1) defp tuple_line_empty?(_, _, []), do: false # Open empty negation makes it empty - defp tuple_line_empty?(_, _, [{:open, []} | _]), do: true + defp tuple_line_empty?(_, _, [bdd_leaf(:open, []) | _]), do: true # Open positive can't be emptied by a single closed negative - defp tuple_line_empty?(:open, _pos, [{:closed, _}]), do: false + defp tuple_line_empty?(:open, _pos, [bdd_leaf(:closed, _)]), do: false - defp tuple_line_empty?(tag, elements, [{neg_tag, neg_elements} | negs]) do + defp tuple_line_empty?(tag, elements, [bdd_leaf(neg_tag, neg_elements) | negs]) do n = length(elements) m = length(neg_elements) @@ -5078,7 +5102,7 @@ defmodule Module.Types.Descr do end defp tuple_eliminate_negations(tag, elements, negs) do - Enum.reduce(negs, [{tag, elements}], fn {neg_tag, neg_elements}, acc -> + Enum.reduce(negs, [{tag, elements}], fn bdd_leaf(neg_tag, neg_elements), acc -> Enum.flat_map(acc, fn {tag, elements} -> tuple_eliminate_single_negation(tag, elements, {neg_tag, neg_elements}) end) @@ -5188,7 +5212,7 @@ defmodule Module.Types.Descr do bdd_leaf(tag2, elements2) = tuple2 ) do case maybe_optimize_tuple_union({tag1, elements1}, {tag2, elements2}) do - {tag, elements} -> bdd_leaf(tag, elements) + {tag, elements} -> bdd_leaf_new(tag, elements) nil -> bdd_union(tuple1, tuple2) end end @@ -5354,7 +5378,7 @@ defmodule Module.Types.Descr do defp tuple_literal_to_quoted({tag, elements, negs}, opts) do pos = tuple_fields_to_quoted(tag, elements, opts) - Enum.reduce(negs, pos, fn {tag, elements}, acc -> + Enum.reduce(negs, pos, fn bdd_leaf(tag, elements), acc -> {:and, [], [acc, {:not, [], [tuple_fields_to_quoted(tag, elements, opts)]}]} end) end @@ -5496,10 +5520,10 @@ defmodule Module.Types.Descr do # {t, s} \ {t₁, s₁} = {t \ t₁, s} ∪ {t ∩ t₁, s \ s₁} defp tuple_split_negative(negs, index, value, bdd) do Enum.reduce(negs, [{value, bdd}], fn - {:open, []}, _acc -> + bdd_leaf(:open, []), _acc -> throw(:empty) - {neg_tag, neg_elements}, acc -> + bdd_leaf(neg_tag, neg_elements), acc -> {found?, neg_value, neg_bdd} = tuple_take_element(neg_elements, index, neg_tag) if not found? and neg_tag == :open do @@ -5746,6 +5770,26 @@ defmodule Module.Types.Descr do ## BDD helpers + defp bdd_leaf_new(arg1, arg2), do: {:erlang.phash2({arg1, arg2}), arg1, arg2} + + defp bdd_node_new(lit, c, u, d), + do: {bdd_compute_hash(lit, c, u, d), lit, c, u, d} + + defp bdd_compute_hash(lit, c, u, d), + do: :erlang.phash2({bdd_hash(lit), bdd_hash(c), bdd_hash(u), bdd_hash(d)}) + + defp bdd_leaf_value(bdd_leaf(arg1, arg2)), do: {arg1, arg2} + + @bdd_bot_hash :erlang.phash2(:bdd_bot) + @bdd_top_hash :erlang.phash2(:bdd_top) + + defp bdd_hash(:bdd_bot), do: @bdd_bot_hash + defp bdd_hash(:bdd_top), do: @bdd_top_hash + defp bdd_hash({hash, _, _}), do: hash + defp bdd_hash({hash, _, _, _, _}), do: hash + + def bdd_union(bdd, bdd), do: bdd + def bdd_union(bdd1, bdd2) do case {bdd1, bdd2} do {:bdd_top, _bdd} -> @@ -5762,26 +5806,26 @@ defmodule Module.Types.Descr do _ -> case bdd_compare(bdd1, bdd2) do - {:lt, {lit1, c1, u1, d1}, bdd2} -> - {lit1, c1, bdd_union(u1, bdd2), d1} + {:lt, {_, lit1, c1, u1, d1}, bdd2} -> + bdd_node_new(lit1, c1, bdd_union(u1, bdd2), d1) - {:gt, bdd1, {lit2, c2, u2, d2}} -> - {lit2, c2, bdd_union(bdd1, u2), d2} + {:gt, bdd1, {_, lit2, c2, u2, d2}} -> + bdd_node_new(lit2, c2, bdd_union(bdd1, u2), d2) - {:eq, {lit, c1, u1, d1}, {_, c2, u2, d2}} -> - {lit, bdd_union(c1, c2), bdd_union(u1, u2), bdd_union(d1, d2)} + {:eq, {_, lit, c1, u1, d1}, {_, _, c2, u2, d2}} -> + bdd_node_new(lit, bdd_union(c1, c2), bdd_union(u1, u2), bdd_union(d1, d2)) - {:eq, {lit, _, u1, d1}, _} -> - {lit, :bdd_top, u1, d1} + {:eq, {_, lit, _, u1, d1}, _} -> + bdd_node_new(lit, :bdd_top, u1, d1) - {:eq, _, {lit, _, u2, d2}} -> - {lit, :bdd_top, u2, d2} + {:eq, _, {_, lit, _, u2, d2}} -> + bdd_node_new(lit, :bdd_top, u2, d2) {:eq, _, _} -> bdd1 end |> case do - {_, :bdd_top, _, :bdd_top} -> :bdd_top + {_, _, bdd, u, bdd} -> bdd_union(bdd, u) other -> other end end @@ -5803,19 +5847,30 @@ defmodule Module.Types.Descr do _ -> case bdd_compare(bdd1, bdd2) do - {:lt, {lit1, c1, u1, d1}, bdd2} -> - {lit1, bdd_difference(c1, bdd2), bdd_difference(u1, bdd2), bdd_difference(d1, bdd2)} + {:lt, {_, lit1, c1, u1, d1}, bdd2} -> + bdd_node_new( + lit1, + bdd_difference(c1, bdd2), + bdd_difference(u1, bdd2), + bdd_difference(d1, bdd2) + ) - {:gt, bdd1, {lit2, c2, u2, d2}} -> + {:gt, bdd1, {_, lit2, c2, u2, d2}} -> # The proper formula is: # # b1 and not (c2 or u2) : bdd_bot : b1 and not (d2 or u2) # # Both extremes have (b1 and not u2), so we compute it once. bdd1_minus_u2 = bdd_difference(bdd1, u2) - {lit2, bdd_difference(bdd1_minus_u2, c2), :bdd_bot, bdd_difference(bdd1_minus_u2, d2)} - {:eq, {lit, c1, u1, d1}, {_, c2, u2, d2}} -> + bdd_node_new( + lit2, + bdd_difference(bdd1_minus_u2, c2), + :bdd_bot, + bdd_difference(bdd1_minus_u2, d2) + ) + + {:eq, {_, lit, c1, u1, d1}, {_, _, c2, u2, d2}} -> # The formula is: # {a1, (C1 or U1) and not (C2 or U2), :bdd_bot, (D1 or U1) and not (D2 or U2)} when a1 == a2 # @@ -5827,7 +5882,13 @@ defmodule Module.Types.Descr do # Constrained = (C1 and not C2 and not U2) # Dual = (D1 and not D2 and not U2) # Hence: - {lit, bdd_difference_union(c1, c2, u2), :bdd_bot, bdd_difference_union(d1, d2, u2)} + + bdd_node_new( + lit, + bdd_difference_union(c1, c2, u2), + :bdd_bot, + bdd_difference_union(d1, d2, u2) + ) else c = if c2 == :bdd_top, @@ -5839,20 +5900,20 @@ defmodule Module.Types.Descr do do: :bdd_bot, else: bdd_difference(bdd_union(d1, u1), bdd_union(d2, u2)) - {lit, c, :bdd_bot, d} + bdd_node_new(lit, c, :bdd_bot, d) end - {:eq, _, {lit, c2, u2, _d2}} -> - {lit, bdd_negation_union(c2, u2), :bdd_bot, :bdd_bot} + {:eq, _, {_, lit, c2, u2, _d2}} -> + bdd_node_new(lit, bdd_negation_union(c2, u2), :bdd_bot, :bdd_bot) - {:eq, {lit, _c1, u1, d1}, _} -> - {lit, :bdd_bot, :bdd_bot, bdd_union(d1, u1)} + {:eq, {_, lit, _c1, u1, d1}, _} -> + bdd_node_new(lit, :bdd_bot, :bdd_bot, bdd_union(d1, u1)) {:eq, _, _} -> :bdd_bot end |> case do - {_, :bdd_bot, u, :bdd_bot} -> u + {_, _, bdd, u, bdd} -> bdd_union(bdd, u) other -> other end end @@ -5884,13 +5945,15 @@ defmodule Module.Types.Descr do # We could use bdd_expand but there was a bug in earlier versions # of the Erlang compiler which would emit bad ,code, so we match one by one. - defp bdd_difference({_, _, _, _} = bdd1, bdd_leaf(_, _) = a2, leaf_compare), - do: bdd_difference(bdd1, {a2, :bdd_top, :bdd_bot, :bdd_bot}, bdd1, a2, leaf_compare) + defp bdd_difference({_, _, _, _, _} = bdd1, bdd_leaf(_, _) = a2, leaf_compare), + do: + bdd_difference(bdd1, bdd_node_new(a2, :bdd_top, :bdd_bot, :bdd_bot), bdd1, a2, leaf_compare) - defp bdd_difference(bdd_leaf(_, _) = a1, {_, _, _, _} = bdd2, leaf_compare), - do: bdd_difference({a1, :bdd_top, :bdd_bot, :bdd_bot}, bdd2, a1, bdd2, leaf_compare) + defp bdd_difference(bdd_leaf(_, _) = a1, {_, _, _, _, _} = bdd2, leaf_compare), + do: + bdd_difference(bdd_node_new(a1, :bdd_top, :bdd_bot, :bdd_bot), bdd2, a1, bdd2, leaf_compare) - defp bdd_difference({_, _, _, _} = bdd1, {_, _, _, _} = bdd2, leaf_compare), + defp bdd_difference({_, _, _, _, _} = bdd1, {_, _, _, _, _} = bdd2, leaf_compare), do: bdd_difference(bdd1, bdd2, bdd1, bdd2, leaf_compare) defp bdd_difference(bdd1, bdd2, _leaf_compare), @@ -5924,7 +5987,13 @@ defmodule Module.Types.Descr do # # ((U1 and not a2) or (D1 and not D2)) and not U2 and not D2 # - defp bdd_difference({a1, :bdd_top, u1, :bdd_bot}, {a2, c2, u2, d2}, bdd1, bdd2, leaf_compare) do + defp bdd_difference( + {_, a1, :bdd_top, u1, :bdd_bot}, + {_, a2, c2, u2, d2}, + bdd1, + bdd2, + leaf_compare + ) do type = if c2 == :bdd_top, do: :none, else: :intersection case leaf_compare.(a1, a2, type) do @@ -5952,13 +6021,19 @@ defmodule Module.Types.Descr do end end - defp bdd_difference({a1, c1, u1, d1}, {a2, :bdd_top, u2, d2}, bdd1, bdd2, leaf_compare) do + defp bdd_difference( + {_, a1, c1, u1, d1}, + {_, a2, :bdd_top, u2, d2}, + bdd1, + bdd2, + leaf_compare + ) do type = if d1 == :bdd_bot, do: :none, else: :union case leaf_compare.(a1, a2, type) do :disjoint -> bdd_difference(u1, a2, leaf_compare) - |> bdd_union(bdd_difference({a1, c1, :bdd_bot, d1}, a2)) + |> bdd_union(bdd_difference(bdd_node_new(a1, c1, :bdd_bot, d1), a2)) |> bdd_difference(d2, leaf_compare) |> bdd_difference(u2, leaf_compare) @@ -5986,6 +6061,8 @@ defmodule Module.Types.Descr do bdd_difference(bdd1, bdd2) end + def bdd_intersection(bdd, bdd), do: bdd + def bdd_intersection(bdd1, bdd2) do case {bdd1, bdd2} do {:bdd_top, bdd} -> @@ -6002,13 +6079,21 @@ defmodule Module.Types.Descr do _ -> case bdd_compare(bdd1, bdd2) do - {:lt, {lit1, c1, u1, d1}, bdd2} -> - {lit1, bdd_intersection(c1, bdd2), bdd_intersection(u1, bdd2), - bdd_intersection(d1, bdd2)} + {:lt, {_, lit1, c1, u1, d1}, bdd2} -> + bdd_node_new( + lit1, + bdd_intersection(c1, bdd2), + bdd_intersection(u1, bdd2), + bdd_intersection(d1, bdd2) + ) - {:gt, bdd1, {lit2, c2, u2, d2}} -> - {lit2, bdd_intersection(bdd1, c2), bdd_intersection(bdd1, u2), - bdd_intersection(bdd1, d2)} + {:gt, bdd1, {_, lit2, c2, u2, d2}} -> + bdd_node_new( + lit2, + bdd_intersection(bdd1, c2), + bdd_intersection(bdd1, u2), + bdd_intersection(bdd1, d2) + ) # Notice that (a, c1, u1, d1) and (a, c2, u2, d2) is described as: # @@ -6026,21 +6111,25 @@ defmodule Module.Types.Descr do # unions in place whenever possible. This change has reduced the algorithmic # complexity in the past, but perhaps it is rendered less useful now due to # the eager literal intersections. - {:eq, {lit, c1, u1, d1}, {_, c2, u2, d2}} -> - {lit, bdd_intersection_eq(c1, c2, u1, u2), bdd_intersection(u1, u2), - bdd_intersection_eq(d1, d2, u1, u2)} + {:eq, {_, lit, c1, u1, d1}, {_, _, c2, u2, d2}} -> + bdd_node_new( + lit, + bdd_intersection_eq(c1, c2, u1, u2), + bdd_intersection(u1, u2), + bdd_intersection_eq(d1, d2, u1, u2) + ) - {:eq, {lit, c1, u1, _}, _} -> - {lit, bdd_union(c1, u1), :bdd_bot, :bdd_bot} + {:eq, {_, lit, c1, u1, _}, _} -> + bdd_node_new(lit, bdd_union(c1, u1), :bdd_bot, :bdd_bot) - {:eq, _, {lit, c2, u2, _}} -> - {lit, bdd_union(c2, u2), :bdd_bot, :bdd_bot} + {:eq, _, {_, lit, c2, u2, _}} -> + bdd_node_new(lit, bdd_union(c2, u2), :bdd_bot, :bdd_bot) {:eq, bdd, _} -> bdd end |> case do - {_, :bdd_bot, u, :bdd_bot} -> u + {_, _, bdd, u, bdd} -> bdd_union(bdd, u) other -> other end end @@ -6099,7 +6188,7 @@ defmodule Module.Types.Descr do leaf_intersection.(leaf1, leaf2) end - defp bdd_non_open_leaf_intersection(leaf, {a, :bdd_top, u, d}, leaf_intersection) do + defp bdd_non_open_leaf_intersection(leaf, {_, a, :bdd_top, u, d}, leaf_intersection) do leaf_intersection.(a, leaf) |> bdd_union(bdd_non_open_leaf_intersection(leaf, u, leaf_intersection)) |> case do @@ -6114,7 +6203,7 @@ defmodule Module.Types.Descr do end end - defp bdd_non_open_leaf_intersection(leaf, {a, :bdd_bot, u, d}, leaf_intersection) do + defp bdd_non_open_leaf_intersection(leaf, {_, a, :bdd_bot, u, d}, leaf_intersection) do case bdd_non_open_leaf_intersection(leaf, u, leaf_intersection) do result when d == :bdd_bot -> result @@ -6135,15 +6224,15 @@ defmodule Module.Types.Descr do # so its negation is ((lit and not c) or (not lit and not d)) and not u. def bdd_negation(:bdd_top), do: :bdd_bot def bdd_negation(:bdd_bot), do: :bdd_top - def bdd_negation({_, _} = pair), do: {pair, :bdd_bot, :bdd_bot, :bdd_top} + def bdd_negation(bdd_leaf(_, _) = pair), do: bdd_node_new(pair, :bdd_bot, :bdd_bot, :bdd_top) - def bdd_negation({lit, c, u, d}) do + def bdd_negation({_, lit, c, u, d}) do inner = - {lit, bdd_negation(c), :bdd_bot, bdd_negation(d)} + bdd_node_new(lit, bdd_negation(c), :bdd_bot, bdd_negation(d)) case bdd_intersection(inner, bdd_negation(u)) do # Full simplification necessary for e.g. formatter.ex compilation - {_lit, c, u, c} -> bdd_union(u, c) + {_, _lit, c, u, c} -> bdd_union(u, c) x -> x end end @@ -6153,12 +6242,12 @@ defmodule Module.Types.Descr do defp bdd_to_dnf(acc, _pos, _neg, :bdd_bot), do: acc defp bdd_to_dnf(acc, pos, neg, :bdd_top), do: [{pos, neg} | acc] - defp bdd_to_dnf(acc, pos, neg, {_, _} = lit) do + defp bdd_to_dnf(acc, pos, neg, bdd_leaf(_, _) = lit) do [{[lit | pos], neg} | acc] end # Lazy node: {lit, C, U, D} ≡ (lit ∧ C) ∪ U ∪ (¬lit ∧ D) - defp bdd_to_dnf(acc, pos, neg, {lit, c, u, d}) do + defp bdd_to_dnf(acc, pos, neg, {_, lit, c, u, d}) do # U is a bdd in itself, we accumulate its lines first bdd_to_dnf(acc, pos, neg, u) # C-part @@ -6183,11 +6272,14 @@ defmodule Module.Types.Descr do :bdd_top -> :bdd_top - {_, _} -> - fun.(bdd) + bdd_leaf(_, _) -> + {arg1, arg2} = fun.(bdd_leaf_value(bdd)) + bdd_leaf_new(arg1, arg2) - {literal, left, union, right} -> - {fun.(literal), bdd_map(left, fun), bdd_map(union, fun), bdd_map(right, fun)} + {_, literal, left, union, right} -> + {arg1, arg2} = fun.(bdd_leaf_value(literal)) + literal = bdd_leaf_new(arg1, arg2) + bdd_node_new(literal, bdd_map(left, fun), bdd_map(union, fun), bdd_map(right, fun)) end end @@ -6199,11 +6291,11 @@ defmodule Module.Types.Descr do :bdd_top -> acc - {_, _} -> - fun.(bdd, acc) + bdd_leaf(_, _) -> + fun.(bdd_leaf_value(bdd), acc) - {literal, left, union, right} -> - acc = fun.(literal, acc) + {_, literal, left, union, right} -> + acc = fun.(bdd_leaf_value(literal), acc) acc = bdd_reduce(left, acc, fun) acc = bdd_reduce(union, acc, fun) acc = bdd_reduce(right, acc, fun) @@ -6212,10 +6304,10 @@ defmodule Module.Types.Descr do end @compile {:inline, bdd_expand: 1, bdd_head: 1} - defp bdd_expand({_, _} = pair), do: {pair, :bdd_top, :bdd_bot, :bdd_bot} + defp bdd_expand(bdd_leaf(_, _) = pair), do: bdd_node_new(pair, :bdd_top, :bdd_bot, :bdd_bot) defp bdd_expand(bdd), do: bdd - defp bdd_head({lit, _, _, _}), do: lit + defp bdd_head({_, lit, _, _, _}), do: lit defp bdd_head(pair), do: pair ## Map helpers diff --git a/lib/elixir/src/elixir_erl.erl b/lib/elixir/src/elixir_erl.erl index 5251556a572..b7f401ba688 100644 --- a/lib/elixir/src/elixir_erl.erl +++ b/lib/elixir/src/elixir_erl.erl @@ -11,7 +11,7 @@ -define(typespecs, 'Elixir.Kernel.Typespec'). checker_version() -> - elixir_checker_v7. + elixir_checker_v8. %% debug_info callback diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index ee3882ae0ca..674965277e7 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -24,6 +24,64 @@ defmodule Module.Types.DescrTest do defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type)) defp map_with_default(descr), do: open_map([{to_domain_keys(:term), descr}]) + defp assert_hash_consed_bdds(descr) do + Enum.each([:fun, :list, :map, :tuple], fn key -> + case descr do + %{^key => bdd} -> + assert_hash_consed_bdd(bdd) + assert_hash_consed_bdd_dnf(bdd) + + %{} -> + :ok + end + end) + end + + defp assert_hash_consed_bdd(:bdd_bot), do: :ok + defp assert_hash_consed_bdd(:bdd_top), do: :ok + + defp assert_hash_consed_bdd({:union, by_arity}) do + Enum.each(by_arity, fn {_arity, bdd} -> assert_hash_consed_bdd(bdd) end) + end + + defp assert_hash_consed_bdd({hash, _arg1, _arg2}) when is_integer(hash), do: :ok + + defp assert_hash_consed_bdd({hash, lit, c, u, d}) when is_integer(hash) do + assert_hash_consed_bdd(lit) + assert_hash_consed_bdd(c) + assert_hash_consed_bdd(u) + assert_hash_consed_bdd(d) + end + + defp assert_hash_consed_bdd(bdd) do + flunk("expected hash-consed BDD, got: #{inspect(bdd)}") + end + + defp assert_hash_consed_bdd_dnf({:union, by_arity}) do + Enum.each(by_arity, fn {_arity, bdd} -> assert_hash_consed_bdd_dnf(bdd) end) + end + + defp assert_hash_consed_bdd_dnf(bdd) do + Enum.each(bdd_to_dnf(bdd), fn {pos, neg} -> + Enum.each(pos ++ neg, &assert_hash_consed_bdd/1) + end) + end + + describe "BDD representation" do + test "leaves and nodes carry hashes at construction" do + descrs = [ + fun([integer()], atom()), + non_empty_list(integer(), atom()), + union(non_empty_list(integer(), atom()), non_empty_list(atom(), binary())), + union(closed_map(a: integer()), closed_map(b: atom())), + difference(open_map(), closed_map(a: integer())), + union(tuple([integer()]), tuple([atom()])) + ] + + Enum.each(descrs, &assert_hash_consed_bdds/1) + end + end + describe "union" do test "bitmap" do assert union(integer(), float()) == union(float(), integer()) @@ -646,7 +704,7 @@ defmodule Module.Types.DescrTest do closed_map(__struct__: difference(atom(), atom_bar)) # Explicitly assert we keep it as cascading differences - assert %{map: {{:closed, _}, :bdd_bot, :bdd_bot, _}} = + assert %{map: {_, {_, :closed, _}, :bdd_bot, :bdd_bot, _}} = difference( difference( open_map(value: term()), @@ -3079,12 +3137,12 @@ defmodule Module.Types.DescrTest do assert fun([integer()], boolean()) |> union(fun([float()], boolean())) |> to_quoted_string() == - "(integer() -> boolean()) or (float() -> boolean())" + "(float() -> boolean()) or (integer() -> boolean())" assert fun([integer()], boolean()) |> intersection(fun([float()], boolean())) |> to_quoted_string() == - "(integer() -> boolean()) and (float() -> boolean())" + "(float() -> boolean()) and (integer() -> boolean())" # Thanks to lazy BDDs, consecutive union of functions come out as the original union assert fun([integer()], integer()) @@ -3134,14 +3192,14 @@ defmodule Module.Types.DescrTest do assert union(domain_part, codomain_part) |> to_quoted_string() == """ - (dynamic(atom()) or integer(), binary() -> float()) or - (pid(), float() -> dynamic(atom()) or integer())\ + (pid(), float() -> dynamic(atom()) or integer()) or + (dynamic(atom()) or integer(), binary() -> float())\ """ assert intersection(domain_part, codomain_part) |> to_quoted_string() == """ - (dynamic(atom()) or integer(), binary() -> float()) and - (pid(), float() -> dynamic(atom()) or integer())\ + (pid(), float() -> dynamic(atom()) or integer()) and + (dynamic(atom()) or integer(), binary() -> float())\ """ end diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index c0a5483a26e..47864485194 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -290,7 +290,7 @@ defmodule Module.Types.ExprTest do but function has type: - (binary() -> integer()) or (non_empty_list(integer()) -> integer()) + (non_empty_list(integer()) -> integer()) or (binary() -> integer()) hint: the function has an empty domain and therefore cannot be applied to any argument. \ This may happen when you have a union of functions, which means the only valid argument \ diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 0ba8c7a3209..115b59928a8 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -1961,7 +1961,7 @@ defmodule Module.Types.IntegrationTest do defp read_chunk(binary) do assert {:ok, {_module, [{~c"ExCk", chunk}]}} = :beam_lib.chunks(binary, [~c"ExCk"]) - assert {:elixir_checker_v7, map} = :erlang.binary_to_term(chunk) + assert {:elixir_checker_v8, map} = :erlang.binary_to_term(chunk) map end diff --git a/lib/elixir/test/elixir/protocol/consolidation_test.exs b/lib/elixir/test/elixir/protocol/consolidation_test.exs index a6706d11f8a..49b5b36d51e 100644 --- a/lib/elixir/test/elixir/protocol/consolidation_test.exs +++ b/lib/elixir/test/elixir/protocol/consolidation_test.exs @@ -165,7 +165,7 @@ defmodule Protocol.ConsolidationTest do defp exports(binary) do {:ok, {_, [{~c"ExCk", check_bin}]}} = :beam_lib.chunks(binary, [~c"ExCk"]) - assert {:elixir_checker_v7, contents} = :erlang.binary_to_term(check_bin) + assert {:elixir_checker_v8, contents} = :erlang.binary_to_term(check_bin) Map.new(contents.exports) end From 5c4149aaffab2dadba8eeef738ae0d87f2db049a Mon Sep 17 00:00:00 2001 From: Guillaume Duboc <27832828+gldubc@users.noreply.github.com> Date: Sun, 3 May 2026 13:47:58 +0200 Subject: [PATCH 2/6] Use pairs for hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/elixir/lib/module/types/descr.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 92c14510518..01c8b77d5a8 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -5770,7 +5770,7 @@ defmodule Module.Types.Descr do ## BDD helpers - defp bdd_leaf_new(arg1, arg2), do: {:erlang.phash2({arg1, arg2}), arg1, arg2} + defp bdd_leaf_new(arg1, arg2), do: {:erlang.phash2([arg1 | arg2]), arg1, arg2} defp bdd_node_new(lit, c, u, d), do: {bdd_compute_hash(lit, c, u, d), lit, c, u, d} From a367a1c9936adfba3b205588fe460af3c050a436 Mon Sep 17 00:00:00 2001 From: Guillaume Duboc <27832828+gldubc@users.noreply.github.com> Date: Sun, 3 May 2026 17:07:59 +0200 Subject: [PATCH 3/6] Boost-style hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Valim --- lib/elixir/lib/module/types/descr.ex | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 01c8b77d5a8..2ab00f0a921 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -5775,8 +5775,18 @@ defmodule Module.Types.Descr do defp bdd_node_new(lit, c, u, d), do: {bdd_compute_hash(lit, c, u, d), lit, c, u, d} - defp bdd_compute_hash(lit, c, u, d), - do: :erlang.phash2({bdd_hash(lit), bdd_hash(c), bdd_hash(u), bdd_hash(d)}) + defp bdd_compute_hash(lit, c, u, d) do + h1 = bdd_combine_hash(bdd_hash(lit), bdd_hash(c)) + h2 = bdd_combine_hash(h1, bdd_hash(u)) + bdd_combine_hash(h2, bdd_hash(d)) &&& 0xFFFFFFFF + end + + # Boost-style hash_combine for four already-hashed integers. + # Cheaper than :erlang.phash2({lit, c, u, d}) and good enough for ruling out equality. + @compile {:inline, bdd_combine_hash: 2, bdd_hash: 1} + defp bdd_combine_hash(acc, x) do + bxor(acc, x + 0x9E3779B9 + (acc <<< 6) + (acc >>> 2)) + end defp bdd_leaf_value(bdd_leaf(arg1, arg2)), do: {arg1, arg2} From 3c9f4489c06dbcf486d1eb266f4b4575d3c1807b Mon Sep 17 00:00:00 2001 From: Guillaume Duboc <27832828+gldubc@users.noreply.github.com> Date: Sun, 3 May 2026 17:35:38 +0200 Subject: [PATCH 4/6] Remove bdd_leaf_value + adjust @tuple_top, etc. --- lib/elixir/lib/module/types/descr.ex | 26 +++++++++---------- .../test/elixir/module/types/descr_test.exs | 2 +- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 2ab00f0a921..46dc9cb6775 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -48,10 +48,10 @@ defmodule Module.Types.Descr do # Remark: those are explicit BDD constructors. The functional constructors are `bdd_new/1` and `bdd_new/3`. @fun_top {:negation, %{}} @atom_top {:negation, :sets.new(version: 2)} - @map_top {:erlang.phash2({:open, @fields_new}), :open, @fields_new} - @non_empty_list_top {:erlang.phash2({:term, :term}), :term, :term} - @tuple_top {:erlang.phash2({:open, []}), :open, []} - @map_empty {:erlang.phash2({:closed, @fields_new}), :closed, @fields_new} + @map_top {:erlang.phash2([:open | @fields_new]), :open, @fields_new} + @non_empty_list_top {:erlang.phash2([:term | :term]), :term, :term} + @tuple_top {:erlang.phash2([:open | []]), :open, []} + @map_empty {:erlang.phash2([:closed | @fields_new]), :closed, @fields_new} defmacrop bdd_leaf(arg1, arg2) do quote do @@ -5788,8 +5788,6 @@ defmodule Module.Types.Descr do bxor(acc, x + 0x9E3779B9 + (acc <<< 6) + (acc >>> 2)) end - defp bdd_leaf_value(bdd_leaf(arg1, arg2)), do: {arg1, arg2} - @bdd_bot_hash :erlang.phash2(:bdd_bot) @bdd_top_hash :erlang.phash2(:bdd_top) @@ -6282,12 +6280,12 @@ defmodule Module.Types.Descr do :bdd_top -> :bdd_top - bdd_leaf(_, _) -> - {arg1, arg2} = fun.(bdd_leaf_value(bdd)) + bdd_leaf(arg1, arg2) -> + {arg1, arg2} = fun.({arg1, arg2}) bdd_leaf_new(arg1, arg2) - {_, literal, left, union, right} -> - {arg1, arg2} = fun.(bdd_leaf_value(literal)) + {_, bdd_leaf(arg1, arg2), left, union, right} -> + {arg1, arg2} = fun.({arg1, arg2}) literal = bdd_leaf_new(arg1, arg2) bdd_node_new(literal, bdd_map(left, fun), bdd_map(union, fun), bdd_map(right, fun)) end @@ -6301,11 +6299,11 @@ defmodule Module.Types.Descr do :bdd_top -> acc - bdd_leaf(_, _) -> - fun.(bdd_leaf_value(bdd), acc) + bdd_leaf(arg1, arg2) -> + fun.({arg1, arg2}, acc) - {_, literal, left, union, right} -> - acc = fun.(bdd_leaf_value(literal), acc) + {_, bdd_leaf(arg1, arg2), left, union, right} -> + acc = fun.({arg1, arg2}, acc) acc = bdd_reduce(left, acc, fun) acc = bdd_reduce(union, acc, fun) acc = bdd_reduce(right, acc, fun) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 674965277e7..88892c83c8f 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -3149,7 +3149,7 @@ defmodule Module.Types.DescrTest do |> union(fun([float()], float())) |> union(fun([pid()], pid())) |> to_quoted_string() == - "(integer() -> integer()) or (float() -> float()) or (pid() -> pid())" + "(integer() -> integer()) or (pid() -> pid()) or (float() -> float())" assert fun(3) |> to_quoted_string() == "(none(), none(), none() -> term())" From c61abf5a5e4dc3694877e811fe56eb5ba7f00d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 4 May 2026 10:27:06 +0200 Subject: [PATCH 5/6] Fix test --- lib/elixir/test/elixir/module/types/expr_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 47864485194..c0a5483a26e 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -290,7 +290,7 @@ defmodule Module.Types.ExprTest do but function has type: - (non_empty_list(integer()) -> integer()) or (binary() -> integer()) + (binary() -> integer()) or (non_empty_list(integer()) -> integer()) hint: the function has an empty domain and therefore cannot be applied to any argument. \ This may happen when you have a union of functions, which means the only valid argument \ From e1eeddd55390c4d629ed2cea2be968d0f7a716d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 4 May 2026 10:40:03 +0200 Subject: [PATCH 6/6] Update descr_test.exs --- .../test/elixir/module/types/descr_test.exs | 58 ------------------- 1 file changed, 58 deletions(-) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 88892c83c8f..4b4f45bece5 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -24,64 +24,6 @@ defmodule Module.Types.DescrTest do defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type)) defp map_with_default(descr), do: open_map([{to_domain_keys(:term), descr}]) - defp assert_hash_consed_bdds(descr) do - Enum.each([:fun, :list, :map, :tuple], fn key -> - case descr do - %{^key => bdd} -> - assert_hash_consed_bdd(bdd) - assert_hash_consed_bdd_dnf(bdd) - - %{} -> - :ok - end - end) - end - - defp assert_hash_consed_bdd(:bdd_bot), do: :ok - defp assert_hash_consed_bdd(:bdd_top), do: :ok - - defp assert_hash_consed_bdd({:union, by_arity}) do - Enum.each(by_arity, fn {_arity, bdd} -> assert_hash_consed_bdd(bdd) end) - end - - defp assert_hash_consed_bdd({hash, _arg1, _arg2}) when is_integer(hash), do: :ok - - defp assert_hash_consed_bdd({hash, lit, c, u, d}) when is_integer(hash) do - assert_hash_consed_bdd(lit) - assert_hash_consed_bdd(c) - assert_hash_consed_bdd(u) - assert_hash_consed_bdd(d) - end - - defp assert_hash_consed_bdd(bdd) do - flunk("expected hash-consed BDD, got: #{inspect(bdd)}") - end - - defp assert_hash_consed_bdd_dnf({:union, by_arity}) do - Enum.each(by_arity, fn {_arity, bdd} -> assert_hash_consed_bdd_dnf(bdd) end) - end - - defp assert_hash_consed_bdd_dnf(bdd) do - Enum.each(bdd_to_dnf(bdd), fn {pos, neg} -> - Enum.each(pos ++ neg, &assert_hash_consed_bdd/1) - end) - end - - describe "BDD representation" do - test "leaves and nodes carry hashes at construction" do - descrs = [ - fun([integer()], atom()), - non_empty_list(integer(), atom()), - union(non_empty_list(integer(), atom()), non_empty_list(atom(), binary())), - union(closed_map(a: integer()), closed_map(b: atom())), - difference(open_map(), closed_map(a: integer())), - union(tuple([integer()]), tuple([atom()])) - ] - - Enum.each(descrs, &assert_hash_consed_bdds/1) - end - end - describe "union" do test "bitmap" do assert union(integer(), float()) == union(float(), integer())