From 4c4d2c2374418249e9065631a946103766f9786c Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 5 Sep 2026 12:15:46 +0200 Subject: [PATCH] Preserve static map domains with none values Map domain entries are optional, so an empty static value excludes keys in that domain instead of making the whole map empty. Preserve those static lower bounds and cover their use in contravariant function arguments. Assisted-by: Codex:GPT-5 --- lib/elixir/lib/module/types/descr.ex | 2 - .../test/elixir/module/types/descr_test.exs | 38 +++++++++++++++++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index bd56b246b42..294d0873103 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -2887,8 +2887,6 @@ defmodule Module.Types.Descr do {dynamic_value, static_value, value_dynamic?} = split_dynamic(value) dynamic? = dynamic? or value_dynamic? - static_empty? = static_empty? or static_value == @none - {fields, map_put_domain(domains, key, static_value), dynamic_fields, map_put_domain(dynamic_domains, key, dynamic_value), dynamic?, static_empty?} end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index b28de407798..a0a5708bee9 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -980,7 +980,7 @@ defmodule Module.Types.DescrTest do end describe "creation" do - test "map hoists dynamic" do + test "map handles dynamic values" do assert dynamic(open_map(a: {integer(), false})) == open_map(a: {dynamic(integer()), false}) assert opt_union( @@ -989,10 +989,15 @@ defmodule Module.Types.DescrTest do ) == open_map(a: {dynamic(integer()) |> opt_union(binary()), false}) - # For domains too - t1 = dynamic(open_map([{domain_key(:integer), integer()}])) + # Domains are optional, so their static part must also be preserved t2 = open_map([{domain_key(:integer), dynamic(integer())}]) - assert t1 == t2 + + assert t2 == + Map.put( + open_map([{domain_key(:integer), none()}]), + :dynamic, + open_map([{domain_key(:integer), integer()}]) + ) # Optional dynamic fields also must work t1 = dynamic(open_map(a: {integer(), true})) @@ -1000,6 +1005,31 @@ defmodule Module.Types.DescrTest do assert opt_union(open_map(a: {none(), true}), t1) == t2 end + test "map domains with empty static values preserve the static map" do + static = closed_map([{domain_key(:integer), none()}, {:b, {integer(), false}}]) + gradual = closed_map([{domain_key(:integer), dynamic()}, {:b, {integer(), false}}]) + + assert lower_bound(gradual) == static + assert upper_bound(fun([gradual], atom())) == fun([static], atom()) + + static = open_map([{domain_key(:fun), none()}, {:a, {none(), true}}]) + gradual = open_map([{domain_key(:fun), none()}, {:a, {dynamic(), true}}]) + + assert lower_bound(gradual) == static + + static = open_map([{domain_key(:tuple), none()}, {:b, {integer(), true}}]) + + gradual = + open_map([ + {domain_key(:tuple), none()}, + {:b, {opt_union(integer(), dynamic(float())), true}} + ]) + + assert lower_bound(gradual) == static + + assert lower_bound(open_map(a: {dynamic(), false})) == none() + end + test "structural types preserve static part of gradual elements" do static = atom([:ok]) gradual = opt_union(static, dynamic(integer()))