diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 0131a7bf828..31954ae45c1 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -83,8 +83,8 @@ defmodule Module.Types.Descr do @boolset :sets.from_list([true, false], version: 2) def boolean(), do: %{atom: {:union, @boolset}} - # Map helpers - # + ## Optional + # `not_set()` is a special base type that represents an not_set field in a map. # E.g., `%{a: integer(), b: not_set(), ...}` represents a map with an integer # field `a` and an not_set field `b`, and possibly other fields. @@ -153,12 +153,15 @@ defmodule Module.Types.Descr do ## Set operations - def term_type?(:term), do: true - def term_type?(descr), do: subtype_static?(unfolded_term(), Map.delete(descr, :dynamic)) - + @doc """ + Returns true if the type has a gradual part. + """ def gradual?(:term), do: false def gradual?(descr), do: is_map_key(descr, :dynamic) + @doc """ + Returns true if hte type only has a gradual part. + """ def only_gradual?(%{dynamic: _} = descr), do: map_size(descr) == 1 def only_gradual?(_), do: false @@ -175,11 +178,17 @@ defmodule Module.Types.Descr do end end + @compile {:inline, maybe_union: 2} + defp maybe_union(nil, _fun), do: nil + defp maybe_union(descr, fun), do: union(descr, fun.()) + @doc """ Computes the union of two descrs. """ def union(:term, other), do: optional_to_term(other) def union(other, :term), do: optional_to_term(other) + def union(none, other) when none == @none, do: other + def union(other, none) when none == @none, do: other def union(left, right) do left = unfold(left) @@ -190,18 +199,22 @@ defmodule Module.Types.Descr do cond do is_gradual_left and not is_gradual_right -> right_with_dynamic = Map.put(right, :dynamic, right) - symmetrical_merge(left, right_with_dynamic, &union/3) + union_static(left, right_with_dynamic) is_gradual_right and not is_gradual_left -> left_with_dynamic = Map.put(left, :dynamic, left) - symmetrical_merge(left_with_dynamic, right, &union/3) + union_static(left_with_dynamic, right) true -> - symmetrical_merge(left, right, &union/3) + union_static(left, right) end end - @compile {:inline, union: 3} + @compile {:inline, union_static: 2} + defp union_static(left, right) do + symmetrical_merge(left, right, &union/3) + end + defp union(:atom, v1, v2), do: atom_union(v1, v2) defp union(:bitmap, v1, v2), do: v1 ||| v2 defp union(:dynamic, v1, v2), do: dynamic_union(v1, v2) @@ -227,19 +240,23 @@ defmodule Module.Types.Descr do cond do is_gradual_left and not is_gradual_right -> right_with_dynamic = Map.put(right, :dynamic, right) - symmetrical_intersection(left, right_with_dynamic, &intersection/3) + intersection_static(left, right_with_dynamic) is_gradual_right and not is_gradual_left -> left_with_dynamic = Map.put(left, :dynamic, left) - symmetrical_intersection(left_with_dynamic, right, &intersection/3) + intersection_static(left_with_dynamic, right) true -> - symmetrical_intersection(left, right, &intersection/3) + intersection_static(left, right) end end + @compile {:inline, intersection_static: 2} + defp intersection_static(left, right) do + symmetrical_intersection(left, right, &intersection/3) + end + # Returning 0 from the callback is taken as none() for that subtype. - @compile {:inline, intersection: 3} defp intersection(:atom, v1, v2), do: atom_intersection(v1, v2) defp intersection(:bitmap, v1, v2), do: v1 &&& v2 defp intersection(:dynamic, v1, v2), do: dynamic_intersection(v1, v2) @@ -299,7 +316,6 @@ defmodule Module.Types.Descr do defp iterator_difference_static(:none, map), do: map # Returning 0 from the callback is taken as none() for that subtype. - @compile {:inline, difference: 3} defp difference(:atom, v1, v2), do: atom_difference(v1, v2) defp difference(:bitmap, v1, v2), do: v1 - (v1 &&& v2) defp difference(:dynamic, v1, v2), do: dynamic_difference(v1, v2) @@ -378,7 +394,6 @@ defmodule Module.Types.Descr do end end - @compile {:inline, to_quoted: 2} defp to_quoted(:atom, val), do: atom_to_quoted(val) defp to_quoted(:bitmap, val), do: bitmap_to_quoted(val) defp to_quoted(:dynamic, descr), do: dynamic_to_quoted(descr) @@ -513,6 +528,12 @@ defmodule Module.Types.Descr do end end + @doc """ + Optimized version of `not empty?(term(), type)`. + """ + def term_type?(:term), do: true + def term_type?(descr), do: subtype_static?(unfolded_term(), Map.delete(descr, :dynamic)) + @doc """ Optimized version of `not empty?(intersection(empty_list(), type))`. """ @@ -1061,7 +1082,7 @@ defmodule Module.Types.Descr do end end - # TODO: Eliminate empty lists from the union + # TODO: Eliminate empty lists from the union. defp list_normalize(dnf), do: dnf # Enum.filter(dnf, fn {list_type, last_type, negs} -> # not Enum.any?(negs, fn neg -> subtype?(list_type, neg) end) @@ -1199,130 +1220,8 @@ defmodule Module.Types.Descr do defp map_new(tag, fields = %{}), do: [{tag, fields, []}] - @doc """ - Fetches the type of the value returned by accessing `key` on `map` - with the assumption that the descr is exclusively a map (or dynamic). - - It returns a two element tuple or `:error`. The first element says - if the type is dynamically optional or not, the second element is - the type. In static mode, optional keys are not allowed. - """ - def map_fetch(:term, _key), do: :badmap - - def map_fetch(%{} = descr, key) when is_atom(key) do - case :maps.take(:dynamic, descr) do - :error -> - if descr_key?(descr, :map) and map_only?(descr) do - {static_optional?, static_type} = map_fetch_static(descr, key) - - if static_optional? or empty?(static_type) do - :badkey - else - {false, static_type} - end - else - :badmap - end - - {dynamic, static} -> - if descr_key?(dynamic, :map) and map_only?(static) do - {dynamic_optional?, dynamic_type} = map_fetch_static(dynamic, key) - {static_optional?, static_type} = map_fetch_static(static, key) - - if static_optional? or empty?(dynamic_type) do - :badkey - else - {dynamic_optional?, union(dynamic(dynamic_type), static_type)} - end - else - :badmap - end - end - end - defp map_only?(descr), do: empty?(Map.delete(descr, :map)) - defp map_fetch_static(:term, _key), do: {true, term()} - - defp map_fetch_static(descr, key) when is_atom(key) do - case descr do - # Optimization: if the key does not exist in the map, - # avoid building if_set/not_set pairs and return the - # popped value directly. - %{map: [{tag, fields, []}]} when not is_map_key(fields, key) -> - case tag do - :open -> {true, term()} - :closed -> {true, none()} - end - - %{map: map} -> - map_get(map, key) |> pop_optional_static() - - %{} -> - {false, none()} - end - end - - @doc """ - Adds a `key` of a given type, assuming that the descr is exclusively - a map (or dynamic). - """ - def map_put(:term, _key, _type), do: :badmap - def map_put(descr, key, :term) when is_atom(key), do: map_put_static_value(descr, key, :term) - - def map_put(descr, key, type) when is_atom(key) do - case :maps.take(:dynamic, type) do - :error -> map_put_static_value(descr, key, type) - {dynamic, _static} -> dynamic(map_put_static_value(descr, key, dynamic)) - end - end - - defp map_put_static_value(descr, key, type) do - case :maps.take(:dynamic, descr) do - :error -> - if map_only?(descr) do - map_put_static_descr(descr, key, type) - else - :badmap - end - - {dynamic, static} when static == @none -> - if descr_key?(dynamic, :map) do - dynamic(map_put_static_descr(dynamic, key, type)) - else - :badmap - end - - {dynamic, static} -> - if descr_key?(dynamic, :map) and map_only?(static) do - dynamic = map_put_static_descr(dynamic, key, type) - static = map_put_static_descr(static, key, type) - union(dynamic(dynamic), static) - else - :badmap - end - end - end - - # Directly inserts a key of a given type into every positive and negative map - defp map_put_static_descr(descr, key, type) do - case map_delete_static(descr, key) do - %{map: dnf} = descr -> - dnf = - Enum.map(dnf, fn {tag, fields, negs} -> - {tag, Map.put(fields, key, type), - Enum.map(negs, fn {neg_tag, neg_fields} -> - {neg_tag, Map.put(neg_fields, key, type)} - end)} - end) - - %{descr | map: dnf} - - %{} -> - descr - end - end - # Union is list concatenation defp map_union(dnf1, dnf2), do: dnf1 ++ dnf2 @@ -1437,8 +1336,159 @@ defmodule Module.Types.Descr do end end + @doc """ + Fetches the type of the value returned by accessing `key` on `map` + with the assumption that the descr is exclusively a map (or dynamic). + + It returns a two element tuple or `:error`. The first element says + if the type is dynamically optional or not, the second element is + the type. In static mode, optional keys are not allowed. + """ + def map_fetch(:term, _key), do: :badmap + + def map_fetch(%{} = descr, key) when is_atom(key) do + case :maps.take(:dynamic, descr) do + :error -> + if descr_key?(descr, :map) and map_only?(descr) do + {static_optional?, static_type} = map_fetch_static(descr, key) + + if static_optional? or empty?(static_type) do + :badkey + else + {false, static_type} + end + else + :badmap + end + + {dynamic, static} -> + if descr_key?(dynamic, :map) and map_only?(static) do + {dynamic_optional?, dynamic_type} = map_fetch_static(dynamic, key) + {static_optional?, static_type} = map_fetch_static(static, key) + + if static_optional? or empty?(dynamic_type) do + :badkey + else + {dynamic_optional?, union(dynamic(dynamic_type), static_type)} + end + else + :badmap + end + end + end + + # Optimization: if the key does not exist in the map, avoid building + # if_set/not_set pairs and return the popped value directly. + defp map_fetch_static(%{map: [{tag, fields, []}]}, key) when not is_map_key(fields, key) do + case tag do + :open -> {true, term()} + :closed -> {true, none()} + end + end + + # Takes a map dnf and returns the union of types it can take for a given key. + # If the key may be undefined, it will contain the `not_set()` type. + defp map_fetch_static(%{map: dnf}, key) do + dnf + |> Enum.reduce(none(), fn + # Optimization: if there are no negatives, + # we can return the value directly. + {_tag, %{^key => value}, []}, acc -> + value |> union(acc) + + # Optimization: if there are no negatives + # and the key does not exist, return the default one. + {tag, %{}, []}, acc -> + tag_to_type(tag) |> union(acc) + + {tag, fields, negs}, acc -> + {fst, snd} = map_pop_key(tag, fields, key) + + case map_split_negative(negs, key) do + :empty -> + acc + + negative -> + negative + |> pair_make_disjoint() + |> pair_eliminate_negations_fst(fst, snd) + |> union(acc) + end + end) + |> pop_optional_static() + end + + defp map_fetch_static(%{}, _key), do: {false, none()} + defp map_fetch_static(:term, _key), do: {true, term()} + + @doc """ + Fetches and puts a `key` of a given type, assuming that the descr is exclusively + a map (or dynamic). + """ + def map_fetch_and_put(:term, _key, _type), do: :badmap + + def map_fetch_and_put(descr, key, :term) when is_atom(key), + do: map_fetch_and_put_shared(descr, key, :term) + + def map_fetch_and_put(descr, key, type) when is_atom(key) do + case :maps.take(:dynamic, type) do + :error -> map_fetch_and_put_shared(descr, key, type) + {dynamic, _static} -> map_fetch_and_put_shared(dynamic(descr), key, dynamic) + end + end + + defp map_fetch_and_put_shared(descr, key, type) do + map_take(descr, key, none(), &map_put_static(&1, key, type)) + end + + @doc """ + Puts a `key` of a given type, assuming that the descr is exclusively + a map (or dynamic). + """ + def map_put(:term, _key, _type), do: :badmap + def map_put(descr, key, :term) when is_atom(key), do: map_put_shared(descr, key, :term) + + def map_put(descr, key, type) when is_atom(key) do + case :maps.take(:dynamic, type) do + :error -> map_put_shared(descr, key, type) + {dynamic, _static} -> map_put_shared(dynamic(descr), key, dynamic) + end + end + + defp map_put_shared(descr, key, type) do + with {nil, descr} <- map_take(descr, key, nil, &map_put_static(&1, key, type)) do + {:ok, descr} + end + end + + # Directly inserts a key of a given type into every positive and negative map. + defp map_put_static(%{map: dnf} = descr, key, type) do + dnf = + Enum.map(dnf, fn {tag, fields, negs} -> + {tag, Map.put(fields, key, type), + Enum.map(negs, fn {neg_tag, neg_fields} -> + {neg_tag, Map.put(neg_fields, key, type)} + end)} + end) + + %{descr | map: dnf} + end + + defp map_put_static(descr, _key, _type), do: descr + @doc """ Removes a key from a map type. + """ + def map_delete(descr, key) do + # We pass nil as the initial value so we can avoid computing the unions. + with {nil, descr} <- + map_take(descr, key, nil, &intersection_static(&1, open_map([{key, not_set()}]))) do + {:ok, descr} + end + end + + @doc """ + Removes a key from a map type and return its type. ## Algorithm @@ -1448,26 +1498,39 @@ defmodule Module.Types.Descr do 3. Intersect this with an open record type where the key is explicitly absent. This step eliminates the key from open record types where it was implicitly present. """ - def map_delete(:term, _key), do: :badmap + def map_take(descr, key) do + map_take(descr, key, none(), &intersection_static(&1, open_map([{key, not_set()}]))) + end + + @compile {:inline, map_take: 4} + defp map_take(:term, _key, _initial, _updater), do: :badmap - def map_delete(descr, key) when is_atom(key) do + defp map_take(descr, key, initial, updater) when is_atom(key) do case :maps.take(:dynamic, descr) do :error -> - # Note: the empty typ is not a valid input if descr_key?(descr, :map) and map_only?(descr) do - map_delete_static(descr, key) - |> intersection(open_map([{key, not_set()}])) + {optional?, taken, result} = map_take_static(descr, key, initial) + + cond do + taken == nil -> {nil, updater.(result)} + optional? or empty?(taken) -> :badkey + true -> {taken, updater.(result)} + end else :badmap end {dynamic, static} -> if descr_key?(dynamic, :map) and map_only?(static) do - dynamic_result = map_delete_static(dynamic, key) - static_result = map_delete_static(static, key) - - union(dynamic(dynamic_result), static_result) - |> intersection(open_map([{key, not_set()}])) + {_, dynamic_taken, dynamic_result} = map_take_static(dynamic, key, initial) + {static_optional?, static_taken, static_result} = map_take_static(static, key, initial) + result = union(dynamic(updater.(dynamic_result)), updater.(static_result)) + + cond do + static_taken == nil and dynamic_taken == nil -> {nil, result} + static_optional? or empty?(dynamic_taken) -> :badkey + true -> {union(dynamic(dynamic_taken), static_taken), result} + end else :badmap end @@ -1475,35 +1538,53 @@ defmodule Module.Types.Descr do end # Takes a static map type and removes a key from it. - defp map_delete_static(%{map: dnf}, key) do - Enum.reduce(dnf, none(), fn - # Optimization: if there are no negatives, we can directly remove the key. - {tag, fields, []}, acc -> - union(acc, %{map: map_new(tag, :maps.remove(key, fields))}) + # This allows the key to be put or deleted later on. + defp map_take_static(%{map: [{tag, fields, []}]} = descr, key, initial) + when not is_map_key(fields, key) do + case tag do + :open -> {true, maybe_union(initial, fn -> term() end), descr} + :closed -> {true, initial, descr} + end + end - {tag, fields, negs}, acc -> - {fst, snd} = map_pop_key(tag, fields, key) + defp map_take_static(%{map: dnf}, key, initial) do + {value, map} = + Enum.reduce(dnf, {initial, none()}, fn + # Optimization: if there are no negatives, we can directly remove the key. + {tag, fields, []}, {value, map} -> + {fst, snd} = map_pop_key(tag, fields, key) + {maybe_union(value, fn -> fst end), union(map, snd)} + + {tag, fields, negs}, {value, map} -> + {fst, snd} = map_pop_key(tag, fields, key) - union( - acc, case map_split_negative(negs, key) do :empty -> - none() + {value, map} negative -> - negative |> pair_make_disjoint() |> pair_eliminate_negations_snd(fst, snd) + disjoint = pair_make_disjoint(negative) + + {maybe_union(value, fn -> pair_eliminate_negations_fst(disjoint, fst, snd) end), + disjoint |> pair_eliminate_negations_snd(fst, snd) |> union(map)} end - ) - end) - end + end) - defp map_delete_static(:term, key), do: open_map([{key, not_set()}]) + if value == nil do + {false, value, map} + else + {optional?, value} = pop_optional_static(value) + {optional?, value, map} + end + end # If there is no map part to this static type, there is nothing to delete. - defp map_delete_static(_type, _key), do: none() + defp map_take_static(%{}, _key, initial), do: {false, initial, none()} + + defp map_take_static(:term, _key, initial) do + {true, maybe_union(initial, fn -> term() end), open_map()} + end - # Emptiness checking for maps. - # # Short-circuits if it finds a non-empty map literal in the union. # Since the algorithm is recursive, we implement the short-circuiting # as throw/catch. @@ -1556,31 +1637,6 @@ defmodule Module.Types.Descr do end)) or map_empty?(tag, fields, negs) end - # Takes a map dnf and returns the union of types it can take for a given key. - # If the key may be undefined, it will contain the `not_set()` type. - defp map_get(dnf, key) do - Enum.reduce(dnf, none(), fn - # Optimization: if there are no negatives, - # we can return the value directly. - {_tag, %{^key => value}, []}, acc -> - value |> union(acc) - - # Optimization: if there are no negatives - # and the key does not exist, return the default one. - {tag, %{}, []}, acc -> - tag_to_type(tag) |> union(acc) - - {tag, fields, negs}, acc -> - {fst, snd} = map_pop_key(tag, fields, key) - - case map_split_negative(negs, key) do - :empty -> none() - negative -> negative |> pair_make_disjoint() |> pair_eliminate_negations_fst(fst, snd) - end - |> union(acc) - end) - end - defp map_pop_key(tag, fields, key) do case :maps.take(key, fields) do {value, fields} -> {value, %{map: map_new(tag, fields)}} @@ -1597,7 +1653,7 @@ defmodule Module.Types.Descr do end # Use heuristics to normalize a map dnf for pretty printing. - # TODO: eliminate some simple negations, those which have only zero or one key in common. + # TODO: Eliminate some simple negations, those which have only zero or one key in common. defp map_normalize(dnf) do dnf |> Enum.reject(&map_empty?([&1])) @@ -1989,10 +2045,15 @@ defmodule Module.Types.Descr do {fst, snd} = tuple_pop_index(tag, elements, index) case tuple_split_negative(negs, index) do - :empty -> none() - negative -> negative |> pair_make_disjoint() |> pair_eliminate_negations_fst(fst, snd) + :empty -> + acc + + negative -> + negative + |> pair_make_disjoint() + |> pair_eliminate_negations_fst(fst, snd) + |> union(acc) end - |> union(acc) end) end @@ -2077,13 +2138,16 @@ defmodule Module.Types.Descr do {tag, elements, negs}, acc -> {fst, snd} = tuple_pop_index(tag, elements, index) - union( - acc, - case tuple_split_negative(negs, index) do - :empty -> none() - negative -> negative |> pair_make_disjoint() |> pair_eliminate_negations_snd(fst, snd) - end - ) + case tuple_split_negative(negs, index) do + :empty -> + acc + + negative -> + negative + |> pair_make_disjoint() + |> pair_eliminate_negations_snd(fst, snd) + |> union(acc) + end end) end @@ -2181,7 +2245,7 @@ defmodule Module.Types.Descr do end ## Pairs - # + # To simplify disjunctive normal forms of e.g., map types, it is useful to # convert them into disjunctive normal forms of pairs of types, and define # normalization algorithms on pairs. diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 29ededaec9c..ef46be2cc5d 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -114,38 +114,67 @@ defmodule Module.Types.Expr do end # %{map | ...} - def of_expr({:%{}, _, [{:|, _, [map, args]}]}, stack, context) do - {_args_type, context} = Of.closed_map(args, stack, context, &of_expr/3) - {_map_type, context} = of_expr(map, stack, context) - # TODO: intersect map with keys of terms for args - # TODO: Merge args_type into map_type with dynamic/static key requirement - {dynamic(open_map()), context} + # TODO: Once we support typed structs, we need to type check them here. + def of_expr({:%{}, meta, [{:|, _, [map, args]}]} = expr, stack, context) do + {map_type, context} = of_expr(map, stack, context) + + Of.permutate_map(args, stack, context, &of_expr/3, fn fallback, keys, pairs -> + # If there is no fallback (i.e. it is closed), we can update the existing map, + # otherwise we only assert the existing keys. + keys = if fallback == none(), do: keys, else: Enum.map(pairs, &elem(&1, 0)) ++ keys + + # Assert the keys exist + Enum.each(keys, fn key -> + case map_fetch(map_type, key) do + {_, _} -> :ok + :badkey -> throw({:badkey, map_type, key, expr, context}) + :badmap -> throw({:badmap, map_type, expr, context}) + end + end) + + if fallback == none() do + Enum.reduce(pairs, map_type, fn {key, type}, acc -> + case map_fetch_and_put(acc, key, type) do + {_value, descr} -> descr + :badkey -> throw({:badkey, map_type, key, expr, context}) + :badmap -> throw({:badmap, map_type, expr, context}) + end + end) + else + # TODO: Use the fallback type to actually indicate if open or closed. + # The fallback must be unioned with the result of map_values with all + # `keys` deleted. + open_map(pairs) + end + end) + catch + error -> {error_type(), error(__MODULE__, error, meta, stack, context)} end # %Struct{map | ...} + # Note this code, by definition, adds missing struct fields to `map` + # because at runtime we do not check for them (only for __struct__ itself). + # TODO: Once we support typed structs, we need to type check them here. def of_expr( {:%, struct_meta, [module, {:%{}, _, [{:|, update_meta, [map, args]}]}]} = expr, stack, context ) do - {args_types, context} = - Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) -> - {type, context} = of_expr(value, stack, context) - {{key, type}, context} - end) - - # TODO: args_types could be an empty list - {struct_type, context} = - Of.struct(module, args_types, :only_defaults, struct_meta, stack, context) - + {info, context} = Of.struct_info(module, struct_meta, stack, context) + struct_type = Of.struct_type(module, info) {map_type, context} = of_expr(map, stack, context) if disjoint?(struct_type, map_type) do - warning = {:badupdate, :struct, expr, struct_type, map_type, context} + warning = {:badstruct, expr, struct_type, map_type, context} {error_type(), error(__MODULE__, warning, update_meta, stack, context)} else - # TODO: Merge args_type into map_type with dynamic/static key requirement - Of.struct(module, args_types, :merge_defaults, struct_meta, stack, context) + map_type = map_put!(map_type, :__struct__, atom([module])) + + Enum.reduce(args, {map_type, context}, fn + {key, value}, {map_type, context} when is_atom(key) -> + {value_type, context} = of_expr(value, stack, context) + {map_put!(map_type, key, value_type), context} + end) end end @@ -155,9 +184,8 @@ defmodule Module.Types.Expr do end # %Struct{} - def of_expr({:%, _, [module, {:%{}, _, args}]} = expr, stack, context) do - # TODO: We should not skip defaults - Of.struct(expr, module, args, :skip_defaults, stack, context, &of_expr/3) + def of_expr({:%, meta, [module, {:%{}, _, args}]}, stack, context) do + Of.struct_instance(module, args, meta, stack, context, &of_expr/3) end # () @@ -375,7 +403,8 @@ defmodule Module.Types.Expr do # Exceptions are not validated in the compiler, # to avoid export dependencies. So we do it here. if Code.ensure_loaded?(exception) and function_exported?(exception, :__struct__, 0) do - Of.struct(exception, args, :merge_defaults, meta, stack, context) + {info, context} = Of.struct_info(exception, meta, stack, context) + {Of.struct_type(exception, info, args), context} else # If the exception cannot be found or is invalid, # we call Of.remote/5 to emit a warning. @@ -515,9 +544,16 @@ defmodule Module.Types.Expr do context end + defp map_put!(map_type, key, value_type) do + case map_put(map_type, key, value_type) do + {:ok, descr} -> descr + error -> raise "unexpected #{inspect(error)}" + end + end + ## Warning formatting - def format_diagnostic({:badupdate, type, expr, expected_type, actual_type, context}) do + def format_diagnostic({:badstruct, expr, expected_type, actual_type, context}) do traces = collect_traces(expr, context) %{ @@ -525,7 +561,7 @@ defmodule Module.Types.Expr do message: IO.iodata_to_binary([ """ - incompatible types in #{type} update: + incompatible types in struct update: #{expr_to_string(expr) |> indent(4)} @@ -542,6 +578,48 @@ defmodule Module.Types.Expr do } end + def format_diagnostic({:badmap, type, expr, context}) do + traces = collect_traces(expr, context) + + %{ + details: %{typing_traces: traces}, + message: + IO.iodata_to_binary([ + """ + expected a map within map update syntax: + + #{expr_to_string(expr) |> indent(4)} + + but got type: + + #{to_quoted_string(type) |> indent(4)} + """, + format_traces(traces) + ]) + } + end + + def format_diagnostic({:badkey, type, key, expr, context}) do + traces = collect_traces(expr, context) + + %{ + details: %{typing_traces: traces}, + message: + IO.iodata_to_binary([ + """ + expected a map with key #{inspect(key)} in map update syntax: + + #{expr_to_string(expr) |> indent(4)} + + but got type: + + #{to_quoted_string(type) |> indent(4)} + """, + format_traces(traces) + ]) + } + end + def format_diagnostic({:badbinary, type, expr, context}) do traces = collect_traces(expr, context) diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index cfed6facdbe..a6f91b602ab 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -107,52 +107,73 @@ defmodule Module.Types.Of do @doc """ Builds a closed map. """ - def closed_map(pairs, extra \\ [], stack, context, of_fun) do - {closed?, single, multiple, context} = - Enum.reduce(pairs, {true, extra, [], context}, fn - {key, value}, {closed?, single, multiple, context} -> - {keys, context} = of_finite_key_type(key, stack, context, of_fun) + def closed_map(pairs, stack, context, of_fun) do + permutate_map(pairs, stack, context, of_fun, fn fallback, _keys, pairs -> + # TODO: Use the fallback type to actually indicate if open or closed. + if fallback == none(), do: closed_map(pairs), else: open_map(pairs) + end) + end + + @doc """ + Builds permutation of maps according to the given keys. + """ + def permutate_map(pairs, stack, context, of_fun, of_map) do + {dynamic?, fallback, single, multiple, assert, context} = + Enum.reduce(pairs, {false, none(), [], [], [], context}, fn + {key, value}, {dynamic?, fallback, single, multiple, assert, context} -> + {dynamic_key?, keys, context} = of_finite_key_type(key, stack, context, of_fun) {value_type, context} = of_fun.(value, stack, context) + dynamic? = dynamic? or dynamic_key? or gradual?(value_type) case keys do :none -> - {false, single, multiple, context} + fallback = union(fallback, value_type) + + {fallback, assert} = + Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} -> + {union(fallback, type), [key | assert]} + end) + + {fallback, assert} = + Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} -> + {union(fallback, type), keys ++ assert} + end) + + {dynamic?, fallback, [], [], assert, context} [key] when multiple == [] -> - {closed?, [{key, value_type} | single], multiple, context} + {dynamic?, fallback, [{key, value_type} | single], multiple, assert, context} keys -> - {closed?, single, [{keys, value_type} | multiple], context} + {dynamic?, fallback, single, [{keys, value_type} | multiple], assert, context} end end) map = case Enum.reverse(multiple) do [] -> - pairs = Enum.reverse(single) - if closed?, do: closed_map(pairs), else: open_map(pairs) + of_map.(fallback, Enum.uniq(assert), Enum.reverse(single)) [{keys, type} | tail] -> for key <- keys, t <- cartesian_map(tail) do - pairs = Enum.reverse(single, [{key, type} | t]) - if closed?, do: closed_map(pairs), else: open_map(pairs) + of_map.(fallback, Enum.uniq(assert), Enum.reverse(single, [{key, type} | t])) end |> Enum.reduce(&union/2) end - {map, context} + if dynamic?, do: {dynamic(map), context}, else: {map, context} end defp of_finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do - {[key], context} + {false, [key], context} end defp of_finite_key_type(key, stack, context, of_fun) do {key_type, context} = of_fun.(key, stack, context) case atom_fetch(key_type) do - {:finite, list} -> {list, context} - _ -> {:none, context} + {:finite, list} -> {gradual?(key_type), list, context} + _ -> {gradual?(key_type), :none, context} end end @@ -167,9 +188,9 @@ defmodule Module.Types.Of do end @doc """ - Handles structs creation. + Handles instantiation of a new struct. """ - def struct({:%, meta, _}, struct, args, default_handling, stack, context, of_fun) + def struct_instance(struct, args, meta, stack, context, of_fun) when is_atom(struct) do # The compiler has already checked the keys are atoms and which ones are required. {args_types, context} = @@ -178,27 +199,8 @@ defmodule Module.Types.Of do {{key, type}, context} end) - struct(struct, args_types, default_handling, meta, stack, context) - end - - @doc """ - Struct handling assuming the args have already been converted. - """ - # TODO: Allow structs fields to be defined and validate args against the struct types. - # TODO: Use the struct default values to define the default types. - def struct(struct, args_types, default_handling, meta, stack, context) do {info, context} = struct_info(struct, meta, stack, context) - term = term() - defaults = for %{field: field} <- info, do: {field, term} - - pairs = - case default_handling do - :merge_defaults -> [{:__struct__, atom([struct])} | defaults] ++ args_types - :skip_defaults -> [{:__struct__, atom([struct])} | args_types] - :only_defaults -> [{:__struct__, atom([struct])} | defaults] - end - - {closed_map(pairs), context} + {struct_type(struct, info, args_types), context} end @doc """ @@ -214,6 +216,17 @@ defmodule Module.Types.Of do {info, context} end + @doc """ + Builds a type from the struct info. + """ + def struct_type(struct, info, args_types \\ []) do + term = term() + pairs = for %{field: field} <- info, do: {field, term} + pairs = [{:__struct__, atom([struct])} | pairs] + pairs = if args_types == [], do: pairs, else: pairs ++ args_types + closed_map(pairs) + end + ## Binary @doc """ diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 204f4950a3b..a0b6f60c9ec 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -657,10 +657,10 @@ defmodule Module.Types.Pattern do end # %Struct{...} - def of_guard({:%, _, [module, {:%{}, _, args}]} = struct, _expected, _expr, stack, context) + def of_guard({:%, meta, [module, {:%{}, _, args}]} = struct, _expected, _expr, stack, context) when is_atom(module) do fun = &of_guard(&1, dynamic(), struct, &2, &3) - Of.struct(struct, module, args, :skip_defaults, stack, context, fun) + Of.struct_instance(module, args, meta, stack, context, fun) end # %{...} diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 331eccec646..316087829d0 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -852,13 +852,12 @@ defmodule Module.Types.DescrTest do assert map_fetch(term(), :a) == :badmap assert map_fetch(union(open_map(), integer()), :a) == :badmap - assert map_fetch(closed_map(a: integer()), :a) == {false, integer()} - - assert map_fetch(union(closed_map(a: integer()), closed_map(b: atom())), :a) == - :badkey + assert map_fetch(open_map(), :a) == :badkey + assert map_fetch(open_map(a: not_set()), :a) == :badkey + assert map_fetch(union(closed_map(a: integer()), closed_map(b: atom())), :a) == :badkey + assert map_fetch(difference(closed_map(a: integer()), closed_map(a: term())), :a) == :badkey - assert map_fetch(difference(closed_map(a: integer()), closed_map(a: term())), :a) == - :badkey + assert map_fetch(closed_map(a: integer()), :a) == {false, integer()} assert map_fetch(union(closed_map(a: integer()), closed_map(a: atom())), :a) == {false, union(integer(), atom())} @@ -945,96 +944,164 @@ defmodule Module.Types.DescrTest do assert map_delete(term(), :a) == :badmap assert map_delete(integer(), :a) == :badmap assert map_delete(union(open_map(), integer()), :a) == :badmap - assert map_delete(closed_map(a: integer(), b: atom()), :a) == closed_map(b: atom()) - assert map_delete(empty_map(), :a) == empty_map() - assert map_delete(closed_map(a: if_set(integer()), b: atom()), :a) == closed_map(b: atom()) + assert map_delete(closed_map(a: integer(), b: atom()), :a) == {:ok, closed_map(b: atom())} + assert map_delete(empty_map(), :a) == {:ok, empty_map()} + + assert map_delete(closed_map(a: if_set(integer()), b: atom()), :a) == + {:ok, closed_map(b: atom())} # Deleting a non-existent key assert map_delete(closed_map(a: integer(), b: atom()), :c) == - closed_map(a: integer(), b: atom()) + {:ok, closed_map(a: integer(), b: atom())} + + # Deleting from a dynamic map + assert map_delete(dynamic(), :a) == {:ok, dynamic(open_map(a: not_set()))} # Deleting from an open map - assert map_delete(open_map(a: integer(), b: atom()), :a) - |> equal?(open_map(a: not_set(), b: atom())) + {:ok, type} = map_delete(open_map(a: integer(), b: atom()), :a) + assert equal?(type, open_map(a: not_set(), b: atom())) # Deleting from a union of maps - assert map_delete(union(closed_map(a: integer()), closed_map(b: atom())), :a) - |> equal?(union(empty_map(), closed_map(b: atom()))) + {:ok, type} = map_delete(union(closed_map(a: integer()), closed_map(b: atom())), :a) + assert equal?(type, union(empty_map(), closed_map(b: atom()))) + + # Deleting from a gradual map + {:ok, type} = map_delete(union(dynamic(), closed_map(a: integer())), :a) + assert equal?(type, union(dynamic(open_map(a: not_set())), empty_map())) + + {:ok, type} = map_delete(dynamic(open_map(a: not_set())), :b) + assert equal?(type, dynamic(open_map(a: not_set(), b: not_set()))) + + # Deleting from an intersection of maps + {:ok, type} = map_delete(intersection(open_map(a: integer()), open_map(b: atom())), :a) + assert equal?(type, open_map(a: not_set(), b: atom())) + + # Deleting from a difference of maps + {:ok, type} = + map_delete(difference(closed_map(a: integer(), b: atom()), closed_map(a: integer())), :b) + + assert equal?(type, closed_map(a: integer())) + + {:ok, type} = map_delete(difference(open_map(), open_map(a: not_set())), :a) + assert equal?(type, open_map(a: not_set())) + end + + test "map_take" do + assert map_take(term(), :a) == :badmap + assert map_take(integer(), :a) == :badmap + assert map_take(union(open_map(), integer()), :a) == :badmap + + assert map_take(closed_map(a: integer(), b: atom()), :a) == + {integer(), closed_map(b: atom())} + + # Deleting a non-existent key + assert map_take(empty_map(), :a) == :badkey + assert map_take(closed_map(a: integer(), b: atom()), :c) == :badkey + assert map_take(closed_map(a: if_set(integer()), b: atom()), :a) == :badkey # Deleting from a dynamic map - assert map_delete(dynamic(), :a) == dynamic(open_map(a: not_set())) + assert map_take(dynamic(), :a) == {dynamic(), dynamic(open_map(a: not_set()))} + + # Deleting from an open map + {value, type} = map_take(open_map(a: integer(), b: atom()), :a) + assert value == integer() + assert equal?(type, open_map(a: not_set(), b: atom())) + + # Deleting from a union of maps + union = union(closed_map(a: integer()), closed_map(b: atom())) + assert map_take(union, :a) == :badkey + {value, type} = map_take(dynamic(union), :a) + assert value == dynamic(integer()) + assert equal?(type, dynamic(union(empty_map(), closed_map(b: atom())))) # Deleting from a gradual map - assert map_delete(union(dynamic(), closed_map(a: integer())), :a) - |> equal?(union(dynamic(open_map(a: not_set())), empty_map())) + {value, type} = map_take(union(dynamic(), closed_map(a: integer())), :a) + assert value == union(dynamic(), integer()) + assert equal?(type, union(dynamic(open_map(a: not_set())), empty_map())) - assert map_delete(dynamic(open_map(a: not_set())), :b) - |> equal?(dynamic(open_map(a: not_set(), b: not_set()))) + {value, type} = map_take(dynamic(open_map(a: not_set())), :b) + assert equal?(value, dynamic()) + assert equal?(type, dynamic(open_map(a: not_set(), b: not_set()))) # Deleting from an intersection of maps - assert map_delete(intersection(open_map(a: integer()), open_map(b: atom())), :a) == - open_map(a: not_set(), b: atom()) + {value, type} = map_take(intersection(open_map(a: integer()), open_map(b: atom())), :a) + assert value == integer() + assert equal?(type, open_map(a: not_set(), b: atom())) # Deleting from a difference of maps - assert difference(closed_map(a: integer(), b: atom()), closed_map(a: integer())) - |> map_delete(:b) - |> equal?(closed_map(a: integer())) + {value, type} = + map_take(difference(closed_map(a: integer(), b: atom()), closed_map(a: integer())), :b) + + assert value == atom() + assert equal?(type, closed_map(a: integer())) - assert difference(open_map(), open_map(a: not_set())) - |> map_delete(:a) == open_map(a: not_set()) + {value, type} = map_take(difference(open_map(), open_map(a: not_set())), :a) + assert equal?(value, term()) + assert equal?(type, open_map(a: not_set())) end - end - test "map_put" do - assert map_put(term(), :a, integer()) == :badmap - assert map_put(integer(), :a, integer()) == :badmap - assert map_put(dynamic(integer()), :a, atom()) == :badmap - assert map_put(union(integer(), dynamic()), :a, atom()) == :badmap - assert map_put(empty_map(), :a, integer()) == closed_map(a: integer()) + test "map_fetch_and_put" do + assert map_fetch_and_put(term(), :a, integer()) == :badmap + assert map_fetch_and_put(open_map(), :a, integer()) == :badkey + end + + test "map_put" do + assert map_put(term(), :a, integer()) == :badmap + assert map_put(integer(), :a, integer()) == :badmap + assert map_put(dynamic(integer()), :a, atom()) == :badmap + assert map_put(union(integer(), dynamic()), :a, atom()) == :badmap + assert map_put(empty_map(), :a, integer()) == {:ok, closed_map(a: integer())} + + # Replace an existing key in a closed map + assert map_put(closed_map(a: integer()), :a, atom()) == {:ok, closed_map(a: atom())} - # Replace an existing key in a closed map - assert map_put(closed_map(a: integer()), :a, atom()) == closed_map(a: atom()) + # Add a new key to a closed map + assert map_put(closed_map(a: integer()), :b, atom()) == + {:ok, closed_map(a: integer(), b: atom())} - # Add a new key to a closed map - assert map_put(closed_map(a: integer()), :b, atom()) == closed_map(a: integer(), b: atom()) + # Replace an existing key in an open map + assert map_put(open_map(a: integer()), :a, atom()) == + {:ok, open_map(a: atom())} - # Replace an existing key in an open map - assert map_put(open_map(a: integer()), :a, atom()) == open_map(a: atom()) + # Add a new key to an open map + assert map_put(open_map(a: integer()), :b, atom()) == + {:ok, open_map(a: integer(), b: atom())} - # Add a new key to an open map - assert map_put(open_map(a: integer()), :b, atom()) == open_map(a: integer(), b: atom()) + # Put a key-value pair in a union of maps + {:ok, type} = + union(closed_map(a: integer()), closed_map(b: atom())) |> map_put(:c, boolean()) - # Put a key-value pair in a union of maps - assert union(closed_map(a: integer()), closed_map(b: atom())) - |> map_put(:c, boolean()) - |> equal?( - union(closed_map(a: integer(), c: boolean()), closed_map(b: atom(), c: boolean())) - ) + assert equal?( + type, + union(closed_map(a: integer(), c: boolean()), closed_map(b: atom(), c: boolean())) + ) - # Put a key-value pair in a dynamic map - assert map_put(dynamic(open_map()), :a, integer()) == dynamic(open_map(a: integer())) + # Put a key-value pair in a dynamic map + assert map_put(dynamic(open_map()), :a, integer()) == {:ok, dynamic(open_map(a: integer()))} - # Put a key-value pair in an intersection of maps - assert intersection(open_map(a: integer()), open_map(b: atom())) - |> map_put(:c, boolean()) - |> equal?(open_map(a: integer(), b: atom(), c: boolean())) + # Put a key-value pair in an intersection of maps + {:ok, type} = + intersection(open_map(a: integer()), open_map(b: atom())) |> map_put(:c, boolean()) - # Put a key-value pair in a difference of maps - assert difference(open_map(), closed_map(a: integer())) - |> map_put(:b, atom()) - |> equal?(difference(open_map(b: atom()), closed_map(a: integer()))) + assert equal?(type, open_map(a: integer(), b: atom(), c: boolean())) - # Put a new key-value pair with dynamic type - # Note: setting a field to a dynamic type makes the whole map become dynamic. - assert map_put(open_map(), :a, dynamic()) == dynamic(open_map(a: term())) + # Put a key-value pair in a difference of maps + {:ok, type} = difference(open_map(), closed_map(a: integer())) |> map_put(:b, atom()) + assert equal?(type, difference(open_map(b: atom()), closed_map(a: integer()))) - # Put a key-value pair in a map with optional fields - assert map_put(closed_map(a: if_set(integer())), :b, atom()) - |> equal?(closed_map(a: if_set(integer()), b: atom())) + # Put a new key-value pair with dynamic type + # Note: setting a field to a dynamic type makes the whole map become dynamic. + assert map_put(open_map(), :a, dynamic()) == {:ok, dynamic(open_map(a: term()))} - # Fetching on a key-value pair that was put to a given type returns {false, type} - {false, type} = union(dynamic(), empty_map()) |> map_put(:a, atom()) |> map_fetch(:a) - assert equal?(type, atom()) + # Put a key-value pair in a map with optional fields + {:ok, type} = closed_map(a: if_set(integer())) |> map_put(:b, atom()) + assert equal?(type, closed_map(a: if_set(integer()), b: atom())) + + # Fetching on a key-value pair that was put to a given type returns {false, type} + {:ok, map} = map_put(union(dynamic(), empty_map()), :a, atom()) + {false, type} = map_fetch(map, :a) + assert equal?(type, atom()) + end end describe "disjoint" do diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 10ef0e94f6a..d94c607a57b 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -481,18 +481,44 @@ defmodule Module.Types.ExprTest do end describe "maps/structs" do - test "creating maps" do + test "creating closed maps" do assert typecheck!(%{foo: :bar}) == closed_map(foo: atom([:bar])) - assert typecheck!(%{123 => 456}) == open_map() - assert typecheck!(%{123 => 456, foo: :bar}) == open_map(foo: atom([:bar])) assert typecheck!([x], %{key: x}) == dynamic(closed_map(key: term())) + end + test "creating closed maps with dynamic keys" do assert typecheck!( ( foo = :foo %{foo => :first, foo => :second} ) ) == closed_map(foo: atom([:second])) + + assert typecheck!( + ( + foo_or_bar = + cond do + :rand.uniform() > 0.5 -> :foo + true -> :bar + end + + %{foo_or_bar => :first, foo_or_bar => :second} + ) + ) + |> equal?( + closed_map(foo: atom([:second])) + |> union(closed_map(bar: atom([:second]))) + |> union(closed_map(foo: atom([:first]), bar: atom([:second]))) + |> union(closed_map(bar: atom([:first]), foo: atom([:second]))) + ) + end + + test "creating open maps" do + assert typecheck!(%{123 => 456}) == open_map() + # Since key cannot override :foo, we preserve it + assert typecheck!([key], %{key => 456, foo: :bar}) == dynamic(open_map(foo: atom([:bar]))) + # Since key can override :foo, we do not preserve it + assert typecheck!([key], %{:foo => :bar, key => :baz}) == dynamic(open_map()) end test "creating structs" do @@ -513,11 +539,179 @@ defmodule Module.Types.ExprTest do ) end + test "updating to closed maps" do + assert typecheck!([x], %{x | x: :zero}) == + dynamic(open_map(x: atom([:zero]))) + + assert typecheck!([x], %{%{x | x: :zero} | y: :one}) == + dynamic(open_map(x: atom([:zero]), y: atom([:one]))) + + assert typecheck!( + ( + foo_or_bar = + cond do + :rand.uniform() > 0.5 -> :key1 + true -> :key2 + end + + x = %{key1: :one, key2: :two} + %{x | foo_or_bar => :one!, foo_or_bar => :two!} + ) + ) + |> equal?( + closed_map(key1: atom([:one]), key2: atom([:two!])) + |> union(closed_map(key1: atom([:two!]), key2: atom([:one!]))) + |> union(closed_map(key1: atom([:one!]), key2: atom([:two!]))) + |> union(closed_map(key1: atom([:two!]), key2: atom([:two]))) + ) + + assert typeerror!([x = :foo], %{x | x: :zero}) == ~l""" + expected a map within map update syntax: + + %{x | x: :zero} + + but got type: + + dynamic(:foo) + + where "x" was given the type: + + # type: dynamic(:foo) + # from: types_test.ex:LINE + x = :foo + """ + + assert typeerror!( + ( + x = %{} + %{x | x: :zero} + ) + ) == ~l""" + expected a map with key :x in map update syntax: + + %{x | x: :zero} + + but got type: + + empty_map() + + where "x" was given the type: + + # type: empty_map() + # from: types_test.ex:LINE-3 + x = %{} + """ + + # Assert we check all possible combinations + assert typeerror!( + ( + foo_or_bar = + cond do + :rand.uniform() > 0.5 -> :foo + true -> :bar + end + + x = %{foo: :baz} + %{x | foo_or_bar => :bat} + ) + ) == ~l""" + expected a map with key :bar in map update syntax: + + %{x | foo_or_bar => :bat} + + but got type: + + %{foo: :baz} + + where "foo_or_bar" was given the type: + + # type: :bar or :foo + # from: types_test.ex:LINE-9 + foo_or_bar = + cond do + :rand.uniform() > 0.5 -> :foo + true -> :bar + end + + where "x" was given the type: + + # type: %{foo: :baz} + # from: types_test.ex:LINE-3 + x = %{foo: :baz} + """ + end + + test "updating to open maps" do + assert typecheck!( + [key], + ( + x = %{foo: :bar} + %{x | key => :baz} + ) + ) == dynamic(open_map()) + + # Since key cannot override :foo, we preserve it + assert typecheck!( + [key], + ( + x = %{foo: :bar} + %{x | key => :baz, foo: :bat} + ) + ) == dynamic(open_map(foo: atom([:bat]))) + + # Since key can override :foo, we do not preserve it + assert typecheck!( + [key], + ( + x = %{foo: :bar} + %{x | :foo => :baz, key => :bat} + ) + ) == dynamic(open_map()) + + # The goal of this assertion is to verify we assert keys, + # even if they may be overridden later. + assert typeerror!( + [key], + ( + x = %{key: :value} + %{x | :foo => :baz, key => :bat} + ) + ) == ~l""" + expected a map with key :foo in map update syntax: + + %{x | :foo => :baz, key => :bat} + + but got type: + + %{key: :value} + + where "key" was given the type: + + # type: dynamic() + # from: types_test.ex:LINE-5 + key + + where "x" was given the type: + + # type: %{key: :value} + # from: types_test.ex:LINE-3 + x = %{key: :value} + """ + end + test "updating structs" do assert typecheck!([x], %Point{x | x: :zero}) == - closed_map(__struct__: atom([Point]), x: atom([:zero]), y: term(), z: term()) + dynamic(open_map(__struct__: atom([Point]), x: atom([:zero]))) + + assert typecheck!([x], %Point{%Point{x | x: :zero} | y: :one}) == + dynamic(open_map(__struct__: atom([Point]), x: atom([:zero]), y: atom([:one]))) - assert typeerror!([x = :foo], %Point{x | x: :zero}) == + assert typeerror!( + ( + x = %{x: 0} + %Point{x | x: :zero} + ) + ) == ~l""" incompatible types in struct update: @@ -529,13 +723,13 @@ defmodule Module.Types.ExprTest do but got type: - dynamic(:foo) + %{x: integer()} where "x" was given the type: - # type: dynamic(:foo) - # from: types_test.ex:LINE-1 - x = :foo + # type: %{x: integer()} + # from: types_test.ex:LINE-4 + x = %{x: 0} """ end