Skip to content

Commit 2abcc52

Browse files
committed
Start typing map and struct updates
1 parent 9f182f9 commit 2abcc52

File tree

5 files changed

+87
-64
lines changed

5 files changed

+87
-64
lines changed

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,7 +1214,7 @@ defmodule Module.Types.Descr do
12141214
end
12151215

12161216
%{map: map} ->
1217-
map_get(map, key) |> pop_optional_static()
1217+
map_fetch_dnf(map, key) |> pop_optional_static()
12181218

12191219
%{} ->
12201220
{false, none()}
@@ -1239,22 +1239,22 @@ defmodule Module.Types.Descr do
12391239
case :maps.take(:dynamic, descr) do
12401240
:error ->
12411241
if map_only?(descr) do
1242-
map_put_static_descr(descr, key, type)
1242+
map_put_static_shared(descr, key, type)
12431243
else
12441244
:badmap
12451245
end
12461246

12471247
{dynamic, static} when static == @none ->
12481248
if descr_key?(dynamic, :map) do
1249-
dynamic(map_put_static_descr(dynamic, key, type))
1249+
dynamic(map_put_static_shared(dynamic, key, type))
12501250
else
12511251
:badmap
12521252
end
12531253

12541254
{dynamic, static} ->
12551255
if descr_key?(dynamic, :map) and map_only?(static) do
1256-
dynamic = map_put_static_descr(dynamic, key, type)
1257-
static = map_put_static_descr(static, key, type)
1256+
dynamic = map_put_static_shared(dynamic, key, type)
1257+
static = map_put_static_shared(static, key, type)
12581258
union(dynamic(dynamic), static)
12591259
else
12601260
:badmap
@@ -1263,7 +1263,7 @@ defmodule Module.Types.Descr do
12631263
end
12641264

12651265
# Directly inserts a key of a given type into every positive and negative map
1266-
defp map_put_static_descr(descr, key, type) do
1266+
defp map_put_static_shared(descr, key, type) do
12671267
case map_delete_static(descr, key) do
12681268
%{map: dnf} = descr ->
12691269
dnf =
@@ -1516,7 +1516,7 @@ defmodule Module.Types.Descr do
15161516

15171517
# Takes a map dnf and returns the union of types it can take for a given key.
15181518
# If the key may be undefined, it will contain the `not_set()` type.
1519-
defp map_get(dnf, key) do
1519+
defp map_fetch_dnf(dnf, key) do
15201520
Enum.reduce(dnf, none(), fn
15211521
# Optimization: if there are no negatives,
15221522
# we can return the value directly.

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

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -114,38 +114,36 @@ defmodule Module.Types.Expr do
114114
end
115115

116116
# %{map | ...}
117+
# TODO: Once we support typed structs, we need to type check them here.
117118
def of_expr({:%{}, _, [{:|, _, [map, args]}]}, stack, context) do
118-
{_args_type, context} = Of.closed_map(args, stack, context, &of_expr/3)
119-
{_map_type, context} = of_expr(map, stack, context)
120-
# TODO: intersect map with keys of terms for args
121-
# TODO: Merge args_type into map_type with dynamic/static key requirement
122-
{dynamic(open_map()), context}
119+
{map_type, context} = of_expr(map, stack, context)
120+
Of.update_map(map_type, args, stack, context, &of_expr/3)
123121
end
124122

125123
# %Struct{map | ...}
124+
# Note this code, by definition, adds missing struct fields to `map`
125+
# because at runtime we do not check for them (only for __struct__ itself).
126+
# TODO: Once we support typed structs, we need to type check them here.
126127
def of_expr(
127128
{:%, struct_meta, [module, {:%{}, _, [{:|, update_meta, [map, args]}]}]} = expr,
128129
stack,
129130
context
130131
) do
131-
{args_types, context} =
132-
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
133-
{type, context} = of_expr(value, stack, context)
134-
{{key, type}, context}
135-
end)
136-
137-
# TODO: args_types could be an empty list
138-
{struct_type, context} =
139-
Of.struct(module, args_types, :only_defaults, struct_meta, stack, context)
140-
132+
{info, context} = Of.struct_info(module, struct_meta, stack, context)
133+
struct_type = Of.struct_type(module, info)
141134
{map_type, context} = of_expr(map, stack, context)
142135

143136
if disjoint?(struct_type, map_type) do
144137
warning = {:badupdate, :struct, expr, struct_type, map_type, context}
145138
{error_type(), error(__MODULE__, warning, update_meta, stack, context)}
146139
else
147-
# TODO: Merge args_type into map_type with dynamic/static key requirement
148-
Of.struct(module, args_types, :merge_defaults, struct_meta, stack, context)
140+
map_type = map_put!(map_type, :__struct__, atom([module]))
141+
142+
Enum.reduce(args, {map_type, context}, fn
143+
{key, value}, {map_type, context} when is_atom(key) ->
144+
{value_type, context} = of_expr(value, stack, context)
145+
{map_put!(map_type, key, value_type), context}
146+
end)
149147
end
150148
end
151149

@@ -155,9 +153,8 @@ defmodule Module.Types.Expr do
155153
end
156154

157155
# %Struct{}
158-
def of_expr({:%, _, [module, {:%{}, _, args}]} = expr, stack, context) do
159-
# TODO: We should not skip defaults
160-
Of.struct(expr, module, args, :skip_defaults, stack, context, &of_expr/3)
156+
def of_expr({:%, meta, [module, {:%{}, _, args}]}, stack, context) do
157+
Of.struct_instance(module, args, meta, stack, context, &of_expr/3)
161158
end
162159

163160
# ()
@@ -347,7 +344,8 @@ defmodule Module.Types.Expr do
347344
# Exceptions are not validated in the compiler,
348345
# to avoid export dependencies. So we do it here.
349346
if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do
350-
Of.struct(exception, args, :merge_defaults, meta, stack, context)
347+
{info, context} = Of.struct_info(exception, meta, stack, context)
348+
{Of.struct_type(exception, info, args), context}
351349
else
352350
# If the exception cannot be found or is invalid,
353351
# we call Of.remote/5 to emit a warning.
@@ -483,6 +481,13 @@ defmodule Module.Types.Expr do
483481
context
484482
end
485483

484+
defp map_put!(map_type, key, value_type) do
485+
case map_put(map_type, key, value_type) do
486+
descr when is_descr(descr) -> descr
487+
error -> raise "unexpected #{inspect(error)}"
488+
end
489+
end
490+
486491
## Warning formatting
487492

488493
def format_diagnostic({:badupdate, type, expr, expected_type, actual_type, context}) do

lib/elixir/lib/module/types/of.ex

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,29 @@ defmodule Module.Types.Of do
107107
@doc """
108108
Builds a closed map.
109109
"""
110-
def closed_map(pairs, extra \\ [], stack, context, of_fun) do
110+
def closed_map(pairs, stack, context, of_fun) do
111+
key_permutations(pairs, stack, context, of_fun, fn closed?, pairs ->
112+
if closed?, do: closed_map(pairs), else: open_map(pairs)
113+
end)
114+
end
115+
116+
@doc """
117+
Updates a map with the given keys.
118+
"""
119+
def update_map(map_type, pairs, stack, context, of_fun) do
120+
key_permutations(pairs, stack, context, of_fun, fn _closed?, pairs ->
121+
Enum.reduce(pairs, map_type, fn {key, type}, acc ->
122+
case map_put(acc, key, type) do
123+
descr when is_descr(descr) -> descr
124+
error -> throw({error, key, type})
125+
end
126+
end)
127+
end)
128+
end
129+
130+
defp key_permutations(pairs, stack, context, of_fun, of_map) do
111131
{closed?, single, multiple, context} =
112-
Enum.reduce(pairs, {true, extra, [], context}, fn
132+
Enum.reduce(pairs, {true, [], [], context}, fn
113133
{key, value}, {closed?, single, multiple, context} ->
114134
{keys, context} = of_finite_key_type(key, stack, context, of_fun)
115135
{value_type, context} = of_fun.(value, stack, context)
@@ -129,13 +149,11 @@ defmodule Module.Types.Of do
129149
map =
130150
case Enum.reverse(multiple) do
131151
[] ->
132-
pairs = Enum.reverse(single)
133-
if closed?, do: closed_map(pairs), else: open_map(pairs)
152+
of_map.(closed?, Enum.reverse(single))
134153

135154
[{keys, type} | tail] ->
136155
for key <- keys, t <- cartesian_map(tail) do
137-
pairs = Enum.reverse(single, [{key, type} | t])
138-
if closed?, do: closed_map(pairs), else: open_map(pairs)
156+
of_map.(closed?, Enum.reverse(single, [{key, type} | t]))
139157
end
140158
|> Enum.reduce(&union/2)
141159
end
@@ -167,9 +185,9 @@ defmodule Module.Types.Of do
167185
end
168186

169187
@doc """
170-
Handles structs creation.
188+
Handles instantiation of a new struct.
171189
"""
172-
def struct({:%, meta, _}, struct, args, default_handling, stack, context, of_fun)
190+
def struct_instance(struct, args, meta, stack, context, of_fun)
173191
when is_atom(struct) do
174192
# The compiler has already checked the keys are atoms and which ones are required.
175193
{args_types, context} =
@@ -178,27 +196,8 @@ defmodule Module.Types.Of do
178196
{{key, type}, context}
179197
end)
180198

181-
struct(struct, args_types, default_handling, meta, stack, context)
182-
end
183-
184-
@doc """
185-
Struct handling assuming the args have already been converted.
186-
"""
187-
# TODO: Allow structs fields to be defined and validate args against the struct types.
188-
# TODO: Use the struct default values to define the default types.
189-
def struct(struct, args_types, default_handling, meta, stack, context) do
190199
{info, context} = struct_info(struct, meta, stack, context)
191-
term = term()
192-
defaults = for %{field: field} <- info, do: {field, term}
193-
194-
pairs =
195-
case default_handling do
196-
:merge_defaults -> [{:__struct__, atom([struct])} | defaults] ++ args_types
197-
:skip_defaults -> [{:__struct__, atom([struct])} | args_types]
198-
:only_defaults -> [{:__struct__, atom([struct])} | defaults]
199-
end
200-
201-
{closed_map(pairs), context}
200+
{struct_type(struct, info, args_types), context}
202201
end
203202

204203
@doc """
@@ -214,6 +213,17 @@ defmodule Module.Types.Of do
214213
{info, context}
215214
end
216215

216+
@doc """
217+
Builds a type from the struct info.
218+
"""
219+
def struct_type(struct, info, args_types \\ []) do
220+
term = term()
221+
pairs = for %{field: field} <- info, do: {field, term}
222+
pairs = [{:__struct__, atom([struct])} | pairs]
223+
pairs = if args_types == [], do: pairs, else: pairs ++ args_types
224+
closed_map(pairs)
225+
end
226+
217227
## Binary
218228

219229
@doc """

lib/elixir/lib/module/types/pattern.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,10 +657,10 @@ defmodule Module.Types.Pattern do
657657
end
658658

659659
# %Struct{...}
660-
def of_guard({:%, _, [module, {:%{}, _, args}]} = struct, _expected, _expr, stack, context)
660+
def of_guard({:%, meta, [module, {:%{}, _, args}]} = struct, _expected, _expr, stack, context)
661661
when is_atom(module) do
662662
fun = &of_guard(&1, dynamic(), struct, &2, &3)
663-
Of.struct(struct, module, args, :skip_defaults, stack, context, fun)
663+
Of.struct_instance(module, args, meta, stack, context, fun)
664664
end
665665

666666
# %{...}

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -477,9 +477,17 @@ defmodule Module.Types.ExprTest do
477477

478478
test "updating structs" do
479479
assert typecheck!([x], %Point{x | x: :zero}) ==
480-
closed_map(__struct__: atom([Point]), x: atom([:zero]), y: term(), z: term())
480+
dynamic(open_map(__struct__: atom([Point]), x: atom([:zero])))
481481

482-
assert typeerror!([x = :foo], %Point{x | x: :zero}) ==
482+
assert typecheck!([x], %Point{%Point{x | x: :zero} | y: :one}) ==
483+
dynamic(open_map(__struct__: atom([Point]), x: atom([:zero]), y: atom([:one])))
484+
485+
assert typeerror!(
486+
(
487+
x = %{x: 0}
488+
%Point{x | x: :zero}
489+
)
490+
) ==
483491
~l"""
484492
incompatible types in struct update:
485493
@@ -491,13 +499,13 @@ defmodule Module.Types.ExprTest do
491499
492500
but got type:
493501
494-
dynamic(:foo)
502+
%{x: integer()}
495503
496504
where "x" was given the type:
497505
498-
# type: dynamic(:foo)
499-
# from: types_test.ex:LINE-1
500-
x = :foo
506+
# type: %{x: integer()}
507+
# from: types_test.ex:LINE-4
508+
x = %{x: 0}
501509
"""
502510
end
503511

0 commit comments

Comments
 (0)