diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index b1c1a8e4757..e975e7f893c 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -5630,31 +5630,33 @@ defmodule Module.Types.Descr do end end - defp tuple_insert_static(descr, _, _) when descr == @none, do: none() - - defp tuple_insert_static(descr, index, type) do - Map.update!(descr, :tuple, fn bdd -> - if tuple_bdd_positive?(bdd) do - # A pure disjunction of leaves: the insert distributes over the union, so - # we rewrite each leaf in place (preserving the structure callers assert on). - bdd_map(bdd, fn bdd_leaf(tag, elements) -> - tuple_insert_leaf(tag, elements, index, type) - end) - else - # The bdd carries negations and/or implicit `:bdd_top` positive paths - # (e.g. from `tuple_difference(open_tuple([]), _) -> bdd_negation`). - # `bdd_map` rewrites only explicit leaves, so it would skip the implicit - # top (losing the insert) and wrongly transform negated leaves. Expand to - # the exact negation-free positive DNF first, then insert into each leaf. - bdd - |> tuple_bdd_to_dnf_no_negations() - |> Enum.reduce(:bdd_bot, fn {tag, elements}, acc -> - tuple_union(tuple_insert_leaf(tag, elements, index, type), acc) - end) - end - end) + defp tuple_insert_static(%{tuple: bdd} = descr, index, type) do + %{ + descr + | tuple: + if tuple_bdd_positive?(bdd) do + # A pure disjunction of leaves: the insert distributes over the union, so + # we rewrite each leaf in place (preserving the structure callers assert on). + bdd_map(bdd, fn bdd_leaf(tag, elements) -> + tuple_insert_leaf(tag, elements, index, type) + end) + else + # The bdd carries negations and/or implicit `:bdd_top` positive paths + # (e.g. from `tuple_difference(open_tuple([]), _) -> bdd_negation`). + # `bdd_map` rewrites only explicit leaves, so it would skip the implicit + # top (losing the insert) and wrongly transform negated leaves. Expand to + # the exact negation-free positive DNF first, then insert into each leaf. + bdd + |> tuple_bdd_to_dnf_no_negations() + |> Enum.reduce(:bdd_bot, fn {tag, elements}, acc -> + tuple_union(tuple_insert_leaf(tag, elements, index, type), acc) + end) + end + } end + defp tuple_insert_static(_descr, _index, _type), do: none() + # Inserts `type` at `index` into a single tuple literal. If the tuple is open, # `List.insert_at` needs the tuple filled with `term()` up to `index` first. # Closed tuples of an incorrect size are cancelled before reaching here (the diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 9207d807fbe..e93b12efe74 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -2199,6 +2199,20 @@ defmodule Module.Types.DescrTest do # Errors must propagate even when the inserted value is dynamic assert tuple_insert_at(integer(), 0, dynamic()) == :badtuple assert tuple_insert_at(tuple([atom([:ok])]), 2, dynamic()) == :badindex + + # Must not crash when a gradual descr's static part is a non-normalized + # empty (semantically empty but syntactically present) non-tuple component. + a1 = opt_union(dynamic(), non_empty_list(integer(), none())) + assert equal?(a1, dynamic()) + + assert tuple_insert_at(a1, 1, atom([:x])) + |> equal?(tuple_insert_at(dynamic(), 1, atom([:x]))) + + a2 = opt_union(tuple([dynamic()]), non_empty_list(none())) + assert equal?(a2, tuple([dynamic()])) + + assert tuple_insert_at(a2, 1, atom([:x])) + |> equal?(tuple_insert_at(tuple([dynamic()]), 1, atom([:x]))) end test "tuple_replace_at" do