Skip to content

Commit 4fd800a

Browse files
committed
Map/struct updates concluded
1 parent 0aa9a8f commit 4fd800a

File tree

4 files changed

+134
-73
lines changed

4 files changed

+134
-73
lines changed

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

Lines changed: 61 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,9 @@ defmodule Module.Types.Descr do
178178
end
179179
end
180180

181-
@compile {:inline, lazy_union: 2}
182-
defp lazy_union(:term, _fun), do: :term
183-
defp lazy_union(descr, fun), do: union(descr, fun.())
181+
@compile {:inline, maybe_union: 2}
182+
defp maybe_union(nil, _fun), do: nil
183+
defp maybe_union(descr, fun), do: union(descr, fun.())
184184

185185
@doc """
186186
Computes the union of two descrs.
@@ -1377,31 +1377,20 @@ defmodule Module.Types.Descr do
13771377
end
13781378
end
13791379

1380-
defp map_fetch_static(:term, _key), do: {true, term()}
1381-
1382-
defp map_fetch_static(descr, key) when is_atom(key) do
1383-
case descr do
1384-
# Optimization: if the key does not exist in the map,
1385-
# avoid building if_set/not_set pairs and return the
1386-
# popped value directly.
1387-
%{map: [{tag, fields, []}]} when not is_map_key(fields, key) ->
1388-
case tag do
1389-
:open -> {true, term()}
1390-
:closed -> {true, none()}
1391-
end
1392-
1393-
%{map: map} ->
1394-
map_fetch_dnf(map, key) |> pop_optional_static()
1395-
1396-
%{} ->
1397-
{false, none()}
1380+
# Optimization: if the key does not exist in the map, avoid building
1381+
# if_set/not_set pairs and return the popped value directly.
1382+
defp map_fetch_static(%{map: [{tag, fields, []}]}, key) when not is_map_key(fields, key) do
1383+
case tag do
1384+
:open -> {true, term()}
1385+
:closed -> {true, none()}
13981386
end
13991387
end
14001388

14011389
# Takes a map dnf and returns the union of types it can take for a given key.
14021390
# If the key may be undefined, it will contain the `not_set()` type.
1403-
defp map_fetch_dnf(dnf, key) do
1404-
Enum.reduce(dnf, none(), fn
1391+
defp map_fetch_static(%{map: dnf}, key) do
1392+
dnf
1393+
|> Enum.reduce(none(), fn
14051394
# Optimization: if there are no negatives,
14061395
# we can return the value directly.
14071396
{_tag, %{^key => value}, []}, acc ->
@@ -1426,8 +1415,12 @@ defmodule Module.Types.Descr do
14261415
|> union(acc)
14271416
end
14281417
end)
1418+
|> pop_optional_static()
14291419
end
14301420

1421+
defp map_fetch_static(%{}, _key), do: {false, none()}
1422+
defp map_fetch_static(:term, _key), do: {true, term()}
1423+
14311424
@doc """
14321425
Fetches and puts a `key` of a given type, assuming that the descr is exclusively
14331426
a map (or dynamic).
@@ -1445,13 +1438,7 @@ defmodule Module.Types.Descr do
14451438
end
14461439

14471440
defp map_fetch_and_put_shared(descr, key, type) do
1448-
with {value, descr} <- map_take(descr, key, none(), &map_put_static(&1, key, type)) do
1449-
if empty?(value) do
1450-
:badkey
1451-
else
1452-
{value, descr}
1453-
end
1454-
end
1441+
map_take(descr, key, none(), &map_put_static(&1, key, type))
14551442
end
14561443

14571444
@doc """
@@ -1469,7 +1456,7 @@ defmodule Module.Types.Descr do
14691456
end
14701457

14711458
defp map_put_shared(descr, key, type) do
1472-
with {_value, descr} <- map_take(descr, key, :term, &map_put_static(&1, key, type)) do
1459+
with {nil, descr} <- map_take(descr, key, nil, &map_put_static(&1, key, type)) do
14731460
{:ok, descr}
14741461
end
14751462
end
@@ -1493,9 +1480,9 @@ defmodule Module.Types.Descr do
14931480
Removes a key from a map type.
14941481
"""
14951482
def map_delete(descr, key) do
1496-
# We pass :term as the initial value so we can avoid computing the unions.
1497-
with {_value, descr} <-
1498-
map_take(descr, key, :term, &intersection_static(&1, open_map([{key, not_set()}]))) do
1483+
# We pass nil as the initial value so we can avoid computing the unions.
1484+
with {nil, descr} <-
1485+
map_take(descr, key, nil, &intersection_static(&1, open_map([{key, not_set()}]))) do
14991486
{:ok, descr}
15001487
end
15011488
end
@@ -1521,25 +1508,29 @@ defmodule Module.Types.Descr do
15211508
defp map_take(descr, key, initial, updater) when is_atom(key) do
15221509
case :maps.take(:dynamic, descr) do
15231510
:error ->
1524-
# Note: the empty type is not a valid input
15251511
if descr_key?(descr, :map) and map_only?(descr) do
1526-
{taken, result} = map_take_static(descr, key, initial)
1527-
{taken, updater.(result)}
1512+
{optional?, taken, result} = map_take_static(descr, key, initial)
1513+
1514+
cond do
1515+
taken == nil -> {nil, updater.(result)}
1516+
optional? or empty?(taken) -> :badkey
1517+
true -> {taken, updater.(result)}
1518+
end
15281519
else
15291520
:badmap
15301521
end
15311522

15321523
{dynamic, static} ->
15331524
if descr_key?(dynamic, :map) and map_only?(static) do
1534-
{dynamic_taken, dynamic_result} = map_take_static(dynamic, key, initial)
1535-
{static_taken, static_result} = map_take_static(static, key, initial)
1536-
1537-
taken =
1538-
if dynamic_taken == :term,
1539-
do: dynamic(),
1540-
else: union(dynamic(dynamic_taken), static_taken)
1541-
1542-
{taken, union(dynamic(updater.(dynamic_result)), updater.(static_result))}
1525+
{_, dynamic_taken, dynamic_result} = map_take_static(dynamic, key, initial)
1526+
{static_optional?, static_taken, static_result} = map_take_static(static, key, initial)
1527+
result = union(dynamic(updater.(dynamic_result)), updater.(static_result))
1528+
1529+
cond do
1530+
static_taken == nil and dynamic_taken == nil -> {nil, result}
1531+
static_optional? or empty?(dynamic_taken) -> :badkey
1532+
true -> {union(dynamic(dynamic_taken), static_taken), result}
1533+
end
15431534
else
15441535
:badmap
15451536
end
@@ -1548,36 +1539,51 @@ defmodule Module.Types.Descr do
15481539

15491540
# Takes a static map type and removes a key from it.
15501541
# This allows the key to be put or deleted later on.
1542+
defp map_take_static(%{map: [{tag, fields, []}]} = descr, key, initial)
1543+
when not is_map_key(fields, key) do
1544+
case tag do
1545+
:open -> {true, maybe_union(initial, fn -> term() end), descr}
1546+
:closed -> {true, initial, descr}
1547+
end
1548+
end
1549+
15511550
defp map_take_static(%{map: dnf}, key, initial) do
15521551
{value, map} =
15531552
Enum.reduce(dnf, {initial, none()}, fn
15541553
# Optimization: if there are no negatives, we can directly remove the key.
1555-
{tag, fields, []}, {taken, map} ->
1554+
{tag, fields, []}, {value, map} ->
15561555
{fst, snd} = map_pop_key(tag, fields, key)
1557-
{lazy_union(taken, fn -> fst end), union(map, snd)}
1556+
{maybe_union(value, fn -> fst end), union(map, snd)}
15581557

1559-
{tag, fields, negs}, {taken, map} ->
1558+
{tag, fields, negs}, {value, map} ->
15601559
{fst, snd} = map_pop_key(tag, fields, key)
15611560

15621561
case map_split_negative(negs, key) do
15631562
:empty ->
1564-
{taken, map}
1563+
{value, map}
15651564

15661565
negative ->
15671566
disjoint = pair_make_disjoint(negative)
15681567

1569-
{lazy_union(taken, fn -> pair_eliminate_negations_fst(disjoint, fst, snd) end),
1568+
{maybe_union(value, fn -> pair_eliminate_negations_fst(disjoint, fst, snd) end),
15701569
disjoint |> pair_eliminate_negations_snd(fst, snd) |> union(map)}
15711570
end
15721571
end)
15731572

1574-
{remove_optional_static(value), map}
1573+
if value == nil do
1574+
{false, value, map}
1575+
else
1576+
{optional?, value} = pop_optional_static(value)
1577+
{optional?, value, map}
1578+
end
15751579
end
15761580

1577-
defp map_take_static(:term, _key, _initial), do: {:term, open_map()}
1578-
15791581
# If there is no map part to this static type, there is nothing to delete.
1580-
defp map_take_static(_type, _key, initial), do: {initial, none()}
1582+
defp map_take_static(%{}, _key, initial), do: {false, initial, none()}
1583+
1584+
defp map_take_static(:term, _key, initial) do
1585+
{true, maybe_union(initial, fn -> term() end), open_map()}
1586+
end
15811587

15821588
# Short-circuits if it finds a non-empty map literal in the union.
15831589
# Since the algorithm is recursive, we implement the short-circuiting

lib/elixir/lib/module/types/expr.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ defmodule Module.Types.Expr do
134134

135135
if fallback == none() do
136136
Enum.reduce(pairs, map_type, fn {key, type}, acc ->
137-
case map_put(acc, key, type) do
137+
case map_fetch_and_put(acc, key, type) do
138138
{_value, descr} -> descr
139139
:badkey -> throw({:badkey, map_type, key, expr, context})
140140
:badmap -> throw({:badmap, map_type, expr, context})

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -994,14 +994,10 @@ defmodule Module.Types.DescrTest do
994994
assert map_take(closed_map(a: integer(), b: atom()), :a) ==
995995
{integer(), closed_map(b: atom())}
996996

997-
assert map_take(empty_map(), :a) == {none(), empty_map()}
998-
999-
assert map_take(closed_map(a: if_set(integer()), b: atom()), :a) ==
1000-
{integer(), closed_map(b: atom())}
1001-
1002997
# Deleting a non-existent key
1003-
assert map_take(closed_map(a: integer(), b: atom()), :c) ==
1004-
{none(), closed_map(a: integer(), b: atom())}
998+
assert map_take(empty_map(), :a) == :badkey
999+
assert map_take(closed_map(a: integer(), b: atom()), :c) == :badkey
1000+
assert map_take(closed_map(a: if_set(integer()), b: atom()), :a) == :badkey
10051001

10061002
# Deleting from a dynamic map
10071003
assert map_take(dynamic(), :a) == {dynamic(), dynamic(open_map(a: not_set()))}
@@ -1012,13 +1008,15 @@ defmodule Module.Types.DescrTest do
10121008
assert equal?(type, open_map(a: not_set(), b: atom()))
10131009

10141010
# Deleting from a union of maps
1015-
{value, type} = map_take(union(closed_map(a: integer()), closed_map(b: atom())), :a)
1016-
assert value == integer()
1017-
assert equal?(type, union(empty_map(), closed_map(b: atom())))
1011+
union = union(closed_map(a: integer()), closed_map(b: atom()))
1012+
assert map_take(union, :a) == :badkey
1013+
{value, type} = map_take(dynamic(union), :a)
1014+
assert value == dynamic(integer())
1015+
assert equal?(type, dynamic(union(empty_map(), closed_map(b: atom()))))
10181016

10191017
# Deleting from a gradual map
10201018
{value, type} = map_take(union(dynamic(), closed_map(a: integer())), :a)
1021-
assert value == dynamic()
1019+
assert value == union(dynamic(), integer())
10221020
assert equal?(type, union(dynamic(open_map(a: not_set())), empty_map()))
10231021

10241022
{value, type} = map_take(dynamic(open_map(a: not_set())), :b)

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

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,25 @@ defmodule Module.Types.ExprTest do
546546
assert typecheck!([x], %{%{x | x: :zero} | y: :one}) ==
547547
dynamic(open_map(x: atom([:zero]), y: atom([:one])))
548548

549+
assert typecheck!(
550+
(
551+
foo_or_bar =
552+
cond do
553+
:rand.uniform() > 0.5 -> :key1
554+
true -> :key2
555+
end
556+
557+
x = %{key1: :one, key2: :two}
558+
%{x | foo_or_bar => :one!, foo_or_bar => :two!}
559+
)
560+
)
561+
|> equal?(
562+
closed_map(key1: atom([:one]), key2: atom([:two!]))
563+
|> union(closed_map(key1: atom([:two!]), key2: atom([:one!])))
564+
|> union(closed_map(key1: atom([:one!]), key2: atom([:two!])))
565+
|> union(closed_map(key1: atom([:two!]), key2: atom([:two])))
566+
)
567+
549568
assert typeerror!([x = :foo], %{x | x: :zero}) == ~l"""
550569
expected a map within map update syntax:
551570
@@ -568,19 +587,57 @@ defmodule Module.Types.ExprTest do
568587
%{x | x: :zero}
569588
)
570589
) == ~l"""
571-
expected a map within map update syntax:
590+
expected a map with key :x in map update syntax:
572591
573592
%{x | x: :zero}
574593
575594
but got type:
576595
577-
dynamic(:foo)
596+
empty_map()
578597
579598
where "x" was given the type:
580599
581-
# type: dynamic(:foo)
582-
# from: types_test.ex:LINE
583-
x = :foo
600+
# type: empty_map()
601+
# from: types_test.ex:LINE-3
602+
x = %{}
603+
"""
604+
605+
# Assert we check all possible combinations
606+
assert typeerror!(
607+
(
608+
foo_or_bar =
609+
cond do
610+
:rand.uniform() > 0.5 -> :foo
611+
true -> :bar
612+
end
613+
614+
x = %{foo: :baz}
615+
%{x | foo_or_bar => :bat}
616+
)
617+
) == ~l"""
618+
expected a map with key :bar in map update syntax:
619+
620+
%{x | foo_or_bar => :bat}
621+
622+
but got type:
623+
624+
%{foo: :baz}
625+
626+
where "foo_or_bar" was given the type:
627+
628+
# type: :bar or :foo
629+
# from: types_test.ex:LINE-9
630+
foo_or_bar =
631+
cond do
632+
:rand.uniform() > 0.5 -> :foo
633+
true -> :bar
634+
end
635+
636+
where "x" was given the type:
637+
638+
# type: %{foo: :baz}
639+
# from: types_test.ex:LINE-3
640+
x = %{foo: :baz}
584641
"""
585642
end
586643

@@ -611,7 +668,7 @@ defmodule Module.Types.ExprTest do
611668
)
612669
) == dynamic(open_map())
613670

614-
# The goal of this test is to verify we assert keys,
671+
# The goal of this assertion is to verify we assert keys,
615672
# even if they may be overridden later.
616673
assert typeerror!(
617674
[key],

0 commit comments

Comments
 (0)