From 8a4837a65309181d0e71b1dd539ff9b7c901e342 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 16 Jun 2026 22:22:21 +0200 Subject: [PATCH 1/2] Fix definition of term_or_dynamic_optional type make dynamic part a upper bound Fixes #15497 --- lib/elixir/lib/module/types/descr.ex | 2 +- lib/elixir/test/elixir/module/types/descr_test.exs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index c2648be488e..0bf6d07f0f1 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -76,7 +76,7 @@ defmodule Module.Types.Descr do @not_set %{optional: 1} @term_or_optional Map.put(@term, :optional, 1) - @term_or_dynamic_optional Map.put(@term, :dynamic, %{optional: 1}) + @term_or_dynamic_optional Map.put(@term, :dynamic, @term_or_optional) @not_atom_or_optional Map.delete(@term_or_optional, :atom) @empty_intersection [0, :bdd_bot] diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 54fa5401d68..dc784533bb6 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -976,6 +976,14 @@ defmodule Module.Types.DescrTest do refute subtype?(term(), dynamic()) assert subtype?(opt_intersection(dynamic(), integer()), integer()) assert subtype?(integer(), opt_union(dynamic(), integer())) + + # Unioning term() with a dynamic-optional keeps the dynamic component a + # supertype of the static one, so subtyping stays transitive. + t = opt_union(term(), dynamic(not_set())) + assert subtype?(term(), t) + assert subtype?(dynamic(), t) + assert subtype?(dynamic(integer()), t) + assert equal?(opt_intersection(t, dynamic(integer())), dynamic(integer())) end test "tuple" do From ee462ef378246e0276f328cc42e8a6e2e23aad07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 17 Jun 2026 08:30:39 +0200 Subject: [PATCH 2/2] Update descr_test.exs --- lib/elixir/test/elixir/module/types/descr_test.exs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index dc784533bb6..fe5206b6c84 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -976,14 +976,6 @@ defmodule Module.Types.DescrTest do refute subtype?(term(), dynamic()) assert subtype?(opt_intersection(dynamic(), integer()), integer()) assert subtype?(integer(), opt_union(dynamic(), integer())) - - # Unioning term() with a dynamic-optional keeps the dynamic component a - # supertype of the static one, so subtyping stays transitive. - t = opt_union(term(), dynamic(not_set())) - assert subtype?(term(), t) - assert subtype?(dynamic(), t) - assert subtype?(dynamic(integer()), t) - assert equal?(opt_intersection(t, dynamic(integer())), dynamic(integer())) end test "tuple" do @@ -1041,6 +1033,12 @@ defmodule Module.Types.DescrTest do refute subtype?(if_set(term()), term()) assert subtype?(if_set(term()), if_set(term())) refute subtype?(if_set(term()), if_set(dynamic(term()))) + + # Test that union+dynamic optimizations preserves properties + t = opt_union(term(), dynamic(not_set())) + assert subtype?(term(), t) + assert subtype?(dynamic(), t) + assert subtype?(dynamic(integer()), t) end test "list" do