Skip to content

Commit 41f9bcf

Browse files
committed
Do not bake nil assumption into map_get/2
1 parent 2b63546 commit 41f9bcf

File tree

2 files changed

+84
-98
lines changed

2 files changed

+84
-98
lines changed

lib/elixir/lib/module/types/descr.ex

Lines changed: 42 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,11 @@ defmodule Module.Types.Descr do
962962
defp atom_only?(descr), do: empty?(Map.delete(descr, :atom))
963963
defp atom_new(as) when is_list(as), do: {:union, :sets.from_list(as, version: 2)}
964964

965+
defp infinite_atoms?(:term), do: true
966+
defp infinite_atoms?(%{dynamic: dynamic}), do: infinite_atoms?(dynamic)
967+
defp infinite_atoms?(%{atom: {:negation, _}}), do: true
968+
defp infinite_atoms?(%{}), do: false
969+
965970
defp atom_intersection({tag1, s1}, {tag2, s2}) do
966971
{tag, s} =
967972
case {tag1, tag2} do
@@ -2705,7 +2710,7 @@ defmodule Module.Types.Descr do
27052710
end
27062711

27072712
defp map_fetch_key_static(%{map: bdd}, key) do
2708-
map_bdd_to_dnf(bdd) |> map_dnf_fetch_static(key)
2713+
map_bdd_to_dnf(bdd) |> map_dnf_fetch_static(key) |> pop_optional_static()
27092714
end
27102715

27112716
defp map_fetch_key_static(%{}, _key), do: {false, none()}
@@ -2737,7 +2742,6 @@ defmodule Module.Types.Descr do
27372742
|> union(acc)
27382743
end
27392744
end)
2740-
|> pop_optional_static()
27412745
end
27422746

27432747
@doc """
@@ -2975,7 +2979,7 @@ defmodule Module.Types.Descr do
29752979
if Map.has_key?(seen, key) do
29762980
{seen, acc}
29772981
else
2978-
{_, value} = map_dnf_fetch_static(dnf, key)
2982+
value = dnf |> map_dnf_fetch_static(key) |> remove_optional()
29792983
{Map.put(seen, key, []), union(acc, value)}
29802984
end
29812985
end)
@@ -2990,8 +2994,8 @@ defmodule Module.Types.Descr do
29902994
if Map.has_key?(acc, key) do
29912995
acc
29922996
else
2993-
{_, value} = map_dnf_fetch_static(dnf, key)
2994-
not empty?(value) and throw(:found_key)
2997+
value = map_dnf_fetch_static(dnf, key)
2998+
not empty_or_optional?(value) and throw(:found_key)
29952999
Map.put(acc, key, [])
29963000
end
29973001
end)
@@ -3183,59 +3187,39 @@ defmodule Module.Types.Descr do
31833187
@doc """
31843188
Computes the union of types for keys matching `key_type` within the `map_type`.
31853189
3186-
This generalizes `map_fetch_key/2` (which operates on a single literal key) to
3187-
work with a key type (e.g., `atom()`, `integer()`, `:a or :b`). It's based
3188-
on the map-selection operator t.[t'] described in Section 4.2 of "Typing Records,
3189-
Maps, and Structs" (Castagna et al., ICFP 2023).
3190-
3191-
## Return Values
3192-
3193-
The function returns a tuple indicating the outcome and the resulting type union:
3194-
3195-
* `{:ok, type}`: Standard success. `type` is the resulting union of types
3196-
found for the matching keys. This covers two sub-cases:
3197-
* **Keys definitely exist:** If `disjoint?(type, not_set())` is true,
3198-
all keys matching `key_type` are guaranteed to exist.
3199-
* **Keys may exist:** If `type` includes `not_set()`, some keys
3200-
matching `key_type` might exist (contributing their types) while
3201-
others might be absent (contributing `not_set()`).
3202-
3203-
* `{:ok_absent, type}`: Success, but the resulting `type` is `none()` or a
3204-
subtype of `not_set()`. This indicates that no key matching `key_type`
3205-
can exist with a value other than `not_set()`. The caller may wish to
3206-
issue a warning, as this often implies selecting a field that is
3207-
effectively undefined.
3208-
3209-
# TODO: implement/decide if worth it (it's from the paper)
3210-
* `{:ok_spillover, type}`: Success, and `type` is the resulting union.
3211-
However, this indicates that the `key_type` included keys not explicitly
3212-
covered by the `map_type`'s fields or domain specifications. The
3213-
projection relied on the map's default behavior (e.g., the `term()`
3214-
value type for unspecified keys in an open map). The caller may wish to
3215-
issue a warning, as this could conceal issues like selecting keys
3216-
not intended by the map's definition.
3217-
3218-
* `:badmap`: The input `map_type` was invalid (e.g., not a map type or
3219-
a dynamic type wrapping a map type).
3220-
3221-
* `:badkeytype`: The input `key_type` was invalid (e.g., not a subtype
3222-
of the allowed key types like `atom()`, `integer()`, etc.).
3190+
Returns `{optional?, descr}`, `:error` (if no value across the whole domain is found),
3191+
or `:badmap`.
3192+
3193+
This is called `map_get/2` but it can be used to power `Map.fetch`, `Map.fetch!`,
3194+
`Map.get`, etc. except `map.key`.
32233195
"""
3224-
# TODO: Figure out how to use this operation from Elixir
32253196
def map_get(:term, _key_descr), do: :badmap
32263197

32273198
def map_get(%{} = descr, key_descr) do
32283199
split_keys = map_split_keys_and_domains(key_descr)
32293200

3201+
# If we are looking for infinite atoms, then either there are required/optional
3202+
# keys which may be selected, so we use not_set().
3203+
#
3204+
# In case, there are no keys, which will fail unless there are domain keys,
3205+
# so `not_set()` is still correct.
3206+
acc =
3207+
if infinite_atoms?(key_descr) do
3208+
not_set()
3209+
else
3210+
none()
3211+
end
3212+
32303213
case :maps.take(:dynamic, descr) do
32313214
:error ->
32323215
if descr_key?(descr, :map) and map_only?(descr) do
3233-
{optional?, type_selected} = map_get_static(descr, split_keys) |> pop_optional_static()
3216+
{optional?, type_selected} =
3217+
map_get_static(descr, split_keys, acc) |> pop_optional_static()
32343218

3235-
cond do
3236-
empty?(type_selected) -> {:ok_absent, atom([nil])}
3237-
optional? -> {:ok, nil_or_type(type_selected)}
3238-
true -> {:ok_present, type_selected}
3219+
if empty?(type_selected) do
3220+
:error
3221+
else
3222+
{optional?, type_selected}
32393223
end
32403224
else
32413225
:badmap
@@ -3244,33 +3228,28 @@ defmodule Module.Types.Descr do
32443228
{dynamic, static} ->
32453229
if descr_key?(dynamic, :map) and map_only?(static) do
32463230
{optional_dynamic?, dynamic_type} =
3247-
map_get_static(dynamic, split_keys) |> pop_optional_static()
3231+
map_get_static(dynamic, split_keys, acc) |> pop_optional_static()
32483232

32493233
{optional_static?, static_type} =
3250-
map_get_static(static, split_keys) |> pop_optional_static()
3234+
map_get_static(static, split_keys, acc) |> pop_optional_static()
32513235

3252-
type_selected = union(dynamic(dynamic_type), static_type)
3253-
3254-
cond do
3255-
empty?(type_selected) -> {:ok_absent, atom([nil])}
3256-
optional_dynamic? or optional_static? -> {:ok, nil_or_type(type_selected)}
3257-
true -> {:ok_present, type_selected}
3236+
if empty?(dynamic_type) do
3237+
:error
3238+
else
3239+
{optional_dynamic? or optional_static?, union(dynamic(dynamic_type), static_type)}
32583240
end
32593241
else
32603242
:badmap
32613243
end
32623244
end
32633245
end
32643246

3265-
defp nil_or_type(type), do: union(type, atom([nil]))
3266-
3267-
defp map_get_static(%{map: bdd}, split_keys) do
3247+
defp map_get_static(%{map: bdd}, split_keys, acc) do
32683248
{required_keys, optional_keys, maybe_negated_set, required_domains, optional_domains} =
32693249
split_keys
32703250

32713251
dnf = map_bdd_to_dnf(bdd)
32723252

3273-
acc = none()
32743253
acc = map_get_keys(dnf, required_keys, acc)
32753254
acc = map_get_keys(dnf, optional_keys, acc)
32763255
acc = map_get_keys(dnf, map_materialize_negated_set(maybe_negated_set, bdd), acc)
@@ -3279,18 +3258,12 @@ defmodule Module.Types.Descr do
32793258
acc
32803259
end
32813260

3282-
defp map_get_static(%{}, _key), do: not_set()
3283-
defp map_get_static(:term, _key), do: term_or_optional()
3261+
defp map_get_static(%{}, _split_keys, acc), do: acc
3262+
defp map_get_static(:term, _split_keys, _acc), do: term_or_optional()
32843263

32853264
defp map_get_keys(dnf, keys, acc) do
32863265
Enum.reduce(keys, acc, fn atom, acc ->
3287-
{static_optional?, type} = map_dnf_fetch_static(dnf, atom)
3288-
3289-
if static_optional? do
3290-
union(type, acc) |> nil_or_type() |> if_set()
3291-
else
3292-
union(type, acc)
3293-
end
3266+
union(map_dnf_fetch_static(dnf, atom), acc)
32943267
end)
32953268
end
32963269

lib/elixir/test/elixir/module/types/descr_test.exs

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ defmodule Module.Types.DescrTest do
2323
defp tuple_of_size(n) when is_integer(n) and n >= 0, do: tuple(List.duplicate(term(), n))
2424
defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type))
2525
defp map_with_default(descr), do: open_map([], if_set(descr))
26-
defp nil_or_type(type), do: union(type, atom([nil]))
2726

2827
describe "union" do
2928
test "bitmap" do
@@ -1655,12 +1654,14 @@ defmodule Module.Types.DescrTest do
16551654
|> difference(closed_map(b: if_set(integer())))
16561655
|> map_fetch_key(:a) == :badkey
16571656
end
1657+
end
16581658

1659-
test "map_get with domain keys" do
1659+
describe "map_get" do
1660+
test "with domain keys" do
16601661
assert map_get(term(), term()) == :badmap
16611662

16621663
map_type = closed_map([{domain_key(:tuple), binary()}])
1663-
assert map_get(map_type, tuple()) == {:ok, nil_or_type(binary())}
1664+
assert map_get(map_type, tuple()) == {true, binary()}
16641665

16651666
# Type with all domain types
16661667
# %{:bar => :ok, integer() => :int, float() => :float, atom() => binary(), binary() => integer(), tuple() => float(), map() => pid(), reference() => port(), pid() => boolean()}
@@ -1678,76 +1679,88 @@ defmodule Module.Types.DescrTest do
16781679
{domain_key(:port), boolean()}
16791680
])
16801681

1681-
assert map_get(all_domains, atom([:bar])) == {:ok_present, atom([:ok])}
1682+
assert map_get(all_domains, atom([:bar])) == {false, atom([:ok])}
16821683

1683-
assert map_get(all_domains, integer()) == {:ok, atom([:int]) |> nil_or_type()}
1684-
assert map_get(all_domains, number()) == {:ok, atom([:int, :float]) |> nil_or_type()}
1684+
assert map_get(all_domains, integer()) == {true, atom([:int])}
1685+
assert map_get(all_domains, number()) == {true, atom([:int, :float])}
16851686

1686-
assert map_get(all_domains, empty_list()) == {:ok_absent, atom([nil])}
1687-
assert map_get(all_domains, atom([:foo])) == {:ok, binary() |> nil_or_type()}
1688-
assert map_get(all_domains, binary()) == {:ok, integer() |> nil_or_type()}
1689-
assert map_get(all_domains, tuple([integer(), atom()])) == {:ok, nil_or_type(float())}
1690-
assert map_get(all_domains, empty_map()) == {:ok, pid() |> nil_or_type()}
1687+
assert map_get(all_domains, empty_list()) == :error
1688+
assert map_get(all_domains, atom([:foo])) == {true, binary()}
1689+
assert map_get(all_domains, binary()) == {true, integer()}
1690+
assert map_get(all_domains, tuple([integer(), atom()])) == {true, float()}
1691+
assert map_get(all_domains, empty_map()) == {true, pid()}
16911692

16921693
# Union
16931694
assert map_get(all_domains, union(tuple(), empty_map())) ==
1694-
{:ok, union(float(), pid() |> nil_or_type())}
1695+
{true, union(float(), pid())}
16951696

16961697
# Removing all maps with tuple keys
16971698
t_no_tuple = difference(all_domains, closed_map([{domain_key(:tuple), float()}]))
16981699
t_really_no_tuple = difference(all_domains, open_map([{domain_key(:tuple), float()}]))
16991700
assert subtype?(all_domains, open_map())
17001701
# It's only closed maps, so it should not change
1701-
assert map_get(t_no_tuple, tuple()) == {:ok, float() |> nil_or_type()}
1702+
assert map_get(t_no_tuple, tuple()) == {true, float()}
17021703
# This time we actually removed all tuple to float keys
1703-
assert map_get(t_really_no_tuple, tuple()) == {:ok_absent, atom([nil])}
1704+
assert map_get(t_really_no_tuple, tuple()) == :error
17041705

17051706
t1 = closed_map([{domain_key(:tuple), integer()}])
17061707
t2 = closed_map([{domain_key(:tuple), float()}])
17071708
t3 = union(t1, t2)
1708-
assert map_get(t3, tuple()) == {:ok, number() |> nil_or_type()}
1709+
assert map_get(t3, tuple()) == {true, number()}
17091710
end
17101711

1711-
test "map_get with dynamic" do
1712+
test "with dynamic" do
17121713
{_answer, type_selected} = map_get(dynamic(), term())
1713-
assert equal?(type_selected, dynamic() |> nil_or_type())
1714+
assert equal?(type_selected, dynamic())
17141715
end
17151716

1716-
test "map_get with atom fall back" do
1717+
test "with atom fall back" do
17171718
map = closed_map([{:a, atom([:a])}, {:b, atom([:b])}, {domain_key(:atom), pid()}])
1718-
assert map_get(map, atom([:a, :b])) == {:ok_present, atom([:a, :b])}
1719-
assert map_get(map, atom([:a, :c])) == {:ok, union(atom([:a]), pid() |> nil_or_type())}
1720-
assert map_get(map, atom() |> difference(atom([:a, :b]))) == {:ok, pid() |> nil_or_type()}
1719+
1720+
assert map_get(map, atom([:a, :b])) ==
1721+
{false, atom([:a, :b])}
1722+
1723+
assert map_get(map, atom([:a, :c])) ==
1724+
{true, union(atom([:a]), pid())}
1725+
1726+
assert map_get(map, atom() |> difference(atom([:a, :b]))) ==
1727+
{true, pid()}
17211728

17221729
assert map_get(map, atom() |> difference(atom([:a]))) ==
1723-
{:ok, union(atom([:b]), pid() |> nil_or_type())}
1730+
{true, union(atom([:b]), pid())}
1731+
1732+
assert map_get(closed_map(a: atom([:a]), b: atom([:b])), atom()) ==
1733+
{true, atom([:a, :b])}
1734+
1735+
assert map_get(closed_map([{domain_key(:atom), integer()}]), atom([:a, :b])) ==
1736+
{true, integer()}
17241737
end
17251738

1726-
test "map_get with lists" do
1739+
test "with lists" do
17271740
# Verify that empty_list() bitmap type maps to :list domain (not :empty_list domain)
17281741
map_with_list_domain = closed_map([{domain_key(:list), atom([:empty])}])
17291742

17301743
# empty_list() should access the :list domain
1731-
assert map_get(map_with_list_domain, empty_list()) == {:ok, atom([:empty]) |> nil_or_type()}
1744+
assert map_get(map_with_list_domain, empty_list()) == {true, atom([:empty])}
17321745

17331746
# non_empty_list() should also access the :list domain
17341747
assert map_get(map_with_list_domain, non_empty_list(integer())) ==
1735-
{:ok, atom([:empty]) |> nil_or_type()}
1748+
{true, atom([:empty])}
17361749

17371750
# list() should also access the :list domain
17381751
assert map_get(map_with_list_domain, list(integer())) ==
1739-
{:ok, atom([:empty]) |> nil_or_type()}
1752+
{true, atom([:empty])}
17401753

17411754
# If I create a map and instantiate both empty_list() and non_empty_list(integer()), it should return the union of the two types
17421755
map =
17431756
closed_map([{domain_key(:list), atom([:empty])}, {domain_key(:list), atom([:non_empty])}])
17441757

1745-
assert map_get(map, empty_list()) == {:ok, atom([:empty, :non_empty]) |> nil_or_type()}
1758+
assert map_get(map, empty_list()) == {true, atom([:empty, :non_empty])}
17461759

17471760
assert map_get(map, non_empty_list(integer())) ==
1748-
{:ok, atom([:empty, :non_empty]) |> nil_or_type()}
1761+
{true, atom([:empty, :non_empty])}
17491762

1750-
assert map_get(map, list(integer())) == {:ok, atom([:empty, :non_empty]) |> nil_or_type()}
1763+
assert map_get(map, list(integer())) == {true, atom([:empty, :non_empty])}
17511764
end
17521765
end
17531766

0 commit comments

Comments
 (0)