diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 5bee8403ed..8892aca496 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -5256,6 +5256,17 @@ defmodule Module.Types.Descr do # Takes a static tuple type and removes an index from it. defp tuple_delete_static(%{tuple: bdd}, index) do + # Restrict to tuples that actually have `index` before projecting. Callers + # already guarantee this (via the is_proper_size? check), so the intersection + # is a semantic no-op, but it makes the element at `index` explicit in the + # positive literals. Without it, an open literal whose size lower bound is + # only carried by negations (e.g. {...} \ {} \ {term()}) projects that index + # to term_or_optional(); the optional marker then pairs with the full + # remaining bdd and the result over-approximates (e.g. admits the empty + # tuple), losing layout. This is the same optional-marker imprecision the + # projection machinery exhibits for tuple_fetch. + bdd = tuple_intersection(bdd, tuple_new(:open, List.duplicate(term(), index + 1))) + bdd = bdd |> tuple_bdd_to_dnf_with_negations() diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 70adcf749e..504a4e7f1a 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -2000,6 +2000,28 @@ defmodule Module.Types.DescrTest do assert dynamic(opt_union(tuple(), integer())) |> tuple_delete_at(1) |> equal?(dynamic(tuple_of_size_at_least(1))) + + # The result must not depend on the representation. A size lower bound + # carried only by negations ({...} \ {} \ {term()} == open_tuple([term(), + # term()])) must delete to the same type as the canonical open tuple and + # must not over-approximate by admitting shorter tuples (e.g. the empty + # tuple). + canonical = open_tuple([term(), term()]) + + by_negation = + open_tuple([]) + |> opt_difference(tuple([])) + |> opt_difference(tuple([term()])) + + assert equal?(canonical, by_negation) + + assert tuple_delete_at(by_negation, 0) + |> equal?(tuple_delete_at(canonical, 0)) + + assert tuple_delete_at(by_negation, 0) + |> equal?(open_tuple([term()])) + + refute subtype?(tuple([]), tuple_delete_at(by_negation, 0)) end test "tuple_insert_at" do