Skip to content

Commit a1c1888

Browse files
committed
Fix checking of closed maps
If a map has unknown keys, because we cannot specify term() => term() with the necessary precision today, we need to mark it as dynamic. We also improve the pretty printing of differences between composites types.
1 parent b15de1c commit a1c1888

File tree

4 files changed

+19
-4
lines changed

4 files changed

+19
-4
lines changed

lib/elixir/lib/module/types/apply.ex

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1140,7 +1140,7 @@ defmodule Module.Types.Apply do
11401140
defp args_to_quoted_string(args_types, domain, converter) do
11411141
docs =
11421142
Enum.zip_with(args_types, domain, fn actual, expected ->
1143-
if compatible?(actual, expected) do
1143+
if compatible?(actual, expected) or not has_simple_difference?(actual, expected) do
11441144
actual |> to_quoted() |> Code.Formatter.to_algebra()
11451145
else
11461146
common = intersection(actual, expected)
@@ -1163,6 +1163,21 @@ defmodule Module.Types.Apply do
11631163
args_docs_to_quoted_string(converter.(docs))
11641164
end
11651165

1166+
@composite_types non_empty_list(term(), term())
1167+
|> union(tuple())
1168+
|> union(open_map())
1169+
|> union(fun())
1170+
1171+
# If actual/expected have a composite type, computing the
1172+
# `intersection(actual, expected) or difference(actual, expected)`
1173+
# can lead to an explosion of terms that actually make debugging
1174+
# harder. So we check that at least one of the two operations
1175+
# return none() (i.e. actual is a subtype or they are disjoint).
1176+
defp has_simple_difference?(actual, expected) do
1177+
composite_types = intersection(actual, @composite_types)
1178+
subtype?(composite_types, expected) or disjoint?(composite_types, expected)
1179+
end
1180+
11661181
defp ansi_red(doc) do
11671182
if IO.ANSI.enabled?() do
11681183
IA.concat(IA.color(doc, IO.ANSI.red()), IA.color(IA.empty(), IO.ANSI.reset()))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ defmodule Module.Types.Expr do
214214
# TODO: Use the fallback type to actually indicate if open or closed.
215215
# The fallback must be unioned with the result of map_values with all
216216
# `keys` deleted.
217-
open_map(pairs)
217+
dynamic(open_map(pairs))
218218
end
219219
end)
220220
catch

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ defmodule Module.Types.Of do
176176
map =
177177
permutate_map(pairs_types, stack, fn fallback, _keys, pairs ->
178178
# TODO: Use the fallback type to actually indicate if open or closed.
179-
if fallback == none(), do: closed_map(pairs), else: open_map(pairs)
179+
if fallback == none(), do: closed_map(pairs), else: dynamic(open_map(pairs))
180180
end)
181181

182182
{map, context}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ defmodule Module.Types.ExprTest do
690690
end
691691

692692
test "creating open maps" do
693-
assert typecheck!(%{123 => 456}) == open_map()
693+
assert typecheck!(%{123 => 456}) == dynamic(open_map())
694694
# Since key cannot override :foo, we preserve it
695695
assert typecheck!([key], %{key => 456, foo: :bar}) == dynamic(open_map(foo: atom([:bar])))
696696
# Since key can override :foo, we do not preserve it

0 commit comments

Comments
 (0)