Skip to content

Commit 8167bb6

Browse files
authored
Move struct handling from unify to AST (#10320)
We also improve error messages to list all available fields and introduce Module.Types.Of module to hold shared functionality between Expr and Pattern.
1 parent ec8cde3 commit 8167bb6

File tree

11 files changed

+412
-465
lines changed

11 files changed

+412
-465
lines changed

lib/elixir/lib/module/types.ex

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -251,16 +251,19 @@ defmodule Module.Types do
251251

252252
def format_warning({:unable_unify, left, right, {location, expr, traces}}) do
253253
cond do
254-
map_type?(left) and map_type?(right) and match?({:ok, _}, missing_field(left, right)) ->
255-
{:ok, atom} = missing_field(left, right)
254+
map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) ->
255+
{:ok, atom, known_atoms} = missing_field(left, right)
256256

257257
# Drop the last trace which is the expression map.foo
258258
traces = Enum.drop(traces, 1)
259-
{traces, hints} = format_traces(traces, false)
259+
{traces, hints} = format_traces(traces, true)
260260

261261
[
262262
"undefined field \"#{atom}\" ",
263263
format_expr(expr, location),
264+
"expected one of the following fields: ",
265+
Enum.map_join(Enum.sort(known_atoms), ", ", & &1),
266+
"\n\n",
264267
traces,
265268
format_message_hints(hints),
266269
"Conflict found at"
@@ -290,26 +293,27 @@ defmodule Module.Types do
290293
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]},
291294
{:map, fields}
292295
) do
293-
if List.keymember?(fields, type, 1) do
294-
:error
295-
else
296-
{:ok, atom}
297-
end
296+
matched_missing_field(fields, type, atom)
298297
end
299298

300299
defp missing_field(
301300
{:map, fields},
302301
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]}
303302
) do
303+
matched_missing_field(fields, type, atom)
304+
end
305+
306+
defp missing_field(_, _), do: :error
307+
308+
defp matched_missing_field(fields, type, atom) do
304309
if List.keymember?(fields, type, 1) do
305310
:error
306311
else
307-
{:ok, atom}
312+
known_atoms = for {_, {:atom, atom}, _} <- fields, do: atom
313+
{:ok, atom, known_atoms}
308314
end
309315
end
310316

311-
defp missing_field(_, _), do: :error
312-
313317
defp format_traces([], _simplify?) do
314318
{[], []}
315319
end

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

Lines changed: 29 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Module.Types.Expr do
22
@moduledoc false
33

4-
alias Module.Types.{Remote, Pattern}
4+
alias Module.Types.{Of, Pattern}
55
import Module.Types.{Helpers, Infer}
66

77
def of_expr(expr, %{context: stack_context} = stack, context) when stack_context != :expr do
@@ -35,7 +35,7 @@ defmodule Module.Types.Expr do
3535

3636
# <<...>>>
3737
def of_expr({:<<>>, _meta, args}, stack, context) do
38-
result = of_binary(args, stack, context, &of_expr/3)
38+
result = Of.binary(args, stack, context, &of_expr/3)
3939

4040
case result do
4141
{:ok, context} -> {:ok, :binary, context}
@@ -142,41 +142,47 @@ defmodule Module.Types.Expr do
142142
def of_expr({:%{}, _, [{:|, _, [map, args]}]} = expr, stack, context) do
143143
stack = push_expr_stack(expr, stack)
144144

145-
additional_pairs = [{:optional, :dynamic, :dynamic}]
146-
map_update(map, args, additional_pairs, stack, context)
145+
with {:ok, map_type, context} <- of_expr(map, stack, context),
146+
{:ok, {:map, arg_pairs}, context} <- Of.closed_map(args, stack, context, &of_expr/3),
147+
dynamic_value_pairs =
148+
Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end),
149+
args_type = {:map, dynamic_value_pairs ++ [{:optional, :dynamic, :dynamic}]},
150+
{:ok, type, context} <- unify(args_type, map_type, stack, context) do
151+
# Retrieve map type and overwrite with the new value types from the map update
152+
{:map, pairs} = resolve_var(type, context)
153+
154+
updated_pairs =
155+
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
156+
List.keyreplace(pairs, key, 1, {:required, key, value})
157+
end)
158+
159+
{:ok, {:map, updated_pairs}, context}
160+
end
147161
end
148162

149163
# %Struct{map | ...}
150-
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [map, args]}]}]} = expr, stack, context) do
151-
context = Remote.check(module, :__struct__, 0, meta, context)
164+
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]} = expr, stack, context) do
152165
stack = push_expr_stack(expr, stack)
153166

154-
additional_pairs = [{:required, {:atom, :__struct__}, {:atom, module}}]
155-
map_update(map, args, additional_pairs, stack, context)
167+
with {:ok, struct, context} <- Of.struct(module, meta, context),
168+
{:ok, update, context} <- of_expr(update, stack, context) do
169+
unify(update, struct, stack, context)
170+
end
156171
end
157172

158173
# %{...}
159174
def of_expr({:%{}, _meta, args} = expr, stack, context) do
160175
stack = push_expr_stack(expr, stack)
161-
162-
case of_pairs(args, stack, context) do
163-
{:ok, pairs, context} -> {:ok, {:map, pairs_to_unions(pairs, context)}, context}
164-
{:error, reason} -> {:error, reason}
165-
end
176+
Of.closed_map(args, stack, context, &of_expr/3)
166177
end
167178

168179
# %Struct{...}
169180
def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context) do
170-
context = Remote.check(module, :__struct__, 0, meta1, context)
171181
stack = push_expr_stack(expr, stack)
172182

173-
case of_pairs(args, stack, context) do
174-
{:ok, pairs, context} ->
175-
pairs = [{:required, {:atom, :__struct__}, {:atom, module}} | pairs]
176-
{:ok, {:map, pairs}, context}
177-
178-
{:error, reason} ->
179-
{:error, reason}
183+
with {:ok, struct, context} <- Of.struct(module, meta1, context),
184+
{:ok, map, context} <- Of.open_map(args, stack, context, &of_expr/3) do
185+
unify(map, struct, stack, context)
180186
end
181187
end
182188

@@ -327,7 +333,7 @@ defmodule Module.Types.Expr do
327333

328334
# expr.fun(arg)
329335
def of_expr({{:., meta1, [expr1, fun]}, _meta2, args} = expr2, stack, context) do
330-
context = Remote.check(expr1, fun, length(args), meta1, context)
336+
context = Of.remote(expr1, fun, length(args), meta1, context)
331337
stack = push_expr_stack(expr2, stack)
332338

333339
with {:ok, _expr_type, context} <- of_expr(expr1, stack, context),
@@ -342,7 +348,7 @@ defmodule Module.Types.Expr do
342348
# &Foo.bar/1
343349
def of_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, _stack, context)
344350
when is_atom(module) and is_atom(fun) do
345-
context = Remote.check(module, fun, arity, meta, context)
351+
context = Of.remote(module, fun, arity, meta, context)
346352
{:ok, :dynamic, context}
347353
end
348354

@@ -363,51 +369,6 @@ defmodule Module.Types.Expr do
363369
end
364370
end
365371

366-
defp of_pairs(pairs, stack, context) do
367-
map_reduce_ok(pairs, context, fn {key, value}, context ->
368-
with {:ok, key_type, context} <- of_expr(key, stack, context),
369-
{:ok, value_type, context} <- of_expr(value, stack, context),
370-
do: {:ok, {:required, key_type, value_type}, context}
371-
end)
372-
end
373-
374-
defp pairs_to_unions(pairs, context) do
375-
# We are currently creating overlapping key types
376-
377-
Enum.reduce(pairs, [], fn {kind_left, key, value_left}, pairs ->
378-
case List.keyfind(pairs, key, 1) do
379-
{kind_right, ^key, value_right} ->
380-
kind = unify_kinds(kind_left, kind_right)
381-
value = to_union([value_left, value_right], context)
382-
List.keystore(pairs, key, 1, {kind, key, value})
383-
384-
nil ->
385-
[{kind_left, key, value_left} | pairs]
386-
end
387-
end)
388-
end
389-
390-
defp map_update(map, args, additional_pairs, stack, context) do
391-
with {:ok, map_type, context} <- of_expr(map, stack, context),
392-
{:ok, arg_pairs, context} <- of_pairs(args, stack, context),
393-
arg_pairs = pairs_to_unions(arg_pairs, context),
394-
# Change value types to dynamic to reuse map unification for map updates
395-
dynamic_value_pairs =
396-
Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end),
397-
args_type = {:map, additional_pairs ++ dynamic_value_pairs},
398-
{:ok, type, context} <- unify(args_type, map_type, stack, context) do
399-
# Retrieve map type and overwrite with the new value types from the map update
400-
{:map, pairs} = resolve_var(type, context)
401-
402-
updated_pairs =
403-
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
404-
List.keyreplace(pairs, key, 1, {:required, key, value})
405-
end)
406-
407-
{:ok, {:map, updated_pairs}, context}
408-
end
409-
end
410-
411372
defp for_clause({:<-, _, [left, expr]}, stack, context) do
412373
{pattern, guards} = extract_head([left])
413374

lib/elixir/lib/module/types/helpers.ex

Lines changed: 1 addition & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
defmodule Module.Types.Helpers do
2+
# AST and enumeration helpers.
23
@moduledoc false
34

4-
alias Module.Types.Infer
5-
6-
@prefix quote(do: ...)
7-
@suffix quote(do: ...)
8-
95
@doc """
106
Guard function to check if an AST node is a variable.
117
"""
@@ -157,103 +153,6 @@ defmodule Module.Types.Helpers do
157153
end
158154
end
159155

160-
@doc """
161-
Handles binaries.
162-
163-
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
164-
based on the position of the expression within the binary.
165-
"""
166-
def of_binary([], _stack, context, _fun) do
167-
{:ok, context}
168-
end
169-
170-
def of_binary([head], stack, context, fun) do
171-
head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack)
172-
of_binary_segment(head, head_stack, context, fun)
173-
end
174-
175-
def of_binary([head | tail], stack, context, fun) do
176-
head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack)
177-
178-
case of_binary_segment(head, head_stack, context, fun) do
179-
{:ok, context} -> of_binary_many(tail, stack, context, fun)
180-
{:error, reason} -> {:error, reason}
181-
end
182-
end
183-
184-
defp of_binary_many([last], stack, context, fun) do
185-
last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack)
186-
of_binary_segment(last, last_stack, context, fun)
187-
end
188-
189-
defp of_binary_many([head | tail], stack, context, fun) do
190-
head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack)
191-
192-
case of_binary_segment(head, head_stack, context, fun) do
193-
{:ok, context} -> of_binary_many(tail, stack, context, fun)
194-
{:error, reason} -> {:error, reason}
195-
end
196-
end
197-
198-
defp of_binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, fun) do
199-
expected_type =
200-
collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer
201-
202-
utf? = collect_binary_specifier(specifiers, &utf_type?/1)
203-
float? = collect_binary_specifier(specifiers, &float_type?/1)
204-
205-
# Special case utf and float specifiers because they can be two types as literals
206-
# but only a specific type as a variable in a pattern
207-
cond do
208-
stack.context == :pattern and utf? and is_binary(expr) ->
209-
{:ok, context}
210-
211-
stack.context == :pattern and float? and is_integer(expr) ->
212-
{:ok, context}
213-
214-
true ->
215-
with {:ok, type, context} <- fun.(expr, stack, context),
216-
{:ok, _type, context} <- Infer.unify(type, expected_type, stack, context),
217-
do: {:ok, context}
218-
end
219-
end
220-
221-
# TODO: Remove this clause once we properly handle comprehensions
222-
defp of_binary_segment({:<-, _, _}, _stack, context, _fun) do
223-
{:ok, context}
224-
end
225-
226-
# Collect binary type specifiers,
227-
# from `<<pattern::integer-size(10)>>` collect `integer`
228-
defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do
229-
collect_binary_specifier(left, fun) || collect_binary_specifier(right, fun)
230-
end
231-
232-
defp collect_binary_specifier(other, fun) do
233-
fun.(other)
234-
end
235-
236-
defp binary_type(:expr, {:float, _, _}), do: :number
237-
defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]}
238-
defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]}
239-
defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]}
240-
defp binary_type(:pattern, {:utf8, _, _}), do: :integer
241-
defp binary_type(:pattern, {:utf16, _, _}), do: :integer
242-
defp binary_type(:pattern, {:utf32, _, _}), do: :integer
243-
defp binary_type(:pattern, {:float, _, _}), do: :float
244-
defp binary_type(_context, {:integer, _, _}), do: :integer
245-
defp binary_type(_context, {:bits, _, _}), do: :binary
246-
defp binary_type(_context, {:bitstring, _, _}), do: :binary
247-
defp binary_type(_context, {:bytes, _, _}), do: :binary
248-
defp binary_type(_context, {:binary, _, _}), do: :binary
249-
defp binary_type(_context, _specifier), do: nil
250-
251-
defp utf_type?({specifier, _, _}), do: specifier in [:utf8, :utf16, :utf32]
252-
defp utf_type?(_), do: false
253-
254-
defp float_type?({:float, _, _}), do: true
255-
defp float_type?(_), do: false
256-
257156
# TODO: Remove this and let multiple when be treated as multiple clauses,
258157
# meaning they will be intersection types
259158
def guards_to_or([]) do

0 commit comments

Comments
 (0)