diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 8469f0002f..d56a88f636 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -5366,27 +5366,32 @@ 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 -> - bdd_map(bdd, fn bdd_leaf(tag, elements) -> - # If the tuple is open, then we want List.insert_at to put the new element at the correct - # index, which requires filling the tuple with `term()` values first. - # Closed tuples of an incorrect size will be ignored (they are cancelled by the earlier - # intersection with `tuple_of_size_at_least`). - elements = - if tag == :open and length(elements) < index do - tuple_fill(elements, index) - else - elements - end + defp tuple_insert_static(%{tuple: bdd} = descr, index, type) do + %{ + descr + | tuple: + bdd_map(bdd, fn bdd_leaf(tag, elements) -> + # If the tuple is open, then we want List.insert_at to put the new element at the correct + # index, which requires filling the tuple with `term()` values first. + # Closed tuples of an incorrect size will be ignored (they are cancelled by the earlier + # intersection with `tuple_of_size_at_least`). + elements = + if tag == :open and length(elements) < index do + tuple_fill(elements, index) + else + elements + end - bdd_leaf_new(tag, List.insert_at(elements, index, type)) - end) - end) + bdd_leaf_new(tag, List.insert_at(elements, index, type)) + end) + } end + # No tuple component: either none() or a semantically-empty static part whose + # non-normalized non-tuple components (e.g. an empty list literal) survive + # syntactically. There is nothing to insert into, so the result is empty. + defp tuple_insert_static(_descr, _index, _type), do: none() + @doc """ Replace an element in the tuple at the given (0-based) index. diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index f1340e357f..2660bd01b8 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -2087,6 +2087,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