diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 3c412a6f316..f45a00e8a54 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -385,7 +385,7 @@ defmodule Module.Types do _ -> inferred |> Enum.map(fn {args, _} -> args end) - |> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end) + |> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.opt_union/2) end) end {{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)} @@ -462,7 +462,7 @@ defmodule Module.Types do # the domain is computed by unioning their inferred types. However, their # inferred types often have the different of the previous clauses: # - # union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1) + # opt_union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1) # # Where `rN` represents the refinement in every function body. # @@ -493,9 +493,9 @@ defmodule Module.Types do # Furthermore, the signature used in type checking is not refined in any way, # so type checking is still sound. if arg == head_arg do - Descr.union(Descr.upper_bound(no_prev_arg), d) + Descr.opt_union(Descr.upper_bound(no_prev_arg), d) else - Descr.union(arg, d) + Descr.opt_union(arg, d) end | compute_domain(args_types, head_args_types, no_prev_args_types, domain) ] @@ -506,7 +506,7 @@ defmodule Module.Types do # We check for term equality of types as an optimization # to reduce the amount of check we do at runtime. defp add_inferred([{args, existing_return} | tail], args, return, index, acc), - do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])} + do: {index, Enum.reverse(acc, [{args, Descr.opt_union(existing_return, return)} | tail])} defp add_inferred([head | tail], args, return, index, acc), do: add_inferred(tail, args, return, index - 1, [head | acc]) @@ -558,7 +558,7 @@ defmodule Module.Types do # Allow exactly one differing argument. That one position is widened # with union/2. A second difference means the clauses must stay separate. defp union_args([existing_arg | existing], [arg | args], acc, false) do - union_args(existing, args, [Descr.union(existing_arg, arg) | acc], true) + union_args(existing, args, [Descr.opt_union(existing_arg, arg) | acc], true) end defp union_args([_ | _], [_ | _], _acc, true), do: nil diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index 1906567e8ce..3faa67410b3 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -33,7 +33,7 @@ defmodule Module.Types.Apply do |> Enum.map(fn {key, type} when is_atom(key) -> tuple([atom([key]), type]) end) - |> Enum.reduce(&union/2) + |> Enum.reduce(&opt_union/2) |> list() end @@ -58,7 +58,7 @@ defmodule Module.Types.Apply do exports_md5: binary(), functions: fas, macros: fas, - struct: struct_info |> union(atom([nil])) + struct: struct_info |> opt_union(atom([nil])) ] ++ shared_info infos = @@ -74,7 +74,7 @@ defmodule Module.Types.Apply do module: atom(), functions: fas, consolidated?: boolean(), - impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))} + impls: opt_union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))} ] for {name, clauses} <- infos do @@ -98,10 +98,10 @@ defmodule Module.Types.Apply do send_destination = pid() - |> union(reference()) - |> union(port()) - |> union(atom()) - |> union(tuple([atom(), atom()])) + |> opt_union(reference()) + |> opt_union(port()) + |> opt_union(atom()) + |> opt_union(tuple([atom(), atom()])) basic_arith_2_args_clauses = [ {[integer(), integer()], integer()}, @@ -110,16 +110,16 @@ defmodule Module.Types.Apply do {[float(), float()], float()} ] - args_or_arity = union(list(term()), integer()) - args_or_none = union(list(term()), atom([:none])) + args_or_arity = opt_union(list(term()), integer()) + args_or_none = opt_union(list(term()), atom([:none])) extra_info = kw.(file: list(integer()), line: integer(), error_info: open_map()) raise_stacktrace = list( tuple([atom(), atom(), args_or_arity, extra_info]) - |> union(tuple([atom(), atom(), args_or_arity])) - |> union(tuple([fun(), args_or_arity, extra_info])) - |> union(tuple([fun(), args_or_arity])) + |> opt_union(tuple([atom(), atom(), args_or_arity])) + |> opt_union(tuple([fun(), args_or_arity, extra_info])) + |> opt_union(tuple([fun(), args_or_arity])) ) not_signature = @@ -147,7 +147,8 @@ defmodule Module.Types.Apply do {:erlang, :-, [{[integer()], integer()}, {[float()], float()}]}, {:erlang, :-, basic_arith_2_args_clauses}, {:erlang, :*, basic_arith_2_args_clauses}, - {:erlang, :/, [{[union(integer(), float()), union(integer(), float())], float()}]}, + {:erlang, :/, + [{[opt_union(integer(), float()), opt_union(integer(), float())], float()}]}, {:erlang, :"/=", [{[term(), term()], boolean()}]}, {:erlang, :"=/=", [{[term(), term()], boolean()}]}, {:erlang, :<, [{[term(), term()], boolean()}]}, @@ -177,12 +178,12 @@ defmodule Module.Types.Apply do {:erlang, :bsr, [{[integer(), integer()], integer()}]}, {:erlang, :bxor, [{[integer(), integer()], integer()}]}, {:erlang, :byte_size, [{[bitstring()], integer()}]}, - {:erlang, :ceil, [{[union(integer(), float())], integer()}]}, + {:erlang, :ceil, [{[opt_union(integer(), float())], integer()}]}, {:erlang, :div, [{[integer(), integer()], integer()}]}, {:erlang, :error, [{[term()], none()}]}, {:erlang, :error, [{[term(), args_or_none], none()}]}, {:erlang, :error, [{[term(), args_or_none, kw.(error_info: open_map())], none()}]}, - {:erlang, :floor, [{[union(integer(), float())], integer()}]}, + {:erlang, :floor, [{[opt_union(integer(), float())], integer()}]}, {:erlang, :function_exported, [{[atom(), atom(), integer()], boolean()}]}, {:erlang, :integer_to_binary, [{[integer()], binary()}]}, {:erlang, :integer_to_binary, [{[integer(), integer()], binary()}]}, @@ -200,12 +201,12 @@ defmodule Module.Types.Apply do {:erlang, :make_tuple, [{[integer(), term()], tuple()}]}, {:erlang, :map_size, [{[open_map()], integer()}]}, {:erlang, :node, [{[], atom()}]}, - {:erlang, :node, [{[pid() |> union(reference()) |> union(port())], atom()}]}, + {:erlang, :node, [{[pid() |> opt_union(reference()) |> opt_union(port())], atom()}]}, {:erlang, :not, not_signature}, {:erlang, :or, or_signature}, {:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]}, {:erlang, :rem, [{[integer(), integer()], integer()}]}, - {:erlang, :round, [{[union(integer(), float())], integer()}]}, + {:erlang, :round, [{[opt_union(integer(), float())], integer()}]}, {:erlang, :self, [{[], pid()}]}, {:erlang, :spawn, [{[fun(0)], pid()}]}, {:erlang, :spawn, [{mfargs, pid()}]}, @@ -215,7 +216,7 @@ defmodule Module.Types.Apply do {:erlang, :spawn_monitor, [{mfargs, tuple([pid(), reference()])}]}, {:erlang, :split_binary, [{[binary(), integer()], tuple([binary(), binary()])}]}, {:erlang, :tuple_size, [{[open_tuple([])], integer()}]}, - {:erlang, :trunc, [{[union(integer(), float())], integer()}]}, + {:erlang, :trunc, [{[opt_union(integer(), float())], integer()}]}, # TODO: Replace term()/dynamic() by parametric types {:erlang, :++, @@ -253,7 +254,7 @@ defmodule Module.Types.Apply do ## Map {Map, :delete, [{[open_map(), term()], open_map()}]}, {Map, :fetch, - [{[open_map(), term()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]}, + [{[open_map(), term()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]}, {Map, :fetch!, [{[open_map(), term()], term()}]}, {Map, :from_struct, [{[open_map()], open_map(__struct__: not_set())}]}, {Map, :get, [{[open_map(), term()], term()}]}, @@ -277,14 +278,14 @@ defmodule Module.Types.Apply do {Tuple, :insert_at, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]}, {:maps, :from_keys, [{[list(term()), term()], open_map()}]}, {:maps, :find, - [{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]}, + [{[term(), open_map()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]}, {:maps, :get, [{[term(), open_map()], term()}]}, {:maps, :is_key, [{[term(), open_map()], boolean()}]}, {:maps, :keys, [{[open_map()], list(term())}]}, {:maps, :put, [{[term(), term(), open_map()], open_map()}]}, {:maps, :remove, [{[term(), open_map()], open_map()}]}, {:maps, :take, - [{[term(), open_map()], tuple([term(), open_map()]) |> union(atom([:error]))}]}, + [{[term(), open_map()], tuple([term(), open_map()]) |> opt_union(atom([:error]))}]}, {:maps, :to_list, [{[open_map()], list(tuple([term(), term()]))}]}, {:maps, :update, [{[term(), term(), open_map()], open_map()}]}, {:maps, :values, [{[open_map()], list(term())}]} @@ -300,7 +301,7 @@ defmodule Module.Types.Apply do domain = clauses |> Enum.map(fn {args, _} -> args end) - |> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end) + |> Enum.zip_with(fn types -> Enum.reduce(types, &opt_union/2) end) {:strong, domain, clauses} end @@ -317,9 +318,9 @@ defmodule Module.Types.Apply do is_float: float(), is_function: fun(), is_integer: integer(), - is_list: union(empty_list(), non_empty_list(term(), term())), + is_list: opt_union(empty_list(), non_empty_list(term(), term())), is_map: open_map(), - is_number: union(float(), integer()), + is_number: opt_union(float(), integer()), is_pid: pid(), is_port: port(), is_reference: reference(), @@ -331,7 +332,7 @@ defmodule Module.Types.Apply do {:strong, [term()], [ {[type], atom([true])}, - {[negation(type)], atom([false])} + {[Module.Types.Descr.opt_negation(type)], atom([false])} ]} def signature(:erlang, unquote(guard), 1), @@ -345,7 +346,7 @@ defmodule Module.Types.Apply do {:strong, [term(), integer(), integer()], [ {[integer(), integer(), integer()], boolean()}, - {[negation(integer()), integer(), integer()], atom([false])} + {[Module.Types.Descr.opt_negation(integer()), integer(), integer()], atom([false])} ]} ) ) @@ -445,7 +446,7 @@ defmodule Module.Types.Apply do # However, during inference, we type it as `a, b -> a and b` only. {left_type, context} = of_fun.(left, expected, expr, stack, context) {right_type, context} = of_fun.(right, expected, expr, stack, context) - result = union(left_type, right_type) + result = opt_union(left_type, right_type) if error = mismatched_ordered_comparison(left_type, right_type, stack) do remote_error(error, :erlang, name, 2, expr, stack, context) @@ -670,10 +671,14 @@ defmodule Module.Types.Apply do {type, context} = of_fun.(literal, term(), expr, stack, context) if singleton?(type) do - acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type)) + acc = + if polarity, + do: opt_union(acc, type), + else: opt_intersection(acc, Module.Types.Descr.opt_negation(type)) + {acc, all_singleton?, context} else - acc = if polarity, do: union(acc, type), else: acc + acc = if polarity, do: opt_union(acc, type), else: acc {acc, false, context} end end) @@ -711,9 +716,9 @@ defmodule Module.Types.Apply do @empty_list empty_list() @non_empty_list non_empty_list(term()) - @list union(empty_list(), non_empty_list(term())) + @list opt_union(empty_list(), non_empty_list(term())) @empty_map empty_map() - @non_empty_map difference(open_map(), empty_map()) + @non_empty_map opt_difference(open_map(), empty_map()) # Limit the size of tuples to 16 entries # as otherwise we may create large nodes @@ -773,7 +778,7 @@ defmodule Module.Types.Apply do {tuple(List.duplicate(term(), literal)), true} :tuple_size -> - {difference(open_tuple([]), tuple(List.duplicate(term(), literal))), true} + {opt_difference(open_tuple([]), tuple(List.duplicate(term(), literal))), true} end {actual, context} = of_fun.(arg, expected, expr, stack, context) @@ -804,7 +809,7 @@ defmodule Module.Types.Apply do # This logic mirrors the code in `Pattern.of_pattern_tree` # If it is a singleton, we can always be precise if singleton?(type) do - expected = if polarity, do: type, else: negation(type) + expected = if polarity, do: type, else: Module.Types.Descr.opt_negation(type) {arg_type, context} = of_fun.(arg, expected, expr, stack, context) result = if subtype?(arg_type, upper_bound(expected)), do: return, else: boolean() @@ -912,13 +917,13 @@ defmodule Module.Types.Apply do defp expected_order(_, :<, 0), do: :none defp expected_order(:tuple_size, :<, size), - do: {difference(open_tuple([]), open_tuple(List.duplicate(term(), size))), true} + do: {opt_difference(open_tuple([]), open_tuple(List.duplicate(term(), size))), true} defp expected_order(:tuple_size, :"=<", 0), do: {tuple([]), true} defp expected_order(:tuple_size, :"=<", size), - do: {difference(open_tuple([]), open_tuple(List.duplicate(term(), size + 1))), true} + do: {opt_difference(open_tuple([]), open_tuple(List.duplicate(term(), size + 1))), true} defp expected_order(:tuple_size, :>, size), do: {open_tuple(List.duplicate(term(), size + 1)), true} @@ -974,7 +979,7 @@ defmodule Module.Types.Apply do {:strong, [term(), integer()], [ {[type, integer()], atom([true])}, - {[negation(type), integer()], atom([false])} + {[Module.Types.Descr.opt_negation(type), integer()], atom([false])} ]} {info, filter_domain(info, expected, 2), context} @@ -1116,7 +1121,7 @@ defmodule Module.Types.Apply do defp remote_apply(Map, :fetch, _info, [map, key] = args_types, stack) do case map_get(map, key) do {_, value} -> - result = tuple([atom([:ok]), value]) |> union(atom([:error])) + result = tuple([atom([:ok]), value]) |> opt_union(atom([:error])) {:ok, return(result, args_types, stack)} :badmap -> @@ -1137,7 +1142,7 @@ defmodule Module.Types.Apply do defp remote_apply(Map, :get, _info, [map, key] = args_types, stack) do case map_get(map, key) do - {:ok, value} -> {:ok, return(union(value, @nil_atom), args_types, stack)} + {:ok, value} -> {:ok, return(opt_union(value, @nil_atom), args_types, stack)} :badmap -> {:error, badremote(Map, :get, args_types)} :error -> {:error, {:badkeydomain, map, key, @nil_atom}} end @@ -1145,7 +1150,7 @@ defmodule Module.Types.Apply do defp remote_apply(Map, :get, _info, [map, key, default] = args_types, stack) do case map_get(map, key) do - {:ok, value} -> {:ok, return(union(value, default), args_types, stack)} + {:ok, value} -> {:ok, return(opt_union(value, default), args_types, stack)} :badmap -> {:error, badremote(Map, :get, args_types)} :error -> {:error, {:badkeydomain, map, key, default}} end @@ -1155,7 +1160,7 @@ defmodule Module.Types.Apply do case fun_apply(fun, []) do {:ok, default} -> case map_get(map, key) do - {:ok, value} -> {:ok, return(union(value, default), args_types, stack)} + {:ok, value} -> {:ok, return(opt_union(value, default), args_types, stack)} :badmap -> {:error, badremote(Map, :get_lazy, args_types)} :error -> {:error, {:badkeydomain, map, key, default}} end @@ -1185,7 +1190,7 @@ defmodule Module.Types.Apply do case map_update(map, key, not_set(), true, false) do {value, descr, _errors} -> - value = union(value, default) + value = opt_union(value, default) {:ok, return(tuple([value, descr]), args_types, stack)} :badmap -> @@ -1201,7 +1206,7 @@ defmodule Module.Types.Apply do {:ok, default} -> case map_update(map, key, not_set(), true, false) do {value, descr, _errors} -> - value = union(value, default) + value = opt_union(value, default) {:ok, return(tuple([value, descr]), args_types, stack)} :badmap -> @@ -1275,7 +1280,7 @@ defmodule Module.Types.Apply do default else case fun_apply(fun, [arg_type]) do - {:ok, res} -> if optional?, do: union(res, default), else: res + {:ok, res} -> if optional?, do: opt_union(res, default), else: res reason -> throw({:badapply, reason, [arg_type]}) end end @@ -1299,7 +1304,7 @@ defmodule Module.Types.Apply do defp remote_apply(:maps, :find, _info, [key, map] = args_types, stack) do case map_get(map, key) do {_, value} -> - result = tuple([atom([:ok]), value]) |> union(atom([:error])) + result = tuple([atom([:ok]), value]) |> opt_union(atom([:error])) {:ok, return(result, args_types, stack)} :badmap -> @@ -1333,7 +1338,7 @@ defmodule Module.Types.Apply do map_and_maybe_empty_map = case empty_list? do true -> map - false -> difference(map, empty_map()) + false -> opt_difference(map, empty_map()) end {:ok, return(map_and_maybe_empty_map, args_types, stack)} @@ -1378,7 +1383,7 @@ defmodule Module.Types.Apply do defp remote_apply(:maps, :take, _info, [key, map] = args_types, stack) do case map_update(map, key, not_set(), true, false) do {value, descr, _errors} -> - result = union(tuple([value, descr]), atom([:error])) + result = opt_union(tuple([value, descr]), atom([:error])) {:ok, return(result, args_types, stack)} :badmap -> @@ -1452,10 +1457,10 @@ defmodule Module.Types.Apply do Enum.reduce(modules, {none(), false, context}, fn module, {type, fallback?, context} -> case signature(module, fun, arity, meta, stack, context) do {{:strong, _, clauses}, context} -> - {union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context} + {opt_union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context} {{:infer, _, clauses}, context} when length(clauses) <= @max_clauses -> - {union(type, fun_from_inferred_clauses(clauses)), fallback?, context} + {opt_union(type, fun_from_inferred_clauses(clauses)), fallback?, context} {_, context} -> {type, true, context} @@ -1744,7 +1749,7 @@ defmodule Module.Types.Apply do defp map_put_new(map, key, value, name, args_types, stack) do fun = fn - true, type -> union(type, value) + true, type -> opt_union(type, value) false, type -> if empty?(type), do: value, else: type end @@ -1800,7 +1805,7 @@ defmodule Module.Types.Apply do case filter_domain(clauses, expected, [], true) do :none -> domain(domain, clauses) :all -> domain(domain, clauses) - args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &union/2) end) + args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &opt_union/2) end) end end @@ -1824,7 +1829,7 @@ defmodule Module.Types.Apply do {used, dynamic()} {_count, used, returns} -> - {used, returns |> Enum.reduce(&union/2) |> dynamic()} + {used, returns |> Enum.reduce(&opt_union/2) |> dynamic()} end end @@ -1841,7 +1846,7 @@ defmodule Module.Types.Apply do # as a non disjoint check. So we skip checking compatibility twice. with true <- zip_compatible_or_only_gradual?(args_types, domain), {count, used, returns} when count > 0 <- apply_clauses(clauses, args_types, 0, 0, [], []) do - {used, returns |> Enum.reduce(&union/2) |> return(args_types, stack)} + {used, returns |> Enum.reduce(&opt_union/2) |> return(args_types, stack)} else _ -> :error end @@ -2186,7 +2191,7 @@ defmodule Module.Types.Apply do clauses = case mod.__protocol__(:impls) do {:consolidated, mods} -> - domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&union/2) + domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&opt_union/2) [{[domain], dynamic()}] _ -> @@ -2396,10 +2401,10 @@ defmodule Module.Types.Apply do term_type?(actual) do actual |> to_quoted() |> Code.Formatter.to_algebra() else - common = intersection(actual, expected) + common = opt_intersection(actual, expected) uncommon_doc = - difference(actual, expected) + opt_difference(actual, expected) |> to_quoted() |> Code.Formatter.to_algebra() |> ansi_red() @@ -2417,17 +2422,17 @@ defmodule Module.Types.Apply do end @composite_types non_empty_list(term(), term()) - |> union(tuple()) - |> union(open_map()) - |> union(fun()) + |> opt_union(tuple()) + |> opt_union(open_map()) + |> opt_union(fun()) # If actual/expected have a composite type, computing the - # `intersection(actual, expected) or difference(actual, expected)` + # `opt_intersection(actual, expected) or opt_difference(actual, expected)` # can lead to an explosion of terms that actually make debugging # harder. So we check that at least one of the two operations # return none() (i.e. actual is a subtype or they are disjoint). defp has_simple_difference?(actual, expected) do - composite_types = intersection(actual, @composite_types) + composite_types = opt_intersection(actual, @composite_types) subtype?(composite_types, expected) or disjoint?(composite_types, expected) end diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 02b6284c038..cee9c6fefcc 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -37,7 +37,6 @@ defmodule Module.Types.Descr do # Map fields and domains are stored as orddicts (sorted key-value lists). @fields_new [] - defguardp is_fields_empty(fields) when fields == [] defguardp fields_size(fields) when length(fields) @domain_key_types :lists.sort( @@ -181,7 +180,7 @@ defmodule Module.Types.Descr do """ def fun_from_non_overlapping_clauses([{args, return} | clauses]) do Enum.reduce(clauses, fun(args, return), fn {args, return}, acc -> - intersection(acc, fun(args, return)) + bare_intersection(acc, fun(args, return)) end) end @@ -201,7 +200,7 @@ defmodule Module.Types.Descr do args <- domain_to_args(domain), do: fun(args, dynamic(union)) - Enum.reduce(funs, &intersection/2) + Enum.reduce(funs, &bare_intersection/2) end # If you have a function with multiple clauses, they may overlap, @@ -233,8 +232,8 @@ defmodule Module.Types.Descr do ] else [ - {acc_domain, acc_return, union(return, acc_union)} - | pivot_overlapping_clause(domain, return, union(acc_return, union), acc) + {acc_domain, acc_return, bare_union(return, acc_union)} + | pivot_overlapping_clause(domain, return, bare_union(acc_return, union), acc) ] end end @@ -293,7 +292,7 @@ defmodule Module.Types.Descr do case domain_to_args(domain) do [] when is_integer(arity_or_args) -> List.duplicate(none(), arity_or_args) [] when is_list(arity_or_args) -> Enum.map(arity_or_args, fn _ -> none() end) - args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &union/2) end) + args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &bare_union/2) end) end end @@ -394,7 +393,7 @@ defmodule Module.Types.Descr do @doc """ Make a whole type dynamic. - It is an optimized version of `intersection(dynamic(), type)`. + It is an optimized version of `bare_intersection(dynamic(), type)`. """ @compile {:inline, dynamic: 1} def dynamic(descr) do @@ -408,182 +407,126 @@ defmodule Module.Types.Descr do defp pop_dynamic(:term), do: {:term, :term} defp pop_dynamic(descr), do: Map.pop(descr, :dynamic, descr) - @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 bare_union(:term, other), do: optional_to_term(other) + def bare_union(other, :term), do: optional_to_term(other) + def bare_union(none, other) when none == @none, do: other + def bare_union(other, none) when none == @none, do: other - def union(left, right) do + def bare_union(left, right) do is_gradual_left = gradual?(left) is_gradual_right = gradual?(right) cond do is_gradual_left and not is_gradual_right -> right_with_dynamic = Map.put(unfold(right), :dynamic, right) - union_static(left, right_with_dynamic) + bare_union_static(left, right_with_dynamic) is_gradual_right and not is_gradual_left -> left_with_dynamic = Map.put(unfold(left), :dynamic, left) - union_static(left_with_dynamic, right) + bare_union_static(left_with_dynamic, right) true -> - union_static(left, right) + bare_union_static(left, right) end end - @compile {:inline, union_static: 2} - defp union_static(left, right) do - symmetrical_merge(left, right, &union/3) + @compile {:inline, bare_union_static: 2} + defp bare_union_static(left, right) do + symmetrical_merge(left, right, &bare_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) - defp union(:list, v1, v2), do: bdd_union(v1, v2) - defp union(:map, v1, v2), do: map_union(v1, v2) - defp union(:optional, 1, 1), do: 1 - defp union(:tuple, v1, v2), do: tuple_union(v1, v2) - defp union(:fun, v1, v2), do: fun_union(v1, v2) + defp bare_union(:atom, v1, v2), do: atom_union(v1, v2) + defp bare_union(:bitmap, v1, v2), do: v1 ||| v2 + defp bare_union(:dynamic, v1, v2), do: dynamic_union(v1, v2, &bare_union/3) + defp bare_union(:list, v1, v2), do: list_union(v1, v2) + defp bare_union(:map, v1, v2), do: map_union(v1, v2) + defp bare_union(:optional, 1, 1), do: 1 + defp bare_union(:tuple, v1, v2), do: tuple_union(v1, v2) + defp bare_union(:fun, v1, v2), do: fun_union(v1, v2) @doc """ Computes the intersection of two descrs. """ - def intersection(:term, other), do: remove_optional(other) - def intersection(other, :term), do: remove_optional(other) + def bare_intersection(:term, other), do: remove_optional(other) + def bare_intersection(other, :term), do: remove_optional(other) - def intersection(left, right) do + def bare_intersection(left, right) do is_gradual_left = gradual?(left) is_gradual_right = gradual?(right) cond do is_gradual_left and not is_gradual_right -> right_with_dynamic = Map.put(unfold(right), :dynamic, right) - intersection_static(left, right_with_dynamic) + bare_intersection_static(left, right_with_dynamic) is_gradual_right and not is_gradual_left -> left_with_dynamic = Map.put(unfold(left), :dynamic, left) - intersection_static(left_with_dynamic, right) + bare_intersection_static(left_with_dynamic, right) true -> - intersection_static(left, right) + bare_intersection_static(left, right) end end - @compile {:inline, intersection_static: 2} - defp intersection_static(left, right) do - symmetrical_intersection(left, right, &intersection/3) + @compile {:inline, bare_intersection_static: 2} + defp bare_intersection_static(left, right) do + symmetrical_intersection(left, right, &bare_intersection/3) end # Returning 0 from the callback is taken as none() for that subtype. - defp intersection(:atom, v1, v2), do: atom_intersection(v1, v2) - defp intersection(:bitmap, v1, v2), do: v1 &&& v2 - defp intersection(:list, v1, v2), do: list_intersection(v1, v2) - defp intersection(:map, v1, v2), do: map_intersection(v1, v2) - defp intersection(:optional, 1, 1), do: 1 - defp intersection(:tuple, v1, v2), do: tuple_intersection(v1, v2) - defp intersection(:fun, v1, v2), do: fun_intersection(v1, v2) - - defp intersection(:dynamic, v1, v2) do - descr = dynamic_intersection(v1, v2) + defp bare_intersection(:atom, v1, v2), do: atom_intersection(v1, v2) + defp bare_intersection(:bitmap, v1, v2), do: v1 &&& v2 + defp bare_intersection(:list, v1, v2), do: list_intersection(v1, v2) + defp bare_intersection(:map, v1, v2), do: map_intersection(v1, v2) + defp bare_intersection(:optional, 1, 1), do: 1 + defp bare_intersection(:tuple, v1, v2), do: tuple_intersection(v1, v2) + defp bare_intersection(:fun, v1, v2), do: fun_intersection(v1, v2) + + defp bare_intersection(:dynamic, v1, v2) do + descr = dynamic_intersection(v1, v2, &bare_intersection/3) if descr == @none, do: 0, else: descr end @doc """ Computes the difference between two types. """ - def difference(left, :term), do: keep_optional(left) - def difference(left, none) when none == @none, do: left + def bare_difference(left, :term), do: keep_optional(left) + def bare_difference(left, none) when none == @none, do: left - def difference(left, right) do + def bare_difference(left, right) do if gradual?(left) or gradual?(right) do {left_dynamic, left_static} = pop_dynamic(left) {right_dynamic, right_static} = pop_dynamic(right) - dynamic_part = difference_static(left_dynamic, right_static) + dynamic_part = bare_difference_static(left_dynamic, right_static) - Map.put(difference_static(left_static, right_dynamic), :dynamic, dynamic_part) + Map.put(bare_difference_static(left_static, right_dynamic), :dynamic, dynamic_part) else - difference_static(left, right) + bare_difference_static(left, right) end end - # For static types, the difference is component-wise - defp difference_static(left, descr) when descr == %{}, do: left - defp difference_static(left, :term), do: keep_optional(left) - - defp difference_static(left, right) do - iterator_difference_static(:maps.next(:maps.iterator(unfold(right))), unfold(left)) - end - - defp iterator_difference_static({key, v2, iterator}, map) do - acc = - case map do - %{^key => v1} -> - value = difference(key, v1, v2) - - if value in @empty_difference do - Map.delete(map, key) - else - %{map | key => value} - end - - %{} -> - map - end - - iterator_difference_static(:maps.next(iterator), acc) - end - - defp iterator_difference_static(:none, map), do: map - - # This function is designed to compute the difference during subtyping efficiently. - # Do not use it for anything else. - defp empty_difference_subtype?(%{dynamic: dyn_left} = left, %{dynamic: dyn_right} = right) do - # Dynamic will either exist on both sides or on none - empty_difference_subtype?(dyn_left, dyn_right) and - empty_difference_subtype?(Map.delete(left, :dynamic), Map.delete(right, :dynamic)) - end - - defp empty_difference_subtype?(left, :term), do: keep_optional(left) == @none - - defp empty_difference_subtype?(left, right) do - iterator_empty_difference_subtype?(:maps.next(:maps.iterator(unfold(left))), unfold(right)) - end - - defp iterator_empty_difference_subtype?({key, v1, iterator}, map) do - case map do - %{^key => v2} -> - value = difference(key, v1, v2) - value in @empty_difference or empty_key?(key, value) - - %{} -> - empty_key?(key, v1) - end and - iterator_empty_difference_subtype?(:maps.next(iterator), map) + @compile {:inline, bare_difference_static: 2} + defp bare_difference_static(left, right) do + dynamic_difference(left, right, &bare_difference/3) end - defp iterator_empty_difference_subtype?(:none, _map), do: true - # Returning 0 from the callback is taken as none() for that subtype. - defp difference(:atom, v1, v2), do: atom_difference(v1, v2) - defp difference(:bitmap, v1, v2), do: v1 - (v1 &&& v2) - defp difference(:list, v1, v2), do: list_difference(v1, v2) - defp difference(:map, v1, v2), do: map_difference(v1, v2) - defp difference(:optional, 1, 1), do: 0 - defp difference(:tuple, v1, v2), do: tuple_difference(v1, v2) - defp difference(:fun, v1, v2), do: fun_difference(v1, v2) + defp bare_difference(:atom, v1, v2), do: atom_difference(v1, v2) + defp bare_difference(:bitmap, v1, v2), do: v1 - (v1 &&& v2) + defp bare_difference(:list, v1, v2), do: list_difference(v1, v2) + defp bare_difference(:map, v1, v2), do: map_difference(v1, v2) + defp bare_difference(:optional, 1, 1), do: 0 + defp bare_difference(:tuple, v1, v2), do: tuple_difference(v1, v2) + defp bare_difference(:fun, v1, v2), do: fun_difference(v1, v2) @doc """ Compute the negation of a type. """ - def negation(:term), do: none() - def negation(%{} = descr), do: difference(term(), descr) + def bare_negation(:term), do: none() + def bare_negation(%{} = descr), do: bare_difference(term(), descr) @doc """ Check if a type is empty. @@ -742,7 +685,7 @@ defmodule Module.Types.Descr do {dynamic, static} -> cond do - # Computing term_type?(difference(dynamic, static)) can be + # Computing term_type?(bare_difference(dynamic, static)) can be # expensive, so we check for term type before hand and check # for :term exclusively in dynamic_to_quoted/2. term_type?(dynamic) -> @@ -755,7 +698,7 @@ defmodule Module.Types.Descr do # Denormalize functions (only unions) before we do the difference true -> {static, dynamic, extra} = fun_denormalize(static, dynamic, opts) - {difference(dynamic, static), static, extra} + {bare_difference(dynamic, static), static, extra} end end @@ -806,7 +749,7 @@ defmodule Module.Types.Descr do defp maybe_negated_term_type_to_quoted(static, opts) do if print_as_negated_type?(static) do static - |> negation() + |> bare_negation() |> unfold() |> static_non_term_type_to_quoted([], opts) |> case do @@ -911,6 +854,34 @@ defmodule Module.Types.Descr do defp subtype_static?(same, same), do: true defp subtype_static?(left, right), do: empty_difference_subtype?(left, right) + # This function is designed to compute the difference during subtyping efficiently. + # Do not use it for anything else. + defp empty_difference_subtype?(%{dynamic: dyn_left} = left, %{dynamic: dyn_right} = right) do + # Dynamic will either exist on both sides or on none + empty_difference_subtype?(dyn_left, dyn_right) and + empty_difference_subtype?(Map.delete(left, :dynamic), Map.delete(right, :dynamic)) + end + + defp empty_difference_subtype?(left, :term), do: keep_optional(left) == @none + + defp empty_difference_subtype?(left, right) do + iterator_empty_difference_subtype?(:maps.next(:maps.iterator(unfold(left))), unfold(right)) + end + + defp iterator_empty_difference_subtype?({key, v1, iterator}, map) do + case map do + %{^key => v2} -> + value = bare_difference(key, v1, v2) + value in @empty_difference or empty_key?(key, value) + + %{} -> + empty_key?(key, v1) + end and + iterator_empty_difference_subtype?(:maps.next(iterator), map) + end + + defp iterator_empty_difference_subtype?(:none, _map), do: true + @doc """ Check if a type is equal to another. @@ -974,9 +945,9 @@ defmodule Module.Types.Descr do Returns the intersection between two types only if they are compatible. Otherwise returns `:error`. - This finds the intersection between the arguments and the - domain of a function. It is used to refine dynamic types - as we traverse the program. + This finds the optimized intersection between the arguments and the + domain of a function. It is used to refine dynamic types as we traverse + the program. """ def compatible_intersection(other, :term), do: {:ok, remove_optional(other)} @@ -991,12 +962,12 @@ defmodule Module.Types.Descr do cond do empty?(left_static) -> - dynamic = intersection_static(unfold(left_dynamic), unfold(right_dynamic)) + dynamic = opt_intersection_static(unfold(left_dynamic), unfold(right_dynamic)) if empty?(dynamic), do: {:error, left}, else: {:ok, dynamic(dynamic)} subtype_static?(left_static, right_dynamic) -> - dynamic = intersection_static(unfold(left_dynamic), unfold(right_dynamic)) - {:ok, union(dynamic(dynamic), left_static)} + dynamic = opt_intersection_static(unfold(left_dynamic), unfold(right_dynamic)) + {:ok, opt_union(dynamic(dynamic), left_static)} true -> {:error, left} @@ -1010,7 +981,7 @@ defmodule Module.Types.Descr do def term_type?(descr), do: subtype_static?(unfolded_term(), Map.delete(descr, :dynamic)) @doc """ - Optimized version of `not empty?(intersection(empty_list(), type))`. + Optimized version of `not empty?(bare_intersection(empty_list(), type))`. """ def empty_list_type?(:term), do: true def empty_list_type?(%{dynamic: :term}), do: true @@ -1022,7 +993,7 @@ defmodule Module.Types.Descr do def empty_list_type?(_), do: false @doc """ - Optimized version of `not empty?(intersection(bitstring(), type))`. + Optimized version of `not empty?(bare_intersection(bitstring(), type))`. """ def bitstring_type?(:term), do: true def bitstring_type?(%{dynamic: :term}), do: true @@ -1034,7 +1005,7 @@ defmodule Module.Types.Descr do def bitstring_type?(_), do: false @doc """ - Optimized version of `not empty?(intersection(difference(bitstring(), binary()), type))`. + Optimized version of `not empty?(bare_intersection(bare_difference(bitstring(), binary()), type))`. Notice that this does not mean it is not a binary. It only means the bitstring bit is up, regardless of the binary bit. @@ -1051,7 +1022,7 @@ defmodule Module.Types.Descr do def bitstring_no_binary_type?(_), do: false @doc """ - Optimized version of `not empty?(intersection(integer() or float(), type))`. + Optimized version of `not empty?(bare_intersection(integer() or float(), type))`. """ def number_type?(:term), do: true def number_type?(%{dynamic: :term}), do: true @@ -1324,7 +1295,7 @@ defmodule Module.Types.Descr do # * Examples: # - fun([integer()], atom()): A function from integer to atom - # - intersection(fun([integer()], atom()), fun([float()], boolean())): A function handling both signatures + # - bare_intersection(fun([integer()], atom()), fun([float()], boolean())): A function handling both signatures # Note: Function domains are expressed as tuple types. We use separate representations # rather than unary functions with tuple domains to handle special cases like representing @@ -1504,7 +1475,7 @@ defmodule Module.Types.Descr do arguments = Enum.map(arguments, &upper_bound/1) {:ok, - union( + bare_union( fun_apply_static(arguments, static_arrows), dynamic(fun_apply_static(arguments, dynamic_arrows)) )} @@ -1548,7 +1519,7 @@ defmodule Module.Types.Descr do [] -> case fun_normalize(fun_dynamic, arity) do {:ok, dynamic_domain, dynamic_arrows} -> - domain = union(dynamic_domain, dynamic(static_domain)) + domain = bare_union(dynamic_domain, dynamic(static_domain)) {:ok, domain, static_arrows, dynamic_arrows} _ -> @@ -1566,7 +1537,7 @@ defmodule Module.Types.Descr do # result is wrapped in dynamic(), reflecting the uncertainty. case fun_normalize(fun_dynamic, arity) do {:ok, dynamic_domain, dynamic_arrows} -> - {:ok, union(dynamic_domain, dynamic()), [], dynamic_arrows} + {:ok, bare_union(dynamic_domain, dynamic()), [], dynamic_arrows} error -> error @@ -1611,7 +1582,7 @@ defmodule Module.Types.Descr do # In that case, we transform the {:negation, _} into a single union # where we add the previous negatives into the specified arity. defp fun_normalize(%{fun: {:negation, _}} = neg_fun, arity) do - fun_normalize(intersection(fun(arity), neg_fun), arity) + fun_normalize(bare_intersection(fun(arity), neg_fun), arity) end defp fun_normalize(%{fun: {:union, bdds}}, arity) do @@ -1620,7 +1591,7 @@ defmodule Module.Types.Descr do {domain, arrows} = Enum.reduce(fun_bdd_to_pos_dnf(arity, bdd), {term(), []}, fn pos_funs, {domain, arrows} -> - {intersection(domain, fetch_domain(pos_funs)), [pos_funs | arrows]} + {bare_intersection(domain, fetch_domain(pos_funs)), [pos_funs | arrows]} end) if arrows == [] do @@ -1657,9 +1628,9 @@ defmodule Module.Types.Descr do type_input = args_to_domain(input_arguments) Enum.reduce(arrows, none(), fn bdd_leaf(args, ret), acc_return -> - if empty?(intersection(args_to_domain(args), type_input)), + if empty?(bare_intersection(args_to_domain(args), type_input)), do: acc_return, - else: union(acc_return, ret) + else: bare_union(acc_return, ret) end) end @@ -1688,15 +1659,15 @@ defmodule Module.Types.Descr do # For the escape case, see Section 13.2 of # https://gldubc.github.io/assets/duboc-phd-thesis-typing-elixir.pdf defp aux_apply(result, _input, rets_reached, []) do - if subtype?(rets_reached, result), do: result, else: union(result, rets_reached) + if subtype?(rets_reached, result), do: result, else: bare_union(result, rets_reached) end defp aux_apply(result, input, returns_reached, [bdd_leaf(args, ret) | arrow_intersections]) do # Calculate the part of the input not covered by this arrow's domain - dom_subtract = difference(input, args_to_domain(args)) + dom_subtract = bare_difference(input, args_to_domain(args)) # Refine the return type by intersecting with this arrow's return type - ret_refine = intersection(returns_reached, ret) + ret_refine = bare_intersection(returns_reached, ret) # Phase 1: Domain partitioning # If the input is not fully covered by the arrow's domain, then the result type should be @@ -1763,7 +1734,7 @@ defmodule Module.Types.Descr do # Returns the union of all domains of the arrows in the intersection of positives. defp fetch_domain(positives) do Enum.reduce(positives, none(), fn bdd_leaf(args, _), acc -> - union(acc, args_to_domain(args)) + bare_union(acc, args_to_domain(args)) end) end @@ -1797,7 +1768,7 @@ defmodule Module.Types.Descr do _ -> # Initialize memoization cache for the recursive phi computation arguments = Enum.map(arguments, &{false, &1}) - {result, _cache} = phi(arguments, {false, negation(return)}, positives, %{}) + {result, _cache} = phi(arguments, {false, bare_negation(return)}, positives, %{}) result end end @@ -1817,7 +1788,7 @@ defmodule Module.Types.Descr do %{} -> # Compute result and cache it - {result1, cache} = phi(args, {true, intersection(ret, return)}, rest_positive, cache) + {result1, cache} = phi(args, {true, bare_intersection(ret, return)}, rest_positive, cache) if not result1 do cache = Map.put(cache, cache_key, false) @@ -1828,7 +1799,7 @@ defmodule Module.Types.Descr do type, {index, acc_result, acc_cache} -> {new_result, new_cache} = args - |> List.update_at(index, fn {_, arg} -> {true, difference(arg, type)} end) + |> List.update_at(index, fn {_, arg} -> {true, bare_difference(arg, type)} end) |> phi({b, ret}, rest_positive, acc_cache) if new_result do @@ -2027,10 +1998,10 @@ defmodule Module.Types.Descr do {pre, [bdd_leaf(dynamic_args, dynamic_return) | post]} -> args = Enum.zip_with(static_args, dynamic_args, fn static_arg, dynamic_arg -> - union(dynamic(static_arg), dynamic_arg) + bare_union(dynamic(static_arg), dynamic_arg) end) - return = union(dynamic(dynamic_return), static_return) + return = bare_union(dynamic(dynamic_return), static_return) fun_denormalize_intersections(statics, pre ++ post, [bdd_leaf_new(args, return) | acc]) end end @@ -2192,7 +2163,7 @@ defmodule Module.Types.Descr do list_bdd_to_pos_dnf(bdd) |> Enum.reduce({list_type, last_type_no_list}, fn {list, last, _negs}, {acc_list, acc_last} -> - {union(list, acc_list), union(last, acc_last)} + {bare_union(list, acc_list), bare_union(last, acc_last)} end) list_new(list_type, last_type) @@ -2208,7 +2179,7 @@ defmodule Module.Types.Descr do {list, last} = Enum.reduce(list_literals, {:term, :term}, fn bdd_leaf(next_list, next_last), {list, last} -> - {intersection(list, next_list), intersection(last, next_last)} + {bare_intersection(list, next_list), bare_intersection(last, next_last)} end) if empty?(list) or empty?(last), do: :empty, else: {list, last} @@ -2233,7 +2204,7 @@ defmodule Module.Types.Descr do Enum.reduce_while(negs, {last, []}, fn bdd_leaf(neg_type, neg_last) = neg, {acc_last, acc_negs} -> if subtype?(list, neg_type) do - difference = difference(acc_last, neg_last) + difference = bare_difference(acc_last, neg_last) if empty?(difference), do: {:halt, nil}, else: {:cont, {difference, acc_negs}} else {:cont, {acc_last, [neg | acc_negs]}} @@ -2250,9 +2221,6 @@ defmodule Module.Types.Descr do defp list_tail_unfold(:term), do: @not_non_empty_list defp list_tail_unfold(other), do: Map.delete(other, :list) - defp list_top?(bdd_leaf(:term, :term)), do: true - defp list_top?(_), do: false - @doc """ Returns the element type of a list, assuming the list is proper. @@ -2295,7 +2263,7 @@ defmodule Module.Types.Descr do %{list: bdd} -> Enum.reduce(list_bdd_to_pos_dnf(bdd), none(), fn {list, last, _negs}, acc -> if last == @empty_list or subtype?(last, @empty_list) do - union(acc, list) + bare_union(acc, list) else acc end @@ -2313,7 +2281,7 @@ defmodule Module.Types.Descr do true -> {empty_list?, nil} end else - {empty_list?, union(static_value, dynamic(dynamic_value))} + {empty_list?, bare_union(static_value, dynamic(dynamic_value))} end end end @@ -2345,7 +2313,7 @@ defmodule Module.Types.Descr do result = Enum.reduce(list_bdd_to_pos_dnf(bdd), none(), fn {list, last, _negs}, acc -> if last == @empty_list or subtype?(last, @empty_list) do - union(acc, list) + bare_union(acc, list) else throw(:badproperlist) end @@ -2361,59 +2329,17 @@ defmodule Module.Types.Descr do {empty_list, none()} end - defp list_intersection(bdd1, bdd2) do - cond do - list_top?(bdd1) and is_tuple(bdd2) -> bdd2 - list_top?(bdd2) and is_tuple(bdd1) -> bdd1 - true -> bdd_intersection(bdd1, bdd2, &list_leaf_intersection/2) - end - end - - defp list_leaf_intersection(bdd_leaf(list1, last1), bdd_leaf(list2, last2)) do - try do - list = non_empty_intersection!(list1, list2) - last = non_empty_intersection!(last1, last2) - bdd_leaf_new(list, last) - catch - :empty -> :bdd_bot - end - end - - defp list_difference(bdd_leaf(:term, :term), bdd_leaf(:term, :term)), - do: :bdd_bot - - defp list_difference(bdd_leaf(:term, :term), bdd2), - do: bdd_negation(bdd2) - - # Computes the difference between two BDD (Binary Decision Diagram) list types. - # It progressively subtracts each type in bdd2 from all types in bdd1. - # The algorithm handles three cases: - # 1. Disjoint types: keeps the original type from bdd1 - # 2. Subtype relationship: - # a) If bdd2 type is a supertype, keeps only the negations - # b) If only the last type differs, subtracts it - # 3. Base case: adds bdd2 type to negations of bdd1 type - # The result may be larger than the initial bdd1, which is maintained in the accumulator. - defp list_difference(bdd_leaf(list1, last1) = bdd1, bdd_leaf(list2, last2) = bdd2) do - if subtype?(list1, list2) do - if subtype?(last1, last2), - do: :bdd_bot, - else: bdd_leaf_new(list1, difference(last1, last2)) - else - bdd_difference(bdd1, bdd2, &list_leaf_difference/3) - end - end + defp list_union(bdd_leaf(:term, :term) = leaf, _), do: leaf + defp list_union(_, bdd_leaf(:term, :term) = leaf), do: leaf + defp list_union(bdd1, bdd2), do: bdd_union(bdd1, bdd2) - defp list_difference(bdd1, bdd2), - do: bdd_difference(bdd1, bdd2, &list_leaf_difference/3) + defp list_intersection(bdd_leaf(:term, :term), bdd), do: bdd + defp list_intersection(bdd, bdd_leaf(:term, :term)), do: bdd + defp list_intersection(bdd1, bdd2), do: bdd_intersection(bdd1, bdd2) - defp list_leaf_difference(bdd_leaf(list1, last1), bdd_leaf(list2, last2), _) do - if disjoint?(list1, list2) or disjoint?(last1, last2) do - :disjoint - else - :none - end - end + defp list_difference(bdd_leaf(:term, :term), bdd_leaf(:term, :term)), do: :bdd_bot + defp list_difference(bdd_leaf(:term, :term), bdd2), do: bdd_negation(bdd2) + defp list_difference(bdd1, bdd2), do: bdd_difference(bdd1, bdd2) defp list_empty?(@non_empty_list_top), do: false @@ -2437,7 +2363,7 @@ defmodule Module.Types.Descr do empty?(list_type) or empty?(last_type) or Enum.reduce_while(negs, last_type, fn bdd_leaf(neg_type, neg_last), acc_last_type -> if subtype?(list_type, neg_type) do - d = difference(acc_last_type, neg_last) + d = bare_difference(acc_last_type, neg_last) if empty?(d), do: {:halt, nil}, else: {:cont, d} else {:cont, acc_last_type} @@ -2472,7 +2398,7 @@ defmodule Module.Types.Descr do dynamic_value = list_hd_static(dynamic) if non_empty_list_only?(static) and not empty?(dynamic_value) do - {:ok, union(dynamic(dynamic_value), list_hd_static(static))} + {:ok, opt_union(dynamic(dynamic_value), list_hd_static(static))} else :badnonemptylist end @@ -2483,7 +2409,7 @@ defmodule Module.Types.Descr do defp list_hd_static(%{list: bdd}) do list_bdd_to_pos_dnf(bdd) - |> Enum.reduce(none(), fn {list, _last, _negs}, acc -> union(acc, list) end) + |> Enum.reduce(none(), fn {list, _last, _negs}, acc -> opt_union(acc, list) end) end defp list_hd_static(%{}), do: none() @@ -2512,7 +2438,7 @@ defmodule Module.Types.Descr do dynamic_value = list_tl_static(dynamic) if non_empty_list_only?(static) and not empty?(dynamic_value) do - {:ok, union(dynamic(dynamic_value), list_tl_static(static))} + {:ok, opt_union(dynamic(dynamic_value), list_tl_static(static))} else :badnonemptylist end @@ -2532,7 +2458,7 @@ defmodule Module.Types.Descr do end list_bdd_to_pos_dnf(bdd) - |> Enum.reduce(initial, fn {_list, last, _negs}, acc -> union(acc, last) end) + |> Enum.reduce(initial, fn {_list, last, _negs}, acc -> opt_union(acc, last) end) end defp list_tl_static(%{}), do: none() @@ -2602,7 +2528,8 @@ defmodule Module.Types.Descr do negs = Enum.uniq(negs) |> Enum.filter(fn bdd_leaf(nlist, nlast) -> - not empty?(intersection(list, nlist)) and not empty?(intersection(last, nlast)) + not empty?(bare_intersection(list, nlist)) and + not empty?(bare_intersection(last, nlast)) end) add_to_list_normalize(acc, list, last, negs) @@ -2616,7 +2543,7 @@ defmodule Module.Types.Descr do cond do subtype?(list, t) and subtype?(last, l) -> [cur | rest] subtype?(t, list) and subtype?(l, last) -> [{list, last, []} | rest] - equal?(t, list) -> [{t, union(l, last), []} | rest] + equal?(t, list) -> [{t, bare_union(l, last), []} | rest] true -> [cur | add_to_list_normalize(rest, list, last, [])] end end @@ -2676,23 +2603,51 @@ defmodule Module.Types.Descr do # `:dynamic` field is not_set, or it contains a type equal to the static component # (that is, there are no extra dynamic values). - defp dynamic_union(:term, other), do: optional_to_term(other) - defp dynamic_union(other, :term), do: optional_to_term(other) + defp dynamic_union(:term, other, _fun), do: optional_to_term(other) + defp dynamic_union(other, :term, _fun), do: optional_to_term(other) + + defp dynamic_union(left, right, fun), + do: symmetrical_merge(unfold(left), unfold(right), fun) + + defp dynamic_intersection(:term, other, _fun), do: remove_optional_static(other) + defp dynamic_intersection(other, :term, _fun), do: remove_optional_static(other) - defp dynamic_union(left, right), - do: symmetrical_merge(unfold(left), unfold(right), &union/3) + defp dynamic_intersection(left, right, fun), + do: symmetrical_intersection(unfold(left), unfold(right), fun) + + defp dynamic_difference(left, descr, _fun) when descr == %{}, do: left + defp dynamic_difference(left, :term, _fun), do: keep_optional(left) + + defp dynamic_difference(left, right, fun) do + iterator_dynamic_difference(:maps.next(:maps.iterator(unfold(right))), unfold(left), fun) + end + + defp iterator_dynamic_difference({key, v2, iterator}, map, fun) do + acc = + case map do + %{^key => v1} -> + value = fun.(key, v1, v2) + + if value in @empty_difference do + Map.delete(map, key) + else + %{map | key => value} + end + + %{} -> + map + end - defp dynamic_intersection(:term, other), do: remove_optional_static(other) - defp dynamic_intersection(other, :term), do: remove_optional_static(other) + iterator_dynamic_difference(:maps.next(iterator), acc, fun) + end - defp dynamic_intersection(left, right), - do: symmetrical_intersection(unfold(left), unfold(right), &intersection/3) + defp iterator_dynamic_difference(:none, map, _fun), do: map defp dynamic_to_quoted(descr, opts) do cond do # We check for :term literally instead of using term_type? # because we check for term_type? in to_quoted before we - # compute the difference(dynamic, static). + # compute the bare_difference(dynamic, static). descr == :term -> [{:dynamic, [], []}] @@ -2803,23 +2758,18 @@ defmodule Module.Types.Descr do end end - defp map_descr_static(tag, fields, domains) do - map_new = - if not is_fields_empty(domains) do - domains = - if tag == :open do - value = term_or_optional() - fields_put_all_new(domains, @domain_key_types, value) - else - domains - end + defp map_descr_static(tag, fields, []) do + %{map: map_new(tag, fields)} + end - map_new(domains, fields) - else - map_new(tag, fields) - end + defp map_descr_static(:open, fields, domains) do + value = term_or_optional() + domains = fields_put_all_new(domains, @domain_key_types, value) + %{map: map_new(domains, fields)} + end - %{map: map_new} + defp map_descr_static(_tag, fields, domains) do + %{map: map_new(domains, fields)} end defp map_put_domain(domain, domain_keys, value) when is_list(domain_keys) do @@ -2831,7 +2781,7 @@ defmodule Module.Types.Descr do end defp map_put_domain([{k1, v1} | t1], [k1 | keys], _initial, value) do - [{k1, union(v1, value)} | map_put_domain(t1, keys, if_set(value), value)] + [{k1, bare_union(v1, value)} | map_put_domain(t1, keys, if_set(value), value)] end defp map_put_domain(domain, [k2 | keys], initial, value) do @@ -2901,394 +2851,160 @@ defmodule Module.Types.Descr do end end - defp map_union(bdd_leaf(:open, fields) = leaf, _) when is_fields_empty(fields), - do: leaf + defp map_union(bdd_leaf(:open, []) = leaf, _), do: leaf + defp map_union(_, bdd_leaf(:open, []) = leaf), do: leaf + defp map_union(bdd1, bdd2), do: bdd_union(bdd1, bdd2) - defp map_union(_, bdd_leaf(:open, fields) = leaf) when is_fields_empty(fields), - do: leaf + defp map_intersection(bdd_leaf(:open, []), bdd), do: bdd + defp map_intersection(bdd, bdd_leaf(:open, [])), do: bdd + defp map_intersection(bdd1, bdd2), do: bdd_intersection(bdd1, bdd2) - defp map_union(bdd_leaf(tag1, fields1), bdd_leaf(tag2, fields2)) do - case maybe_optimize_map_union(tag1, fields1, tag2, fields2) do - {tag, fields} -> bdd_leaf_new(tag, fields) - nil -> bdd_union(bdd_leaf_new(tag1, fields1), bdd_leaf_new(tag2, fields2)) - end - end + defp map_difference(_, bdd_leaf(:open, [])), do: :bdd_bot + defp map_difference(bdd_leaf(:open, []), {_, _, _, _, _} = bdd2), do: bdd_negation(bdd2) + defp map_difference(bdd1, bdd2), do: bdd_difference(bdd1, bdd2) - defp map_union(bdd1, bdd2), do: bdd_union(bdd1, bdd2) + # Intersects two map literals; throws if their intersection is empty. + # Both open: the result is open. + defp map_literal_intersection(tag1, map1, tag2, map2, intersection_fun \\ &bare_intersection/2) - defp maybe_optimize_map_union(:open, empty, _, _) when is_fields_empty(empty), - do: {:open, @fields_new} + defp map_literal_intersection(:open, map1, :open, map2, intersection_fun) do + new_fields = + fields_merge( + fn _, type1, type2 -> non_empty_intersection!(type1, type2, intersection_fun) end, + map1, + map2 + ) - defp maybe_optimize_map_union(_, _, :open, empty) when is_fields_empty(empty), - do: {:open, @fields_new} + {:open, new_fields} + end - defp maybe_optimize_map_union(tag1, pos1, tag2, pos2) - when is_atom(tag1) and is_atom(tag2) do - case map_union_strategy(pos1, pos2, tag1, tag2, :all_equal) do - :all_equal when tag1 == :open -> {tag1, pos1} - :all_equal -> {tag2, pos2} - {:one_key_difference, key, v1, v2} -> {tag1, fields_store(key, union(v1, v2), pos1)} - :left_subtype_of_right -> {tag2, pos2} - :right_subtype_of_left -> {tag1, pos1} - :none -> nil - end + # Both closed: the result is closed. + defp map_literal_intersection(:closed, map1, :closed, map2, intersection_fun) do + {:closed, map_literal_intersection_closed(map1, map2, intersection_fun)} end - defp maybe_optimize_map_union(_, _, _, _), do: nil + # Open and closed: result is closed, all fields from open should be in closed, except not_set ones. + defp map_literal_intersection(:open, open, :closed, closed, intersection_fun) do + {:closed, map_literal_intersection_open_closed(open, closed, intersection_fun)} + end - defp map_union_strategy([{k1, _} | t1], [{k2, _} | _] = l2, tag1, tag2, status) - when k1 < k2 do - # Left side has a key the right side does not have, - # left can only be a subtype if the right side is open. - case status do - _ when tag2 != :open -> - :none + defp map_literal_intersection(:closed, closed, :open, open, intersection_fun) do + {:closed, map_literal_intersection_open_closed(open, closed, intersection_fun)} + end - :all_equal -> - map_union_strategy(t1, l2, tag1, tag2, :left_subtype_of_right) + # At least one tag is a tag-domain pair. + defp map_literal_intersection(tag_or_domains1, map1, tag_or_domains2, map2, intersection_fun) do + # For a closed map with domains intersected with an open map with domains: + # 1. The result is closed (more restrictive) + # 2. We need to check each domain in the open map against the closed map + default1 = map_key_tag_to_type(tag_or_domains1) + default2 = map_key_tag_to_type(tag_or_domains2) - {:one_key_difference, _, p1, p2} -> - if subtype?(p1, p2), - do: map_union_strategy(t1, l2, tag1, tag2, :left_subtype_of_right), - else: :none + # Compute the new domain + tag_or_domains = map_domain_intersection(tag_or_domains1, tag_or_domains2) - :left_subtype_of_right -> - map_union_strategy(t1, l2, tag1, tag2, :left_subtype_of_right) + # Go over all fields in map1 and map2 with default atom types atom1 and atom2 + # 1. If key is in both maps, compute non empty intersection (:error if it is none) + # 2. If key is only in map1, compute non empty intersection with atom2 + # 3. If key is only in map2, compute non empty intersection with atom1 + # We do that by computing intersection on all key labels in both map1 and map2, + # using default values when a key is not present. + {tag_or_domains, + fields_merge_with_defaults(map1, default1, map2, default2, fn _key, v1, v2 -> + non_empty_intersection!(v1, v2, intersection_fun) + end)} + end - _ -> - :none + # Compute the intersection of two tags or tag-domain pairs. + defp map_domain_intersection(:closed, _), do: :closed + defp map_domain_intersection(_, :closed), do: :closed + defp map_domain_intersection(:open, tag_or_domains), do: tag_or_domains + defp map_domain_intersection(tag_or_domains, :open), do: tag_or_domains + + defp map_domain_intersection(domains1, domains2) do + # If the explicit domains are empty, use simple atom tags + case map_domain_intersection_fields(domains1, domains2) do + [] -> :closed + new_domains -> new_domains end end - defp map_union_strategy([{k1, _} | _] = l1, [{k2, _} | t2], tag1, tag2, status) - when k1 > k2 do - # Right side has a key the left side does not have, - # right can only be a subtype if the left side is open. - case status do - _ when tag1 != :open -> - :none + defp map_domain_intersection_fields([{k1, _} | t1], [{k2, _} | _] = l2) when k1 < k2 do + map_domain_intersection_fields(t1, l2) + end - :all_equal -> - map_union_strategy(l1, t2, tag1, tag2, :right_subtype_of_left) + defp map_domain_intersection_fields([{k1, _} | _] = l1, [{k2, _} | t2]) when k1 > k2 do + map_domain_intersection_fields(l1, t2) + end - {:one_key_difference, _, p1, p2} -> - if subtype?(p2, p1), - do: map_union_strategy(l1, t2, tag1, tag2, :right_subtype_of_left), - else: :none + defp map_domain_intersection_fields([{k, type1} | t1], [{_, type2} | t2]) do + inter = bare_intersection(type1, type2) - :right_subtype_of_left -> - map_union_strategy(l1, t2, tag1, tag2, :right_subtype_of_left) + if empty_or_optional?(inter) do + map_domain_intersection_fields(t1, t2) + else + [{k, inter} | map_domain_intersection_fields(t1, t2)] + end + end - _ -> - :none + defp map_domain_intersection_fields(_, _), do: [] + + defp map_literal_intersection_open_closed([{k1, v1} | t1], [{k2, _} | _] = l2, intersection_fun) + when k1 < k2 do + # If the type in the open map is optional, we continue + case v1 do + %{optional: 1} -> map_literal_intersection_open_closed(t1, l2, intersection_fun) + _ -> throw(:empty) end end - defp map_union_strategy([{_, v} | t1], [{_, v} | t2], tag1, tag2, status) do - # Same key and same value, nothing changes - map_union_strategy(t1, t2, tag1, tag2, status) + defp map_literal_intersection_open_closed([{k1, _} | _] = l1, [{k2, v2} | t2], intersection_fun) + when k1 > k2 do + # Anything in the closed map not in open is preserved + [{k2, v2} | map_literal_intersection_open_closed(l1, t2, intersection_fun)] end - defp map_union_strategy([{k1, v1} | t1], [{_, v2} | t2], tag1, tag2, status) do - # They have the same key but different values - case status do - :all_equal -> - cond do - # Don't do difference on struct keys - k1 != :__struct__ and tag1 == tag2 -> - map_union_strategy(t1, t2, tag1, tag2, {:one_key_difference, k1, v1, v2}) + defp map_literal_intersection_open_closed([{key, v1} | t1], [{_, v2} | t2], intersection_fun) do + [ + {key, non_empty_intersection!(v1, v2, intersection_fun)} + | map_literal_intersection_open_closed(t1, t2, intersection_fun) + ] + end - subtype?(v1, v2) -> - map_union_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) + defp map_literal_intersection_open_closed(t1, t2, _intersection_fun) do + if Enum.all?(t1, fn {_, v} -> match?(%{optional: 1}, v) end) do + t2 + else + throw(:empty) + end + end - subtype?(v2, v1) -> - map_union_strategy(t1, t2, tag1, tag2, :right_subtype_of_left) - - true -> - :none - end - - :left_subtype_of_right -> - if subtype?(v1, v2), do: map_union_strategy(t1, t2, tag1, tag2, status), else: :none - - :right_subtype_of_left -> - if subtype?(v2, v1), do: map_union_strategy(t1, t2, tag1, tag2, status), else: :none - - {:one_key_difference, _key, p1, p2} -> - cond do - subtype?(p1, p2) and subtype?(v1, v2) -> - map_union_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) - - subtype?(p2, p1) and subtype?(v2, v1) -> - map_union_strategy(t1, t2, tag1, tag2, :right_subtype_of_left) - - true -> - :none - end - - _ -> - :none - end - end - - defp map_union_strategy([], [], tag1, tag2, status) do - case status do - :left_subtype_of_right when tag1 == :open and tag2 == :closed -> :none - :right_subtype_of_left when tag1 == :closed and tag2 == :open -> :none - _ -> status - end - end - - defp map_union_strategy(l1, l2, tag1, tag2, status) do - lhs? = tag2 == :open and l2 == [] - rhs? = tag1 == :open and l1 == [] - - case status do - :all_equal when lhs? -> - :left_subtype_of_right - - :all_equal when rhs? -> - :right_subtype_of_left - - {:one_key_difference, _, p1, p2} -> - cond do - lhs? and subtype?(p1, p2) -> :left_subtype_of_right - rhs? and subtype?(p2, p1) -> :right_subtype_of_left - true -> :none - end - - :left_subtype_of_right when lhs? -> - :left_subtype_of_right - - :right_subtype_of_left when rhs? -> - :right_subtype_of_left - - _ -> - :none - end - end - - defp map_intersection(bdd_leaf(:open, fields), bdd) when is_fields_empty(fields), do: bdd - defp map_intersection(bdd, bdd_leaf(:open, fields)) when is_fields_empty(fields), do: bdd - defp map_intersection(bdd1, bdd2), do: bdd_intersection(bdd1, bdd2, &map_leaf_intersection/2) - - defp map_leaf_intersection(bdd_leaf(tag1, fields1), bdd_leaf(tag2, fields2)) do - try do - {tag, fields} = map_literal_intersection(tag1, fields1, tag2, fields2) - bdd_leaf_new(tag, fields) - catch - :empty -> :bdd_bot - end - end - - defp map_difference(_, bdd_leaf(:open, fields)) when is_fields_empty(fields), - do: :bdd_bot - - defp map_difference(bdd_leaf(:open, fields), {_, _, _, _, _} = bdd2) - when is_fields_empty(fields), - do: bdd_negation(bdd2) - - defp map_difference(bdd1, bdd2), - do: bdd_difference(bdd1, bdd2, &map_leaf_difference/3) - - defp map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(:open, [{key, v2}]), type) do - {found?, v1} = - case fields_find(key, fields) do - {:ok, value} -> {true, value} - :error -> {false, map_key_tag_to_type(tag)} - end - - cond do - tag == :closed and not found? -> - if is_optional_static(v2), do: :subtype, else: :disjoint - - # In case the left-side is open, we will only be adding new keys - # to the open map, which makes future eliminations harder. - tag == :open and not found? and fields != [] -> - :none - - disjoint?(v1, v2) -> - :disjoint - - true -> - map_leaf_one_key_difference(tag, fields, key, v1, v2, type) - end - end - - defp map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(neg_tag, neg_fields), type) do - case map_difference_strategy(fields, neg_fields, tag, neg_tag) do - :disjoint -> - :disjoint - - :left_subtype_of_right -> - :subtype - - {:one_key_difference, key, v1, v2} -> - map_leaf_one_key_difference(tag, fields, key, v1, v2, type) - - :none -> - :none - end - end - - defp map_leaf_one_key_difference(tag, fields, key, v1, v2, type) do - v_diff = difference(v1, v2) - - if empty?(v_diff) do - :subtype - else - a_diff = bdd_leaf_new(tag, fields_store(key, v_diff, fields)) - - a_type = - case type do - :none -> - :bdd_bot - - :union -> - bdd_leaf_new(tag, fields_store(key, union(v1, v2), fields)) - - :intersection -> - v_int = intersection(v1, v2) - - if empty?(v_int), - do: :bdd_bot, - else: bdd_leaf_new(tag, fields_store(key, v_int, fields)) - end - - {:one_key_difference, a_diff, a_type} - end - end - - # Intersects two map literals; throws if their intersection is empty. - # Both open: the result is open. - defp map_literal_intersection(:open, map1, :open, map2) do - new_fields = - fields_merge( - fn _, type1, type2 -> non_empty_intersection!(type1, type2) end, - map1, - map2 - ) - - {:open, new_fields} - end - - # Both closed: the result is closed. - defp map_literal_intersection(:closed, map1, :closed, map2) do - {:closed, map_literal_intersection_closed(map1, map2)} - end - - # Open and closed: result is closed, all fields from open should be in closed, except not_set ones. - defp map_literal_intersection(:open, open, :closed, closed) do - {:closed, map_literal_intersection_open_closed(open, closed)} - end - - defp map_literal_intersection(:closed, closed, :open, open) do - {:closed, map_literal_intersection_open_closed(open, closed)} - end - - # At least one tag is a tag-domain pair. - defp map_literal_intersection(tag_or_domains1, map1, tag_or_domains2, map2) do - # For a closed map with domains intersected with an open map with domains: - # 1. The result is closed (more restrictive) - # 2. We need to check each domain in the open map against the closed map - default1 = map_key_tag_to_type(tag_or_domains1) - default2 = map_key_tag_to_type(tag_or_domains2) - - # Compute the new domain - tag_or_domains = map_domain_intersection(tag_or_domains1, tag_or_domains2) - - # Go over all fields in map1 and map2 with default atom types atom1 and atom2 - # 1. If key is in both maps, compute non empty intersection (:error if it is none) - # 2. If key is only in map1, compute non empty intersection with atom2 - # 3. If key is only in map2, compute non empty intersection with atom1 - # We do that by computing intersection on all key labels in both map1 and map2, - # using default values when a key is not present. - {tag_or_domains, - fields_merge_with_defaults(map1, default1, map2, default2, fn _key, v1, v2 -> - non_empty_intersection!(v1, v2) - end)} - end - - # Compute the intersection of two tags or tag-domain pairs. - defp map_domain_intersection(:closed, _), do: :closed - defp map_domain_intersection(_, :closed), do: :closed - defp map_domain_intersection(:open, tag_or_domains), do: tag_or_domains - defp map_domain_intersection(tag_or_domains, :open), do: tag_or_domains - - defp map_domain_intersection(domains1, domains2) do - # If the explicit domains are empty, use simple atom tags - case map_domain_intersection_fields(domains1, domains2) do - [] -> :closed - new_domains -> new_domains - end - end - - defp map_domain_intersection_fields([{k1, _} | t1], [{k2, _} | _] = l2) when k1 < k2 do - map_domain_intersection_fields(t1, l2) - end - - defp map_domain_intersection_fields([{k1, _} | _] = l1, [{k2, _} | t2]) when k1 > k2 do - map_domain_intersection_fields(l1, t2) - end - - defp map_domain_intersection_fields([{k, type1} | t1], [{_, type2} | t2]) do - inter = intersection(type1, type2) - - if empty_or_optional?(inter) do - map_domain_intersection_fields(t1, t2) - else - [{k, inter} | map_domain_intersection_fields(t1, t2)] - end - end - - defp map_domain_intersection_fields(_, _), do: [] - - defp map_literal_intersection_open_closed([{k1, v1} | t1], [{k2, _} | _] = l2) when k1 < k2 do - # If the type in the open map is optional, we continue - case v1 do - %{optional: 1} -> map_literal_intersection_open_closed(t1, l2) - _ -> throw(:empty) - end - end - - defp map_literal_intersection_open_closed([{k1, _} | _] = l1, [{k2, v2} | t2]) when k1 > k2 do - # Anything in the closed map not in open is preserved - [{k2, v2} | map_literal_intersection_open_closed(l1, t2)] - end - - defp map_literal_intersection_open_closed([{key, v1} | t1], [{_, v2} | t2]) do - [{key, non_empty_intersection!(v1, v2)} | map_literal_intersection_open_closed(t1, t2)] - end - - defp map_literal_intersection_open_closed(t1, t2) do - if Enum.all?(t1, fn {_, v} -> match?(%{optional: 1}, v) end) do - t2 - else - throw(:empty) - end - end - - defp map_literal_intersection_closed([{k1, v1} | t1], [{k2, _} | _] = l2) when k1 < k2 do + defp map_literal_intersection_closed([{k1, v1} | t1], [{k2, _} | _] = l2, intersection_fun) + when k1 < k2 do if is_optional_static(v1) do - map_literal_intersection_closed(t1, l2) + map_literal_intersection_closed(t1, l2, intersection_fun) else throw(:empty) end end - defp map_literal_intersection_closed([{k1, _} | _] = l1, [{k2, v2} | t2]) when k1 > k2 do + defp map_literal_intersection_closed([{k1, _} | _] = l1, [{k2, v2} | t2], intersection_fun) + when k1 > k2 do if is_optional_static(v2) do - map_literal_intersection_closed(l1, t2) + map_literal_intersection_closed(l1, t2, intersection_fun) else throw(:empty) end end - defp map_literal_intersection_closed([{key, v1} | t1], [{_, v2} | t2]) do - [{key, non_empty_intersection!(v1, v2)} | map_literal_intersection_closed(t1, t2)] + defp map_literal_intersection_closed([{key, v1} | t1], [{_, v2} | t2], intersection_fun) do + [ + {key, non_empty_intersection!(v1, v2, intersection_fun)} + | map_literal_intersection_closed(t1, t2, intersection_fun) + ] end - defp map_literal_intersection_closed(t1, t2) do + defp map_literal_intersection_closed(t1, t2, _intersection_fun) do if Enum.any?(t1, fn {_, v} -> not is_optional_static(v) end) or Enum.any?(t2, fn {_, v} -> not is_optional_static(v) end) do throw(:empty) @@ -3297,8 +3013,8 @@ defmodule Module.Types.Descr do [] end - defp non_empty_intersection!(type1, type2) do - type = intersection(type1, type2) + defp non_empty_intersection!(type1, type2, intersection_fun) do + type = intersection_fun.(type1, type2) if empty?(type), do: throw(:empty), else: type end @@ -3368,7 +3084,7 @@ defmodule Module.Types.Descr do if static_optional? or empty?(dynamic_type) do :badkey else - {dynamic_optional?, union(dynamic(dynamic_type), static_type)} + {dynamic_optional?, opt_union(dynamic(dynamic_type), static_type)} end else :badmap @@ -3400,9 +3116,9 @@ defmodule Module.Types.Descr do # Optimization: if there are no negatives {tag, fields, []}, acc -> case fields_find(key, fields) do - {:ok, value} -> union(value, acc) + {:ok, value} -> opt_union(value, acc) :error when tag == :open -> throw(:open) - :error -> map_key_tag_to_type(tag) |> union(acc) + :error -> map_key_tag_to_type(tag) |> opt_union(acc) end {tag, fields, negs}, acc -> @@ -3419,10 +3135,10 @@ defmodule Module.Types.Descr do else negs |> map_split_negative_key(key, value, bdd) - |> Enum.reduce(none(), fn {value, _}, acc -> union(value, acc) end) + |> Enum.reduce(none(), fn {value, _}, acc -> opt_union(value, acc) end) end - union(value, acc) + opt_union(value, acc) end end) catch @@ -3434,7 +3150,7 @@ defmodule Module.Types.Descr do defp map_split_negative_pairs_key(negs, key) do Enum.reduce_while(negs, [], fn - bdd_leaf(:open, empty), _acc when is_fields_empty(empty) -> + bdd_leaf(:open, []), _acc -> {:halt, :empty} bdd_leaf(tag, fields), neg_acc -> @@ -3459,10 +3175,10 @@ defmodule Module.Types.Descr do defp map_pair_projection_keeps_full_snd?(negative, value) do neg_values = Enum.reduce(negative, none(), fn {neg_value, _neg_bdd}, acc -> - union(neg_value, acc) + bare_union(neg_value, acc) end) - not empty?(difference(value, neg_values)) + not empty?(bare_difference(value, neg_values)) end defp map_split_negative_key(negs, key, value, bdd) do @@ -3478,7 +3194,7 @@ defmodule Module.Types.Descr do # {t, s} \ {t₁, s₁} = {t \ t₁, s} ∪ {t ∩ t₁, s \ s₁} defp map_split_negative(negs, value, bdd, take_fun) do Enum.reduce(negs, [{value, bdd}], fn - bdd_leaf(:open, empty), _acc when is_fields_empty(empty) -> + bdd_leaf(:open, []), _acc -> throw(:empty) bdd_leaf(neg_tag, neg_fields), acc -> @@ -3503,7 +3219,7 @@ defmodule Module.Types.Descr do if neg_tag == :closed and map_empty?(map_intersection(bdd, neg_bdd)) do [{value, bdd} | acc] else - intersection_value = intersection(value, neg_value) + intersection_value = bare_intersection(value, neg_value) if empty?(intersection_value) do [{value, bdd} | acc] @@ -3533,7 +3249,7 @@ defmodule Module.Types.Descr do end defp prepend_pair_unless_empty_diff(value, neg_value, bdd, acc) do - diff_value = difference(value, neg_value) + diff_value = bare_difference(value, neg_value) if empty?(diff_value), do: acc, else: [{diff_value, bdd} | acc] end @@ -3558,7 +3274,7 @@ defmodule Module.Types.Descr do with {:ok, dynamic_type} <- map_to_list_static(dynamic, fun) do if descr_key?(static, :map) do with {:ok, static_type} <- map_to_list_static(static, fun) do - {:ok, union(static_type, dynamic(dynamic_type))} + {:ok, opt_union(static_type, dynamic(dynamic_type))} end else {:ok, dynamic(dynamic_type)} @@ -3625,7 +3341,7 @@ defmodule Module.Types.Descr do if empty?(value) do acc else - union(acc, fun.(domain_key_to_descr(domain_key), value)) + opt_union(acc, fun.(domain_key_to_descr(domain_key), value)) end end) @@ -3649,7 +3365,7 @@ defmodule Module.Types.Descr do if empty?(value) do {seen, acc} else - {seen, union(acc, fun.(atom([key]), value))} + {seen, opt_union(acc, fun.(atom([key]), value))} end end end) @@ -3751,8 +3467,8 @@ defmodule Module.Types.Descr do # We can exceptionally check for none() here because # we already check for empty downstream if dynamic_found? do - {union(static_value, dynamic(dynamic_value)), - union(static_descr, dynamic(dynamic_descr)), static_errors ++ dynamic_errors} + {opt_union(static_value, dynamic(dynamic_value)), + opt_union(static_descr, dynamic(dynamic_descr)), static_errors ++ dynamic_errors} else {:error, static_errors ++ dynamic_errors} end @@ -3870,8 +3586,10 @@ defmodule Module.Types.Descr do acc_errors = if required_key?, do: [{:badkey, key} | acc_errors], else: acc_errors {acc_value, acc_descr, acc_errors, acc_found?} else - acc_value = union(value, acc_value) - acc_descr = union(map_put_key_static(descr, key, type_fun.(optional?, value)), acc_descr) + acc_value = opt_union(value, acc_value) + + acc_descr = + opt_union(map_put_key_static(descr, key, type_fun.(optional?, value)), acc_descr) # The field will be missing if we are not forcing, # we are in static mode and the value is optional. @@ -3917,7 +3635,7 @@ defmodule Module.Types.Descr do # Optimization: if there are no negatives, we can directly remove the key. {tag, fields, []}, {value, bdd} -> {fst, snd} = map_pop_key_bdd(tag, fields, key) - {maybe_union(value, fn -> fst end), map_union(bdd, snd)} + {maybe_opt_union(value, fn -> fst end), opt_map_union(bdd, snd)} {tag, fields, negs}, {value, bdd} -> {fst, snd} = map_pop_key_bdd(tag, fields, key) @@ -3937,17 +3655,17 @@ defmodule Module.Types.Descr do do: [], else: map_split_negative_key(negs, key, fst, snd) - {maybe_union(value, fn -> + {maybe_opt_union(value, fn -> if keep_fst? do fst else - Enum.reduce(pairs, none(), &union(elem(&1, 0), &2)) + Enum.reduce(pairs, none(), &opt_union(elem(&1, 0), &2)) end end), if keep_snd? do - map_union(bdd, snd) + opt_map_union(bdd, snd) else - Enum.reduce(pairs, bdd, &map_union(elem(&1, 1), &2)) + Enum.reduce(pairs, bdd, &opt_map_union(elem(&1, 1), &2)) end} end end) @@ -3967,7 +3685,7 @@ defmodule Module.Types.Descr do {seen, acc} else {_, value} = map_dnf_fetch_static(dnf, key) - {Map.put(seen, key, []), union(acc, value)} + {Map.put(seen, key, []), opt_union(acc, value)} end end) end) @@ -4008,12 +3726,12 @@ defmodule Module.Types.Descr do cond do # Domain has a direct match: valid, union both atom keys and domain value not empty?(value) -> - acc = if require_type?, do: union(union(atom_acc, acc), value), else: acc + acc = if require_type?, do: opt_union(opt_union(atom_acc, acc), value), else: acc {true, [:atom | valid], invalid, acc} # No direct match, but individual atom keys exist: found but domain is invalid not empty?(atom_acc) -> - acc = if require_type?, do: union(atom_acc, acc), else: acc + acc = if require_type?, do: opt_union(atom_acc, acc), else: acc {true, valid, [:atom | invalid], acc} # No match at all @@ -4023,7 +3741,7 @@ defmodule Module.Types.Descr do # Non-atom domain key has a match: mark as valid not empty?(value) -> - acc = if require_type?, do: union(acc, value), else: acc + acc = if require_type?, do: opt_union(acc, value), else: acc {true, [domain_key | valid], invalid, acc} # Non-atom domain key not found: mark as invalid @@ -4086,7 +3804,11 @@ defmodule Module.Types.Descr do Enum.reduce(domain_keys, domains, fn domain_key, acc -> case fields_find(domain_key, acc) do {:ok, value} -> - fields_store(domain_key, union(value, type_fun.(true, remove_optional(value))), acc) + fields_store( + domain_key, + opt_union(value, type_fun.(true, remove_optional(value))), + acc + ) :error -> # Likewise, only forced updates may synthesize missing domain keys. @@ -4149,7 +3871,7 @@ defmodule Module.Types.Descr do if descr_key?(dynamic, :map) and map_only?(static) do static_descr = map_put_static(static, split_keys, type) dynamic_descr = map_put_static(dynamic, split_keys, type) - {:ok, union(static_descr, dynamic(dynamic_descr))} + {:ok, opt_union(static_descr, dynamic(dynamic_descr))} else :badmap end @@ -4198,7 +3920,7 @@ defmodule Module.Types.Descr do defp map_put_keys_static(dnf, keys, type, acc) do Enum.reduce(keys, acc, fn key, acc -> {nil, descr} = map_dnf_pop_key_static(dnf, key, nil) - union(map_put_key_static(descr, key, type), acc) + opt_union(map_put_key_static(descr, key, type), acc) end) end @@ -4238,7 +3960,7 @@ defmodule Module.Types.Descr do if empty?(dynamic_type) do :error else - {:ok, union(dynamic(dynamic_type), static_type)} + {:ok, opt_union(dynamic(dynamic_type), static_type)} end else :badmap @@ -4267,7 +3989,7 @@ defmodule Module.Types.Descr do defp map_get_keys(dnf, keys, acc) do Enum.reduce(keys, acc, fn atom, acc -> {_, value} = map_dnf_fetch_static(dnf, atom) - union(value, acc) + opt_union(value, acc) end) end @@ -4277,14 +3999,14 @@ defmodule Module.Types.Descr do Enum.reduce(dnf, acc, fn # Optimization: if there are no negatives, get the domain tag directly {tag, _fields, []}, acc -> - map_domain_tag_to_type(tag, domain_key) |> union(acc) + map_domain_tag_to_type(tag, domain_key) |> opt_union(acc) {tag_or_domains, fields, negs}, acc -> if init_map_line_empty?(tag_or_domains, fields, negs) do acc else {_found, value, _bdd} = map_pop_domain_bdd(tag_or_domains, fields, domain_key) - union(value, acc) + opt_union(value, acc) end end) end @@ -4351,10 +4073,10 @@ defmodule Module.Types.Descr do {[], [], nil, to_domain_keys(key_descr), []} end - defp non_empty_map_literals_intersection(maps) do + defp non_empty_map_literals_intersection(maps, intersection_fun \\ &bare_intersection/2) do try do Enum.reduce(maps, {:open, []}, fn bdd_leaf(next_tag, next_fields), {tag, fields} -> - map_literal_intersection(tag, fields, next_tag, next_fields) + map_literal_intersection(tag, fields, next_tag, next_fields, intersection_fun) end) catch :empty -> :empty @@ -4389,9 +4111,7 @@ defmodule Module.Types.Descr do # an intersection or difference is computed, its emptiness is checked again. # So they are all necessarily non-empty. defp map_line_empty?(_, _pos, []), do: false - - defp map_line_empty?(_, _, [bdd_leaf(:open, neg_fields) | _]) when is_fields_empty(neg_fields), - do: true + defp map_line_empty?(_, _, [bdd_leaf(:open, []) | _]), do: true defp map_line_empty?(:open, fs, [bdd_leaf(:closed, _) | negs]), do: map_line_empty?(:open, fs, negs) @@ -4461,8 +4181,8 @@ defmodule Module.Types.Descr do end defp map_line_meet_empty?(key, type, neg_type, t1, t2, tag, neg_tag, acc_meet, negs) do - diff = difference(type, neg_type) - meet = intersection(type, neg_type) + diff = bare_difference(type, neg_type) + meet = bare_intersection(type, neg_type) (empty?(diff) or map_line_empty?(tag, Enum.reverse(acc_meet, [{key, diff} | t1]), negs)) and (empty?(meet) or map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, meet} | acc_meet], negs)) @@ -4518,7 +4238,7 @@ defmodule Module.Types.Descr do end defp map_line_fields_empty_recur?(key, v1, v2, tag, fields, negs) do - diff = difference(v1, v2) + diff = bare_difference(v1, v2) empty?(diff) or map_line_empty?(tag, fields_store(key, diff, fields), negs) end @@ -4560,210 +4280,14 @@ defmodule Module.Types.Descr do defp map_pop_domain_bdd(tag, fields, _domain_key), do: {false, map_domain_tag_to_type(tag), map_new(tag, fields)} - # Continue to eliminate negations while length of list of negs decreases - defp map_eliminate_while_negs_decrease(tag, fields, []), do: [{tag, fields, []}] - - defp map_eliminate_while_negs_decrease(tag, fields, negs) do - try do - maybe_eliminate_map_negations(tag, fields, negs) - catch - :empty -> [] - else - {fields, new_negs} -> - if length(new_negs) < length(negs) do - map_eliminate_while_negs_decrease(tag, fields, new_negs) - else - [{tag, fields, new_negs}] - end - end - end - - defp maybe_eliminate_map_negations(tag, fields, negs) do - Enum.reduce(negs, {fields, []}, fn neg = bdd_leaf(neg_tag, neg_fields), - {acc_fields, acc_negs} -> - # If the intersection with the negative is empty, we can remove the negative. - empty_intersection? = - try do - _ = map_literal_intersection(tag, acc_fields, neg_tag, neg_fields) - false - catch - :empty -> true - end - - if empty_intersection? do - {acc_fields, acc_negs} - else - case map_difference_strategy(acc_fields, neg_fields, tag, neg_tag) do - {:one_key_difference, key, v1, v2} -> - {fields_store(key, difference(v1, v2), acc_fields), acc_negs} - - :disjoint -> - {acc_fields, acc_negs} - - :left_subtype_of_right -> - throw(:empty) - - :none -> - {acc_fields, [neg | acc_negs]} - end - end - end) - end - - defp map_difference_strategy(fields1, fields2, tag1, tag2) do - if is_atom(tag1) and is_atom(tag2) do - status = if tag1 == tag2 or tag2 == :open, do: :all_equal, else: :none - map_difference_strategy(fields1, fields2, tag1, tag2, status) - else - :none - end - end - - defp map_difference_strategy([{k1, value} | t1], [{k2, _} | _] = l2, tag1, tag2, status) - when k1 < k2 do - # Left side has a key the right side does not have, - # left can only be a subtype if the right side is open. - # If the right side is closed and the key is not optional, they are disjoint. - case status do - _ when tag2 == :closed -> - if not is_optional_static(value) do - :disjoint - else - map_difference_strategy(t1, l2, tag1, tag2, :none) - end - - :all_equal -> - map_difference_strategy(t1, l2, tag1, tag2, :left_subtype_of_right) - - {:one_key_difference, _, p1, p2} -> - if subtype?(p1, p2), - do: map_difference_strategy(t1, l2, tag1, tag2, :left_subtype_of_right), - else: :none - - :left_subtype_of_right -> - map_difference_strategy(t1, l2, tag1, tag2, :left_subtype_of_right) - - _ -> - :none - end - end - - defp map_difference_strategy([{k1, _} | _] = l1, [{k2, value} | t2], tag1, tag2, _status) - when k1 > k2 do - # Right side has a key the left side does not have, - # if left-side is closed, they are disjoint. - if tag1 == :closed and not is_optional_static(value) do - :disjoint - else - map_difference_strategy(l1, t2, tag1, tag2, :none) - end - end - - defp map_difference_strategy([{_, v} | t1], [{_, v} | t2], tag1, tag2, status) do - # Same key and same value, nothing changes - map_difference_strategy(t1, t2, tag1, tag2, status) - end - - defp map_difference_strategy([{k1, v1} | t1], [{_, v2} | t2], tag1, tag2, status) do - # They have the same key but different values - if disjoint?(v1, v2) do - :disjoint - else - case status do - :all_equal when tag1 == tag2 -> - map_difference_strategy(t1, t2, tag1, tag2, {:one_key_difference, k1, v1, v2}) - - {:one_key_difference, _key, p1, p2} -> - if subtype?(p1, p2) and subtype?(v1, v2) do - map_difference_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) - else - :none - end - - _ -> - if status in [:all_equal, :left_subtype_of_right] and subtype?(v1, v2), - do: map_difference_strategy(t1, t2, tag1, tag2, :left_subtype_of_right), - else: map_difference_strategy(t1, t2, tag1, tag2, :none) - end - end - end - - defp map_difference_strategy([], [], _tag1, _tag2, status) do - if status == :all_equal, do: :left_subtype_of_right, else: status - end - - defp map_difference_strategy(l1, l2, tag1, tag2, status) do - cond do - tag2 == :open and l2 == [] -> - case status do - :all_equal -> - :left_subtype_of_right - - {:one_key_difference, _, p1, p2} -> - if subtype?(p1, p2), do: :left_subtype_of_right, else: :none - - :left_subtype_of_right -> - :left_subtype_of_right - - :none -> - :none - end - - tag1 == :closed and l2 != [] and Enum.all?(l2, fn {_, v} -> not is_optional_static(v) end) -> - :disjoint - - tag2 == :closed and l1 != [] and Enum.all?(l1, fn {_, v} -> not is_optional_static(v) end) -> - :disjoint - - true -> - :none - end - end - - # Given a dnf, fuse maps when possible - # e.g. %{a: integer(), b: atom()} or %{a: float(), b: atom()} into %{a: number(), b: atom()} - defp map_fusion(dnf) do - # Steps: - # 1. Group maps by tags and keys - # 2. Try fusions for each group until no fusion is found - # 3. Merge the groups back into a dnf - {without_negs, with_negs} = Enum.split_with(dnf, fn {_tag, _fields, negs} -> negs == [] end) - - without_negs = - without_negs - |> Enum.group_by(fn {tag, fields, _} -> {tag, fields_keys(fields)} end) - |> Enum.flat_map(fn {_, maps} -> map_non_negated_fuse(maps) end) - - without_negs ++ with_negs - end - - defp map_non_negated_fuse(maps) do - Enum.reduce(maps, [], fn map, acc -> - map_fuse_with_first_fusible(map, acc) - end) - end - - defp map_fuse_with_first_fusible(map, []), do: [map] - - defp map_fuse_with_first_fusible(map, [candidate | rest]) do - {tag1, fields1, []} = map - {tag2, fields2, []} = candidate - - case maybe_optimize_map_union(tag1, fields1, tag2, fields2) do - nil -> [candidate | map_fuse_with_first_fusible(map, rest)] - # we found a fusible candidate, we're done - {tag, fields} -> [{tag, fields, []} | rest] - end - end - - defp map_to_quoted(bdd, opts) do - bdd - |> map_bdd_to_dnf_with_empty() - |> Enum.flat_map(fn {tag, fields, negs} -> - map_eliminate_while_negs_decrease(tag, fields, negs) - end) - |> map_fusion() - |> Enum.map(&map_each_to_quoted(&1, opts)) + defp map_to_quoted(bdd, opts) do + bdd + |> map_bdd_to_dnf_with_empty() + |> Enum.flat_map(fn {tag, fields, negs} -> + opt_map_eliminate_while_negs_decrease(tag, fields, negs) + end) + |> opt_map_fusion() + |> Enum.map(&map_each_to_quoted(&1, opts)) end defp map_each_to_quoted({tag, positive_map, negative_maps}, opts) do @@ -4780,11 +4304,11 @@ defmodule Module.Types.Descr do end end - defp map_literal_to_quoted({:closed, empty}, _opts) when is_fields_empty(empty) do + defp map_literal_to_quoted({:closed, []}, _opts) do {:empty_map, [], []} end - defp map_literal_to_quoted({:open, empty}, _opts) when is_fields_empty(empty) do + defp map_literal_to_quoted({:open, []}, _opts) do {:map, [], []} end @@ -4792,8 +4316,7 @@ defmodule Module.Types.Descr do map_literal_to_quoted({tag, fields}, opts) end - defp map_literal_to_quoted({domains, empty}, _opts) - when is_fields_empty(domains) and is_fields_empty(empty) do + defp map_literal_to_quoted({[], []}, _opts) do {:empty_map, [], []} end @@ -5013,26 +4536,22 @@ defmodule Module.Types.Descr do defp tuple_intersection(bdd_leaf(:open, []), bdd), do: bdd defp tuple_intersection(bdd, bdd_leaf(:open, [])), do: bdd - - defp tuple_intersection(bdd1, bdd2) do - bdd_intersection(bdd1, bdd2, &tuple_leaf_intersection/2) - end - - defp tuple_leaf_intersection(bdd_leaf(tag1, elements1), bdd_leaf(tag2, elements2)) do - case tuple_literal_intersection(tag1, elements1, tag2, elements2) do - {tag, elements} -> bdd_leaf_new(tag, elements) - :empty -> :bdd_bot - end - end - - defp tuple_literal_intersection(tag1, elements1, tag2, elements2) do + defp tuple_intersection(bdd1, bdd2), do: bdd_intersection(bdd1, bdd2) + + defp tuple_literal_intersection( + tag1, + elements1, + tag2, + elements2, + intersection_fun + ) do case tuple_sizes_strategy(tag1, length(elements1), tag2, length(elements2)) do :disjoint -> :empty _ -> try do - zip_non_empty_intersection!(elements1, elements2, []) + zip_non_empty_intersection!(elements1, elements2, [], intersection_fun) catch :empty -> :empty else @@ -5043,18 +4562,26 @@ defmodule Module.Types.Descr do end # Intersects two lists of types, and _appends_ the extra elements to the result. - defp zip_non_empty_intersection!([], types2, acc), do: Enum.reverse(acc, types2) - defp zip_non_empty_intersection!(types1, [], acc), do: Enum.reverse(acc, types1) - - defp zip_non_empty_intersection!([type1 | rest1], [type2 | rest2], acc) do - zip_non_empty_intersection!(rest1, rest2, [non_empty_intersection!(type1, type2) | acc]) + defp zip_non_empty_intersection!([], types2, acc, _intersection_fun), + do: Enum.reverse(acc, types2) + + defp zip_non_empty_intersection!(types1, [], acc, _intersection_fun), + do: Enum.reverse(acc, types1) + + defp zip_non_empty_intersection!([type1 | rest1], [type2 | rest2], acc, intersection_fun) do + zip_non_empty_intersection!( + rest1, + rest2, + [non_empty_intersection!(type1, type2, intersection_fun) | acc], + intersection_fun + ) end defp zip_empty_intersection?([], _types2), do: false defp zip_empty_intersection?(_types1, []), do: false defp zip_empty_intersection?([type1 | rest1], [type2 | rest2]) do - case empty?(intersection(type1, type2)) do + case empty?(bare_intersection(type1, type2)) do true -> true false -> zip_empty_intersection?(rest1, rest2) end @@ -5074,28 +4601,9 @@ defmodule Module.Types.Descr do do: bdd_negation(bdd2) defp tuple_difference(bdd1, bdd2), - do: bdd_difference(bdd1, bdd2, &tuple_leaf_difference/3) - - defp tuple_leaf_difference(bdd_leaf(tag1, elements1), bdd_leaf(tag2, elements2), _) do - case tuple_sizes_strategy(tag1, length(elements1), tag2, length(elements2)) do - :disjoint -> :disjoint - other -> tuple_leaf_difference(elements1, elements2, other == :left_subtype_of_right) - end - end - - defp tuple_leaf_difference([head1 | tail1], [head2 | tail2], subtype?) do - cond do - disjoint?(head1, head2) -> :disjoint - subtype? and subtype?(head1, head2) -> tuple_leaf_difference(tail1, tail2, subtype?) - true -> :none - end - end - - defp tuple_leaf_difference(_tail1, _tail2, subtype?) do - if subtype?, do: :subtype, else: :none - end + do: bdd_difference(bdd1, bdd2, &opt_tuple_leaf_difference/3) - defp non_empty_tuple_literals_intersection(tuples) do + defp non_empty_tuple_literals_intersection(tuples, intersection_fun \\ &bare_intersection/2) do try do Enum.reduce(tuples, {:open, []}, fn bdd_leaf(tag1, elements1), {tag2, elements2} -> case tuple_sizes_strategy(tag1, length(elements1), tag2, length(elements2)) do @@ -5104,7 +4612,7 @@ defmodule Module.Types.Descr do _ -> tag = if tag1 == :open and tag2 == :open, do: :open, else: :closed - {tag, zip_intersection(elements1, elements2, [])} + {tag, zip_intersection(elements1, elements2, [], intersection_fun)} end end) catch @@ -5119,11 +4627,11 @@ defmodule Module.Types.Descr do end end - defp zip_intersection([], types2, acc), do: Enum.reverse(acc, types2) - defp zip_intersection(types1, [], acc), do: Enum.reverse(acc, types1) + defp zip_intersection([], types2, acc, _intersection_fun), do: Enum.reverse(acc, types2) + defp zip_intersection(types1, [], acc, _intersection_fun), do: Enum.reverse(acc, types1) - defp zip_intersection([type1 | rest1], [type2 | rest2], acc) do - zip_intersection(rest1, rest2, [intersection(type1, type2) | acc]) + defp zip_intersection([type1 | rest1], [type2 | rest2], acc, intersection_fun) do + zip_intersection(rest1, rest2, [intersection_fun.(type1, type2) | acc], intersection_fun) end defp tuple_empty?(bdd) do @@ -5167,8 +4675,8 @@ defmodule Module.Types.Descr do defp tuple_elements_empty?(acc_meet, tag, elements, [neg_type | neg_elements], negs) do # Handles the case where {tag, elements} is an open tuple, like {:open, []} {ty, elements} = List.pop_at(elements, 0, term()) - diff = difference(ty, neg_type) - meet = intersection(ty, neg_type) + diff = bare_difference(ty, neg_type) + meet = bare_intersection(ty, neg_type) # In this case, there is no intersection between the positive and this negative. # So we should just "go next" @@ -5223,8 +4731,8 @@ defmodule Module.Types.Descr do # - `neg_elements` are the corresponding negative tuple element types. # # For position i: - # - One branch mismatches here: (ti \ ui), with earlier positions fixed to intersection(tj, uj). - # - The other recursive path forces match here: intersection(ti, ui) and continues to i+1. + # - One branch mismatches here: (ti \ ui), with earlier positions fixed to bare_intersection(tj, uj). + # - The other recursive path forces match here: bare_intersection(ti, ui) and continues to i+1. # # This yields disjoint branches like: # {t1 /\ not u1, t2, t3, ...} OR {t1 /\ u1, t2 /\ not u2, t3, ...} OR {t1/\u1, t2/\u2, t3/\ not u3, ...} ... @@ -5237,9 +4745,9 @@ defmodule Module.Types.Descr do end # ti \ ui (i.e., ti and not ui) - diff = difference(ty, neg_type) + diff = bare_difference(ty, neg_type) # ti /\ ui - meet = intersection(ty, neg_type) + meet = bare_intersection(ty, neg_type) # Branch where the earliest difference is *here*. # Earlier positions are the accumulated matches in `acc`; @@ -5287,100 +4795,13 @@ defmodule Module.Types.Descr do end) end - defp tuple_union(bdd_leaf(:open, fields) = leaf, _) when is_fields_empty(fields), - do: leaf - - defp tuple_union(_, bdd_leaf(:open, fields) = leaf) when is_fields_empty(fields), - do: leaf - - defp tuple_union( - bdd_leaf(tag1, elements1) = tuple1, - bdd_leaf(tag2, elements2) = tuple2 - ) do - case maybe_optimize_tuple_union({tag1, elements1}, {tag2, elements2}) do - {tag, elements} -> bdd_leaf_new(tag, elements) - nil -> bdd_union(tuple1, tuple2) - end - end - + defp tuple_union(bdd_leaf(:open, []) = leaf, _), do: leaf + defp tuple_union(_, bdd_leaf(:open, []) = leaf), do: leaf defp tuple_union(bdd1, bdd2), do: bdd_union(bdd1, bdd2) - defp maybe_optimize_tuple_union({tag1, pos1} = tuple1, {tag2, pos2} = tuple2) do - case tuple_union_strategy(tag1, pos1, tag2, pos2) do - :all_equal -> - tuple1 - - {:one_index_difference, index, v1, v2} -> - new_pos = List.replace_at(pos1, index, union(v1, v2)) - {tag1, new_pos} - - :left_subtype_of_right -> - tuple2 - - :right_subtype_of_left -> - tuple1 - - :none -> - nil - end - end - - defp tuple_union_strategy(tag1, pos1, tag2, pos2) do - case {tag1, tag2} do - {tag, tag} when length(pos1) == length(pos2) -> - tuple_union_strategy_index(pos1, pos2, 0, :all_equal) - - {:open, _} when length(pos1) <= length(pos2) -> - tuple_union_strategy_index(pos1, pos2, 0, :right_subtype_of_left) - - {_, :open} when length(pos1) >= length(pos2) -> - tuple_union_strategy_index(pos1, pos2, 0, :left_subtype_of_right) - - {_, _} -> - :none - end - end - - defp tuple_union_strategy_index([v | pos1], [v | pos2], i, status) do - tuple_union_strategy_index(pos1, pos2, i + 1, status) - end - - defp tuple_union_strategy_index([v1 | pos1], [v2 | pos2], i, status) do - case status do - :all_equal -> - tuple_union_strategy_index(pos1, pos2, i + 1, {:one_index_difference, i, v1, v2}) - - {:one_index_difference, _, d1, d2} -> - cond do - subtype?(d1, d2) and subtype?(v1, v2) -> - tuple_union_strategy_index(pos1, pos2, i + 1, :left_subtype_of_right) - - subtype?(d2, d1) and subtype?(v2, v1) -> - tuple_union_strategy_index(pos1, pos2, i + 1, :right_subtype_of_left) - - true -> - :none - end - - :left_subtype_of_right -> - if subtype?(v1, v2), - do: tuple_union_strategy_index(pos1, pos2, i + 1, :left_subtype_of_right), - else: :none - - :right_subtype_of_left -> - if subtype?(v2, v1), - do: tuple_union_strategy_index(pos1, pos2, i + 1, :right_subtype_of_left), - else: :none - end - end - - defp tuple_union_strategy_index(_pos1, _pos2, _i, status) do - status - end - defp tuple_to_quoted(bdd, opts) do tuple_bdd_to_dnf_with_negations(bdd) - |> tuple_fusion() + |> opt_tuple_fusion() |> Enum.map(&tuple_literal_to_quoted(&1, opts)) end @@ -5415,50 +4836,7 @@ defmodule Module.Types.Descr do end) end - # Given a union of tuples, fuses the tuple unions when possible, - # e.g. {integer(), atom()} or {float(), atom()} into {number(), atom()} - # The negations of two fused tuples are just concatenated. - # - # Steps: - # 1. Consider tuples without negations apart from those with - # 2. Group tuples by size and tag - # 3. Try fusions for each group until no fusion is found - # 4. Merge the groups back into a dnf - defp tuple_fusion(dnf) do - {with_negs, without_negs} = - Enum.reduce(dnf, {[], %{}}, fn - {tag, elements, []}, {with, without} -> - key = {tag, length(elements)} - value = {tag, elements} - {with, Map.update(without, key, [value], &[value | &1])} - - triplet, {with, without} -> - {[triplet | with], without} - end) - - Enum.reduce(without_negs, with_negs, fn {_, tuples}, with_negs -> - tuples - |> Enum.reduce([], fn tuple, acc -> - tuple_fuse_with_first_fusible(tuple, acc) - end) - |> Enum.reduce(with_negs, fn {tag, elements}, with_negs -> - [{tag, elements, []} | with_negs] - end) - end) - end - - defp tuple_fuse_with_first_fusible(tuple, []), do: [tuple] - - defp tuple_fuse_with_first_fusible(tuple, [candidate | rest]) do - if fused = maybe_optimize_tuple_union(tuple, candidate) do - # we found a fusible candidate, we're done - [fused | rest] - else - [candidate | tuple_fuse_with_first_fusible(tuple, rest)] - end - end - - defp tuple_literal_to_quoted({:closed, [], []}, _opts), do: {:{}, [], []} + defp tuple_literal_to_quoted({:closed, [], []}, _opts), do: {:{}, [], []} defp tuple_literal_to_quoted({tag, elements, negs}, opts) do pos = tuple_fields_to_quoted(tag, elements, opts) @@ -5502,7 +4880,7 @@ defmodule Module.Types.Descr do iex> tuple_fetch(tuple([integer(), atom()]), 0) {false, integer()} - iex> tuple_fetch(union(tuple([integer()]), tuple([integer(), atom()])), 1) + iex> tuple_fetch(bare_union(tuple([integer()]), tuple([integer(), atom()])), 1) {true, atom()} iex> tuple_fetch(dynamic(), 0) @@ -5552,7 +4930,7 @@ defmodule Module.Types.Descr do if empty?(dynamic_type) do :badindex else - {static_optional? or dynamic_optional?, union(dynamic(dynamic_type), static_type)} + {static_optional? or dynamic_optional?, opt_union(dynamic(dynamic_type), static_type)} end else :badtuple @@ -5585,7 +4963,7 @@ defmodule Module.Types.Descr do # Optimization: if there are no negatives {tag, elements, []}, {acc_optional?, acc_descr} -> {optional?, descr} = tuple_fetch_element(elements, index, tag) - {optional? or acc_optional?, union(descr, acc_descr)} + {optional? or acc_optional?, opt_union(descr, acc_descr)} {tag, elements, negs}, {acc_optional?, acc_descr} -> {_, value, bdd} = tuple_take_element(elements, index, tag) @@ -5601,11 +4979,11 @@ defmodule Module.Types.Descr do else negs |> tuple_split_negative(index, value, bdd) - |> Enum.reduce(none(), fn {value, _}, acc -> union(value, acc) end) + |> Enum.reduce(none(), fn {value, _}, acc -> opt_union(value, acc) end) end {optional?, descr} = pop_optional_static(value) - {optional? or acc_optional?, union(descr, acc_descr)} + {optional? or acc_optional?, opt_union(descr, acc_descr)} end end) catch @@ -5641,7 +5019,7 @@ defmodule Module.Types.Descr do if neg_tag == :closed and tuple_empty?(tuple_intersection(bdd, neg_bdd)) do [{value, bdd} | acc] else - intersection_value = intersection(value, neg_value) + intersection_value = bare_intersection(value, neg_value) if empty?(intersection_value) do [{value, bdd} | acc] @@ -5691,10 +5069,10 @@ defmodule Module.Types.Descr do defp tuple_pair_projection_keeps_full_snd?(negative, value) do neg_values = Enum.reduce(negative, none(), fn {neg_value, _neg_bdd}, acc -> - union(neg_value, acc) + bare_union(neg_value, acc) end) - not empty?(difference(value, neg_values)) + not empty?(bare_difference(value, neg_values)) end defp tuple_fetch_element([], _, :open), do: {true, term()} @@ -5740,7 +5118,7 @@ defmodule Module.Types.Descr do end dynamic(dynamic_value) - |> union(process_tuples_values(Map.get(static, :tuple, :bdd_bot))) + |> opt_union(process_tuples_values(Map.get(static, :tuple, :bdd_bot))) else :badtuple end @@ -5753,9 +5131,9 @@ defmodule Module.Types.Descr do cond do Enum.any?(elements, &empty?/1) -> none() tag == :open -> term() - tag == :closed -> Enum.reduce(elements, none(), &union/2) + tag == :closed -> Enum.reduce(elements, none(), &opt_union/2) end - |> union(acc) + |> opt_union(acc) end) end @@ -5789,13 +5167,13 @@ defmodule Module.Types.Descr do static_result = tuple_delete_static(static, index) # Prune for dynamic values that make the operation succeed. - dynamic_input = intersection(dynamic, tuple_of_size_at_least(index + 1)) + dynamic_input = opt_intersection(dynamic, tuple_of_size_at_least(index + 1)) if empty?(dynamic_input) and empty?(static) do :badindex else dynamic_result = tuple_delete_static(dynamic_input, index) - union(dynamic(dynamic_result), static_result) + opt_union(dynamic(dynamic_result), static_result) end # Highlight the case where the issue is an index out of range from the tuple @@ -5818,7 +5196,7 @@ defmodule Module.Types.Descr do |> Enum.reduce(:bdd_bot, fn {tag, elements, []}, acc -> {_, _, bdd} = tuple_take_element(elements, index, tag) - tuple_union(bdd, acc) + opt_tuple_union(bdd, acc) {tag, elements, negs}, acc -> {_, value, bdd} = tuple_take_element(elements, index, tag) @@ -5829,11 +5207,11 @@ defmodule Module.Types.Descr do negative -> if tuple_pair_projection_keeps_full_snd?(negative, value) do - tuple_union(bdd, acc) + opt_tuple_union(bdd, acc) else negs |> tuple_split_negative(index, value, bdd) - |> Enum.reduce(acc, fn {_, bdd}, acc -> tuple_union(bdd, acc) end) + |> Enum.reduce(acc, fn {_, bdd}, acc -> opt_tuple_union(bdd, acc) end) end end end) @@ -5865,8 +5243,11 @@ defmodule Module.Types.Descr do dynamic_result else case tuple_insert_at_checked(descr, index, static_type) do - static_result when is_descr(static_result) -> union(dynamic_result, static_result) - error -> error + static_result when is_descr(static_result) -> + opt_union(dynamic_result, static_result) + + error -> + error end end @@ -5900,13 +5281,13 @@ defmodule Module.Types.Descr do static_result = tuple_insert_static(static, index, type) # Prune for dynamic values that make the intersection succeed - dynamic_input = intersection(dynamic, tuple_of_size_at_least(index)) + dynamic_input = opt_intersection(dynamic, tuple_of_size_at_least(index)) if empty?(dynamic_input) and empty?(static) do :badindex else dynamic_result = tuple_insert_static(dynamic_input, index, type) - union(dynamic(dynamic_result), static_result) + opt_union(dynamic(dynamic_result), static_result) end # Highlight the case where the issue is an index out of range from the tuple @@ -6634,7 +6015,7 @@ defmodule Module.Types.Descr do defp iterator_non_disjoint_intersection?({key, v1, iterator}, map) do with %{^key => v2} <- map, - value when value not in @empty_intersection <- intersection(key, v1, v2), + value when value not in @empty_intersection <- bare_intersection(key, v1, v2), false <- empty_key?(key, value) do true else @@ -6647,4 +6028,798 @@ defmodule Module.Types.Descr do defp non_empty_map_or([head | tail], fun) do Enum.reduce(tail, fun.(head), &{:or, [], [&2, fun.(&1)]}) end + + ## Optimizations + + @doc """ + Computes the union of two descrs using optimized composite operations. + """ + def opt_union(:term, other), do: optional_to_term(other) + def opt_union(other, :term), do: optional_to_term(other) + def opt_union(none, other) when none == @none, do: other + def opt_union(other, none) when none == @none, do: other + + def opt_union(left, right) do + is_gradual_left = gradual?(left) + is_gradual_right = gradual?(right) + + cond do + is_gradual_left and not is_gradual_right -> + right_with_dynamic = Map.put(unfold(right), :dynamic, right) + opt_union_static(left, right_with_dynamic) + + is_gradual_right and not is_gradual_left -> + left_with_dynamic = Map.put(unfold(left), :dynamic, left) + opt_union_static(left_with_dynamic, right) + + true -> + opt_union_static(left, right) + end + end + + @compile {:inline, opt_union_static: 2} + defp opt_union_static(left, right) do + symmetrical_merge(left, right, &opt_union/3) + end + + defp opt_union(:atom, v1, v2), do: atom_union(v1, v2) + defp opt_union(:bitmap, v1, v2), do: v1 ||| v2 + defp opt_union(:dynamic, v1, v2), do: dynamic_union(v1, v2, &opt_union/3) + defp opt_union(:list, v1, v2), do: list_union(v1, v2) + defp opt_union(:map, v1, v2), do: opt_map_union(v1, v2) + defp opt_union(:optional, 1, 1), do: 1 + defp opt_union(:tuple, v1, v2), do: opt_tuple_union(v1, v2) + defp opt_union(:fun, v1, v2), do: fun_union(v1, v2) + + @doc """ + Computes the intersection of two descrs using optimized composite operations. + """ + def opt_intersection(:term, other), do: remove_optional(other) + def opt_intersection(other, :term), do: remove_optional(other) + + def opt_intersection(left, right) do + is_gradual_left = gradual?(left) + is_gradual_right = gradual?(right) + + cond do + is_gradual_left and not is_gradual_right -> + right_with_dynamic = Map.put(unfold(right), :dynamic, right) + opt_intersection_static(left, right_with_dynamic) + + is_gradual_right and not is_gradual_left -> + left_with_dynamic = Map.put(unfold(left), :dynamic, left) + opt_intersection_static(left_with_dynamic, right) + + true -> + opt_intersection_static(left, right) + end + end + + @compile {:inline, opt_intersection_static: 2} + defp opt_intersection_static(left, right) do + symmetrical_intersection(left, right, &opt_intersection/3) + end + + defp opt_intersection(:atom, v1, v2), do: atom_intersection(v1, v2) + defp opt_intersection(:bitmap, v1, v2), do: v1 &&& v2 + defp opt_intersection(:list, v1, v2), do: opt_list_intersection(v1, v2) + defp opt_intersection(:map, v1, v2), do: opt_map_intersection(v1, v2) + defp opt_intersection(:optional, 1, 1), do: 1 + defp opt_intersection(:tuple, v1, v2), do: opt_tuple_intersection(v1, v2) + defp opt_intersection(:fun, v1, v2), do: fun_intersection(v1, v2) + + defp opt_intersection(:dynamic, v1, v2) do + descr = dynamic_intersection(v1, v2, &opt_intersection/3) + if descr == @none, do: 0, else: descr + end + + @doc """ + Computes the difference of two descrs using optimized composite operations. + """ + def opt_difference(left, :term), do: keep_optional(left) + def opt_difference(left, none) when none == @none, do: left + + def opt_difference(left, right) do + if gradual?(left) or gradual?(right) do + {left_dynamic, left_static} = pop_dynamic(left) + {right_dynamic, right_static} = pop_dynamic(right) + dynamic_part = opt_difference_static(left_dynamic, right_static) + + Map.put(opt_difference_static(left_static, right_dynamic), :dynamic, dynamic_part) + else + opt_difference_static(left, right) + end + end + + @compile {:inline, opt_difference_static: 2} + defp opt_difference_static(left, right) do + dynamic_difference(left, right, &opt_difference/3) + end + + # Returning 0 from the callback is taken as none() for that subtype. + defp opt_difference(:atom, v1, v2), do: atom_difference(v1, v2) + defp opt_difference(:bitmap, v1, v2), do: v1 - (v1 &&& v2) + defp opt_difference(:list, v1, v2), do: opt_list_difference(v1, v2) + defp opt_difference(:map, v1, v2), do: opt_map_difference(v1, v2) + defp opt_difference(:optional, 1, 1), do: 0 + defp opt_difference(:tuple, v1, v2), do: opt_tuple_difference(v1, v2) + defp opt_difference(:fun, v1, v2), do: fun_difference(v1, v2) + + @doc """ + Compute the negation of a type. + """ + def opt_negation(:term), do: none() + def opt_negation(%{} = descr), do: opt_difference(term(), descr) + + @compile {:inline, maybe_opt_union: 2} + defp maybe_opt_union(nil, _fun), do: nil + defp maybe_opt_union(descr, fun), do: opt_union(descr, fun.()) + + defp opt_list_intersection(bdd_leaf(:term, :term), bdd), do: bdd + defp opt_list_intersection(bdd, bdd_leaf(:term, :term)), do: bdd + + defp opt_list_intersection(bdd1, bdd2), + do: bdd_intersection(bdd1, bdd2, &opt_list_leaf_intersection/2) + + defp opt_list_leaf_intersection(bdd_leaf(list1, last1), bdd_leaf(list2, last2)) do + try do + list = non_empty_intersection!(list1, list2, &opt_intersection/2) + last = non_empty_intersection!(last1, last2, &opt_intersection/2) + bdd_leaf_new(list, last) + catch + :empty -> :bdd_bot + end + end + + defp opt_list_difference(bdd_leaf(:term, :term), bdd_leaf(:term, :term)), do: :bdd_bot + defp opt_list_difference(bdd_leaf(:term, :term), bdd2), do: bdd_negation(bdd2) + + # Computes the difference between two BDD (Binary Decision Diagram) list types. + # It progressively subtracts each type in bdd2 from all types in bdd1. + # The algorithm handles three cases: + # 1. Disjoint types: keeps the original type from bdd1 + # 2. Subtype relationship: + # a) If bdd2 type is a supertype, keeps only the negations + # b) If only the last type differs, subtracts it + # 3. Base case: adds bdd2 type to negations of bdd1 type + # The result may be larger than the initial bdd1, which is maintained in the accumulator. + defp opt_list_difference(bdd_leaf(list1, last1) = bdd1, bdd_leaf(list2, last2) = bdd2) do + if subtype?(list1, list2) do + if subtype?(last1, last2), + do: :bdd_bot, + else: bdd_leaf_new(list1, opt_difference(last1, last2)) + else + bdd_difference(bdd1, bdd2, &opt_list_leaf_difference/3) + end + end + + defp opt_list_difference(bdd1, bdd2), + do: bdd_difference(bdd1, bdd2, &opt_list_leaf_difference/3) + + defp opt_list_leaf_difference(bdd_leaf(list1, last1), bdd_leaf(list2, last2), _) do + if disjoint?(list1, list2) or disjoint?(last1, last2) do + :disjoint + else + :none + end + end + + defp opt_map_union(bdd_leaf(tag1, fields1), bdd_leaf(tag2, fields2)) do + case opt_map_union(tag1, fields1, tag2, fields2) do + {tag, fields} -> bdd_leaf_new(tag, fields) + nil -> bdd_union(bdd_leaf_new(tag1, fields1), bdd_leaf_new(tag2, fields2)) + end + end + + defp opt_map_union(bdd1, bdd2), do: map_union(bdd1, bdd2) + + defp opt_map_union(:open, [], _, _), do: {:open, @fields_new} + defp opt_map_union(_, _, :open, []), do: {:open, @fields_new} + + defp opt_map_union(tag1, pos1, tag2, pos2) + when is_atom(tag1) and is_atom(tag2) do + case opt_map_union_strategy(pos1, pos2, tag1, tag2, :all_equal) do + :all_equal when tag1 == :open -> {tag1, pos1} + :all_equal -> {tag2, pos2} + {:one_key_difference, key, v1, v2} -> {tag1, fields_store(key, opt_union(v1, v2), pos1)} + :left_subtype_of_right -> {tag2, pos2} + :right_subtype_of_left -> {tag1, pos1} + :none -> nil + end + end + + defp opt_map_union(_, _, _, _), do: nil + + defp opt_map_union_strategy([{k1, _} | t1], [{k2, _} | _] = l2, tag1, tag2, status) + when k1 < k2 do + # Left side has a key the right side does not have, + # left can only be a subtype if the right side is open. + case status do + _ when tag2 != :open -> + :none + + :all_equal -> + opt_map_union_strategy(t1, l2, tag1, tag2, :left_subtype_of_right) + + {:one_key_difference, _, p1, p2} -> + if subtype?(p1, p2), + do: opt_map_union_strategy(t1, l2, tag1, tag2, :left_subtype_of_right), + else: :none + + :left_subtype_of_right -> + opt_map_union_strategy(t1, l2, tag1, tag2, :left_subtype_of_right) + + _ -> + :none + end + end + + defp opt_map_union_strategy([{k1, _} | _] = l1, [{k2, _} | t2], tag1, tag2, status) + when k1 > k2 do + # Right side has a key the left side does not have, + # right can only be a subtype if the left side is open. + case status do + _ when tag1 != :open -> + :none + + :all_equal -> + opt_map_union_strategy(l1, t2, tag1, tag2, :right_subtype_of_left) + + {:one_key_difference, _, p1, p2} -> + if subtype?(p2, p1), + do: opt_map_union_strategy(l1, t2, tag1, tag2, :right_subtype_of_left), + else: :none + + :right_subtype_of_left -> + opt_map_union_strategy(l1, t2, tag1, tag2, :right_subtype_of_left) + + _ -> + :none + end + end + + defp opt_map_union_strategy([{_, v} | t1], [{_, v} | t2], tag1, tag2, status) do + # Same key and same value, nothing changes + opt_map_union_strategy(t1, t2, tag1, tag2, status) + end + + defp opt_map_union_strategy([{k1, v1} | t1], [{_, v2} | t2], tag1, tag2, status) do + # They have the same key but different values + case status do + :all_equal -> + cond do + # Don't do difference on struct keys + k1 != :__struct__ and tag1 == tag2 -> + opt_map_union_strategy(t1, t2, tag1, tag2, {:one_key_difference, k1, v1, v2}) + + subtype?(v1, v2) -> + opt_map_union_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) + + subtype?(v2, v1) -> + opt_map_union_strategy(t1, t2, tag1, tag2, :right_subtype_of_left) + + true -> + :none + end + + :left_subtype_of_right -> + if subtype?(v1, v2), do: opt_map_union_strategy(t1, t2, tag1, tag2, status), else: :none + + :right_subtype_of_left -> + if subtype?(v2, v1), do: opt_map_union_strategy(t1, t2, tag1, tag2, status), else: :none + + {:one_key_difference, _key, p1, p2} -> + cond do + subtype?(p1, p2) and subtype?(v1, v2) -> + opt_map_union_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) + + subtype?(p2, p1) and subtype?(v2, v1) -> + opt_map_union_strategy(t1, t2, tag1, tag2, :right_subtype_of_left) + + true -> + :none + end + + _ -> + :none + end + end + + defp opt_map_union_strategy([], [], tag1, tag2, status) do + case status do + :left_subtype_of_right when tag1 == :open and tag2 == :closed -> :none + :right_subtype_of_left when tag1 == :closed and tag2 == :open -> :none + _ -> status + end + end + + defp opt_map_union_strategy(l1, l2, tag1, tag2, status) do + lhs? = tag2 == :open and l2 == [] + rhs? = tag1 == :open and l1 == [] + + case status do + :all_equal when lhs? -> + :left_subtype_of_right + + :all_equal when rhs? -> + :right_subtype_of_left + + {:one_key_difference, _, p1, p2} -> + cond do + lhs? and subtype?(p1, p2) -> :left_subtype_of_right + rhs? and subtype?(p2, p1) -> :right_subtype_of_left + true -> :none + end + + :left_subtype_of_right when lhs? -> + :left_subtype_of_right + + :right_subtype_of_left when rhs? -> + :right_subtype_of_left + + _ -> + :none + end + end + + defp opt_map_intersection(bdd_leaf(:open, []), bdd), do: bdd + defp opt_map_intersection(bdd, bdd_leaf(:open, [])), do: bdd + + defp opt_map_intersection(bdd1, bdd2), + do: bdd_intersection(bdd1, bdd2, &opt_map_leaf_intersection/2) + + defp opt_map_leaf_intersection(bdd_leaf(tag1, fields1), bdd_leaf(tag2, fields2)) do + try do + {tag, fields} = map_literal_intersection(tag1, fields1, tag2, fields2, &opt_intersection/2) + bdd_leaf_new(tag, fields) + catch + :empty -> :bdd_bot + end + end + + defp opt_map_difference(_, bdd_leaf(:open, [])), do: :bdd_bot + defp opt_map_difference(bdd_leaf(:open, []), {_, _, _, _, _} = bdd2), do: bdd_negation(bdd2) + + defp opt_map_difference(bdd1, bdd2), + do: bdd_difference(bdd1, bdd2, &opt_map_leaf_difference/3) + + defp opt_map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(:open, [{key, v2}]), type) do + {found?, v1} = + case fields_find(key, fields) do + {:ok, value} -> {true, value} + :error -> {false, map_key_tag_to_type(tag)} + end + + cond do + tag == :closed and not found? -> + if is_optional_static(v2), do: :subtype, else: :disjoint + + # In case the left-side is open, we will only be adding new keys + # to the open map, which makes future eliminations harder. + tag == :open and not found? and fields != [] -> + :none + + disjoint?(v1, v2) -> + :disjoint + + true -> + opt_map_leaf_one_key_difference(tag, fields, key, v1, v2, type) + end + end + + defp opt_map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(neg_tag, neg_fields), type) do + case opt_map_difference_strategy(fields, neg_fields, tag, neg_tag) do + :disjoint -> + :disjoint + + :left_subtype_of_right -> + :subtype + + {:one_key_difference, key, v1, v2} -> + opt_map_leaf_one_key_difference(tag, fields, key, v1, v2, type) + + :none -> + :none + end + end + + defp opt_map_leaf_one_key_difference(tag, fields, key, v1, v2, type) do + v_diff = opt_difference(v1, v2) + + if empty?(v_diff) do + :subtype + else + a_diff = bdd_leaf_new(tag, fields_store(key, v_diff, fields)) + + a_type = + case type do + :none -> + :bdd_bot + + :union -> + bdd_leaf_new(tag, fields_store(key, opt_union(v1, v2), fields)) + + :intersection -> + v_int = opt_intersection(v1, v2) + + if empty?(v_int), + do: :bdd_bot, + else: bdd_leaf_new(tag, fields_store(key, v_int, fields)) + end + + {:one_key_difference, a_diff, a_type} + end + end + + defp opt_map_difference_strategy(fields1, fields2, tag1, tag2) do + if is_atom(tag1) and is_atom(tag2) do + status = if tag1 == tag2 or tag2 == :open, do: :all_equal, else: :none + opt_map_difference_strategy(fields1, fields2, tag1, tag2, status) + else + :none + end + end + + defp opt_map_difference_strategy([{k1, value} | t1], [{k2, _} | _] = l2, tag1, tag2, status) + when k1 < k2 do + # Left side has a key the right side does not have, + # left can only be a subtype if the right side is open. + # If the right side is closed and the key is not optional, they are disjoint. + case status do + _ when tag2 == :closed -> + if not is_optional_static(value) do + :disjoint + else + opt_map_difference_strategy(t1, l2, tag1, tag2, :none) + end + + :all_equal -> + opt_map_difference_strategy(t1, l2, tag1, tag2, :left_subtype_of_right) + + {:one_key_difference, _, p1, p2} -> + if subtype?(p1, p2), + do: opt_map_difference_strategy(t1, l2, tag1, tag2, :left_subtype_of_right), + else: :none + + :left_subtype_of_right -> + opt_map_difference_strategy(t1, l2, tag1, tag2, :left_subtype_of_right) + + _ -> + :none + end + end + + defp opt_map_difference_strategy([{k1, _} | _] = l1, [{k2, value} | t2], tag1, tag2, _status) + when k1 > k2 do + # Right side has a key the left side does not have, + # if left-side is closed, they are disjoint. + if tag1 == :closed and not is_optional_static(value) do + :disjoint + else + opt_map_difference_strategy(l1, t2, tag1, tag2, :none) + end + end + + defp opt_map_difference_strategy([{_, v} | t1], [{_, v} | t2], tag1, tag2, status) do + # Same key and same value, nothing changes + opt_map_difference_strategy(t1, t2, tag1, tag2, status) + end + + defp opt_map_difference_strategy([{k1, v1} | t1], [{_, v2} | t2], tag1, tag2, status) do + # They have the same key but different values + if disjoint?(v1, v2) do + :disjoint + else + case status do + :all_equal when tag1 == tag2 -> + opt_map_difference_strategy(t1, t2, tag1, tag2, {:one_key_difference, k1, v1, v2}) + + {:one_key_difference, _key, p1, p2} -> + if subtype?(p1, p2) and subtype?(v1, v2) do + opt_map_difference_strategy(t1, t2, tag1, tag2, :left_subtype_of_right) + else + :none + end + + _ -> + if status in [:all_equal, :left_subtype_of_right] and subtype?(v1, v2), + do: opt_map_difference_strategy(t1, t2, tag1, tag2, :left_subtype_of_right), + else: opt_map_difference_strategy(t1, t2, tag1, tag2, :none) + end + end + end + + defp opt_map_difference_strategy([], [], _tag1, _tag2, status) do + if status == :all_equal, do: :left_subtype_of_right, else: status + end + + defp opt_map_difference_strategy(l1, l2, tag1, tag2, status) do + cond do + tag2 == :open and l2 == [] -> + case status do + :all_equal -> + :left_subtype_of_right + + {:one_key_difference, _, p1, p2} -> + if subtype?(p1, p2), do: :left_subtype_of_right, else: :none + + :left_subtype_of_right -> + :left_subtype_of_right + + :none -> + :none + end + + tag1 == :closed and l2 != [] and Enum.all?(l2, fn {_, v} -> not is_optional_static(v) end) -> + :disjoint + + tag2 == :closed and l1 != [] and Enum.all?(l1, fn {_, v} -> not is_optional_static(v) end) -> + :disjoint + + true -> + :none + end + end + + # Continue to eliminate negations while length of list of negs decreases + defp opt_map_eliminate_while_negs_decrease(tag, fields, []), do: [{tag, fields, []}] + + defp opt_map_eliminate_while_negs_decrease(tag, fields, negs) do + try do + opt_map_eliminate_negations(tag, fields, negs) + catch + :empty -> [] + else + {fields, new_negs} -> + if length(new_negs) < length(negs) do + opt_map_eliminate_while_negs_decrease(tag, fields, new_negs) + else + [{tag, fields, new_negs}] + end + end + end + + defp opt_map_eliminate_negations(tag, fields, negs) do + Enum.reduce(negs, {fields, []}, fn neg = bdd_leaf(neg_tag, neg_fields), + {acc_fields, acc_negs} -> + # If the intersection with the negative is empty, we can remove the negative. + empty_intersection? = + try do + _ = map_literal_intersection(tag, acc_fields, neg_tag, neg_fields) + false + catch + :empty -> true + end + + if empty_intersection? do + {acc_fields, acc_negs} + else + case opt_map_difference_strategy(acc_fields, neg_fields, tag, neg_tag) do + {:one_key_difference, key, v1, v2} -> + {fields_store(key, opt_difference(v1, v2), acc_fields), acc_negs} + + :disjoint -> + {acc_fields, acc_negs} + + :left_subtype_of_right -> + throw(:empty) + + :none -> + {acc_fields, [neg | acc_negs]} + end + end + end) + end + + # Given a dnf, fuse maps when possible + # e.g. %{a: integer(), b: atom()} or %{a: float(), b: atom()} into %{a: number(), b: atom()} + defp opt_map_fusion(dnf) do + # Steps: + # 1. Group maps by tags and keys + # 2. Try fusions for each group until no fusion is found + # 3. Merge the groups back into a dnf + {without_negs, with_negs} = Enum.split_with(dnf, fn {_tag, _fields, negs} -> negs == [] end) + + without_negs = + without_negs + |> Enum.group_by(fn {tag, fields, _} -> {tag, fields_keys(fields)} end) + |> Enum.flat_map(fn {_, maps} -> opt_map_non_negated_fuse(maps) end) + + without_negs ++ with_negs + end + + defp opt_map_non_negated_fuse(maps) do + Enum.reduce(maps, [], fn map, acc -> + opt_map_fuse_with_first_fusible(map, acc) + end) + end + + defp opt_map_fuse_with_first_fusible(map, []), do: [map] + + defp opt_map_fuse_with_first_fusible(map, [candidate | rest]) do + {tag1, fields1, []} = map + {tag2, fields2, []} = candidate + + case opt_map_union(tag1, fields1, tag2, fields2) do + nil -> [candidate | opt_map_fuse_with_first_fusible(map, rest)] + # we found a fusible candidate, we're done + {tag, fields} -> [{tag, fields, []} | rest] + end + end + + defp opt_tuple_intersection(bdd_leaf(:open, []), bdd), do: bdd + defp opt_tuple_intersection(bdd, bdd_leaf(:open, [])), do: bdd + + defp opt_tuple_intersection(bdd1, bdd2), + do: bdd_intersection(bdd1, bdd2, &opt_tuple_leaf_intersection/2) + + defp opt_tuple_leaf_intersection(bdd_leaf(tag1, elements1), bdd_leaf(tag2, elements2)) do + case tuple_literal_intersection(tag1, elements1, tag2, elements2, &opt_intersection/2) do + {tag, elements} -> bdd_leaf_new(tag, elements) + :empty -> :bdd_bot + end + end + + defp opt_tuple_difference(_, bdd_leaf(:open, [])), + do: :bdd_bot + + defp opt_tuple_difference(bdd_leaf(:open, []), {_, _, _, _, _} = bdd2), + do: bdd_negation(bdd2) + + defp opt_tuple_difference(bdd1, bdd2), + do: bdd_difference(bdd1, bdd2, &opt_tuple_leaf_difference/3) + + defp opt_tuple_leaf_difference(bdd_leaf(tag1, elements1), bdd_leaf(tag2, elements2), _) do + case tuple_sizes_strategy(tag1, length(elements1), tag2, length(elements2)) do + :disjoint -> :disjoint + other -> opt_tuple_leaf_difference(elements1, elements2, other == :left_subtype_of_right) + end + end + + defp opt_tuple_leaf_difference([head1 | tail1], [head2 | tail2], subtype?) do + cond do + disjoint?(head1, head2) -> :disjoint + subtype? and subtype?(head1, head2) -> opt_tuple_leaf_difference(tail1, tail2, subtype?) + true -> :none + end + end + + defp opt_tuple_leaf_difference(_tail1, _tail2, subtype?) do + if subtype?, do: :subtype, else: :none + end + + defp opt_tuple_union({tag1, elements1}, {tag2, elements2}) do + opt_tuple_union_literal({tag1, elements1}, {tag2, elements2}) + end + + defp opt_tuple_union( + bdd_leaf(tag1, elements1), + bdd_leaf(tag2, elements2) + ) do + case opt_tuple_union_literal({tag1, elements1}, {tag2, elements2}) do + {tag, elements} -> bdd_leaf_new(tag, elements) + nil -> bdd_union(bdd_leaf_new(tag1, elements1), bdd_leaf_new(tag2, elements2)) + end + end + + defp opt_tuple_union(bdd1, bdd2), do: tuple_union(bdd1, bdd2) + + defp opt_tuple_union_literal({:open, []} = tuple1, _tuple2), do: tuple1 + defp opt_tuple_union_literal(_tuple1, {:open, []} = tuple2), do: tuple2 + + defp opt_tuple_union_literal({tag1, pos1} = tuple1, {tag2, pos2} = tuple2) do + case opt_tuple_union_strategy(tag1, pos1, tag2, pos2) do + :all_equal -> + tuple1 + + {:one_index_difference, index, v1, v2} -> + new_pos = List.replace_at(pos1, index, bare_union(v1, v2)) + {tag1, new_pos} + + :left_subtype_of_right -> + tuple2 + + :right_subtype_of_left -> + tuple1 + + :none -> + nil + end + end + + defp opt_tuple_union_strategy(tag1, pos1, tag2, pos2) do + case {tag1, tag2} do + {tag, tag} when length(pos1) == length(pos2) -> + opt_tuple_union_strategy_index(pos1, pos2, 0, :all_equal) + + {:open, _} when length(pos1) <= length(pos2) -> + opt_tuple_union_strategy_index(pos1, pos2, 0, :right_subtype_of_left) + + {_, :open} when length(pos1) >= length(pos2) -> + opt_tuple_union_strategy_index(pos1, pos2, 0, :left_subtype_of_right) + + {_, _} -> + :none + end + end + + defp opt_tuple_union_strategy_index([v | pos1], [v | pos2], i, status) do + opt_tuple_union_strategy_index(pos1, pos2, i + 1, status) + end + + defp opt_tuple_union_strategy_index([v1 | pos1], [v2 | pos2], i, status) do + case status do + :all_equal -> + opt_tuple_union_strategy_index(pos1, pos2, i + 1, {:one_index_difference, i, v1, v2}) + + {:one_index_difference, _, d1, d2} -> + cond do + subtype?(d1, d2) and subtype?(v1, v2) -> + opt_tuple_union_strategy_index(pos1, pos2, i + 1, :left_subtype_of_right) + + subtype?(d2, d1) and subtype?(v2, v1) -> + opt_tuple_union_strategy_index(pos1, pos2, i + 1, :right_subtype_of_left) + + true -> + :none + end + + :left_subtype_of_right -> + if subtype?(v1, v2), + do: opt_tuple_union_strategy_index(pos1, pos2, i + 1, :left_subtype_of_right), + else: :none + + :right_subtype_of_left -> + if subtype?(v2, v1), + do: opt_tuple_union_strategy_index(pos1, pos2, i + 1, :right_subtype_of_left), + else: :none + end + end + + defp opt_tuple_union_strategy_index(_pos1, _pos2, _i, status) do + status + end + + # Given a union of tuples, fuses the tuple unions when possible, + # e.g. {integer(), atom()} or {float(), atom()} into {number(), atom()} + # The negations of two fused tuples are just concatenated. + # + # Steps: + # 1. Consider tuples without negations apart from those with + # 2. Group tuples by size and tag + # 3. Try fusions for each group until no fusion is found + # 4. Merge the groups back into a dnf + defp opt_tuple_fusion(dnf) do + {with_negs, without_negs} = + Enum.reduce(dnf, {[], %{}}, fn + {tag, elements, []}, {with, without} -> + key = {tag, length(elements)} + value = {tag, elements} + {with, Map.update(without, key, [value], &[value | &1])} + + triplet, {with, without} -> + {[triplet | with], without} + end) + + Enum.reduce(without_negs, with_negs, fn {_, tuples}, with_negs -> + tuples + |> Enum.reduce([], fn tuple, acc -> + opt_tuple_fuse_with_first_fusible(tuple, acc) + end) + |> Enum.reduce(with_negs, fn {tag, elements}, with_negs -> + [{tag, elements, []} | with_negs] + end) + end) + end + + defp opt_tuple_fuse_with_first_fusible(tuple, []), do: [tuple] + + defp opt_tuple_fuse_with_first_fusible(tuple, [candidate | rest]) do + if fused = opt_tuple_union(tuple, candidate) do + # we found a fusible candidate, we're done + [fused | rest] + else + [candidate | opt_tuple_fuse_with_first_fusible(tuple, rest)] + end + end end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 4dc3f74436f..dc6c0638e67 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -22,9 +22,9 @@ defmodule Module.Types.Expr do context: atom([:match, :guard, nil]), context_modules: list_of_modules, file: binary(), - function: union(tuple([atom(), integer()]), atom([nil])), + function: opt_union(tuple([atom(), integer()]), atom([nil])), functions: functions_and_macros, - lexical_tracker: union(pid(), atom([nil])), + lexical_tracker: opt_union(pid(), atom([nil])), line: integer(), macro_aliases: aliases, macros: functions_and_macros, @@ -38,24 +38,24 @@ defmodule Module.Types.Expr do # then we will assume you need to statically handle all possible exceptions. @exception open_map(__struct__: atom(), __exception__: term()) - args_or_arity = union(list(term()), integer()) + args_or_arity = opt_union(list(term()), integer()) extra_info = list( tuple([atom([:file]), list(integer())]) - |> union(tuple([atom([:line]), integer()])) - |> union(tuple([atom([:error_info]), open_map()])) + |> opt_union(tuple([atom([:line]), integer()])) + |> opt_union(tuple([atom([:error_info]), open_map()])) ) @stacktrace list( - union( + opt_union( tuple([atom(), atom(), args_or_arity, extra_info]), tuple([fun(), args_or_arity, extra_info]) ) ) @falsy atom([false, nil]) - @truthy negation(atom([false, nil])) + @truthy Module.Types.Descr.opt_negation(atom([false, nil])) # :atom def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom), @@ -106,7 +106,7 @@ defmodule Module.Types.Expr do of_expr(suffix, tl_type, expr, stack, context) end - {non_empty_list(Enum.reduce(prefix, &union/2), suffix), context} + {non_empty_list(Enum.reduce(prefix, &opt_union/2), suffix), context} end # {left, right} @@ -150,13 +150,13 @@ defmodule Module.Types.Expr do fn pattern_type, context -> # See if we can use the expected type to further refine the pattern type, # if we cannot, use the pattern type as that will fail later on. - type = intersection(pattern_type, expected) + type = opt_intersection(pattern_type, expected) type = if empty?(type), do: pattern_type, else: type {result, context} = of_expr(right_expr, type, expr, stack, context) # The function may still return a too broad type, # so we refine once again to assign the most appropriate to the pattern. - {intersection(result, expected), context} + {opt_intersection(result, expected), context} end Pattern.of_match(left_expr, type_fun, match, meta, stack, context) @@ -187,7 +187,7 @@ defmodule Module.Types.Expr do end end) - expected = intersection(expected, open_map(expected_pairs)) + expected = opt_intersection(expected, open_map(expected_pairs)) {map_type, context} = of_expr(map, expected, expr, stack, context) try do @@ -330,7 +330,7 @@ defmodule Module.Types.Expr do {neg_body_type, neg_body_context} = of_expr(neg_body, expected, expr, stack, falsy_context) - body_type = union(pos_body_type, neg_body_type) + body_type = opt_union(pos_body_type, neg_body_type) context = cond do @@ -374,7 +374,7 @@ defmodule Module.Types.Expr do reset_warnings(falsy_context, context) end - {union(body_type, acc), context} + {opt_union(body_type, acc), context} end) dynamic_unless_static({body_type, Of.reset_vars(acc_context, context)}, stack) @@ -419,7 +419,7 @@ defmodule Module.Types.Expr do reset_warnings(context, original) {_, filtered} -> - case_expected = Enum.reduce(filtered, none(), &union(elem(&1, 0), &2)) + case_expected = Enum.reduce(filtered, none(), &opt_union(elem(&1, 0), &2)) {_, context} = of_expr(case_expr, case_expected, case_expr, stack, context) reset_warnings(context, original) end @@ -480,13 +480,13 @@ defmodule Module.Types.Expr do else [arg_type] = Pattern.of_domain(trees, stack, context) clauses_acc = [{arg_type, body_type, clause} | clauses_acc] - {{none?, union(body_type, body_acc), clauses_acc}, context} + {{none?, opt_union(body_type, body_acc), clauses_acc}, context} end end) context = if none? or stack.mode != :static do - head_type = Enum.reduce(clauses_acc, none(), &union(elem(&1, 0), &2)) + head_type = Enum.reduce(clauses_acc, none(), &opt_union(elem(&1, 0), &2)) {_, refined_context} = of_expr( @@ -558,7 +558,7 @@ defmodule Module.Types.Expr do end {type, context} = of_expr(body, expected, body, stack, context) - {union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)} + {opt_union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)} end) {:catch, clauses}, {acc, context} -> @@ -576,7 +576,7 @@ defmodule Module.Types.Expr do end) end - @timeout_type union(integer(), atom([:infinity])) + @timeout_type opt_union(integer(), atom([:infinity])) def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do cache_result(meta, stack, original, fn -> @@ -593,10 +593,10 @@ defmodule Module.Types.Expr do {body_type, context} = of_expr(body, expected, expr, stack, context) if compatible?(timeout_type, @timeout_type) do - {union(body_type, acc), Of.reset_vars(context, original)} + {opt_union(body_type, acc), Of.reset_vars(context, original)} else error = {:badtimeout, timeout_type, timeout, context} - {union(body_type, acc), error(__MODULE__, error, meta, stack, context)} + {opt_union(body_type, acc), error(__MODULE__, error, meta, stack, context)} end end) |> dynamic_unless_static(stack) @@ -625,13 +625,13 @@ defmodule Module.Types.Expr do case into_kind do :bitstring -> {block_type, context} = of_expr(block, bitstring(), block, stack, context) - intersection = intersection(block_type, bitstring()) + intersection = opt_intersection(block_type, bitstring()) if empty?(intersection) do error = {:badbitbody, block_type, block, context} {error_type(), error(__MODULE__, error, meta, stack, context)} else - {union(into_type, intersection), context} + {opt_union(into_type, intersection), context} end :non_empty_list -> @@ -642,7 +642,7 @@ defmodule Module.Types.Expr do end {block_type, context} = of_expr(block, expected, block, stack, context) - {union(into_type, non_empty_list(block_type)), context} + {opt_union(into_type, non_empty_list(block_type)), context} :none -> {_, context} = of_expr(block, term(), block, stack, context) @@ -673,7 +673,7 @@ defmodule Module.Types.Expr do {else_types, context} end - dynamic_unless_static({union(do_result, else_result), context}, stack) + dynamic_unless_static({opt_union(do_result, else_result), context}, stack) end) end @@ -786,7 +786,7 @@ defmodule Module.Types.Expr do context _ -> - expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2) + expected = if structs == [], do: @exception, else: Enum.reduce(structs, &opt_union/2) expr = {:__block__, [type_check: info], [expr]} context = Of.declare_var(var, context) {_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context) @@ -830,7 +830,7 @@ defmodule Module.Types.Expr do context end - @into_compile union(bitstring(), empty_list()) + @into_compile opt_union(bitstring(), empty_list()) defp for_into([], _meta, _stack, context), do: {empty_list(), :non_empty_list, context} @@ -859,7 +859,7 @@ defmodule Module.Types.Expr do # If they can be both be true, then we don't know # what the contents of the block are for {true, true} -> - type = union(bitstring(), list(term())) + type = opt_union(bitstring(), list(term())) {if(gradual?(type), do: dynamic(type), else: type), :none, context} {false, true} -> @@ -892,8 +892,8 @@ defmodule Module.Types.Expr do {_, refined_context} = of_expr(right, pattern_type, right, %{stack | reverse_arrow: :except_none}, context) - else_type = if precise?, do: difference(type, pattern_type), else: type - {union(else_type, else_types), reset_warnings(refined_context, context)} + else_type = if precise?, do: opt_difference(type, pattern_type), else: type + {opt_union(else_type, else_types), reset_warnings(refined_context, context)} end defp with_clause(expr, stack, {else_types, context}) do @@ -917,7 +917,7 @@ defmodule Module.Types.Expr do Of.with_conditional_vars(mods, none(), call, stack, context, fn mod, acc, context -> expr = {remote, [type_check: {:invoked_as, mod, fun, length(args)}] ++ meta, args} {type, context} = Apply.remote(mod, fun, args, expected, expr, stack, context, &of_expr/5) - {union(acc, type), context} + {opt_union(acc, type), context} end) end @@ -981,7 +981,7 @@ defmodule Module.Types.Expr do defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do of_acc = fn _args_types, _precise?, {:->, _, [_, body]}, context, acc -> {body_type, context} = of_expr(body, expected, body, stack, context) - {union(acc, body_type), context} + {opt_union(acc, body_type), context} end of_clauses_fun(clauses, domain, base_info, stack, context, acc, of_acc) @@ -1036,7 +1036,7 @@ defmodule Module.Types.Expr do do: {left_expr, right_expr} defp add_inferred([{args, existing_return} | tail], args, return), - do: [{args, union(existing_return, return)} | tail] + do: [{args, opt_union(existing_return, return)} | tail] defp add_inferred([head | tail], args, return), do: [head | add_inferred(tail, args, return)] diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 5cfc8e5682f..30476d491cf 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -11,7 +11,7 @@ defmodule Module.Types.Of do @prefix quote(do: ...) @suffix quote(do: ...) - @integer_or_float union(integer(), float()) + @integer_or_float opt_union(integer(), float()) @integer integer() @float float() @binary binary() @@ -111,7 +111,7 @@ defmodule Module.Types.Of do {old_type, context} %{reverse_arrow: reverse_arrow} when reverse_arrow in [:except_none, :include_none] -> - new_type = intersection(old_type, type) + new_type = opt_intersection(old_type, type) case empty?(new_type) do true when reverse_arrow == :include_none -> @@ -171,7 +171,7 @@ defmodule Module.Types.Of do {:ok, error_type(), context} %{^version => %{type: old_type, off_traces: off_traces} = data} = vars -> - new_type = intersection(type, old_type) + new_type = opt_intersection(type, old_type) data = %{ data @@ -245,7 +245,7 @@ defmodule Module.Types.Of do type = Enum.reduce(vars_conds, type, fn {vars, _cond}, acc -> %{^version => %{type: type}} = vars - union(acc, type) + opt_union(acc, type) end) {_, context} = refine_body_var(version, type, expr, stack, context) @@ -264,8 +264,8 @@ defmodule Module.Types.Of do {Float, float()}, {Function, fun()}, {Integer, integer()}, - {List, union(empty_list(), non_empty_list(term(), term()))}, - {Map, open_map(__struct__: if_set(negation(atom())))}, + {List, opt_union(empty_list(), non_empty_list(term(), term()))}, + {Map, open_map(__struct__: if_set(Module.Types.Descr.opt_negation(atom())))}, {Port, port()}, {PID, pid()}, {Reference, reference()}, @@ -384,15 +384,18 @@ defmodule Module.Types.Of do for key <- keys, t <- cartesian_map(tail) do closed_map(non_multiple ++ [{key, type} | t]) end - |> Enum.reduce(&union/2) + |> Enum.reduce(&opt_union/2) end {if(dynamic?, do: dynamic(map), else: map), context} end defp union_negated([], new_type, single, multiple) do - single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end) - multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end) + single = Enum.map(single, fn {key, old_type} -> {key, opt_union(old_type, new_type)} end) + + multiple = + Enum.map(multiple, fn {keys, old_type} -> {keys, opt_union(old_type, new_type)} end) + {single, multiple} end @@ -402,13 +405,13 @@ defmodule Module.Types.Of do if key in negated do {{key, old_type}, [key | matched]} else - {{key, union(old_type, new_type)}, matched} + {{key, opt_union(old_type, new_type)}, matched} end end) multiple = Enum.map(multiple, fn {keys, old_type} -> - {keys, union(old_type, new_type)} + {keys, opt_union(old_type, new_type)} end) {Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple} diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index c61eec46f6e..07139536747 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -27,7 +27,7 @@ defmodule Module.Types.Pattern do do: previous defp concat_previous(types, {list, descr}), - do: {[types | list], union(descr, args_to_previous(types))} + do: {[types | list], opt_union(descr, args_to_previous(types))} defp args_to_previous([type]), do: upper_bound(type) defp args_to_previous(types), do: args_to_static_domain(types) @@ -39,8 +39,8 @@ defmodule Module.Types.Pattern do defp of_pattern_previous(types, {_, descr}, stack) do refined_types = case types do - [type] -> [difference(type, descr)] - [_ | _] -> args_to_domain(types) |> difference(descr) |> domain_to_flat_args(types) + [type] -> [opt_difference(type, descr)] + [_ | _] -> args_to_domain(types) |> opt_difference(descr) |> domain_to_flat_args(types) end # check_previous? is an optimization. If types have not changed, @@ -349,7 +349,7 @@ defmodule Module.Types.Pattern do defp of_pattern_intersect([head | tail], index, acc, pattern_info, tag, stack, context) do {tree, expected, pattern} = head actual = of_pattern_tree(tree, stack, context) - type = intersection(actual, expected) + type = opt_intersection(actual, expected) if empty?(type) do context = badpattern_error(pattern, index, tag, stack, context) @@ -498,7 +498,7 @@ defmodule Module.Types.Pattern do defp of_pattern_var([{:subpattern, key} | rest], type, context) do %{^key => subpattern} = context.subpatterns - of_pattern_var(rest, intersection(type, subpattern), context) + of_pattern_var(rest, opt_intersection(type, subpattern), context) end defp of_pattern_var([:tail | rest], type, context) do @@ -518,7 +518,7 @@ defmodule Module.Types.Pattern do # This logic mirrors the code in `Apply.compare` cond do # If it is a singleton, we can always be precise - singleton?(type) -> if polarity, do: type, else: negation(type) + singleton?(type) -> if polarity, do: type, else: Module.Types.Descr.opt_negation(type) # We are checking for `not x == 1` or similar, we can't say anything about x polarity == false -> term() # We are checking for `x == 1`, make sure x is integer or float @@ -550,7 +550,7 @@ defmodule Module.Types.Pattern do tail |> Enum.reduce( of_pattern_tree(head, stack, context), - &union(of_pattern_tree(&1, stack, context), &2) + &opt_union(of_pattern_tree(&1, stack, context), &2) ) |> non_empty_list(of_pattern_tree(suffix, stack, context)) end @@ -558,7 +558,7 @@ defmodule Module.Types.Pattern do defp of_pattern_tree({:intersection, entries}, stack, context) do entries |> Enum.map(&of_pattern_tree(&1, stack, context)) - |> Enum.reduce(&intersection/2) + |> Enum.reduce(&opt_intersection/2) end defp of_pattern_tree({:var, version}, _stack, context) do @@ -690,7 +690,7 @@ defmodule Module.Types.Pattern do if static == [] do {:intersection, dynamic} else - {:intersection, [Enum.reduce(static, &intersection/2) | dynamic]} + {:intersection, [Enum.reduce(static, &opt_intersection/2) | dynamic]} end context = of_var(var, version, [%{root: return, expr: match}], context) @@ -950,13 +950,13 @@ defmodule Module.Types.Pattern do type = case {static, dynamic} do {static, []} when is_descr(suffix_type) -> - non_empty_list(Enum.reduce(static, &union/2), suffix_type) + non_empty_list(Enum.reduce(static, &opt_union/2), suffix_type) {[], dynamic} -> {:non_empty_list, dynamic, suffix_type} {static, dynamic} -> - {:non_empty_list, [Enum.reduce(static, &union/2) | dynamic], suffix_type} + {:non_empty_list, [Enum.reduce(static, &opt_union/2) | dynamic], suffix_type} end {type, false, context} @@ -1117,7 +1117,7 @@ defmodule Module.Types.Pattern do Enum.map_reduce(prefix, context, &of_guard(&1, term(), expr, stack, &2)) {suffix, context} = of_guard(suffix, term(), expr, stack, context) - {non_empty_list(Enum.reduce(prefix, &union/2), suffix), context} + {non_empty_list(Enum.reduce(prefix, &opt_union/2), suffix), context} end # {left, right} @@ -1258,7 +1258,7 @@ defmodule Module.Types.Pattern do # `:fail` as dynamic as its whole purpose is to cause failures. defp of_remote(:orelse, [left, :fail], _call, expected, stack, context) do {type, context} = of_guard(left, expected, left, stack, context) - {union(type, @dynamic_fail), context} + {opt_union(type, @dynamic_fail), context} end defp of_remote(fun, _args, call, expected, stack, context) @@ -1430,7 +1430,7 @@ defmodule Module.Types.Pattern do case disjoint? do true -> {type, context} - false -> {union(to_abort, type), context} + false -> {opt_union(to_abort, type), context} end end @@ -1445,7 +1445,7 @@ defmodule Module.Types.Pattern do of_guard(head, expected, head, stack, context) acc_vars = [{vars, cond_vars} | acc_vars] - of_logical_cond(tail, union(acc_type, type), acc_vars, expected, stack, context) + of_logical_cond(tail, opt_union(acc_type, type), acc_vars, expected, stack, context) end defp of_logical_cond([], acc_type, acc_vars, _expected, _stack, _context) do diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index b1536c5a4ba..f44a847ddc1 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -694,10 +694,10 @@ defmodule Protocol do domain = Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc -> - Descr.union(Module.Types.Of.impl(impl, :open), acc) + Descr.opt_union(Module.Types.Of.impl(impl, :open), acc) end) - not_domain = Descr.negation(domain) + not_domain = Descr.opt_negation(domain) if Any in types do clauses = diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 445fcf32f48..b48049f2c48 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -17,16 +17,19 @@ defmodule Module.Types.DescrTest do import Module.Types.Descr defmacro domain_key(arg) when is_atom(arg), do: [arg] - defp number(), do: union(integer(), float()) + defp number(), do: opt_union(integer(), float()) defp empty_tuple(), do: tuple([]) defp tuple_of_size_at_least(n) when is_integer(n), do: open_tuple(List.duplicate(term(), n)) defp tuple_of_size(n) when is_integer(n) and n >= 0, do: tuple(List.duplicate(term(), n)) - defp list(elem_type, tail_type), do: union(empty_list(), non_empty_list(elem_type, tail_type)) + + defp list(elem_type, tail_type), + do: opt_union(empty_list(), non_empty_list(elem_type, tail_type)) + defp map_with_default(descr), do: open_map([{to_domain_keys(:term), descr}]) defp projected_negative_map(size) do Enum.reduce(1..size, open_map(k: open_map(), x: term()), fn index, acc -> - difference( + opt_difference( acc, open_map([ {:k, open_map([{:"value#{index}", integer()}])}, @@ -38,7 +41,7 @@ defmodule Module.Types.DescrTest do defp projected_negative_tuple(size) do Enum.reduce(1..size, open_tuple([open_tuple([]), term()]), fn index, acc -> - difference( + opt_difference( acc, open_tuple([ open_tuple([atom([:"value#{index}"])]), @@ -50,27 +53,27 @@ defmodule Module.Types.DescrTest do describe "union" do test "bitmap" do - assert union(integer(), float()) == union(float(), integer()) + assert opt_union(integer(), float()) == opt_union(float(), integer()) end test "term" do - assert union(term(), float()) == term() - assert union(term(), binary()) == term() - assert union(term(), if_set(binary())) == if_set(term()) + assert opt_union(term(), float()) == term() + assert opt_union(term(), binary()) == term() + assert opt_union(term(), if_set(binary())) == if_set(term()) end test "none" do - assert union(none(), float()) == float() - assert union(none(), binary()) == binary() + assert opt_union(none(), float()) == float() + assert opt_union(none(), binary()) == binary() end test "atom" do - assert union(atom(), atom([:a])) == atom() - assert union(atom([:a]), atom([:b])) == atom([:a, :b]) - assert union(atom([:a]), negation(atom([:b]))) == negation(atom([:b])) + assert opt_union(atom(), atom([:a])) == atom() + assert opt_union(atom([:a]), atom([:b])) == atom([:a, :b]) + assert opt_union(atom([:a]), opt_negation(atom([:b]))) == opt_negation(atom([:b])) - assert union(negation(atom([:a, :b])), negation(atom([:b, :c]))) - |> equal?(negation(atom([:b]))) + assert opt_union(opt_negation(atom([:a, :b])), opt_negation(atom([:b, :c]))) + |> equal?(opt_negation(atom([:b]))) end test "all primitive types" do @@ -89,115 +92,134 @@ defmodule Module.Types.DescrTest do reference() ] - assert Enum.reduce(all, &union/2) |> equal?(term()) + assert Enum.reduce(all, &opt_union/2) |> equal?(term()) end test "dynamic" do - assert equal?(union(dynamic(), dynamic()), dynamic()) - assert equal?(union(dynamic(), term()), term()) - assert equal?(union(term(), dynamic()), term()) + assert equal?(opt_union(dynamic(), dynamic()), dynamic()) + assert equal?(opt_union(dynamic(), term()), term()) + assert equal?(opt_union(term(), dynamic()), term()) + + assert equal?(opt_union(dynamic(atom()), atom()), atom()) + refute equal?(opt_union(dynamic(atom()), atom()), dynamic(atom())) - assert equal?(union(dynamic(atom()), atom()), atom()) - refute equal?(union(dynamic(atom()), atom()), dynamic(atom())) + assert equal?( + opt_union(term(), dynamic(if_set(integer()))), + opt_union(term(), dynamic(not_set())) + ) - assert equal?(union(term(), dynamic(if_set(integer()))), union(term(), dynamic(not_set()))) - refute equal?(union(term(), dynamic(if_set(integer()))), dynamic(union(term(), not_set()))) + refute equal?( + opt_union(term(), dynamic(if_set(integer()))), + dynamic(opt_union(term(), not_set())) + ) end test "tuple" do - assert equal?(union(tuple(), tuple()), tuple()) + assert equal?(opt_union(tuple(), tuple()), tuple()) t = tuple([integer(), atom()]) - assert equal?(union(t, t), t) + assert equal?(opt_union(t, t), t) - assert union(tuple([integer(), atom()]), tuple([float(), atom()])) - |> equal?(tuple([union(integer(), float()), atom()])) + assert opt_union(tuple([integer(), atom()]), tuple([float(), atom()])) + |> equal?(tuple([opt_union(integer(), float()), atom()])) - assert union(tuple([integer(), atom()]), tuple([integer(), binary()])) - |> equal?(tuple([integer(), union(atom(), binary())])) + assert opt_union(tuple([integer(), atom()]), tuple([integer(), binary()])) + |> equal?(tuple([integer(), opt_union(atom(), binary())])) assert open_tuple([atom()]) - |> union(tuple([atom(), integer()])) + |> opt_union(tuple([atom(), integer()])) |> equal?(open_tuple([atom()])) - assert tuple([union(integer(), atom())]) - |> difference(open_tuple([atom()])) + assert tuple([opt_union(integer(), atom())]) + |> opt_difference(open_tuple([atom()])) |> equal?(tuple([integer()])) # Test union of open tuple # We assert using == on purpose as we want to return open tuples - assert union(open_tuple([]), tuple([atom()])) == open_tuple([]) - assert union(open_tuple([]), difference(open_tuple([]), tuple([atom()]))) == open_tuple([]) - assert union(difference(open_tuple([]), tuple([atom()])), open_tuple([])) == open_tuple([]) + assert opt_union(open_tuple([]), tuple([atom()])) == open_tuple([]) + + assert opt_union(open_tuple([]), opt_difference(open_tuple([]), tuple([atom()]))) == + open_tuple([]) + + assert opt_union(opt_difference(open_tuple([]), tuple([atom()])), open_tuple([])) == + open_tuple([]) end test "map" do - assert equal?(union(open_map(), open_map()), open_map()) - assert equal?(union(closed_map(a: integer()), open_map()), open_map()) - assert equal?(union(closed_map(a: integer()), negation(closed_map(a: integer()))), term()) + assert equal?(opt_union(open_map(), open_map()), open_map()) + assert equal?(opt_union(closed_map(a: integer()), open_map()), open_map()) + + assert equal?( + opt_union(closed_map(a: integer()), opt_negation(closed_map(a: integer()))), + term() + ) a_integer_open = open_map(a: integer()) - assert equal?(union(closed_map(a: integer()), a_integer_open), a_integer_open) + assert equal?(opt_union(closed_map(a: integer()), a_integer_open), a_integer_open) closed = closed_map(a: integer(), b: atom()) open = open_map(a: integer(), b: boolean()) - assert subtype?(closed, union(closed, open)) - assert subtype?(open, union(closed, open)) - assert subtype?(closed, union(open, closed)) - assert subtype?(open, union(open, closed)) + assert subtype?(closed, opt_union(closed, open)) + assert subtype?(open, opt_union(closed, open)) + assert subtype?(closed, opt_union(open, closed)) + assert subtype?(open, opt_union(open, closed)) # Domain key types atom_to_atom = open_map([{domain_key(:atom), atom()}]) atom_to_integer = open_map([{domain_key(:atom), integer()}]) # Test union identity and different type maps - assert union(atom_to_atom, atom_to_atom) == atom_to_atom + assert opt_union(atom_to_atom, atom_to_atom) == atom_to_atom # Test subtype relationships with domain key maps - refute open_map([{domain_key(:atom), union(atom(), integer())}]) - |> subtype?(union(atom_to_atom, atom_to_integer)) + refute open_map([{domain_key(:atom), opt_union(atom(), integer())}]) + |> subtype?(opt_union(atom_to_atom, atom_to_integer)) - assert union(atom_to_atom, atom_to_integer) - |> subtype?(open_map([{domain_key(:atom), union(atom(), integer())}])) + assert opt_union(atom_to_atom, atom_to_integer) + |> subtype?(open_map([{domain_key(:atom), opt_union(atom(), integer())}])) # Test unions with empty map - assert union(empty_map(), open_map([{domain_key(:integer), atom()}])) + assert opt_union(empty_map(), open_map([{domain_key(:integer), atom()}])) |> equal?(open_map([{domain_key(:integer), atom()}])) # Test union of open map # We assert using == on purpose as we want to return open maps - assert union(open_map(), open_map([{domain_key(:integer), atom()}])) == open_map() - assert union(open_map(), difference(open_map(), closed_map(foo: atom()))) == open_map() - assert union(difference(open_map(), closed_map(foo: atom())), open_map()) == open_map() + assert opt_union(open_map(), open_map([{domain_key(:integer), atom()}])) == open_map() + + assert opt_union(open_map(), opt_difference(open_map(), closed_map(foo: atom()))) == + open_map() + + assert opt_union(opt_difference(open_map(), closed_map(foo: atom())), open_map()) == + open_map() # Ensure no duplicate, no matter the order - assert union( + assert opt_union( open_map(a: integer()), open_map(a: number(), b: binary()) ) - |> union(open_map(a: integer())) == - union( + |> opt_union(open_map(a: integer())) == + opt_union( open_map(a: number(), b: binary()), open_map(a: integer()) ) - |> union(open_map(a: integer())) + |> opt_union(open_map(a: integer())) end test "list" do - assert union(list(term()), list(term())) |> equal?(list(term())) - assert union(list(integer()), list(term())) |> equal?(list(term())) + assert opt_union(list(term()), list(term())) |> equal?(list(term())) + assert opt_union(list(integer()), list(term())) |> equal?(list(term())) - assert union(difference(list(term()), list(integer())), list(integer())) + assert opt_union(opt_difference(list(term()), list(integer())), list(integer())) |> equal?(list(term())) end test "fun" do - assert equal?(union(fun(), fun()), fun()) - assert equal?(union(fun(), none_fun(1)), fun()) + assert equal?(opt_union(fun(), fun()), fun()) + assert equal?(opt_union(fun(), none_fun(1)), fun()) - dynamic_fun = intersection(fun(), dynamic()) - assert equal?(union(dynamic_fun, fun()), fun()) + dynamic_fun = opt_intersection(fun(), dynamic()) + assert equal?(opt_union(dynamic_fun, fun()), fun()) end test "optimizations (maps)" do @@ -207,34 +229,34 @@ defmodule Module.Types.DescrTest do # these might have an important impact on compile times. # Optimization one: same tags, all but one key are structurally equal - assert union( + assert opt_union( open_map(a: float(), b: atom()), open_map(a: integer(), b: atom()) ) - |> equal?(open_map(a: union(float(), integer()), b: atom())) + |> equal?(open_map(a: opt_union(float(), integer()), b: atom())) - assert union( + assert opt_union( closed_map(a: float(), b: atom()), closed_map(a: integer(), b: atom()) - ) == closed_map(a: union(float(), integer()), b: atom()) + ) == closed_map(a: opt_union(float(), integer()), b: atom()) # Optimization two: we can tell that one map is a subtype of the other: - assert union( + assert opt_union( closed_map(a: term(), b: term()), closed_map(a: float(), b: binary()) ) == closed_map(a: term(), b: term()) - assert union( + assert opt_union( open_map(a: term()), closed_map(a: float(), b: binary()) ) == open_map(a: term()) - assert union( + assert opt_union( closed_map(a: float(), b: binary()), open_map(a: term()) ) == open_map(a: term()) - assert union( + assert opt_union( closed_map(a: term(), b: tuple([term(), term()])), closed_map(a: float(), b: tuple([atom(), binary()])) ) == closed_map(a: term(), b: tuple([term(), term()])) @@ -242,28 +264,28 @@ defmodule Module.Types.DescrTest do test "optimizations (tuples)" do # Optimization one: same tags, all but one key are structurally equal - assert union( + assert opt_union( open_tuple([float(), atom()]), open_tuple([integer(), atom()]) - ) == open_tuple([union(float(), integer()), atom()]) + ) == open_tuple([opt_union(float(), integer()), atom()]) - assert union( + assert opt_union( tuple([float(), atom()]), tuple([integer(), atom()]) - ) == tuple([union(float(), integer()), atom()]) + ) == tuple([opt_union(float(), integer()), atom()]) # Optimization two: we can tell that one tuple is a subtype of the other - assert union( + assert opt_union( tuple([term(), term()]), tuple([float(), binary()]) ) == tuple([term(), term()]) - assert union( + assert opt_union( open_tuple([term()]), tuple([float(), binary()]) ) == open_tuple([term()]) - assert union( + assert opt_union( tuple([float(), binary()]), open_tuple([term()]) ) == open_tuple([term()]) @@ -272,123 +294,129 @@ defmodule Module.Types.DescrTest do describe "if_set" do test "preserves static parts alongside dynamic term" do - type = union(atom([:value]), dynamic()) |> if_set() + type = opt_union(atom([:value]), dynamic()) |> if_set() - assert equal?(type, union(if_set(atom([:value])), dynamic(if_set(term())))) + assert equal?(type, opt_union(if_set(atom([:value])), dynamic(if_set(term())))) refute equal?(type, dynamic(if_set(term()))) end end describe "intersection" do test "bitmap" do - assert intersection(integer(), union(integer(), float())) == integer() - assert intersection(integer(), float()) == none() + assert opt_intersection(integer(), opt_union(integer(), float())) == integer() + assert opt_intersection(integer(), float()) == none() end test "term" do - assert intersection(term(), term()) == term() - assert intersection(term(), float()) == float() - assert intersection(term(), binary()) == binary() + assert opt_intersection(term(), term()) == term() + assert opt_intersection(term(), float()) == float() + assert opt_intersection(term(), binary()) == binary() end test "none" do - assert intersection(none(), float()) == none() - assert intersection(none(), binary()) == none() + assert opt_intersection(none(), float()) == none() + assert opt_intersection(none(), binary()) == none() end test "atom" do - assert intersection(atom(), atom()) == atom() - assert intersection(atom(), atom([:a])) == atom([:a]) - assert intersection(atom([:a]), atom([:b])) == none() - assert intersection(atom([:a]), negation(atom([:b]))) == atom([:a]) + assert opt_intersection(atom(), atom()) == atom() + assert opt_intersection(atom(), atom([:a])) == atom([:a]) + assert opt_intersection(atom([:a]), atom([:b])) == none() + assert opt_intersection(atom([:a]), opt_negation(atom([:b]))) == atom([:a]) end test "dynamic" do - assert equal?(intersection(dynamic(), dynamic()), dynamic()) - assert equal?(intersection(dynamic(), term()), dynamic()) - assert equal?(intersection(term(), dynamic()), dynamic()) - assert empty?(intersection(dynamic(), none())) - assert empty?(intersection(intersection(dynamic(), atom()), integer())) + assert equal?(opt_intersection(dynamic(), dynamic()), dynamic()) + assert equal?(opt_intersection(dynamic(), term()), dynamic()) + assert equal?(opt_intersection(term(), dynamic()), dynamic()) + assert empty?(opt_intersection(dynamic(), none())) + assert empty?(opt_intersection(opt_intersection(dynamic(), atom()), integer())) - assert empty?(intersection(dynamic(not_set()), term())) - refute empty?(intersection(dynamic(if_set(integer())), term())) + assert empty?(opt_intersection(dynamic(not_set()), term())) + refute empty?(opt_intersection(dynamic(if_set(integer())), term())) # Check for structural equivalence - assert intersection(dynamic(not_set()), term()) == none() - assert equal?(intersection(if_set(dynamic(integer())), term()), dynamic(integer())) + assert opt_intersection(dynamic(not_set()), term()) == none() + assert equal?(opt_intersection(if_set(dynamic(integer())), term()), dynamic(integer())) assert equal?( - intersection(if_set(union(atom(), dynamic())), term()), - union(atom(), dynamic()) + opt_intersection(if_set(opt_union(atom(), dynamic())), term()), + opt_union(atom(), dynamic()) ) end test "tuple" do - assert empty?(intersection(open_tuple([atom()]), open_tuple([integer()]))) + assert empty?(opt_intersection(open_tuple([atom()]), open_tuple([integer()]))) - assert intersection(open_tuple([atom()]), tuple([term(), integer()])) + assert opt_intersection(open_tuple([atom()]), tuple([term(), integer()])) |> equal?(tuple([atom(), integer()])) - assert intersection(tuple([term(), integer()]), tuple([atom(), term()])) + assert opt_intersection(tuple([term(), integer()]), tuple([atom(), term()])) |> equal?(tuple([atom(), integer()])) empty_field = closed_map(key: atom([:value])) - |> difference(open_map(key: atom(), optional: if_set(atom()))) + |> opt_difference(open_map(key: atom(), optional: if_set(atom()))) assert empty?(empty_field) refute empty_field == none() - assert intersection(open_tuple([integer()]), tuple([integer(), empty_field])) + assert opt_intersection(open_tuple([integer()]), tuple([integer(), empty_field])) |> equal?(none()) - assert intersection(tuple([integer(), empty_field]), open_tuple([integer()])) + assert opt_intersection(tuple([integer(), empty_field]), open_tuple([integer()])) |> equal?(none()) - assert intersection(tuple(), tuple([integer(), empty_field])) |> equal?(none()) + assert opt_intersection(tuple(), tuple([integer(), empty_field])) |> equal?(none()) end test "map" do - assert intersection(open_map(), open_map()) == open_map() - assert equal?(intersection(closed_map(a: integer()), open_map()), closed_map(a: integer())) + assert opt_intersection(open_map(), open_map()) == open_map() + + assert equal?( + opt_intersection(closed_map(a: integer()), open_map()), + closed_map(a: integer()) + ) assert equal?( - intersection(closed_map(a: integer()), open_map(a: integer())), + opt_intersection(closed_map(a: integer()), open_map(a: integer())), closed_map(a: integer()) ) - assert intersection(closed_map(a: integer()), open_map(b: not_set())) + assert opt_intersection(closed_map(a: integer()), open_map(b: not_set())) |> equal?(closed_map(a: integer())) - assert intersection(closed_map(a: integer()), open_map(b: if_set(integer()))) + assert opt_intersection(closed_map(a: integer()), open_map(b: if_set(integer()))) |> equal?(closed_map(a: integer())) assert equal?( - intersection(closed_map(a: integer()), closed_map(a: if_set(integer()))), + opt_intersection(closed_map(a: integer()), closed_map(a: if_set(integer()))), closed_map(a: integer()) ) - assert empty?(intersection(closed_map(a: integer()), closed_map(a: atom()))) + assert empty?(opt_intersection(closed_map(a: integer()), closed_map(a: atom()))) # Maps leaves are actually optimized, so some of the code branches # can only be tested through negations. This is the intersection between # open_map(a: integer()) and open_map(b: integer()) a_and_b = - negation(union(negation(open_map(a: integer())), negation(open_map(b: integer())))) + opt_negation( + opt_union(opt_negation(open_map(a: integer())), opt_negation(open_map(b: integer()))) + ) assert equal?( # The additional parts we are intersecting are empty - intersection( - union(a_and_b, closed_map(c: float())), - union(a_and_b, closed_map(d: float())) + opt_intersection( + opt_union(a_and_b, closed_map(c: float())), + opt_union(a_and_b, closed_map(d: float())) ), a_and_b ) # This is a regression triggered by an optimization - assert intersection( + assert opt_intersection( closed_map(tag: atom([true]), halted: atom([true]), assigns: term()), - union( + opt_union( closed_map(tag: atom([true]), halted: atom([true]), assigns: term()), closed_map(tag: atom([true]), halted: term(), assigns: open_map()) ) @@ -398,23 +426,23 @@ defmodule Module.Types.DescrTest do # Closed maps with not set keys should have no impact test "map closed with not set keys" do - assert intersection(closed_map(a: integer()), closed_map(a: integer(), b: not_set())) == + assert opt_intersection(closed_map(a: integer()), closed_map(a: integer(), b: not_set())) == closed_map(a: integer()) - assert intersection(closed_map(a: integer(), b: not_set()), closed_map(a: integer())) == + assert opt_intersection(closed_map(a: integer(), b: not_set()), closed_map(a: integer())) == closed_map(a: integer()) - assert intersection(closed_map(a: integer()), closed_map(a: not_set())) == + assert opt_intersection(closed_map(a: integer()), closed_map(a: not_set())) == none() - assert intersection( + assert opt_intersection( closed_map(a: integer(), b: not_set()), closed_map(a: integer(), c: not_set()) ) == closed_map(a: integer()) - assert intersection(empty_map(), closed_map(a: if_set(integer()))) == empty_map() - assert intersection(closed_map(a: if_set(integer())), empty_map()) == empty_map() + assert opt_intersection(empty_map(), closed_map(a: if_set(integer()))) == empty_map() + assert opt_intersection(closed_map(a: if_set(integer())), empty_map()) == empty_map() refute disjoint?(empty_map(), closed_map(a: if_set(integer()))) end @@ -424,7 +452,7 @@ defmodule Module.Types.DescrTest do map1 = open_map([{domain_key(:integer), integer()}, {domain_key(:atom), atom()}]) map2 = closed_map([{domain_key(:integer), number()}]) - intersection = intersection(map1, map2) + intersection = opt_intersection(map1, map2) expected = closed_map([{domain_key(:integer), integer()}, {domain_key(:atom), none()}]) @@ -436,11 +464,11 @@ defmodule Module.Types.DescrTest do map1 = open_map([{domain_key(:integer), integer()}, {domain_key(:atom), atom()}]) map2 = closed_map([{domain_key(:integer), float()}, {domain_key(:pid), binary()}]) - intersection = intersection(map1, map2) + intersection = opt_intersection(map1, map2) expected = closed_map([ - {domain_key(:integer), intersection(integer(), float())}, + {domain_key(:integer), opt_intersection(integer(), float())}, {domain_key(:atom), none()}, {domain_key(:pid), binary()} ]) @@ -452,12 +480,12 @@ defmodule Module.Types.DescrTest do map1 = open_map([{domain_key(:integer), integer()}, {domain_key(:binary), binary()}]) map2 = closed_map([{domain_key(:integer), float()}]) - intersection = intersection(map1, map2) + intersection = opt_intersection(map1, map2) assert equal?( intersection, closed_map([ - {domain_key(:integer), intersection(integer(), float())}, + {domain_key(:integer), opt_intersection(integer(), float())}, {domain_key(:binary), none()} ]) ) @@ -466,218 +494,231 @@ defmodule Module.Types.DescrTest do t1 = closed_map([{domain_key(:integer), atom()}]) t2 = closed_map([{domain_key(:integer), binary()}]) - assert equal?(intersection(t1, t2), empty_map()) + assert equal?(opt_intersection(t1, t2), empty_map()) t1 = closed_map([{domain_key(:integer), atom()}]) t2 = closed_map([{domain_key(:atom), term()}]) - refute empty?(intersection(t1, t2)) - assert equal?(intersection(t1, t2), empty_map()) + refute empty?(opt_intersection(t1, t2)) + assert equal?(opt_intersection(t1, t2), empty_map()) end test "list" do - assert intersection(list(term()), list(term())) == list(term()) - assert intersection(list(integer()), list(integer())) == list(integer()) - assert intersection(list(integer()), list(number())) == list(integer()) - assert intersection(list(integer()), list(atom())) == empty_list() + assert opt_intersection(list(term()), list(term())) == list(term()) + assert opt_intersection(list(integer()), list(integer())) == list(integer()) + assert opt_intersection(list(integer()), list(number())) == list(integer()) + assert opt_intersection(list(integer()), list(atom())) == empty_list() # Empty list intersections - assert intersection(empty_list(), list(term())) == empty_list() - assert intersection(empty_list(), list(integer())) == empty_list() - assert intersection(empty_list(), empty_list()) == empty_list() + assert opt_intersection(empty_list(), list(term())) == empty_list() + assert opt_intersection(empty_list(), list(integer())) == empty_list() + assert opt_intersection(empty_list(), empty_list()) == empty_list() # List with any type - assert intersection(list(term()), list(integer())) == list(integer()) - assert intersection(list(term()), list(integer())) == list(integer()) + assert opt_intersection(list(term()), list(integer())) == list(integer()) + assert opt_intersection(list(term()), list(integer())) == list(integer()) # Intersection with more specific types - assert intersection(list(integer()), list(atom([:a, :b]))) == empty_list() + assert opt_intersection(list(integer()), list(atom([:a, :b]))) == empty_list() # Intersection with union types - assert intersection(list(union(integer(), atom())), list(number())) == list(integer()) + assert opt_intersection(list(opt_union(integer(), atom())), list(number())) == + list(integer()) # Intersection with dynamic assert equal?( - intersection(dynamic(list(term())), list(integer())), + opt_intersection(dynamic(list(term())), list(integer())), dynamic(list(integer())) ) - assert equal?(intersection(dynamic(list(term())), list(term())), dynamic(list(term()))) + assert equal?(opt_intersection(dynamic(list(term())), list(term())), dynamic(list(term()))) # Nested list intersections - assert intersection(list(list(integer())), list(list(number()))) == list(list(integer())) - assert intersection(list(list(integer())), list(list(atom()))) == list(empty_list()) + assert opt_intersection(list(list(integer())), list(list(number()))) == + list(list(integer())) + + assert opt_intersection(list(list(integer())), list(list(atom()))) == list(empty_list()) # Intersection with non-list types - assert intersection(list(integer()), integer()) == none() + assert opt_intersection(list(integer()), integer()) == none() # Tests for list with last element - assert intersection(list(float(), atom()), list(number(), term())) == list(float(), atom()) + assert opt_intersection(list(float(), atom()), list(number(), term())) == + list(float(), atom()) - assert intersection(list(number(), atom()), list(float(), boolean())) == + assert opt_intersection(list(number(), atom()), list(float(), boolean())) == list(float(), boolean()) - assert intersection(list(integer(), float()), list(number(), integer())) == empty_list() + assert opt_intersection(list(integer(), float()), list(number(), integer())) == empty_list() # Empty list with last element - assert intersection(empty_list(), list(integer(), atom())) == empty_list() - assert intersection(list(integer(), atom()), empty_list()) == empty_list() + assert opt_intersection(empty_list(), list(integer(), atom())) == empty_list() + assert opt_intersection(list(integer(), atom()), empty_list()) == empty_list() # List with any type and specific last element - assert intersection(list(term(), atom()), list(float(), boolean())) == + assert opt_intersection(list(term(), atom()), list(float(), boolean())) == list(float(), boolean()) - assert intersection(list(term(), term()), list(float(), atom())) == list(float(), atom()) + assert opt_intersection(list(term(), term()), list(float(), atom())) == + list(float(), atom()) # Nested lists with last element - assert intersection(list(list(integer()), atom()), list(list(number()), boolean())) == + assert opt_intersection(list(list(integer()), atom()), list(list(number()), boolean())) == list(list(integer()), boolean()) assert list(list(integer(), atom()), float()) - |> intersection(list(list(number(), boolean()), integer())) == empty_list() + |> opt_intersection(list(list(number(), boolean()), integer())) == empty_list() # Union types in last element - assert intersection(list(integer(), union(atom(), binary())), list(number(), atom())) == + assert opt_intersection( + list(integer(), opt_union(atom(), binary())), + list(number(), atom()) + ) == list(integer(), atom()) # Dynamic with last element - assert intersection(dynamic(list(term(), atom())), list(integer(), boolean())) + assert opt_intersection(dynamic(list(term(), atom())), list(integer(), boolean())) |> equal?(dynamic(list(integer(), boolean()))) # Intersection with proper list (should result in empty list) - assert intersection(list(integer(), atom()), list(integer())) == empty_list() + assert opt_intersection(list(integer(), atom()), list(integer())) == empty_list() end test "function" do - assert not empty?(intersection(negation(none_fun(2)), negation(none_fun(3)))) + assert not empty?(opt_intersection(opt_negation(none_fun(2)), opt_negation(none_fun(3)))) end end describe "difference" do test "bitmap" do - assert difference(float(), integer()) == float() - assert difference(union(float(), integer()), integer()) == float() - assert difference(union(float(), integer()), binary()) == union(float(), integer()) + assert opt_difference(float(), integer()) == float() + assert opt_difference(opt_union(float(), integer()), integer()) == float() + + assert opt_difference(opt_union(float(), integer()), binary()) == + opt_union(float(), integer()) end test "term" do - assert difference(float(), term()) == none() - assert difference(integer(), term()) == none() + assert opt_difference(float(), term()) == none() + assert opt_difference(integer(), term()) == none() end test "none" do - assert difference(none(), integer()) == none() - assert difference(none(), float()) == none() + assert opt_difference(none(), integer()) == none() + assert opt_difference(none(), float()) == none() - assert difference(integer(), none()) == integer() - assert difference(float(), none()) == float() + assert opt_difference(integer(), none()) == integer() + assert opt_difference(float(), none()) == float() end test "atom" do - assert difference(atom([:a]), atom()) == none() - assert difference(atom([:a]), atom([:b])) == atom([:a]) + assert opt_difference(atom([:a]), atom()) == none() + assert opt_difference(atom([:a]), atom([:b])) == atom([:a]) end test "dynamic" do - assert equal?(dynamic(), difference(dynamic(), dynamic())) - assert equal?(dynamic(), difference(term(), dynamic())) - assert empty?(difference(dynamic(), term())) - assert empty?(difference(none(), dynamic())) - assert empty?(difference(dynamic(integer()), integer())) + assert equal?(dynamic(), opt_difference(dynamic(), dynamic())) + assert equal?(dynamic(), opt_difference(term(), dynamic())) + assert empty?(opt_difference(dynamic(), term())) + assert empty?(opt_difference(none(), dynamic())) + assert empty?(opt_difference(dynamic(integer()), integer())) end test "tuple" do - assert empty?(difference(open_tuple([atom()]), open_tuple([term()]))) - refute empty?(difference(tuple(), empty_tuple())) - refute tuple_of_size_at_least(2) |> difference(tuple_of_size(2)) |> empty?() - assert tuple_of_size_at_least(2) |> difference(tuple_of_size_at_least(1)) |> empty?() - assert tuple_of_size_at_least(3) |> difference(tuple_of_size_at_least(3)) |> empty?() - refute tuple_of_size_at_least(2) |> difference(tuple_of_size_at_least(3)) |> empty?() - refute tuple([term(), term()]) |> difference(tuple([atom(), term()])) |> empty?() - refute tuple([term(), term()]) |> difference(tuple([atom()])) |> empty?() - assert tuple([term(), term()]) |> difference(tuple([term(), term()])) |> empty?() + assert empty?(opt_difference(open_tuple([atom()]), open_tuple([term()]))) + refute empty?(opt_difference(tuple(), empty_tuple())) + refute tuple_of_size_at_least(2) |> opt_difference(tuple_of_size(2)) |> empty?() + assert tuple_of_size_at_least(2) |> opt_difference(tuple_of_size_at_least(1)) |> empty?() + assert tuple_of_size_at_least(3) |> opt_difference(tuple_of_size_at_least(3)) |> empty?() + refute tuple_of_size_at_least(2) |> opt_difference(tuple_of_size_at_least(3)) |> empty?() + refute tuple([term(), term()]) |> opt_difference(tuple([atom(), term()])) |> empty?() + refute tuple([term(), term()]) |> opt_difference(tuple([atom()])) |> empty?() + assert tuple([term(), term()]) |> opt_difference(tuple([term(), term()])) |> empty?() assert tuple_of_size_at_least(2) - |> difference(tuple_of_size(2)) - |> difference(tuple_of_size_at_least(3)) + |> opt_difference(tuple_of_size(2)) + |> opt_difference(tuple_of_size_at_least(3)) |> empty?() assert tuple([term(), term()]) - |> difference(tuple([atom()])) - |> difference(open_tuple([term()])) - |> difference(empty_tuple()) + |> opt_difference(tuple([atom()])) + |> opt_difference(open_tuple([term()])) + |> opt_difference(empty_tuple()) |> empty?() - refute difference(tuple(), empty_tuple()) - |> difference(open_tuple([term(), term()])) + refute opt_difference(tuple(), empty_tuple()) + |> opt_difference(open_tuple([term(), term()])) |> empty?() - assert difference(open_tuple([term()]), open_tuple([term(), term()])) - |> difference(tuple([term()])) + assert opt_difference(open_tuple([term()]), open_tuple([term(), term()])) + |> opt_difference(tuple([term()])) |> empty?() - assert tuple([union(atom(), integer()), term()]) - |> difference(open_tuple([atom(), term()])) - |> difference(open_tuple([integer(), term()])) + assert tuple([opt_union(atom(), integer()), term()]) + |> opt_difference(open_tuple([atom(), term()])) + |> opt_difference(open_tuple([integer(), term()])) |> empty?() - assert tuple([union(atom(), integer()), term()]) - |> difference(open_tuple([atom(), term()])) + assert tuple([opt_union(atom(), integer()), term()]) + |> opt_difference(open_tuple([atom(), term()])) |> equal?(tuple([integer(), term()])) - assert tuple([term(), union(atom(), integer()), term()]) - |> difference(open_tuple([term(), integer()])) + assert tuple([term(), opt_union(atom(), integer()), term()]) + |> opt_difference(open_tuple([term(), integer()])) |> equal?(tuple([term(), atom(), term()])) - assert difference(tuple(), open_tuple([term(), term()])) - |> equal?(union(tuple([term()]), tuple([]))) + assert opt_difference(tuple(), open_tuple([term(), term()])) + |> equal?(opt_union(tuple([term()]), tuple([]))) end test "tuple optimizations" do # We do direct assertions because we want to check how it works underneath - assert difference(tuple([]), tuple([atom()])) == tuple([]) - assert difference(tuple([]), open_tuple([atom()])) == tuple([]) - assert difference(open_tuple([atom()]), tuple([])) == open_tuple([atom()]) - assert difference(tuple([atom([:ok])]), tuple([atom([:error])])) == tuple([atom([:ok])]) - assert difference(tuple([atom([:ok])]), tuple([integer()])) == tuple([atom([:ok])]) - assert difference(tuple([integer()]), tuple([atom()])) == tuple([integer()]) + assert opt_difference(tuple([]), tuple([atom()])) == tuple([]) + assert opt_difference(tuple([]), open_tuple([atom()])) == tuple([]) + assert opt_difference(open_tuple([atom()]), tuple([])) == open_tuple([atom()]) + assert opt_difference(tuple([atom([:ok])]), tuple([atom([:error])])) == tuple([atom([:ok])]) + assert opt_difference(tuple([atom([:ok])]), tuple([integer()])) == tuple([atom([:ok])]) + assert opt_difference(tuple([integer()]), tuple([atom()])) == tuple([integer()]) end test "map" do - assert difference(open_map(), open_map()) == none() - assert difference(open_map(), term()) == none() - assert difference(open_map(), none()) == open_map() + assert opt_difference(open_map(), open_map()) == none() + assert opt_difference(open_map(), term()) == none() + assert opt_difference(open_map(), none()) == open_map() - assert empty?(difference(closed_map(a: integer()), open_map())) + assert empty?(opt_difference(closed_map(a: integer()), open_map())) - assert difference(closed_map(a: integer(), b: if_set(atom())), closed_map(a: integer())) - |> difference(closed_map(a: integer(), b: atom())) + assert opt_difference(closed_map(a: integer(), b: if_set(atom())), closed_map(a: integer())) + |> opt_difference(closed_map(a: integer(), b: atom())) |> empty?() - refute empty?(difference(open_map(), empty_map())) + refute empty?(opt_difference(open_map(), empty_map())) # Difference with single field closed map on rhs - assert difference(closed_map(a: integer()), closed_map(a: integer())) == none() + assert opt_difference(closed_map(a: integer()), closed_map(a: integer())) == none() - assert difference(open_map(a: atom()), closed_map(b: integer())) + assert opt_difference(open_map(a: atom()), closed_map(b: integer())) |> equal?(open_map(a: atom())) - assert difference(open_map(a: integer()), closed_map(b: boolean())) + assert opt_difference(open_map(a: integer()), closed_map(b: boolean())) |> equal?(open_map(a: integer())) # Difference with single field open map on rhs (they are optimized) - assert difference(closed_map(a: integer()), open_map(a: integer())) == none() - assert difference(closed_map(a: integer()), open_map(a: if_set(integer()))) == none() + assert opt_difference(closed_map(a: integer()), open_map(a: integer())) == none() + assert opt_difference(closed_map(a: integer()), open_map(a: if_set(integer()))) == none() - assert difference(closed_map(a: integer()), open_map(b: integer())) == + assert opt_difference(closed_map(a: integer()), open_map(b: integer())) == closed_map(a: integer()) - assert difference(closed_map(a: integer()), open_map(b: if_set(integer()))) == none() + assert opt_difference(closed_map(a: integer()), open_map(b: if_set(integer()))) == none() end test "map double negation with redundant empty map" do - type = closed_map(a: atom()) |> union(open_map(a: if_set(integer()))) |> union(empty_map()) + type = + closed_map(a: atom()) + |> opt_union(open_map(a: if_set(integer()))) + |> opt_union(empty_map()) - assert negation(negation(type)) |> equal?(type) + assert opt_negation(opt_negation(type)) |> equal?(type) end test "map (struct optimizations)" do @@ -685,28 +726,28 @@ defmodule Module.Types.DescrTest do atom_foo = atom([:foo]) atom_bar = atom([:bar]) - assert difference(open_map(__struct__: atom_foo), open_map(__struct__: atom_bar)) == + assert opt_difference(open_map(__struct__: atom_foo), open_map(__struct__: atom_bar)) == open_map(__struct__: atom_foo) - assert difference(closed_map(__struct__: atom_foo), open_map(__struct__: atom_bar)) == + assert opt_difference(closed_map(__struct__: atom_foo), open_map(__struct__: atom_bar)) == closed_map(__struct__: atom_foo) - assert difference(open_map(__struct__: atom_foo), closed_map(__struct__: atom_bar)) == + assert opt_difference(open_map(__struct__: atom_foo), closed_map(__struct__: atom_bar)) == open_map(__struct__: atom_foo) - assert difference(closed_map(__struct__: atom_foo), closed_map(__struct__: atom_bar)) == + assert opt_difference(closed_map(__struct__: atom_foo), closed_map(__struct__: atom_bar)) == closed_map(__struct__: atom_foo) - assert difference(closed_map(__struct__: atom_foo), closed_map(__struct__: term())) == + assert opt_difference(closed_map(__struct__: atom_foo), closed_map(__struct__: term())) == none() - assert difference(closed_map(__struct__: atom()), closed_map(__struct__: atom_bar)) == - closed_map(__struct__: difference(atom(), atom_bar)) + assert opt_difference(closed_map(__struct__: atom()), closed_map(__struct__: atom_bar)) == + closed_map(__struct__: opt_difference(atom(), atom_bar)) # Explicitly assert we keep it as cascading differences assert %{map: {_, {_, :closed, _}, :bdd_bot, :bdd_bot, _}} = - difference( - difference( + opt_difference( + opt_difference( open_map(value: term()), closed_map(__struct__: atom_foo, value: term()) ), @@ -719,21 +760,21 @@ defmodule Module.Types.DescrTest do t1 = closed_map([{domain_key(:integer), atom()}]) t2 = closed_map([{domain_key(:atom), binary()}]) - assert equal?(difference(t1, t2) |> union(empty_map()), t1) - assert empty?(difference(t1, t1)) + assert equal?(opt_difference(t1, t2) |> opt_union(empty_map()), t1) + assert empty?(opt_difference(t1, t1)) # %{atom() => t1} and not %{atom() => t2} is not %{atom() => t1 and not t2} t3 = closed_map([{domain_key(:integer), atom()}]) t4 = closed_map([{domain_key(:integer), atom([:ok])}]) - assert subtype?(difference(t3, t4), t3) + assert subtype?(opt_difference(t3, t4), t3) - refute difference(t3, t4) - |> equal?(closed_map([{domain_key(:integer), difference(atom(), atom([:ok]))}])) + refute opt_difference(t3, t4) + |> equal?(closed_map([{domain_key(:integer), opt_difference(atom(), atom([:ok]))}])) # Difference with a non-domain key map - t5 = closed_map([{domain_key(:integer), union(atom(), integer())}]) + t5 = closed_map([{domain_key(:integer), opt_union(atom(), integer())}]) t6 = closed_map(a: atom()) - assert equal?(difference(t5, t6), t5) + assert equal?(opt_difference(t5, t6), t5) # Removing atom keys from a map with defined atom keys a_number = closed_map(a: number()) @@ -741,12 +782,13 @@ defmodule Module.Types.DescrTest do atom_to_float = closed_map([{domain_key(:atom), float()}]) atom_to_term = closed_map([{domain_key(:atom), term()}]) atom_to_pid = closed_map([{domain_key(:atom), pid()}]) - t_diff = difference(a_number, atom_to_float) + t_diff = opt_difference(a_number, atom_to_float) # Removing atom keys that map to float, make the :a key point to integer only. assert map_fetch_key(t_diff, :a) == {false, integer()} # %{a => number, atom => pid} and not %{atom => float} gives numbers on :a - assert map_fetch_key(difference(a_number_and_pids, atom_to_float), :a) == {false, number()} + assert map_fetch_key(opt_difference(a_number_and_pids, atom_to_float), :a) == + {false, number()} assert map_fetch_key(t_diff, :foo) == :badkey @@ -754,60 +796,63 @@ defmodule Module.Types.DescrTest do refute subtype?(a_number, atom_to_float) # Removing all atom keys from map %{:a => type} means there is nothing left. - assert empty?(difference(a_number, atom_to_term)) - refute empty?(intersection(atom_to_term, a_number)) - assert empty?(intersection(atom_to_pid, a_number)) + assert empty?(opt_difference(a_number, atom_to_term)) + refute empty?(opt_intersection(atom_to_term, a_number)) + assert empty?(opt_intersection(atom_to_pid, a_number)) # (%{:a => number} and not %{:a => float}) is %{:a => integer} - assert equal?(difference(a_number, atom_to_float), closed_map(a: integer())) + assert equal?(opt_difference(a_number, atom_to_float), closed_map(a: integer())) end test "list" do # Basic list type differences - assert difference(list(term()), empty_list()) == non_empty_list(term()) - assert difference(list(integer()), list(term())) |> empty?() + assert opt_difference(list(term()), empty_list()) == non_empty_list(term()) + assert opt_difference(list(integer()), list(term())) |> empty?() - assert difference(list(integer()), list(float())) + assert opt_difference(list(integer()), list(float())) |> equal?(non_empty_list(integer())) # All list of integers and floats, minus all lists of integers, is NOT all lists of floats - refute difference(list(union(integer(), float())), list(integer())) + refute opt_difference(list(opt_union(integer(), float())), list(integer())) |> equal?(non_empty_list(float())) # Interactions with empty_list() - assert difference(empty_list(), list(term())) == none() - assert difference(list(integer()), empty_list()) == non_empty_list(integer()) + assert opt_difference(empty_list(), list(term())) == none() + assert opt_difference(list(integer()), empty_list()) == non_empty_list(integer()) # Nested list structures - assert difference(list(list(integer())), list(list(float()))) - |> equal?(difference(list(list(integer())), list(empty_list()))) + assert opt_difference(list(list(integer())), list(list(float()))) + |> equal?(opt_difference(list(list(integer())), list(empty_list()))) # Lists with union types - refute difference(list(union(integer(), float())), list(integer())) == list(float()) - refute difference(list(union(atom(), binary())), list(atom())) == list(binary()) + refute opt_difference(list(opt_union(integer(), float())), list(integer())) == list(float()) + refute opt_difference(list(opt_union(atom(), binary())), list(atom())) == list(binary()) # Tests for list with last element - assert difference(list(integer(), atom()), list(number(), term())) |> empty?() + assert opt_difference(list(integer(), atom()), list(number(), term())) |> empty?() - assert difference( + assert opt_difference( list(atom(), term()), - difference(list(atom(), term()), list(atom())) + opt_difference(list(atom(), term()), list(atom())) ) |> equal?(list(atom())) - assert difference(list(integer(), float()), list(number(), integer())) - |> equal?(non_empty_list(integer(), difference(float(), integer()))) + assert opt_difference(list(integer(), float()), list(number(), integer())) + |> equal?(non_empty_list(integer(), opt_difference(float(), integer()))) # Empty list with last element - assert difference(empty_list(), list(integer(), atom())) == none() + assert opt_difference(empty_list(), list(integer(), atom())) == none() - assert difference(list(integer(), atom()), empty_list()) == + assert opt_difference(list(integer(), atom()), empty_list()) == non_empty_list(integer(), atom()) # List with any type and specific last element - assert difference(list(term(), term()), list(term(), integer())) + assert opt_difference(list(term(), term()), list(term(), integer())) |> equal?( - non_empty_list(term(), negation(union(integer(), non_empty_list(term(), term())))) + non_empty_list( + term(), + opt_negation(opt_union(integer(), non_empty_list(term(), term()))) + ) ) # Nested lists with last element @@ -817,46 +862,46 @@ defmodule Module.Types.DescrTest do # gives: # "non empty lists of (lists of integers), ending with (atom and not boolean)" - assert difference(list(list(integer()), atom()), list(list(number()), boolean())) - |> equal?(non_empty_list(list(integer()), difference(atom(), boolean()))) + assert opt_difference(list(list(integer()), atom()), list(list(number()), boolean())) + |> equal?(non_empty_list(list(integer()), opt_difference(atom(), boolean()))) # Union types in last element - assert difference(list(integer(), union(atom(), binary())), list(number(), atom())) + assert opt_difference(list(integer(), opt_union(atom(), binary())), list(number(), atom())) |> equal?( - union( + opt_union( non_empty_list(integer(), binary()), - non_empty_list(difference(integer(), number()), union(atom(), binary())) + non_empty_list(opt_difference(integer(), number()), opt_union(atom(), binary())) ) ) # Dynamic with last element assert equal?( - difference(dynamic(list(term(), atom())), list(integer(), term())), - dynamic(difference(list(term(), atom()), list(integer(), term()))) + opt_difference(dynamic(list(term(), atom())), list(integer(), term())), + dynamic(opt_difference(list(term(), atom()), list(integer(), term()))) ) # Difference with proper list - assert difference(list(integer(), atom()), list(integer())) + assert opt_difference(list(integer(), atom()), list(integer())) |> equal?(non_empty_list(integer(), atom())) end test "fun" do for arity <- [0, 1, 2, 3] do - assert empty?(difference(none_fun(arity), none_fun(arity))) + assert empty?(opt_difference(none_fun(arity), none_fun(arity))) end - assert empty?(difference(fun(), fun())) - assert empty?(difference(none_fun(3), fun())) - refute empty?(difference(fun(), none_fun(1))) - refute empty?(difference(none_fun(2), none_fun(3))) - assert empty?(intersection(none_fun(2), none_fun(3))) + assert empty?(opt_difference(fun(), fun())) + assert empty?(opt_difference(none_fun(3), fun())) + refute empty?(opt_difference(fun(), none_fun(1))) + refute empty?(opt_difference(none_fun(2), none_fun(3))) + assert empty?(opt_intersection(none_fun(2), none_fun(3))) - f1f2 = union(none_fun(1), none_fun(2)) - assert f1f2 |> difference(none_fun(1)) |> difference(none_fun(2)) |> empty?() - assert none_fun(1) |> difference(difference(f1f2, none_fun(2))) |> empty?() - assert f1f2 |> difference(none_fun(1)) |> equal?(none_fun(2)) + f1f2 = opt_union(none_fun(1), none_fun(2)) + assert f1f2 |> opt_difference(none_fun(1)) |> opt_difference(none_fun(2)) |> empty?() + assert none_fun(1) |> opt_difference(opt_difference(f1f2, none_fun(2))) |> empty?() + assert f1f2 |> opt_difference(none_fun(1)) |> equal?(none_fun(2)) - assert fun([integer()], term()) |> difference(fun([none()], term())) |> empty?() + assert fun([integer()], term()) |> opt_difference(fun([none()], term())) |> empty?() end end @@ -864,8 +909,11 @@ defmodule Module.Types.DescrTest do test "map hoists dynamic" do assert dynamic(open_map(a: integer())) == open_map(a: dynamic(integer())) - assert union(open_map(a: binary()), dynamic(open_map(a: union(integer(), binary())))) == - open_map(a: dynamic(integer()) |> union(binary())) + assert opt_union( + open_map(a: binary()), + dynamic(open_map(a: opt_union(integer(), binary()))) + ) == + open_map(a: dynamic(integer()) |> opt_union(binary())) # For domains too t1 = dynamic(open_map([{domain_key(:integer), integer()}])) @@ -875,12 +923,12 @@ defmodule Module.Types.DescrTest do # if_set on dynamic fields also must work t1 = dynamic(open_map(a: if_set(integer()))) t2 = open_map(a: if_set(dynamic(integer()))) - assert union(open_map(a: not_set()), t1) == t2 + assert opt_union(open_map(a: not_set()), t1) == t2 end test "structural types preserve static part of gradual elements" do static = atom([:ok]) - gradual = union(static, dynamic(integer())) + gradual = opt_union(static, dynamic(integer())) upper_bound = upper_bound(gradual) x = atom([:x]) head = atom([:head]) @@ -905,11 +953,11 @@ defmodule Module.Types.DescrTest do describe "subtype" do test "bitmap" do - assert subtype?(integer(), union(integer(), float())) + assert subtype?(integer(), opt_union(integer(), float())) assert subtype?(integer(), integer()) assert subtype?(integer(), term()) assert subtype?(none(), integer()) - assert subtype?(integer(), negation(float())) + assert subtype?(integer(), opt_negation(float())) end test "atom" do @@ -918,15 +966,15 @@ defmodule Module.Types.DescrTest do assert subtype?(atom([:a]), term()) assert subtype?(none(), atom([:a])) assert subtype?(atom([:a]), atom([:a, :b])) - assert subtype?(atom([:a]), negation(atom([:b]))) + assert subtype?(atom([:a]), opt_negation(atom([:b]))) end test "dynamic" do assert subtype?(dynamic(), term()) assert subtype?(dynamic(), dynamic()) refute subtype?(term(), dynamic()) - assert subtype?(intersection(dynamic(), integer()), integer()) - assert subtype?(integer(), union(dynamic(), integer())) + assert subtype?(opt_intersection(dynamic(), integer()), integer()) + assert subtype?(integer(), opt_union(dynamic(), integer())) end test "tuple" do @@ -949,7 +997,7 @@ defmodule Module.Types.DescrTest do assert subtype?(closed_map(a: integer()), closed_map(a: integer())) assert subtype?(closed_map(a: integer()), open_map(a: integer())) assert subtype?(closed_map(a: integer(), b: atom()), open_map(a: integer())) - assert subtype?(closed_map(a: integer()), closed_map(a: union(integer(), atom()))) + assert subtype?(closed_map(a: integer()), closed_map(a: opt_union(integer(), atom()))) # optional refute subtype?(closed_map(a: if_set(integer())), closed_map(a: integer())) @@ -963,18 +1011,18 @@ defmodule Module.Types.DescrTest do assert subtype?(t2, t1) - t1_minus_t2 = difference(t1, t2) + t1_minus_t2 = opt_difference(t1, t2) refute empty?(t1_minus_t2) assert subtype?(map_with_default(number()), open_map()) - t = difference(open_map(), map_with_default(number())) + t = opt_difference(open_map(), map_with_default(number())) refute empty?(t) refute subtype?(open_map(), map_with_default(number())) assert subtype?(map_with_default(integer()), map_with_default(number())) refute subtype?(map_with_default(float()), map_with_default(atom())) assert equal?( - intersection(map_with_default(number()), map_with_default(float())), + opt_intersection(map_with_default(number()), map_with_default(float())), map_with_default(float()) ) end @@ -987,7 +1035,7 @@ defmodule Module.Types.DescrTest do end test "list" do - refute subtype?(non_empty_list(integer()), difference(list(number()), list(integer()))) + refute subtype?(non_empty_list(integer()), opt_difference(list(number()), list(integer()))) assert subtype?(list(term(), boolean()), list(term(), atom())) assert subtype?(list(integer()), list(term())) assert subtype?(list(term()), list(term(), term())) @@ -1054,19 +1102,19 @@ defmodule Module.Types.DescrTest do describe "compatible" do test "intersection" do - assert compatible?(integer(), intersection(dynamic(), integer())) - refute compatible?(intersection(dynamic(), integer()), atom()) - refute compatible?(atom(), intersection(dynamic(), integer())) - refute compatible?(atom(), intersection(dynamic(), atom([:foo, :bar]))) - assert compatible?(intersection(dynamic(), atom()), atom([:foo, :bar])) - assert compatible?(atom([:foo, :bar]), intersection(dynamic(), atom())) + assert compatible?(integer(), opt_intersection(dynamic(), integer())) + refute compatible?(opt_intersection(dynamic(), integer()), atom()) + refute compatible?(atom(), opt_intersection(dynamic(), integer())) + refute compatible?(atom(), opt_intersection(dynamic(), atom([:foo, :bar]))) + assert compatible?(opt_intersection(dynamic(), atom()), atom([:foo, :bar])) + assert compatible?(atom([:foo, :bar]), opt_intersection(dynamic(), atom())) end test "static" do refute compatible?(atom(), atom([:foo, :bar])) - refute compatible?(union(integer(), atom()), integer()) + refute compatible?(opt_union(integer(), atom()), integer()) refute compatible?(none(), integer()) - refute compatible?(union(atom(), dynamic()), integer()) + refute compatible?(opt_union(atom(), dynamic()), integer()) end test "dynamic" do @@ -1082,7 +1130,7 @@ defmodule Module.Types.DescrTest do test "map" do assert compatible?(closed_map(a: integer()), open_map()) - assert compatible?(intersection(dynamic(), open_map()), closed_map(a: integer())) + assert compatible?(opt_intersection(dynamic(), open_map()), closed_map(a: integer())) end test "list" do @@ -1097,28 +1145,38 @@ defmodule Module.Types.DescrTest do assert tuple([none()]) |> empty?() assert open_tuple([integer(), none()]) |> empty?() - assert intersection(tuple([integer(), atom()]), open_tuple([atom()])) |> empty?() - refute open_tuple([integer(), integer()]) |> difference(empty_tuple()) |> empty?() - refute open_tuple([integer(), integer()]) |> difference(open_tuple([atom()])) |> empty?() - refute open_tuple([term()]) |> difference(tuple([term()])) |> empty?() - assert difference(tuple(), empty_tuple()) |> difference(open_tuple([term()])) |> empty?() - assert difference(tuple(), open_tuple([term()])) |> difference(empty_tuple()) |> empty?() + assert opt_intersection(tuple([integer(), atom()]), open_tuple([atom()])) |> empty?() + refute open_tuple([integer(), integer()]) |> opt_difference(empty_tuple()) |> empty?() + + refute open_tuple([integer(), integer()]) + |> opt_difference(open_tuple([atom()])) + |> empty?() + + refute open_tuple([term()]) |> opt_difference(tuple([term()])) |> empty?() + + assert opt_difference(tuple(), empty_tuple()) + |> opt_difference(open_tuple([term()])) + |> empty?() + + assert opt_difference(tuple(), open_tuple([term()])) + |> opt_difference(empty_tuple()) + |> empty?() refute open_tuple([term()]) - |> difference(tuple([term()])) - |> difference(tuple([term()])) + |> opt_difference(tuple([term()])) + |> opt_difference(tuple([term()])) |> empty?() - assert tuple([integer(), union(integer(), atom())]) - |> difference(tuple([integer(), integer()])) - |> difference(tuple([integer(), atom()])) + assert tuple([integer(), opt_union(integer(), atom())]) + |> opt_difference(tuple([integer(), integer()])) + |> opt_difference(tuple([integer(), atom()])) |> empty?() end test "map" do assert open_map(a: none()) |> empty?() assert closed_map(a: integer(), b: none()) |> empty?() - assert intersection(closed_map(b: atom()), open_map(a: integer())) |> empty?() + assert opt_intersection(closed_map(b: atom()), open_map(a: integer())) |> empty?() end test "fun" do @@ -1126,23 +1184,23 @@ defmodule Module.Types.DescrTest do refute empty?(none_fun(1)) refute empty?(fun([integer()], atom())) - assert empty?(intersection(none_fun(1), none_fun(2))) - refute empty?(intersection(fun(), none_fun(1))) - assert empty?(difference(none_fun(1), union(none_fun(1), none_fun(2)))) + assert empty?(opt_intersection(none_fun(1), none_fun(2))) + refute empty?(opt_intersection(fun(), none_fun(1))) + assert empty?(opt_difference(none_fun(1), opt_union(none_fun(1), none_fun(2)))) end end describe "function creation" do test "fun_from_non_overlapping_clauses" do assert fun_from_non_overlapping_clauses([{[integer()], atom()}, {[float()], binary()}]) == - intersection(fun([integer()], atom()), fun([float()], binary())) + opt_intersection(fun([integer()], atom()), fun([float()], binary())) end test "fun_from_inferred_clauses" do # No overlap assert fun_from_inferred_clauses([{[integer()], atom()}, {[float()], binary()}]) |> equal?( - intersection( + opt_intersection( fun_from_non_overlapping_clauses([{[integer()], atom()}, {[float()], binary()}]), fun([number()], dynamic()) ) @@ -1152,40 +1210,40 @@ defmodule Module.Types.DescrTest do assert fun_from_inferred_clauses([{[integer()], atom()}, {[number()], binary()}]) |> equal?( fun_from_non_overlapping_clauses([ - {[integer()], dynamic(union(atom(), binary()))}, - {[number()], dynamic(union(atom(), binary()))} + {[integer()], dynamic(opt_union(atom(), binary()))}, + {[number()], dynamic(opt_union(atom(), binary()))} ]) ) assert fun_from_inferred_clauses([{[number()], binary()}, {[integer()], atom()}]) |> equal?( fun_from_non_overlapping_clauses([ - {[integer()], dynamic(union(atom(), binary()))}, - {[number()], dynamic(union(atom(), binary()))} + {[integer()], dynamic(opt_union(atom(), binary()))}, + {[number()], dynamic(opt_union(atom(), binary()))} ]) ) # Partial assert fun_from_inferred_clauses([ - {[union(integer(), pid())], atom()}, - {[union(float(), pid())], binary()} + {[opt_union(integer(), pid())], atom()}, + {[opt_union(float(), pid())], binary()} ]) |> equal?( fun_from_non_overlapping_clauses([ - {[union(integer(), pid())], dynamic(union(atom(), binary()))}, - {[union(float(), pid())], dynamic(union(atom(), binary()))} + {[opt_union(integer(), pid())], dynamic(opt_union(atom(), binary()))}, + {[opt_union(float(), pid())], dynamic(opt_union(atom(), binary()))} ]) ) # Difference assert fun_from_inferred_clauses([ - {[integer(), union(pid(), atom())], atom()}, + {[integer(), opt_union(pid(), atom())], atom()}, {[number(), pid()], binary()} ]) |> equal?( fun_from_non_overlapping_clauses([ - {[integer(), union(pid(), atom())], dynamic(union(atom(), binary()))}, - {[number(), pid()], dynamic(union(atom(), binary()))} + {[integer(), opt_union(pid(), atom())], dynamic(opt_union(atom(), binary()))}, + {[number(), pid()], dynamic(opt_union(atom(), binary()))} ]) ) end @@ -1197,15 +1255,17 @@ defmodule Module.Types.DescrTest do test "non funs" do assert fun_apply(term(), [integer()]) == :badfun assert fun_apply(integer(), [integer()]) == :badfun - assert fun_apply(union(integer(), none_fun(1)), [integer()]) == :badfun - assert fun_apply(union(integer(), fun([integer()], atom())), [integer()]) == :badfun - assert fun_apply(union(integer(), dynamic()), [integer()]) == :badfun + assert fun_apply(opt_union(integer(), none_fun(1)), [integer()]) == :badfun + assert fun_apply(opt_union(integer(), fun([integer()], atom())), [integer()]) == :badfun + assert fun_apply(opt_union(integer(), dynamic()), [integer()]) == :badfun end test "static" do # Full static assert fun_apply(fun(), [integer()]) == {:badarg, [none()], false} - assert fun_apply(difference(fun(), none_fun(2)), [integer()]) == {:badarg, [none()], false} + + assert fun_apply(opt_difference(fun(), none_fun(2)), [integer()]) == + {:badarg, [none()], false} # Basic function application scenarios assert fun_apply(fun([integer()], atom()), [integer()]) == {:ok, atom()} @@ -1213,11 +1273,11 @@ defmodule Module.Types.DescrTest do assert fun_apply(fun([integer()], atom()), [term()]) == {:badarg, [integer()], false} # Union argument type: domain is int | float - assert fun_apply(fun([union(integer(), float())], atom()), [integer()]) == {:ok, atom()} - assert fun_apply(fun([union(integer(), float())], atom()), [float()]) == {:ok, atom()} + assert fun_apply(fun([opt_union(integer(), float())], atom()), [integer()]) == {:ok, atom()} + assert fun_apply(fun([opt_union(integer(), float())], atom()), [float()]) == {:ok, atom()} - assert fun_apply(fun([union(integer(), float())], atom()), [atom()]) == - {:badarg, [union(integer(), float())], false} + assert fun_apply(fun([opt_union(integer(), float())], atom()), [atom()]) == + {:badarg, [opt_union(integer(), float())], false} # 2-arity function assert fun_apply(fun([integer(), atom()], binary()), [integer(), atom()]) == {:ok, binary()} @@ -1246,31 +1306,35 @@ defmodule Module.Types.DescrTest do assert fun_apply(fun([integer(), atom()], boolean()), [integer()]) == {:badarity, [2]} # Union of two different arities: always badarity regardless of which arity is called - fun_mixed = union(fun([integer()], integer()), fun([integer(), atom()], boolean())) + fun_mixed = opt_union(fun([integer()], integer()), fun([integer(), atom()], boolean())) assert fun_apply(fun_mixed, [integer()]) == {:badarity, [1, 2]} assert fun_apply(fun_mixed, [integer(), atom()]) == {:badarity, [2, 1]} # Function intersection tests (no overlap) - fun0 = intersection(fun([integer()], atom()), fun([float()], binary())) + fun0 = opt_intersection(fun([integer()], atom()), fun([float()], binary())) assert fun_apply(fun0, [integer()]) == {:ok, atom()} assert fun_apply(fun0, [float()]) == {:ok, binary()} - assert fun_apply(fun0, [number()]) == {:ok, union(atom(), binary())} + assert fun_apply(fun0, [number()]) == {:ok, opt_union(atom(), binary())} assert fun_apply(fun0, [dynamic(integer())]) |> elem(1) |> equal?(atom()) assert fun_apply(fun0, [dynamic(float())]) |> elem(1) |> equal?(binary()) - assert fun_apply(fun0, [dynamic(number())]) |> elem(1) |> equal?(union(atom(), binary())) + + assert fun_apply(fun0, [dynamic(number())]) + |> elem(1) + |> equal?(opt_union(atom(), binary())) + assert fun_apply(fun0, [dynamic()]) == {:ok, dynamic()} # Function intersection tests (overlap) - fun1 = intersection(fun([integer()], atom()), fun([number()], term())) + fun1 = opt_intersection(fun([integer()], atom()), fun([number()], term())) assert fun_apply(fun1, [integer()]) == {:ok, atom()} assert fun_apply(fun1, [float()]) == {:ok, term()} # Function intersection with unions fun2 = - intersection( - fun([union(integer(), atom())], term()), - fun([union(integer(), pid())], atom()) + opt_intersection( + fun([opt_union(integer(), atom())], term()), + fun([opt_union(integer(), pid())], atom()) ) assert fun_apply(fun2, [integer()]) == {:ok, atom()} @@ -1279,11 +1343,13 @@ defmodule Module.Types.DescrTest do # Function intersection with same domain, different codomains assert fun([integer()], term()) - |> intersection(fun([integer()], atom())) + |> opt_intersection(fun([integer()], atom())) |> fun_apply([integer()]) == {:ok, atom()} # Function intersection with singleton atoms - fun3 = intersection(fun([atom([:ok])], atom([:success])), fun([atom([:ok])], atom([:done]))) + fun3 = + opt_intersection(fun([atom([:ok])], atom([:success])), fun([atom([:ok])], atom([:done]))) + assert fun_apply(fun3, [atom([:ok])]) == {:ok, none()} end @@ -1292,13 +1358,13 @@ defmodule Module.Types.DescrTest do assert fun_apply(fun([integer()], dynamic()), [integer()]) == {:ok, dynamic()} assert fun_apply(fun([dynamic()], integer()), [dynamic()]) == - {:ok, union(integer(), dynamic())} + {:ok, opt_union(integer(), dynamic())} assert fun_apply(fun([dynamic(), atom()], float()), [dynamic(), atom()]) == - {:ok, union(float(), dynamic())} + {:ok, opt_union(float(), dynamic())} fun = fun([dynamic(integer())], atom()) - assert fun_apply(fun, [dynamic(integer())]) == {:ok, union(atom(), dynamic())} + assert fun_apply(fun, [dynamic(integer())]) == {:ok, opt_union(atom(), dynamic())} assert fun_apply(fun, [dynamic(number())]) == {:ok, dynamic()} assert fun_apply(fun, [integer()]) == {:ok, dynamic()} assert fun_apply(fun, [float()]) == {:badarg, [dynamic(integer())], false} @@ -1310,7 +1376,7 @@ defmodule Module.Types.DescrTest do # Full dynamic assert fun_apply(dynamic(), [integer()]) == {:ok, dynamic()} assert fun_apply(dynamic(none_fun(1)), [integer()]) == {:ok, dynamic()} - assert fun_apply(difference(dynamic(), none_fun(2)), [integer()]) == {:ok, dynamic()} + assert fun_apply(opt_difference(dynamic(), none_fun(2)), [integer()]) == {:ok, dynamic()} # Basic function application scenarios assert fun_apply(dynamic_fun([integer()], atom()), [integer()]) == {:ok, dynamic(atom())} @@ -1340,7 +1406,10 @@ defmodule Module.Types.DescrTest do # Union of two dynamic functions with different arities: the call may succeed, # so we pick the matching-arity arrows and wrap in dynamic(). fun_dyn_mixed = - union(dynamic_fun([integer()], integer()), dynamic_fun([integer(), atom()], boolean())) + opt_union( + dynamic_fun([integer()], integer()), + dynamic_fun([integer(), atom()], boolean()) + ) # picks arity-1 arrows → dynamic(integer()) assert fun_apply(fun_dyn_mixed, [integer()]) == {:ok, dynamic(integer())} @@ -1352,15 +1421,15 @@ defmodule Module.Types.DescrTest do assert fun_apply(fun_dyn_mixed, [atom()]) == {:ok, dynamic()} # Function intersection tests - fun0 = intersection(dynamic_fun([integer()], atom()), dynamic_fun([float()], binary())) + fun0 = opt_intersection(dynamic_fun([integer()], atom()), dynamic_fun([float()], binary())) assert fun_apply(fun0, [integer()]) == {:ok, dynamic(atom())} assert fun_apply(fun0, [float()]) == {:ok, dynamic(binary())} assert fun_apply(fun0, [dynamic(integer())]) == {:ok, dynamic(atom())} assert fun_apply(fun0, [dynamic(float())]) == {:ok, dynamic(binary())} - assert fun_apply(fun0, [dynamic(number())]) == {:ok, dynamic(union(binary(), atom()))} + assert fun_apply(fun0, [dynamic(number())]) == {:ok, dynamic(opt_union(binary(), atom()))} # Function intersection with subset domain - fun1 = intersection(dynamic_fun([integer()], atom()), dynamic_fun([number()], term())) + fun1 = opt_intersection(dynamic_fun([integer()], atom()), dynamic_fun([number()], term())) assert fun_apply(fun1, [integer()]) == {:ok, dynamic(atom())} assert fun_apply(fun1, [float()]) == {:ok, dynamic()} assert fun_apply(fun1, [dynamic(integer())]) == {:ok, dynamic(atom())} @@ -1368,14 +1437,14 @@ defmodule Module.Types.DescrTest do # Function intersection with same domain, different codomains assert dynamic_fun([integer()], term()) - |> intersection(dynamic_fun([integer()], atom())) + |> opt_intersection(dynamic_fun([integer()], atom())) |> fun_apply([integer()]) == {:ok, dynamic(atom())} # Function intersection with overlapping domains fun2 = - intersection( - dynamic_fun([union(integer(), atom())], term()), - dynamic_fun([union(integer(), pid())], atom()) + opt_intersection( + dynamic_fun([opt_union(integer(), atom())], term()), + dynamic_fun([opt_union(integer(), pid())], atom()) ) assert fun_apply(fun2, [integer()]) == {:ok, dynamic(atom())} @@ -1388,7 +1457,7 @@ defmodule Module.Types.DescrTest do # Function intersection with singleton atoms fun3 = - intersection( + opt_intersection( dynamic_fun([atom([:ok])], atom([:success])), dynamic_fun([atom([:ok])], atom([:done])) ) @@ -1398,7 +1467,7 @@ defmodule Module.Types.DescrTest do # Testing the special case of uplifiting both the function and argument # when the function is purely dynamic fun4 = - intersection( + opt_intersection( dynamic_fun([integer()], integer()), dynamic_fun([boolean()], boolean()) ) @@ -1407,7 +1476,7 @@ defmodule Module.Types.DescrTest do assert fun_apply(fun4, [dynamic(integer())]) == {:ok, dynamic(integer())} # float escapes the domain so the result is dynamic() - arg = dynamic(union(integer(), float())) + arg = dynamic(opt_union(integer(), float())) assert fun_apply(fun4, [arg]) == {:ok, dynamic()} assert fun_apply(dynamic(), [integer()]) == {:ok, dynamic()} @@ -1416,7 +1485,7 @@ defmodule Module.Types.DescrTest do test "static and dynamic" do # Bad arity fun_arities = - union( + opt_union( fun([atom()], integer()), dynamic_fun([integer(), float()], binary()) ) @@ -1430,7 +1499,7 @@ defmodule Module.Types.DescrTest do # Bad argument fun_args = - union( + opt_union( fun([atom()], integer()), dynamic_fun([integer()], binary()) ) @@ -1440,25 +1509,28 @@ defmodule Module.Types.DescrTest do # ((bool->bool) or dyn(int->int)) # booleans work, but not integers - fun_mixed_gdom = union(fun([boolean()], boolean()), dynamic_fun([integer()], integer())) + fun_mixed_gdom = opt_union(fun([boolean()], boolean()), dynamic_fun([integer()], integer())) assert fun_apply(fun_mixed_gdom, [boolean()]) == {:ok, dynamic()} - assert fun_apply(fun_mixed_gdom, [dynamic(boolean())]) == {:ok, union(dynamic(), boolean())} + + assert fun_apply(fun_mixed_gdom, [dynamic(boolean())]) == + {:ok, opt_union(dynamic(), boolean())} + assert fun_apply(fun_mixed_gdom, [integer()]) == {:badarg, [dynamic(boolean())], false} assert fun_apply(fun_mixed_gdom, [dynamic(integer())]) == {:badarg, [dynamic(boolean())], false} # Badfun - assert union( + assert opt_union( fun([atom()], integer()), - dynamic_fun([integer()], binary()) |> intersection(none_fun(2)) + dynamic_fun([integer()], binary()) |> opt_intersection(none_fun(2)) ) |> fun_apply([atom()]) |> elem(1) |> equal?(integer()) - assert union( - fun([atom()], integer()) |> intersection(none_fun(2)), + assert opt_union( + fun([atom()], integer()) |> opt_intersection(none_fun(2)), dynamic_fun([integer()], binary()) ) |> fun_apply([integer()]) == {:ok, dynamic(binary())} @@ -1473,12 +1545,12 @@ defmodule Module.Types.DescrTest do # The dynamic application (int -> bool) o float is not well-defined (float not <: int), # but since it is dynamic it returns term wrapped in dynamic, which is dynamic. # Result: bool or dynamic. - fun_type = fun([union(dynamic(), integer())], boolean()) + fun_type = fun([opt_union(dynamic(), integer())], boolean()) arg = dynamic(float()) # Application yields bool or dynamic assert {:ok, result} = fun_apply(fun_type, [arg]) - assert equal?(union(boolean(), dynamic()), result) + assert equal?(opt_union(boolean(), dynamic()), result) end end @@ -1496,32 +1568,35 @@ defmodule Module.Types.DescrTest do end @disguised_empty_map closed_map(key: atom([:value])) - |> difference(open_map(key: atom(), optional: if_set(atom()))) + |> opt_difference(open_map(key: atom(), optional: if_set(atom()))) test "atoms" do assert singleton?(atom([:foo])) refute singleton?(atom([:foo, :bar])) - assert singleton?(atom([:foo]) |> union(@disguised_empty_map)) - refute singleton?(atom() |> difference(atom([:foo]))) + assert singleton?(atom([:foo]) |> opt_union(@disguised_empty_map)) + refute singleton?(atom() |> opt_difference(atom([:foo]))) end test "empty list" do assert singleton?(empty_list()) refute singleton?(non_empty_list(term())) - refute singleton?(union(empty_list(), atom([:foo]))) - assert singleton?(union(empty_list(), @disguised_empty_map)) + refute singleton?(opt_union(empty_list(), atom([:foo]))) + assert singleton?(opt_union(empty_list(), @disguised_empty_map)) end test "maps" do assert singleton?(empty_map()) assert singleton?(closed_map(key: atom([:value]))) - assert singleton?(closed_map(key: atom([:value])) |> union(@disguised_empty_map)) + assert singleton?(closed_map(key: atom([:value])) |> opt_union(@disguised_empty_map)) refute singleton?(closed_map(key: binary())) refute singleton?(closed_map(key: if_set(atom([:value])))) refute singleton?(closed_map(__struct__: :term)) refute singleton?(open_map()) refute singleton?(open_map(key: atom([:value]))) - refute singleton?(union(closed_map(key: atom([:value])), closed_map(other: atom([:value])))) + + refute singleton?( + opt_union(closed_map(key: atom([:value])), closed_map(other: atom([:value]))) + ) end test "tuples" do @@ -1529,15 +1604,15 @@ defmodule Module.Types.DescrTest do assert singleton?(tuple([atom([:foo])])) refute singleton?(tuple([binary()])) refute singleton?(open_tuple([])) - refute singleton?(union(tuple([atom([:value])]), tuple([atom([:other_value])]))) - refute singleton?(union(tuple([atom([:value])]), closed_map(other: atom([:value])))) + refute singleton?(opt_union(tuple([atom([:value])]), tuple([atom([:other_value])]))) + refute singleton?(opt_union(tuple([atom([:value])]), closed_map(other: atom([:value])))) # Both BDD lines produce the same singleton tuple, so the tuple DNF must not duplicate it. - a = tuple([union(integer(), atom([:ok])), atom([:x])]) + a = tuple([opt_union(integer(), atom([:ok])), atom([:x])]) b = tuple([integer(), atom([:x, :y])]) - c = tuple([integer(), union(atom([:x]), binary())]) + c = tuple([integer(), opt_union(atom([:x]), binary())]) - t = union(difference(a, b), difference(a, c)) + t = opt_union(opt_difference(a, b), opt_difference(a, c)) # Semantically t ~= {:ok, :x}, confirmed by equal? assert equal?(t, tuple([atom([:ok]), atom([:x])])) assert singleton?(t) @@ -1546,7 +1621,7 @@ defmodule Module.Types.DescrTest do describe "projections" do test "booleaness" do - for type <- [none(), open_map(), negation(boolean()), difference(atom(), boolean())] do + for type <- [none(), open_map(), opt_negation(boolean()), opt_difference(atom(), boolean())] do assert booleaness(type) == :none assert booleaness(dynamic(type)) == :none end @@ -1559,16 +1634,16 @@ defmodule Module.Types.DescrTest do assert booleaness(atom([false])) == {false, :always} assert booleaness(dynamic(atom([false]))) == {false, :always} assert booleaness(dynamic(atom([false, :other]))) == {false, :maybe} - assert booleaness(negation(atom([false]))) == {true, :maybe} + assert booleaness(opt_negation(atom([false]))) == {true, :maybe} assert booleaness(atom([true])) == {true, :always} assert booleaness(dynamic(atom([true]))) == {true, :always} assert booleaness(dynamic(atom([true, :other]))) == {true, :maybe} - assert booleaness(negation(atom([true]))) == {false, :maybe} + assert booleaness(opt_negation(atom([true]))) == {false, :maybe} end test "truthiness" do - for type <- [term(), none(), atom(), boolean(), union(atom([false]), integer())] do + for type <- [term(), none(), atom(), boolean(), opt_union(atom([false]), integer())] do assert truthiness(type) == :undefined assert truthiness(dynamic(type)) == :undefined end @@ -1579,34 +1654,40 @@ defmodule Module.Types.DescrTest do end assert equal?( - intersection(union(atom(), dynamic()), union(atom(), dynamic())), - union(atom(), dynamic()) + opt_intersection(opt_union(atom(), dynamic()), opt_union(atom(), dynamic())), + opt_union(atom(), dynamic()) ) for type <- - [negation(atom()), atom([true]), negation(atom([false, nil])), atom([:ok]), integer()] do + [ + opt_negation(atom()), + atom([true]), + opt_negation(atom([false, nil])), + atom([:ok]), + integer() + ] do assert truthiness(type) == :always_true assert truthiness(dynamic(type)) == :always_true end - assert truthiness(union(atom([true]), integer())) == :always_true + assert truthiness(opt_union(atom([true]), integer())) == :always_true empty_descr = - difference(tuple([number(), integer()]), open_tuple([float(), term()])) - |> difference(tuple([integer(), integer()])) + opt_difference(tuple([number(), integer()]), open_tuple([float(), term()])) + |> opt_difference(tuple([integer(), integer()])) assert empty?(empty_descr) assert truthiness(empty_descr) == :undefined assert truthiness(dynamic(empty_descr)) == :undefined - assert truthiness(union(atom([nil]), empty_descr)) == :always_false + assert truthiness(opt_union(atom([nil]), empty_descr)) == :always_false - assert truthiness(union(atom([false]), empty_descr)) == :always_false + assert truthiness(opt_union(atom([false]), empty_descr)) == :always_false end test "atom_fetch" do assert atom_fetch(term()) == :error - assert atom_fetch(union(term(), dynamic(atom([:foo, :bar])))) == :error + assert atom_fetch(opt_union(term(), dynamic(atom([:foo, :bar])))) == :error assert atom_fetch(atom()) == {:infinite, []} assert atom_fetch(dynamic()) == {:infinite, []} @@ -1614,8 +1695,8 @@ defmodule Module.Types.DescrTest do assert atom_fetch(atom([:foo, :bar])) == {:finite, [:foo, :bar] |> :sets.from_list(version: 2) |> :sets.to_list()} - assert atom_fetch(union(atom([:foo, :bar]), dynamic(atom()))) == {:infinite, []} - assert atom_fetch(union(atom([:foo, :bar]), dynamic(term()))) == {:infinite, []} + assert atom_fetch(opt_union(atom([:foo, :bar]), dynamic(atom()))) == {:infinite, []} + assert atom_fetch(opt_union(atom([:foo, :bar]), dynamic(term()))) == {:infinite, []} end test "list_hd" do @@ -1625,47 +1706,47 @@ defmodule Module.Types.DescrTest do assert list_hd(empty_list()) == :badnonemptylist assert list_hd(non_empty_list(term())) == {:ok, term()} assert list_hd(non_empty_list(integer())) == {:ok, integer()} - assert list_hd(difference(list(number()), list(integer()))) == {:ok, number()} + assert list_hd(opt_difference(list(number()), list(integer()))) == {:ok, number()} assert list_hd(non_empty_list(atom(), float())) == {:ok, atom()} assert list_hd(dynamic()) == {:ok, dynamic()} assert list_hd(dynamic(list(integer()))) == {:ok, dynamic(integer())} - assert list_hd(union(dynamic(), atom())) == :badnonemptylist - assert list_hd(union(dynamic(), list(term()))) == :badnonemptylist + assert list_hd(opt_union(dynamic(), atom())) == :badnonemptylist + assert list_hd(opt_union(dynamic(), list(term()))) == :badnonemptylist - assert list_hd(difference(list(number()), list(number()))) == :badnonemptylist - assert list_hd(dynamic(difference(list(number()), list(number())))) == :badnonemptylist + assert list_hd(opt_difference(list(number()), list(number()))) == :badnonemptylist + assert list_hd(dynamic(opt_difference(list(number()), list(number())))) == :badnonemptylist - assert list_hd(union(dynamic(list(float())), non_empty_list(atom()))) == - {:ok, union(dynamic(float()), atom())} + assert list_hd(opt_union(dynamic(list(float())), non_empty_list(atom()))) == + {:ok, opt_union(dynamic(float()), atom())} # If term() is in the tail, it means list(term()) is in the tail # and therefore any term can be returned from hd. assert list_hd(non_empty_list(atom(), term())) == {:ok, term()} - assert list_hd(non_empty_list(atom(), negation(list(term(), term())))) == {:ok, atom()} + assert list_hd(non_empty_list(atom(), opt_negation(list(term(), term())))) == {:ok, atom()} end test "list_of" do assert list_of(term()) == :badproperlist assert list_of(none()) == :badproperlist assert list_of(empty_list()) == {true, none()} - assert list_of(union(empty_list(), integer())) == :badproperlist + assert list_of(opt_union(empty_list(), integer())) == :badproperlist assert list_of(non_empty_list(integer())) == {false, integer()} assert list_of(non_empty_list(integer(), atom())) == :badproperlist assert list_of(non_empty_list(integer(), term())) == :badproperlist assert list_of(non_empty_list(integer(), list(term()))) == {false, term()} - assert list_of(list(integer()) |> union(list(integer(), integer()))) == :badproperlist - assert list_of(list(integer()) |> union(integer())) == :badproperlist + assert list_of(list(integer()) |> opt_union(list(integer(), integer()))) == :badproperlist + assert list_of(list(integer()) |> opt_union(integer())) == :badproperlist assert list_of(dynamic(list(integer()))) == {true, dynamic(integer())} assert list_of(dynamic(list(integer(), atom()))) == {true, nil} assert list_of(dynamic(non_empty_list(integer(), atom()))) == :badproperlist - assert list_of(dynamic(union(empty_list(), integer()))) == {true, nil} + assert list_of(dynamic(opt_union(empty_list(), integer()))) == {true, nil} # A list that the difference resolves to nothing list_with_tail = - non_empty_list(atom(), union(integer(), empty_list())) - |> difference(non_empty_list(atom([:ok]), integer())) - |> difference(non_empty_list(atom(), term())) + non_empty_list(atom(), opt_union(integer(), empty_list())) + |> opt_difference(non_empty_list(atom([:ok]), integer())) + |> opt_difference(non_empty_list(atom(), term())) assert list_of(list_with_tail) == :badproperlist end @@ -1675,27 +1756,29 @@ defmodule Module.Types.DescrTest do assert list_tl(term()) == :badnonemptylist assert list_tl(empty_list()) == :badnonemptylist assert list_tl(list(integer())) == :badnonemptylist - assert list_tl(difference(list(number()), list(number()))) == :badnonemptylist + assert list_tl(opt_difference(list(number()), list(number()))) == :badnonemptylist assert list_tl(non_empty_list(integer())) == {:ok, list(integer())} assert list_tl(non_empty_list(integer(), atom())) == - {:ok, union(atom(), non_empty_list(integer(), atom()))} + {:ok, opt_union(atom(), non_empty_list(integer(), atom()))} # The tail of either a (non empty) list of integers with an atom tail or a (non empty) list # of tuples with a float tail is either an atom, or a float, or a (possibly empty) list of # integers with an atom tail, or a (possibly empty) list of tuples with a float tail. - assert list_tl(union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float()))) == + assert list_tl( + opt_union(non_empty_list(integer(), atom()), non_empty_list(tuple(), float())) + ) == {:ok, - union(atom(), float()) - |> union(non_empty_list(integer(), atom())) - |> union(non_empty_list(tuple(), float()))} + opt_union(atom(), float()) + |> opt_union(non_empty_list(integer(), atom())) + |> opt_union(non_empty_list(tuple(), float()))} assert list_tl(dynamic()) == {:ok, dynamic()} assert list_tl(dynamic(list(integer()))) == {:ok, dynamic(list(integer()))} assert list_tl(dynamic(list(integer(), atom()))) == - {:ok, dynamic(union(atom(), list(integer(), atom())))} + {:ok, dynamic(opt_union(atom(), list(integer(), atom())))} end test "tuple_fetch" do @@ -1714,35 +1797,35 @@ defmodule Module.Types.DescrTest do assert tuple_fetch(tuple([integer(), atom()]), -1) == :badindex assert tuple_fetch(empty_tuple(), 0) == :badindex - assert difference(tuple(), tuple()) |> tuple_fetch(0) == :badtuple + assert opt_difference(tuple(), tuple()) |> tuple_fetch(0) == :badtuple - assert tuple([atom()]) |> difference(empty_tuple()) |> tuple_fetch(0) == + assert tuple([atom()]) |> opt_difference(empty_tuple()) |> tuple_fetch(0) == {false, atom()} - assert difference(tuple([union(integer(), atom())]), open_tuple([atom()])) + assert opt_difference(tuple([opt_union(integer(), atom())]), open_tuple([atom()])) |> tuple_fetch(0) == {false, integer()} - assert tuple_fetch(union(tuple([integer(), atom()]), dynamic(open_tuple([atom()]))), 1) - |> Kernel.then(fn {opt, ty} -> opt and equal?(ty, union(atom(), dynamic())) end) + assert tuple_fetch(opt_union(tuple([integer(), atom()]), dynamic(open_tuple([atom()]))), 1) + |> Kernel.then(fn {opt, ty} -> opt and equal?(ty, opt_union(atom(), dynamic())) end) - assert tuple_fetch(union(tuple([integer()]), tuple([atom()])), 0) == - {false, union(integer(), atom())} + assert tuple_fetch(opt_union(tuple([integer()]), tuple([atom()])), 0) == + {false, opt_union(integer(), atom())} - assert tuple([integer(), atom(), union(atom(), integer())]) - |> difference(tuple([integer(), term(), atom()])) + assert tuple([integer(), atom(), opt_union(atom(), integer())]) + |> opt_difference(tuple([integer(), term(), atom()])) |> tuple_fetch(2) == {false, integer()} - assert tuple([integer(), atom(), union(union(atom(), integer()), list(term()))]) - |> difference(tuple([integer(), term(), atom()])) - |> difference(open_tuple([term(), atom(), list(term())])) + assert tuple([integer(), atom(), opt_union(opt_union(atom(), integer()), list(term()))]) + |> opt_difference(tuple([integer(), term(), atom()])) + |> opt_difference(open_tuple([term(), atom(), list(term())])) |> tuple_fetch(2) == {false, integer()} assert tuple([integer(), atom(), integer()]) - |> difference(tuple([integer(), term(), integer()])) + |> opt_difference(tuple([integer(), term(), integer()])) |> tuple_fetch(1) == :badtuple assert tuple([integer(), atom(), integer()]) - |> difference(tuple([integer(), term(), atom()])) + |> opt_difference(tuple([integer(), term(), atom()])) |> tuple_fetch(2) == {false, integer()} assert tuple_fetch(tuple(), 0) == :badindex @@ -1754,14 +1837,14 @@ defmodule Module.Types.DescrTest do assert tuple_fetch(dynamic(), 0) == {true, dynamic()} assert tuple_fetch(dynamic(empty_tuple()), 0) == :badindex assert tuple_fetch(dynamic(tuple([integer(), atom()])), 2) == :badindex - assert tuple_fetch(union(dynamic(), integer()), 0) == :badtuple + assert tuple_fetch(opt_union(dynamic(), integer()), 0) == :badtuple assert tuple_fetch(tuple([none()]), 0) == :badtuple assert tuple_fetch(dynamic(tuple()), 0) |> Kernel.then(fn {opt, type} -> opt and equal?(type, dynamic()) end) - assert tuple_fetch(union(dynamic(), open_tuple([atom()])), 0) == - {true, union(atom(), dynamic())} + assert tuple_fetch(opt_union(dynamic(), open_tuple([atom()])), 0) == + {true, opt_union(atom(), dynamic())} end test "tuple_delete_at" do @@ -1791,19 +1874,19 @@ defmodule Module.Types.DescrTest do assert tuple_delete_at(dynamic(tuple([integer(), atom()])), 2) == :badindex # Test deleting from a union of tuples - assert tuple_delete_at(union(tuple([integer(), atom()]), tuple([float(), binary()])), 1) - |> equal?(tuple([union(integer(), float())])) + assert tuple_delete_at(opt_union(tuple([integer(), atom()]), tuple([float(), binary()])), 1) + |> equal?(tuple([opt_union(integer(), float())])) # Test deleting from an intersection of tuples - assert intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) + assert opt_intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) |> tuple_delete_at(1) == tuple([integer()]) # Test deleting from a difference of tuples - assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) + assert opt_difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) |> tuple_delete_at(1) |> equal?(tuple([integer(), boolean()])) - assert difference( + assert opt_difference( open_tuple([open_tuple([]), term()]), open_tuple([open_tuple([atom([:value])]), integer()]) ) @@ -1811,16 +1894,16 @@ defmodule Module.Types.DescrTest do |> equal?(open_tuple([open_tuple([])])) # Test deleting from a complex union involving dynamic - assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) + assert opt_union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) |> tuple_delete_at(1) - |> equal?(union(tuple([integer()]), dynamic(tuple([float()])))) + |> equal?(opt_union(tuple([integer()]), dynamic(tuple([float()])))) # Successfully deleting at position `index` in a tuple means that the dynamic # values that succeed are intersected with tuples of size at least `index` assert dynamic(tuple()) |> tuple_delete_at(0) == dynamic(tuple()) assert dynamic(term()) |> tuple_delete_at(0) == dynamic(tuple()) - assert dynamic(union(tuple(), integer())) + assert dynamic(opt_union(tuple(), integer())) |> tuple_delete_at(1) |> equal?(dynamic(tuple_of_size_at_least(1))) end @@ -1833,7 +1916,7 @@ defmodule Module.Types.DescrTest do assert tuple_insert_at(tuple([none()]), 0, boolean()) == :badtuple # Out-of-bounds in a union - assert union(tuple([integer(), atom()]), tuple([float()])) + assert opt_union(tuple([integer(), atom()]), tuple([float()])) |> tuple_insert_at(2, boolean()) == :badindex # Test inserting into a closed tuple @@ -1862,26 +1945,26 @@ defmodule Module.Types.DescrTest do assert tuple_insert_at( tuple([boolean()]), 1, - union(dynamic(integer()), atom([:inserted])) + opt_union(dynamic(integer()), atom([:inserted])) ) |> equal?( - union( + opt_union( tuple([boolean(), atom([:inserted])]), dynamic(tuple([boolean(), integer()])) ) ) # Test inserting into a dynamic tuple - assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 1, boolean()) == - dynamic(tuple([integer(), boolean(), atom()])) + assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 1, boolean()) + |> equal?(dynamic(tuple([integer(), boolean(), atom()]))) assert tuple_insert_at(dynamic(tuple([integer(), atom()])), 3, boolean()) == :badindex # Test inserting into a union of tuples - assert tuple_insert_at(union(tuple([integer()]), tuple([atom()])), 0, boolean()) == - union(tuple([boolean(), integer()]), tuple([boolean(), atom()])) + assert tuple_insert_at(opt_union(tuple([integer()]), tuple([atom()])), 0, boolean()) == + opt_union(tuple([boolean(), integer()]), tuple([boolean(), atom()])) - assert difference(tuple(), empty_tuple()) + assert opt_difference(tuple(), empty_tuple()) |> tuple_insert_at(1, boolean()) |> equal?(open_tuple([term(), boolean()])) @@ -1889,15 +1972,15 @@ defmodule Module.Types.DescrTest do assert tuple_fetch(inserted, 1) == {false, atom([:inserted])} # Test inserting into a difference of tuples - assert difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) + assert opt_difference(tuple([integer(), atom(), boolean()]), tuple([term(), term()])) |> tuple_insert_at(1, float()) |> equal?(tuple([integer(), float(), atom(), boolean()])) # Test inserting into a complex union involving dynamic - assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) + assert opt_union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) |> tuple_insert_at(1, boolean()) |> equal?( - union( + opt_union( tuple([integer(), boolean(), atom()]), dynamic(tuple([float(), boolean(), binary()])) ) @@ -1905,7 +1988,7 @@ defmodule Module.Types.DescrTest do # If you successfully intersect at position index in a type, then the dynamic values # that succeed are intersected with tuples of size at least index - assert dynamic(union(tuple(), integer())) + assert dynamic(opt_union(tuple(), integer())) |> tuple_insert_at(1, boolean()) |> equal?(dynamic(open_tuple([term(), boolean()]))) @@ -1923,7 +2006,7 @@ defmodule Module.Types.DescrTest do assert tuple_replace_at(tuple([none()]), 0, boolean()) == :badtuple # Out-of-bounds in a union - assert union(tuple([integer(), atom()]), tuple([float()])) + assert opt_union(tuple([integer(), atom()]), tuple([float()])) |> tuple_replace_at(1, boolean()) == :badindex # Test replacing an element in a closed tuple @@ -1947,27 +2030,27 @@ defmodule Module.Types.DescrTest do dynamic(tuple([integer(), term()])) # Test replacing in a dynamic tuple - assert tuple_replace_at(dynamic(tuple([integer(), atom()])), 1, boolean()) == - dynamic(tuple([integer(), boolean()])) + assert tuple_replace_at(dynamic(tuple([integer(), atom()])), 1, boolean()) + |> equal?(dynamic(tuple([integer(), boolean()]))) # Test replacing in a union of tuples - assert tuple_replace_at(union(tuple([integer()]), tuple([atom()])), 0, boolean()) == + assert tuple_replace_at(opt_union(tuple([integer()]), tuple([atom()])), 0, boolean()) == tuple([boolean()]) # Test replacing in an intersection of tuples - assert intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) + assert opt_intersection(tuple([integer(), atom()]), tuple([term(), boolean()])) |> tuple_replace_at(1, float()) == tuple([integer(), float()]) # Replacing in a difference where the negation actually constrains the # positive (not just by arity). The replaced position drops its negative # constraint, the other positions keep theirs. - assert difference(tuple([atom(), atom()]), tuple([atom([:a]), term()])) + assert opt_difference(tuple([atom(), atom()]), tuple([atom([:a]), term()])) |> tuple_replace_at(0, boolean()) |> equal?(tuple([boolean(), atom()])) - assert difference(tuple([atom(), atom()]), tuple([atom([:a]), term()])) + assert opt_difference(tuple([atom(), atom()]), tuple([atom([:a]), term()])) |> tuple_replace_at(1, boolean()) - |> equal?(difference(tuple([atom(), boolean()]), tuple([atom([:a]), boolean()]))) + |> equal?(opt_difference(tuple([atom(), boolean()]), tuple([atom([:a]), boolean()]))) # Errors must propagate even when the replacement value is dynamic assert tuple_replace_at(integer(), 0, dynamic()) == :badtuple @@ -1982,10 +2065,10 @@ defmodule Module.Types.DescrTest do assert tuple_replace_at(dynamic(tuple([atom([:ok])])), 1, binary()) == :badindex # Test replacing in a complex union involving dynamic - assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) + assert opt_union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()]))) |> tuple_replace_at(1, boolean()) |> equal?( - union( + opt_union( tuple([integer(), boolean()]), dynamic(tuple([float(), boolean()])) ) @@ -2001,7 +2084,7 @@ defmodule Module.Types.DescrTest do |> tuple_replace_at(0, boolean()) |> equal?(dynamic(open_tuple([boolean()]))) - assert dynamic(union(tuple(), integer())) + assert dynamic(opt_union(tuple(), integer())) |> tuple_replace_at(1, boolean()) |> equal?(dynamic(open_tuple([term(), boolean()]))) end @@ -2014,44 +2097,46 @@ defmodule Module.Types.DescrTest do assert tuple_values(tuple([])) == none() assert tuple_values(tuple()) == term() assert tuple_values(open_tuple([integer()])) == term() - assert tuple_values(tuple([integer(), atom()])) == union(integer(), atom()) + assert tuple_values(tuple([integer(), atom()])) == opt_union(integer(), atom()) - assert tuple_values(union(tuple([float(), pid()]), tuple([reference()]))) == - union(float(), union(pid(), reference())) + assert tuple_values(opt_union(tuple([float(), pid()]), tuple([reference()]))) == + opt_union(float(), opt_union(pid(), reference())) - assert tuple_values(difference(tuple([number(), atom()]), tuple([float(), term()]))) == - union(integer(), atom()) + assert tuple_values(opt_difference(tuple([number(), atom()]), tuple([float(), term()]))) == + opt_union(integer(), atom()) - assert union(tuple([atom([:ok])]), open_tuple([integer()])) - |> difference(open_tuple([term(), term()])) - |> tuple_values() == union(atom([:ok]), integer()) + assert opt_union(tuple([atom([:ok])]), open_tuple([integer()])) + |> opt_difference(open_tuple([term(), term()])) + |> tuple_values() == opt_union(atom([:ok]), integer()) - assert tuple_values(difference(tuple([number(), atom()]), tuple([float(), atom([:ok])]))) == - union(number(), atom()) + assert tuple_values( + opt_difference(tuple([number(), atom()]), tuple([float(), atom([:ok])])) + ) == + opt_union(number(), atom()) assert tuple_values(dynamic(tuple())) == dynamic() assert tuple_values(dynamic(tuple([integer()]))) == dynamic(integer()) - assert tuple_values(union(dynamic(tuple([integer()])), tuple([atom()]))) == - union(dynamic(integer()), atom()) + assert tuple_values(opt_union(dynamic(tuple([integer()])), tuple([atom()]))) == + opt_union(dynamic(integer()), atom()) - assert tuple_values(union(dynamic(tuple()), integer())) == :badtuple - assert tuple_values(dynamic(union(integer(), tuple([atom()])))) == dynamic(atom()) + assert tuple_values(opt_union(dynamic(tuple()), integer())) == :badtuple + assert tuple_values(dynamic(opt_union(integer(), tuple([atom()])))) == dynamic(atom()) - assert tuple_values(union(dynamic(tuple([integer()])), tuple([integer()]))) + assert tuple_values(opt_union(dynamic(tuple([integer()])), tuple([integer()]))) |> equal?(integer()) end test "map_to_list" do assert map_to_list(:term) == :badmap assert map_to_list(integer()) == :badmap - assert map_to_list(union(open_map(), integer())) == :badmap + assert map_to_list(opt_union(open_map(), integer())) == :badmap assert map_to_list(none()) == :badmap assert map_to_list(dynamic()) == {:ok, dynamic(list(tuple([term(), term()])))} # A non existent map type is refused assert open_map() - |> difference(open_map(a: if_set(term()), c: if_set(term()))) + |> opt_difference(open_map(a: if_set(term()), c: if_set(term()))) |> map_to_list() == :badmap assert map_to_list(empty_map()) == {:ok, empty_list()} @@ -2067,14 +2152,14 @@ defmodule Module.Types.DescrTest do {:ok, non_empty_list( tuple([atom([:a]), integer()]) - |> union(tuple([atom([:b]), atom()])) + |> opt_union(tuple([atom([:b]), atom()])) )} - assert map_to_list(union(closed_map(a: float()), closed_map(b: pid()))) == + assert map_to_list(opt_union(closed_map(a: float()), closed_map(b: pid()))) == {:ok, non_empty_list( tuple([atom([:a]), float()]) - |> union(tuple([atom([:b]), pid()])) + |> opt_union(tuple([atom([:b]), pid()])) )} # Test with struct-like descrs @@ -2101,9 +2186,9 @@ defmodule Module.Types.DescrTest do {:ok, non_empty_list( tuple([atom([:a]), atom([:ok])]) - |> union(tuple([atom([:b]), float()])) - |> union(tuple([integer(), binary()])) - |> union(tuple([tuple(), pid()])) + |> opt_union(tuple([atom([:b]), float()])) + |> opt_union(tuple([integer(), binary()])) + |> opt_union(tuple([tuple(), pid()])) )} # Test open maps - should return list of key-value tuples @@ -2117,7 +2202,7 @@ defmodule Module.Types.DescrTest do [binary(), float(), pid(), port(), reference()] ++ [fun(), atom(), tuple(), open_map(), list(term(), term())], tuple([integer(), binary()]), - fn domain, acc -> union(acc, tuple([domain, term()])) end + fn domain, acc -> opt_union(acc, tuple([domain, term()])) end ) ) |> equal?(list) @@ -2136,10 +2221,10 @@ defmodule Module.Types.DescrTest do {:ok, list( tuple([integer(), atom([:int])]) - |> union(tuple([float(), atom([:float])])) - |> union(tuple([atom(), binary()])) - |> union(tuple([binary(), integer()])) - |> union(tuple([tuple(), float()])) + |> opt_union(tuple([float(), atom([:float])])) + |> opt_union(tuple([atom(), binary()])) + |> opt_union(tuple([binary(), integer()])) + |> opt_union(tuple([tuple(), float()])) )} # Test dynamic maps @@ -2149,13 +2234,13 @@ defmodule Module.Types.DescrTest do assert map_to_list(dynamic(closed_map(a: integer()))) == {:ok, dynamic(non_empty_list(tuple([atom([:a]), integer()])))} - assert map_to_list(union(dynamic(closed_map(a: integer())), closed_map(b: atom()))) == + assert map_to_list(opt_union(dynamic(closed_map(a: integer())), closed_map(b: atom()))) == {:ok, - union( + opt_union( non_empty_list(tuple([atom([:b]), atom()])), dynamic( non_empty_list( - union( + opt_union( tuple([atom([:a]), integer()]), tuple([atom([:b]), atom()]) ) @@ -2164,21 +2249,21 @@ defmodule Module.Types.DescrTest do )} # A static integer is refused - assert map_to_list(union(dynamic(open_map()), integer())) == :badmap + assert map_to_list(opt_union(dynamic(open_map()), integer())) == :badmap # Test with negations assert map_to_list( - difference(closed_map(a: integer(), b: atom()), closed_map(a: integer())) + opt_difference(closed_map(a: integer(), b: atom()), closed_map(a: integer())) ) == {:ok, non_empty_list( tuple([atom([:a]), integer()]) - |> union(tuple([atom([:b]), atom()])) + |> opt_union(tuple([atom([:b]), atom()])) )} # If a key is removed entirely by a negation, it should not appear in the result assert closed_map(a: if_set(integer()), b: atom()) - |> difference(closed_map(a: integer(), b: term())) + |> opt_difference(closed_map(a: integer(), b: term())) |> map_to_list() == {:ok, non_empty_list(tuple([atom([:b]), atom()]))} end @@ -2187,95 +2272,97 @@ defmodule Module.Types.DescrTest do # take complex tuples, normalize them, and check if they are still equal complex_tuples = [ tuple([term(), atom(), number()]) - |> difference(tuple([atom(), atom(), float()])), + |> opt_difference(tuple([atom(), atom(), float()])), # overlapping union and difference producing multiple variants - difference( - tuple([union(atom(), pid()), union(integer(), float())]), - tuple([union(atom(), pid()), float()]) + opt_difference( + tuple([opt_union(atom(), pid()), opt_union(integer(), float())]), + tuple([opt_union(atom(), pid()), float()]) ) ] Enum.each(complex_tuples, fn domain -> args = domain_to_args(domain) - assert Enum.reduce(args, none(), &union(args_to_domain(&1), &2)) + assert Enum.reduce(args, none(), &opt_union(args_to_domain(&1), &2)) |> equal?(domain) end) end test "map_fetch_key" do assert map_fetch_key(term(), :a) == :badmap - assert map_fetch_key(union(open_map(), integer()), :a) == :badmap - assert map_fetch_key(difference(open_map(), open_map()), :a) == :badmap + assert map_fetch_key(opt_union(open_map(), integer()), :a) == :badmap + assert map_fetch_key(opt_difference(open_map(), open_map()), :a) == :badmap - assert map_fetch_key(difference(closed_map(a: integer()), closed_map(a: term())), :a) == + assert map_fetch_key(opt_difference(closed_map(a: integer()), closed_map(a: term())), :a) == :badmap assert map_fetch_key(open_map(), :a) == :badkey assert map_fetch_key(open_map(a: not_set()), :a) == :badkey - assert map_fetch_key(union(closed_map(a: integer()), closed_map(b: atom())), :a) == :badkey + + assert map_fetch_key(opt_union(closed_map(a: integer()), closed_map(b: atom())), :a) == + :badkey assert map_fetch_key(closed_map(a: integer()), :a) == {false, integer()} - assert map_fetch_key(union(closed_map(a: integer()), closed_map(a: atom())), :a) == - {false, union(integer(), atom())} + assert map_fetch_key(opt_union(closed_map(a: integer()), closed_map(a: atom())), :a) == + {false, opt_union(integer(), atom())} {false, value_type} = open_map(my_map: open_map(foo: integer())) - |> intersection(open_map(my_map: open_map(bar: boolean()))) + |> opt_intersection(open_map(my_map: open_map(bar: boolean()))) |> map_fetch_key(:my_map) assert equal?(value_type, open_map(foo: integer(), bar: boolean())) {false, value_type} = - closed_map(a: union(integer(), atom())) - |> difference(open_map(a: integer())) + closed_map(a: opt_union(integer(), atom())) + |> opt_difference(open_map(a: integer())) |> map_fetch_key(:a) assert equal?(value_type, atom()) {false, value_type} = closed_map(a: integer(), b: atom()) - |> difference(closed_map(a: integer(), b: atom([:foo]))) + |> opt_difference(closed_map(a: integer(), b: atom([:foo]))) |> map_fetch_key(:a) assert equal?(value_type, integer()) {false, value_type} = closed_map(a: integer()) - |> difference(closed_map(a: atom())) + |> opt_difference(closed_map(a: atom())) |> map_fetch_key(:a) assert equal?(value_type, integer()) {false, value_type} = open_map(a: integer(), b: atom()) - |> union(closed_map(a: tuple())) + |> opt_union(closed_map(a: tuple())) |> map_fetch_key(:a) - assert equal?(value_type, union(integer(), tuple())) + assert equal?(value_type, opt_union(integer(), tuple())) {false, value_type} = closed_map(a: atom()) - |> difference(closed_map(a: atom([:foo, :bar]))) - |> difference(closed_map(a: atom([:bar]))) + |> opt_difference(closed_map(a: atom([:foo, :bar]))) + |> opt_difference(closed_map(a: atom([:bar]))) |> map_fetch_key(:a) - assert equal?(value_type, intersection(atom(), negation(atom([:foo, :bar])))) + assert equal?(value_type, opt_intersection(atom(), opt_negation(atom([:foo, :bar])))) - assert closed_map(a: union(atom([:ok]), pid()), b: integer(), c: tuple()) - |> difference(open_map(a: atom([:ok]), b: integer())) - |> difference(open_map(a: atom(), c: tuple())) + assert closed_map(a: opt_union(atom([:ok]), pid()), b: integer(), c: tuple()) + |> opt_difference(open_map(a: atom([:ok]), b: integer())) + |> opt_difference(open_map(a: atom(), c: tuple())) |> map_fetch_key(:a) == {false, pid()} - assert closed_map(a: union(atom([:foo]), pid()), b: integer(), c: tuple()) - |> difference(open_map(a: atom([:foo]), b: integer())) - |> difference(open_map(a: atom(), c: tuple())) + assert closed_map(a: opt_union(atom([:foo]), pid()), b: integer(), c: tuple()) + |> opt_difference(open_map(a: atom([:foo]), b: integer())) + |> opt_difference(open_map(a: atom(), c: tuple())) |> map_fetch_key(:a) == {false, pid()} - assert closed_map(a: union(atom([:foo, :bar, :baz]), integer())) - |> difference(open_map(a: atom([:foo, :bar]))) - |> difference(open_map(a: atom([:foo, :baz]))) + assert closed_map(a: opt_union(atom([:foo, :bar, :baz]), integer())) + |> opt_difference(open_map(a: atom([:foo, :bar]))) + |> opt_difference(open_map(a: atom([:foo, :baz]))) |> map_fetch_key(:a) == {false, integer()} end @@ -2286,21 +2373,21 @@ defmodule Module.Types.DescrTest do test "map_fetch_key with dynamic" do assert map_fetch_key(dynamic(), :a) == {true, dynamic()} - assert map_fetch_key(union(dynamic(), integer()), :a) == :badmap - assert map_fetch_key(union(dynamic(open_map(a: integer())), integer()), :a) == :badmap - assert map_fetch_key(union(dynamic(integer()), integer()), :a) == :badmap + assert map_fetch_key(opt_union(dynamic(), integer()), :a) == :badmap + assert map_fetch_key(opt_union(dynamic(open_map(a: integer())), integer()), :a) == :badmap + assert map_fetch_key(opt_union(dynamic(integer()), integer()), :a) == :badmap - assert intersection(dynamic(), open_map(a: integer())) - |> map_fetch_key(:a) == {false, intersection(integer(), dynamic())} + assert opt_intersection(dynamic(), open_map(a: integer())) + |> map_fetch_key(:a) == {false, opt_intersection(integer(), dynamic())} - {false, type} = union(dynamic(integer()), open_map(a: integer())) |> map_fetch_key(:a) + {false, type} = opt_union(dynamic(integer()), open_map(a: integer())) |> map_fetch_key(:a) assert equal?(type, integer()) - assert union(dynamic(integer()), open_map(a: if_set(integer()))) |> map_fetch_key(:a) == + assert opt_union(dynamic(integer()), open_map(a: if_set(integer()))) |> map_fetch_key(:a) == :badkey - assert union(dynamic(open_map(a: atom())), open_map(a: integer())) - |> map_fetch_key(:a) == {false, union(dynamic(atom()), integer())} + assert opt_union(dynamic(open_map(a: atom())), open_map(a: integer())) + |> map_fetch_key(:a) == {false, opt_union(dynamic(atom()), integer())} end test "map_fetch_key with domain keys" do @@ -2314,11 +2401,11 @@ defmodule Module.Types.DescrTest do t3 = open_map(a: not_set()) # Indeed, t2 is equivalent to the empty map - assert map_fetch_key(difference(t1, t2), :a) == :badkey - assert map_fetch_key(difference(t1, t3), :a) == {false, pid()} + assert map_fetch_key(opt_difference(t1, t2), :a) == :badkey + assert map_fetch_key(opt_difference(t1, t3), :a) == {false, pid()} t4 = closed_map([{domain_key(:pid), atom()}]) - assert map_fetch_key(difference(t1, t4) |> difference(t3), :a) == {false, pid()} + assert map_fetch_key(opt_difference(t1, t4) |> opt_difference(t3), :a) == {false, pid()} assert map_fetch_key(closed_map([{domain_key(:atom), pid()}]), :a) == :badkey @@ -2326,11 +2413,11 @@ defmodule Module.Types.DescrTest do {true, dynamic(pid())} assert closed_map([{domain_key(:atom), number()}]) - |> difference(open_map(a: if_set(integer()))) + |> opt_difference(open_map(a: if_set(integer()))) |> map_fetch_key(:a) == {false, float()} assert closed_map([{domain_key(:atom), number()}]) - |> difference(closed_map(b: if_set(integer()))) + |> opt_difference(closed_map(b: if_set(integer()))) |> map_fetch_key(:a) == :badkey end end @@ -2390,12 +2477,12 @@ defmodule Module.Types.DescrTest do assert map_get(all_domains, empty_map()) == {:ok, pid()} # Union - assert map_get(all_domains, union(tuple(), empty_map())) == - {:ok, union(float(), pid())} + assert map_get(all_domains, opt_union(tuple(), empty_map())) == + {:ok, opt_union(float(), pid())} # Removing all maps with tuple keys - t_no_tuple = difference(all_domains, closed_map([{domain_key(:tuple), float()}])) - t_really_no_tuple = difference(all_domains, open_map([{domain_key(:tuple), float()}])) + t_no_tuple = opt_difference(all_domains, closed_map([{domain_key(:tuple), float()}])) + t_really_no_tuple = opt_difference(all_domains, open_map([{domain_key(:tuple), float()}])) assert subtype?(all_domains, open_map()) # It's only closed maps, so it should not change assert map_get(t_no_tuple, tuple()) == {:ok, float()} @@ -2404,7 +2491,7 @@ defmodule Module.Types.DescrTest do t1 = closed_map([{domain_key(:tuple), integer()}]) t2 = closed_map([{domain_key(:tuple), float()}]) - t3 = union(t1, t2) + t3 = opt_union(t1, t2) assert map_get(t3, tuple()) == {:ok, number()} end @@ -2419,13 +2506,13 @@ defmodule Module.Types.DescrTest do {:ok, atom([:a, :b])} assert map_get(map, atom([:a, :c])) == - {:ok, union(atom([:a]), pid())} + {:ok, opt_union(atom([:a]), pid())} - assert map_get(map, atom() |> difference(atom([:a, :b]))) == + assert map_get(map, atom() |> opt_difference(atom([:a, :b]))) == {:ok, pid()} - assert map_get(map, atom() |> difference(atom([:a]))) == - {:ok, union(atom([:b]), pid())} + assert map_get(map, atom() |> opt_difference(atom([:a]))) == + {:ok, opt_union(atom([:b]), pid())} assert map_get(closed_map(a: atom([:a]), b: atom([:b])), atom()) == {:ok, atom([:a, :b])} @@ -2435,12 +2522,12 @@ defmodule Module.Types.DescrTest do # Have one of the keys be a __struct__ map = closed_map([{:a, atom([:a])}, {:__struct__, term()}, {domain_key(:atom), pid()}]) - {:ok, term} = map_get(map, atom() |> difference(atom([:a]))) + {:ok, term} = map_get(map, atom() |> opt_difference(atom([:a]))) assert equal?(term, term()) base = open_map([{domain_key(:atom), term()}]) - bad = open_map(a: if_set(negation(integer()))) - map = negation(union(negation(base), bad)) + bad = open_map(a: if_set(opt_negation(integer()))) + map = opt_negation(opt_union(opt_negation(base), bad)) assert equal?(map, open_map(a: integer())) @@ -2458,33 +2545,33 @@ defmodule Module.Types.DescrTest do {:ok, type} = map_get(map, atom([:a])) assert equal?(type, term()) - {:ok, type} = map_get(map, difference(atom(), atom([:a]))) + {:ok, type} = map_get(map, opt_difference(atom(), atom([:a]))) assert equal?(type, integer()) map = closed_map([{:a, term()}, {domain_key(:atom), integer()}]) - |> difference(open_map(a: negation(pid()))) + |> opt_difference(open_map(a: opt_negation(pid()))) {:ok, type} = map_get(map, atom()) - assert equal?(type, union(integer(), pid())) + assert equal?(type, opt_union(integer(), pid())) {:ok, type} = map_get(map, atom([:a])) assert equal?(type, pid()) - {:ok, type} = map_get(map, difference(atom(), atom([:a]))) + {:ok, type} = map_get(map, opt_difference(atom(), atom([:a]))) assert equal?(type, integer()) map = closed_map([{:a, term()}, {:b, binary()}, {domain_key(:atom), integer()}]) - |> difference(open_map(a: negation(pid()))) + |> opt_difference(open_map(a: opt_negation(pid()))) {:ok, type} = map_get(map, atom()) - assert equal?(type, union(union(integer(), pid()), binary())) + assert equal?(type, opt_union(opt_union(integer(), pid()), binary())) {:ok, type} = map_get(map, atom([:a, :b])) - assert equal?(type, union(pid(), binary())) + assert equal?(type, opt_union(pid(), binary())) - {:ok, type} = map_get(map, difference(atom(), atom([:a, :b]))) + {:ok, type} = map_get(map, opt_difference(atom(), atom([:a, :b]))) assert equal?(type, integer()) end @@ -2560,8 +2647,8 @@ defmodule Module.Types.DescrTest do # When putting multiple keys, we don't know which one will be set assert map_update(open_map(key1: atom(), key2: binary()), atom([:key1, :key2]), integer()) == - {union(atom(), binary()), - union( + {opt_union(atom(), binary()), + opt_union( open_map(key1: atom(), key2: integer()), open_map(key1: integer(), key2: binary()) ), []} @@ -2569,7 +2656,7 @@ defmodule Module.Types.DescrTest do # When putting multiple keys, all have to be set assert map_update(open_map(key1: atom(), key2: binary()), atom([:key1, :key3]), integer()) == {term(), - union( + opt_union( open_map(key1: integer(), key2: binary()), open_map(key1: atom(), key2: binary(), key3: integer()) ), [badkey: :key3]} @@ -2583,24 +2670,24 @@ defmodule Module.Types.DescrTest do true ) == {term(), - union( + opt_union( open_map(key1: integer(), key2: binary()), open_map(key1: atom(), key2: binary(), key3: integer()) ), []} # ...unless dynamic assert map_update(dynamic(open_map()), atom([:key1, :key2]), integer()) == - {dynamic(), dynamic(union(open_map(key1: integer()), open_map(key2: integer()))), - []} + {dynamic(), + dynamic(opt_union(open_map(key1: integer()), open_map(key2: integer()))), []} # A "none" map assert open_map() - |> difference(open_map(a: if_set(term()), c: if_set(term()))) + |> opt_difference(open_map(a: if_set(term()), c: if_set(term()))) |> map_update(atom([:b]), integer()) == {:error, [badkey: :b]} # ... even when forcing assert open_map() - |> difference(open_map(a: if_set(term()), c: if_set(term()))) + |> opt_difference(open_map(a: if_set(term()), c: if_set(term()))) |> map_update(atom([:b]), integer(), true, true) == {none(), none(), []} end @@ -2614,7 +2701,7 @@ defmodule Module.Types.DescrTest do # This is a test of the map_update_fun/5 with forced?: false parameter. # We check that it does not call its typed_fun argument with `none()` # due to the key being absent in the map. - type = dynamic(difference(open_map(), empty_map())) + type = dynamic(opt_difference(open_map(), empty_map())) fun = fn _optional?, value -> send(self(), :callback_invoked) @@ -2626,15 +2713,31 @@ defmodule Module.Types.DescrTest do end test "with dynamic atom keys" do - assert map_update(closed_map(key: atom([:value])), dynamic(), atom([:new_value])) == - {atom([:value]), closed_map(key: atom([:value, :new_value])), []} + assert {type, descr, errors} = + map_update(closed_map(key: atom([:value])), dynamic(), atom([:new_value])) + + assert equal?(type, atom([:value])) + assert equal?(descr, closed_map(key: atom([:value, :new_value]))) + assert errors == [] + + assert {type, descr, errors} = + map_update( + dynamic(closed_map(key: atom([:value]))), + dynamic(), + atom([:new_value]) + ) - assert map_update(dynamic(closed_map(key: atom([:value]))), dynamic(), atom([:new_value])) == - {dynamic(atom([:value])), dynamic(closed_map(key: atom([:value, :new_value]))), []} + assert equal?(type, dynamic(atom([:value]))) + assert equal?(descr, dynamic(closed_map(key: atom([:value, :new_value])))) + assert errors == [] # Check struct fields - assert map_update(open_map(__struct__: term()), dynamic(atom()), integer()) == - {term(), open_map(__struct__: term()), []} + assert {type, descr, errors} = + map_update(open_map(__struct__: term()), dynamic(atom()), integer()) + + assert type == term() + assert equal?(descr, open_map(__struct__: term())) + assert errors == [] # When precise dynamic keys are given, at least one must succeed assert map_update( @@ -2659,7 +2762,7 @@ defmodule Module.Types.DescrTest do true ) == {none(), - union( + opt_union( closed_map(key1: atom(), key2: binary(), key3: integer()), closed_map(key1: atom(), key2: binary(), key4: integer()) ), []} @@ -2671,7 +2774,7 @@ defmodule Module.Types.DescrTest do integer() ) == {term(), - union( + opt_union( open_map(key1: integer(), key2: binary()), open_map(key1: atom(), key2: binary(), key3: integer()) ), []} @@ -2697,48 +2800,56 @@ defmodule Module.Types.DescrTest do assert map_update(map, integer(), integer()) == {binary(), closed_map([ - {domain_key(:integer), union(integer(), binary())}, + {domain_key(:integer), opt_union(integer(), binary())}, {domain_key(:pid), binary()}, {domain_key(:port), binary()} ]), []} - assert map_update(map, union(pid(), integer()), integer()) == + assert map_update(map, opt_union(pid(), integer()), integer()) == {binary(), closed_map([ - {domain_key(:integer), union(integer(), binary())}, - {domain_key(:pid), union(integer(), binary())}, + {domain_key(:integer), opt_union(integer(), binary())}, + {domain_key(:pid), opt_union(integer(), binary())}, {domain_key(:port), binary()} ]), []} - assert map_update(map, union(pid(), reference()), integer()) == + assert map_update(map, opt_union(pid(), reference()), integer()) == {binary(), closed_map([ {domain_key(:integer), binary()}, - {domain_key(:pid), union(integer(), binary())}, + {domain_key(:pid), opt_union(integer(), binary())}, {domain_key(:port), binary()} ]), [baddomain: reference()]} - assert map_update(map, union(pid(), dynamic(union(reference(), integer()))), integer()) == + assert map_update( + map, + opt_union(pid(), dynamic(opt_union(reference(), integer()))), + integer() + ) == {binary(), closed_map([ - {domain_key(:integer), union(integer(), binary())}, - {domain_key(:pid), union(integer(), binary())}, + {domain_key(:integer), opt_union(integer(), binary())}, + {domain_key(:pid), opt_union(integer(), binary())}, {domain_key(:port), binary()} ]), []} - assert map_update(map, union(pid(), dynamic(union(reference(), binary()))), integer()) == + assert map_update( + map, + opt_union(pid(), dynamic(opt_union(reference(), binary()))), + integer() + ) == {binary(), closed_map([ {domain_key(:integer), binary()}, - {domain_key(:pid), union(integer(), binary())}, + {domain_key(:pid), opt_union(integer(), binary())}, {domain_key(:port), binary()} ]), []} - assert map_update(map, dynamic(union(reference(), binary())), integer()) == + assert map_update(map, dynamic(opt_union(reference(), binary())), integer()) == {:error, []} # ... unless forcing - assert map_update(map, dynamic(union(reference(), binary())), integer(), true, true) == + assert map_update(map, dynamic(opt_union(reference(), binary())), integer(), true, true) == {none(), closed_map([ {domain_key(:integer), binary()}, @@ -2749,39 +2860,55 @@ defmodule Module.Types.DescrTest do ]), []} # Putting dynamic atom over record keys - assert map_update(closed_map(key1: binary(), key2: pid()), atom(), integer()) == - {union(binary(), pid()), - union( - closed_map(key1: binary(), key2: integer()), - closed_map(key1: union(integer(), binary()), key2: pid()) - ), [baddomain: atom()]} + assert {type, descr, errors} = + map_update(closed_map(key1: binary(), key2: pid()), atom(), integer()) + + assert equal?(type, opt_union(binary(), pid())) + + assert equal?( + descr, + opt_union( + closed_map(key1: binary(), key2: integer()), + closed_map(key1: opt_union(integer(), binary()), key2: pid()) + ) + ) + + assert errors == [baddomain: atom()] # ... unless forcing assert map_update(closed_map(key1: binary(), key2: pid()), atom(), integer(), true, true) == - {union(binary(), pid()), + {opt_union(binary(), pid()), [ closed_map([{domain_key(:atom), integer()}, key1: binary(), key2: pid()]), closed_map(key1: integer(), key2: pid()), closed_map(key1: binary(), key2: integer()) ] - |> Enum.reduce(&union/2), [baddomain: atom()]} + |> Enum.reduce(&opt_union/2), [baddomain: atom()]} - assert map_update(closed_map(key1: binary(), key2: pid()), dynamic(atom()), integer()) == - {union(binary(), pid()), - union( - closed_map(key1: binary(), key2: integer()), - closed_map(key1: union(integer(), binary()), key2: pid()) - ), []} + assert {type, descr, errors} = + map_update(closed_map(key1: binary(), key2: pid()), dynamic(atom()), integer()) + + assert equal?(type, opt_union(binary(), pid())) + + assert equal?( + descr, + opt_union( + closed_map(key1: binary(), key2: integer()), + closed_map(key1: opt_union(integer(), binary()), key2: pid()) + ) + ) + + assert errors == [] # A "none()" map assert open_map() - |> difference(open_map(a: if_set(term()), c: if_set(term()))) + |> opt_difference(open_map(a: if_set(term()), c: if_set(term()))) |> map_update(binary(), integer()) == {:error, [baddomain: binary()]} # ... even when forcing {type, descr, errors} = open_map() - |> difference(open_map(a: if_set(term()), c: if_set(term()))) + |> opt_difference(open_map(a: if_set(term()), c: if_set(term()))) |> map_update(binary(), integer(), true, true) assert empty?(type) @@ -2790,40 +2917,46 @@ defmodule Module.Types.DescrTest do end test "with mixed keys" do - assert map_update(dynamic(), union(atom([:key]), binary()), integer()) == + assert map_update(dynamic(), opt_union(atom([:key]), binary()), integer()) == {dynamic(), dynamic(open_map()), []} # When precise dynamic keys are given, at least one must succeed assert map_update( closed_map([{:key, atom()}, {domain_key(:integer), binary()}]), - dynamic(union(atom([:key]), integer())), + dynamic(opt_union(atom([:key]), integer())), integer() ) == - {union(atom(), binary()), - union( + {opt_union(atom(), binary()), + opt_union( closed_map([{:key, integer()}, {domain_key(:integer), binary()}]), - closed_map([{:key, atom()}, {domain_key(:integer), union(binary(), integer())}]) + closed_map([ + {:key, atom()}, + {domain_key(:integer), opt_union(binary(), integer())} + ]) ), []} # Negated keys - assert map_update( - closed_map(key1: binary(), key2: binary()), - difference(atom(), atom([:key1])), - integer() - ) == - {binary(), closed_map(key1: binary(), key2: union(integer(), binary())), - [baddomain: atom()]} + assert {type, descr, errors} = + map_update( + closed_map(key1: binary(), key2: binary()), + opt_difference(atom(), atom([:key1])), + integer() + ) + + assert equal?(type, binary()) + assert equal?(descr, closed_map(key1: binary(), key2: opt_union(integer(), binary()))) + assert errors == [baddomain: atom()] assert map_update( closed_map([key1: binary(), key2: binary()] ++ [{domain_key(:atom), pid()}]), - difference(atom(), atom([:key1])), + opt_difference(atom(), atom([:key1])), integer() ) == - {union(binary(), pid()), - union( + {opt_union(binary(), pid()), + opt_union( closed_map([{domain_key(:atom), pid()}, key1: binary(), key2: integer()]), closed_map([ - {domain_key(:atom), union(pid(), integer())}, + {domain_key(:atom), opt_union(pid(), integer())}, key1: binary(), key2: binary() ]) @@ -2832,20 +2965,20 @@ defmodule Module.Types.DescrTest do # Missing keys assert map_update( closed_map([{:key, atom()}, {domain_key(:integer), binary()}]), - dynamic(union(atom([:other_key]), pid())), + dynamic(opt_union(atom([:other_key]), pid())), integer() ) == {:error, []} # ...unless forcing assert map_update( closed_map([{:key, atom()}, {domain_key(:integer), binary()}]), - dynamic(union(atom([:other_key]), pid())), + dynamic(opt_union(atom([:other_key]), pid())), integer(), true, true ) == {none(), - union( + opt_union( closed_map([ {:key, atom()}, {domain_key(:integer), binary()}, @@ -2859,10 +2992,10 @@ defmodule Module.Types.DescrTest do ), []} # Popping dynamic keys - non_struct_map = difference(open_map(), open_map(__struct__: atom())) + non_struct_map = opt_difference(open_map(), open_map(__struct__: atom())) {type, descr, []} = map_update(non_struct_map, dynamic(), not_set(), true, true) assert type == term() - assert equal?(descr, open_map(__struct__: if_set(negation(atom())))) + assert equal?(descr, open_map(__struct__: if_set(opt_negation(atom())))) end end @@ -2892,7 +3025,7 @@ defmodule Module.Types.DescrTest do # When putting multiple keys, we don't know which one will be set assert map_put(open_map(key1: atom(), key2: binary()), atom([:key1, :key2]), integer()) == {:ok, - union( + opt_union( open_map(key1: atom(), key2: integer()), open_map(key1: integer(), key2: binary()) )} @@ -2900,13 +3033,13 @@ defmodule Module.Types.DescrTest do # When putting multiple keys, set even missing keys assert map_put(open_map(key1: atom(), key2: binary()), atom([:key1, :key3]), integer()) == {:ok, - union( + opt_union( open_map(key1: atom(), key2: binary(), key3: integer()), open_map(key1: integer(), key2: binary()) )} assert map_put(dynamic(open_map()), atom([:key1, :key2]), integer()) == - {:ok, dynamic(union(open_map(key1: integer()), open_map(key2: integer())))} + {:ok, dynamic(opt_union(open_map(key1: integer()), open_map(key2: integer())))} end test "with dynamic/term as key-value" do @@ -2933,7 +3066,7 @@ defmodule Module.Types.DescrTest do integer() ) == {:ok, - union( + opt_union( open_map(key1: atom(), key2: binary(), key3: integer()), open_map(key1: integer(), key2: binary()) )} @@ -2944,7 +3077,7 @@ defmodule Module.Types.DescrTest do integer() ) == {:ok, - union( + opt_union( open_map(key1: atom(), key2: binary(), key3: integer()), open_map(key1: atom(), key2: binary(), key4: integer()) )} @@ -2961,38 +3094,38 @@ defmodule Module.Types.DescrTest do assert map_put(map, integer(), integer()) == {:ok, closed_map([ - {domain_key(:integer), union(integer(), binary())}, + {domain_key(:integer), opt_union(integer(), binary())}, {domain_key(:pid), binary()}, {domain_key(:port), binary()} ])} - assert map_put(map, union(pid(), integer()), integer()) == + assert map_put(map, opt_union(pid(), integer()), integer()) == {:ok, closed_map([ - {domain_key(:integer), union(integer(), binary())}, - {domain_key(:pid), union(integer(), binary())}, + {domain_key(:integer), opt_union(integer(), binary())}, + {domain_key(:pid), opt_union(integer(), binary())}, {domain_key(:port), binary()} ])} - assert map_put(map, union(pid(), reference()), integer()) == + assert map_put(map, opt_union(pid(), reference()), integer()) == {:ok, closed_map([ {domain_key(:integer), binary()}, - {domain_key(:pid), union(integer(), binary())}, + {domain_key(:pid), opt_union(integer(), binary())}, {domain_key(:port), binary()}, {domain_key(:reference), integer()} ])} - assert map_put(map, union(pid(), dynamic(union(reference(), integer()))), integer()) == + assert map_put(map, opt_union(pid(), dynamic(opt_union(reference(), integer()))), integer()) == {:ok, closed_map([ - {domain_key(:integer), union(integer(), binary())}, - {domain_key(:pid), union(integer(), binary())}, + {domain_key(:integer), opt_union(integer(), binary())}, + {domain_key(:pid), opt_union(integer(), binary())}, {domain_key(:port), binary()}, {domain_key(:reference), integer()} ])} - assert map_put(map, dynamic(union(reference(), binary())), integer()) == + assert map_put(map, dynamic(opt_union(reference(), binary())), integer()) == {:ok, closed_map([ {domain_key(:integer), binary()}, @@ -3010,32 +3143,35 @@ defmodule Module.Types.DescrTest do closed_map(key1: integer(), key2: binary()), closed_map([{domain_key(:atom), integer()}, key1: binary(), key2: binary()]) ] - |> Enum.reduce(&union/2)} + |> Enum.reduce(&opt_union/2)} end test "with mixed keys" do - assert map_put(dynamic(), union(atom([:key]), binary()), integer()) == + assert map_put(dynamic(), opt_union(atom([:key]), binary()), integer()) == {:ok, dynamic(open_map())} # When precise dynamic keys are given, at least one must succeed assert map_put( closed_map([{:key, atom()}, {domain_key(:integer), binary()}]), - dynamic(union(atom([:key]), integer())), + dynamic(opt_union(atom([:key]), integer())), integer() ) == {:ok, - union( + opt_union( closed_map([{:key, integer()}, {domain_key(:integer), binary()}]), - closed_map([{:key, atom()}, {domain_key(:integer), union(binary(), integer())}]) + closed_map([ + {:key, atom()}, + {domain_key(:integer), opt_union(binary(), integer())} + ]) )} assert map_put( closed_map([{:key, atom()}, {domain_key(:integer), binary()}]), - dynamic(union(atom([:other_key]), pid())), + dynamic(opt_union(atom([:other_key]), pid())), integer() ) == {:ok, - union( + opt_union( closed_map([ {:key, atom()}, {:other_key, integer()}, @@ -3051,25 +3187,25 @@ defmodule Module.Types.DescrTest do # Negated keys assert map_put( closed_map(key1: binary(), key2: binary()), - difference(atom(), atom([:key1])), + opt_difference(atom(), atom([:key1])), integer() ) == {:ok, - union( + opt_union( closed_map(key1: binary(), key2: integer()), closed_map([{domain_key(:atom), integer()}, key1: binary(), key2: binary()]) )} assert map_put( closed_map([key1: binary(), key2: binary()] ++ [{domain_key(:atom), pid()}]), - difference(atom(), atom([:key1])), + opt_difference(atom(), atom([:key1])), integer() ) == {:ok, - union( + opt_union( closed_map([{domain_key(:atom), pid()}, key1: binary(), key2: integer()]), closed_map([ - {domain_key(:atom), union(integer(), pid())}, + {domain_key(:atom), opt_union(integer(), pid())}, key1: binary(), key2: binary() ]) @@ -3082,13 +3218,17 @@ defmodule Module.Types.DescrTest do assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), x: term())} - map = difference(open_map(k: integer(), x: term()), open_map(k: integer(), a: integer())) + map = + opt_difference(open_map(k: integer(), x: term()), open_map(k: integer(), a: integer())) {:ok, type} = map_put(map, atom([:k]), binary()) assert equal?( type, - difference(open_map(k: binary(), x: term()), open_map(k: binary(), a: integer())) + opt_difference( + open_map(k: binary(), x: term()), + open_map(k: binary(), a: integer()) + ) ) end @@ -3096,7 +3236,7 @@ defmodule Module.Types.DescrTest do # map_put/3 passes nil as the popped value accumulator because it only needs the map side. map = projected_negative_map(100) - |> difference(open_map(k: atom(), x: term())) + |> opt_difference(open_map(k: atom(), x: term())) assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), x: term())} end @@ -3105,7 +3245,7 @@ defmodule Module.Types.DescrTest do describe "disjoint" do test "optional" do assert disjoint?(term(), if_set(none())) - assert disjoint?(term(), if_set(none()) |> union(non_empty_list(none()))) + assert disjoint?(term(), if_set(none()) |> opt_union(non_empty_list(none()))) end test "map" do @@ -3120,23 +3260,23 @@ defmodule Module.Types.DescrTest do end test "bitmap" do - assert union(pid(), bitstring()) |> to_quoted_string() == + assert opt_union(pid(), bitstring()) |> to_quoted_string() == "bitstring() or pid()" - assert union(integer(), union(float(), binary())) |> to_quoted_string() == + assert opt_union(integer(), opt_union(float(), binary())) |> to_quoted_string() == "binary() or float() or integer()" - assert difference(bitstring(), binary()) |> union(integer()) |> to_quoted_string() == + assert opt_difference(bitstring(), binary()) |> opt_union(integer()) |> to_quoted_string() == "(bitstring() and not binary()) or integer()" end test "bitmap (negation)" do - assert union(pid(), bitstring()) |> negation() |> to_quoted_string() == + assert opt_union(pid(), bitstring()) |> opt_negation() |> to_quoted_string() == "not bitstring() and not pid()" - assert difference(bitstring(), binary()) - |> union(integer()) - |> negation() + assert opt_difference(bitstring(), binary()) + |> opt_union(integer()) + |> opt_negation() |> to_quoted_string() == "not (bitstring() and not binary()) and not integer()" end @@ -3145,7 +3285,7 @@ defmodule Module.Types.DescrTest do assert atom() |> to_quoted_string() == "atom()" assert atom([:a]) |> to_quoted_string() == ":a" assert atom([:a, :b]) |> to_quoted_string() == ":a or :b" - assert difference(atom(), atom([:a])) |> to_quoted_string() == "atom() and not :a" + assert opt_difference(atom(), atom([:a])) |> to_quoted_string() == "atom() and not :a" assert atom([Elixir]) |> to_quoted_string() == "Elixir" assert atom([Foo.Bar]) |> to_quoted_string() == "Foo.Bar" @@ -3155,55 +3295,57 @@ defmodule Module.Types.DescrTest do assert boolean() |> to_quoted_string() == "boolean()" assert atom([true, false, :a]) |> to_quoted_string() == ":a or boolean()" assert atom([true, :a]) |> to_quoted_string() == ":a or true" - assert difference(atom(), boolean()) |> to_quoted_string() == "atom() and not boolean()" + assert opt_difference(atom(), boolean()) |> to_quoted_string() == "atom() and not boolean()" end test "atom/boolean (negation)" do - assert atom() |> negation() |> to_quoted_string() == "not atom()" - assert atom([:a, :b]) |> negation() |> to_quoted_string() == "not :a and not :b" - assert boolean() |> negation() |> to_quoted_string() == "not boolean()" + assert atom() |> opt_negation() |> to_quoted_string() == "not atom()" + assert atom([:a, :b]) |> opt_negation() |> to_quoted_string() == "not :a and not :b" + assert boolean() |> opt_negation() |> to_quoted_string() == "not boolean()" - assert atom([true, false, :a]) |> negation() |> to_quoted_string() == + assert atom([true, false, :a]) |> opt_negation() |> to_quoted_string() == "not :a and not boolean()" end test "dynamic" do assert dynamic() |> to_quoted_string() == "dynamic()" - assert intersection(atom(), dynamic()) |> to_quoted_string() == "dynamic(atom())" + assert opt_intersection(atom(), dynamic()) |> to_quoted_string() == "dynamic(atom())" - assert dynamic(union(atom(), integer())) |> union(integer()) |> to_quoted_string() == + assert dynamic(opt_union(atom(), integer())) |> opt_union(integer()) |> to_quoted_string() == "dynamic(atom()) or integer()" - assert intersection(binary(), dynamic()) |> to_quoted_string() == + assert opt_intersection(binary(), dynamic()) |> to_quoted_string() == "binary()" - assert intersection(bitstring(), dynamic()) |> to_quoted_string() == + assert opt_intersection(bitstring(), dynamic()) |> to_quoted_string() == "dynamic(bitstring())" - assert intersection(bitstring_no_binary(), dynamic()) |> to_quoted_string() == + assert opt_intersection(bitstring_no_binary(), dynamic()) |> to_quoted_string() == "bitstring() and not binary()" - assert intersection(union(binary(), pid()), dynamic()) |> to_quoted_string() == + assert opt_intersection(opt_union(binary(), pid()), dynamic()) |> to_quoted_string() == "dynamic(binary() or pid())" - assert union(atom([:foo, :bar]), dynamic()) |> to_quoted_string() == + assert opt_union(atom([:foo, :bar]), dynamic()) |> to_quoted_string() == "dynamic() or :bar or :foo" - assert intersection(dynamic(), closed_map(a: integer())) |> to_quoted_string() == + assert opt_intersection(dynamic(), closed_map(a: integer())) |> to_quoted_string() == "dynamic(%{a: integer()})" end test "dynamic (negation)" do - assert dynamic(negation(integer())) |> to_quoted_string() == "dynamic(not integer())" - assert negation(dynamic(integer())) |> to_quoted_string() == "dynamic() or not integer()" + assert dynamic(opt_negation(integer())) |> to_quoted_string() == "dynamic(not integer())" + + assert opt_negation(dynamic(integer())) |> to_quoted_string() == + "dynamic() or not integer()" - assert union(atom(), dynamic(integer())) |> negation() |> to_quoted_string() == + assert opt_union(atom(), dynamic(integer())) |> opt_negation() |> to_quoted_string() == "dynamic(not atom()) or (not atom() and not integer())" - assert dynamic(union(atom(), integer())) - |> negation() - |> union(integer()) + assert dynamic(opt_union(atom(), integer())) + |> opt_negation() + |> opt_union(integer()) |> to_quoted_string() == "dynamic() or not atom()" end @@ -3213,22 +3355,22 @@ defmodule Module.Types.DescrTest do assert list(term()) |> to_quoted_string() == "list(term())" assert list(integer()) |> to_quoted_string() == "list(integer())" - assert list(term()) |> difference(empty_list()) |> to_quoted_string() == + assert list(term()) |> opt_difference(empty_list()) |> to_quoted_string() == "non_empty_list(term())" - assert list(term()) |> difference(list(integer())) |> to_quoted_string() == + assert list(term()) |> opt_difference(list(integer())) |> to_quoted_string() == "non_empty_list(term()) and not non_empty_list(integer())" assert list(term()) - |> difference(list(integer())) - |> difference(list(atom())) + |> opt_difference(list(integer())) + |> opt_difference(list(atom())) |> to_quoted_string() == "non_empty_list(term()) and not (non_empty_list(integer()) or non_empty_list(atom()))" assert list(term(), integer()) |> to_quoted_string() == "empty_list() or non_empty_list(term(), integer())" - assert difference(list(term(), atom()), list(term(), boolean())) |> to_quoted_string() == + assert opt_difference(list(term(), atom()), list(term(), boolean())) |> to_quoted_string() == "non_empty_list(term(), atom() and not boolean())" assert list(term(), term()) |> to_quoted_string() == @@ -3237,38 +3379,39 @@ defmodule Module.Types.DescrTest do # Test normalization # Remove duplicates - assert union(list(integer()), list(integer())) |> to_quoted_string() == "list(integer())" + assert opt_union(list(integer()), list(integer())) |> to_quoted_string() == + "list(integer())" # Merge subtypes - assert union(list(float(), pid()), list(number(), pid())) |> to_quoted_string() == + assert opt_union(list(float(), pid()), list(number(), pid())) |> to_quoted_string() == "empty_list() or non_empty_list(float() or integer(), pid())" # Merge last element types - assert union(list(atom([:ok]), integer()), list(atom([:ok]), float())) + assert opt_union(list(atom([:ok]), integer()), list(atom([:ok]), float())) |> to_quoted_string() == "empty_list() or non_empty_list(:ok, float() or integer())" - assert union(dynamic(list(integer(), float())), dynamic(list(integer(), pid()))) + assert opt_union(dynamic(list(integer(), float())), dynamic(list(integer(), pid()))) |> to_quoted_string() == "dynamic(empty_list() or non_empty_list(integer(), float() or pid()))" list_with_tail = - non_empty_list(atom(), union(integer(), empty_list())) - |> difference(non_empty_list(atom([:ok]), integer())) - |> difference(non_empty_list(atom(), integer())) + non_empty_list(atom(), opt_union(integer(), empty_list())) + |> opt_difference(non_empty_list(atom([:ok]), integer())) + |> opt_difference(non_empty_list(atom(), integer())) # Check that simplifications occur. assert to_quoted_string(list_with_tail) == "non_empty_list(atom())" end test "list (negation)" do - assert list(term()) |> negation() |> to_quoted_string() == "not list(term())" - assert list(negation(integer())) |> to_quoted_string() == "list(not integer())" + assert list(term()) |> opt_negation() |> to_quoted_string() == "not list(term())" + assert list(opt_negation(integer())) |> to_quoted_string() == "list(not integer())" - assert list(term()) |> difference(empty_list()) |> negation() |> to_quoted_string() == + assert list(term()) |> opt_difference(empty_list()) |> opt_negation() |> to_quoted_string() == "not non_empty_list(term())" - assert non_empty_list(integer(), integer()) |> negation() |> to_quoted_string() == + assert non_empty_list(integer(), integer()) |> opt_negation() |> to_quoted_string() == "not non_empty_list(integer(), integer())" end @@ -3280,51 +3423,53 @@ defmodule Module.Types.DescrTest do assert open_tuple([integer(), atom()]) |> to_quoted_string() == "{integer(), atom(), ...}" - assert union(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() == + assert opt_union(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() == "{atom(), ...} or {integer(), atom()}" - assert difference(tuple([integer(), atom()]), open_tuple([atom()])) |> to_quoted_string() == + assert opt_difference(tuple([integer(), atom()]), open_tuple([atom()])) + |> to_quoted_string() == "{integer(), atom()}" assert tuple([closed_map(a: integer()), open_map()]) |> to_quoted_string() == "{%{a: integer()}, map()}" - assert union(tuple([integer(), atom()]), tuple([integer(), atom()])) |> to_quoted_string() == + assert opt_union(tuple([integer(), atom()]), tuple([integer(), atom()])) + |> to_quoted_string() == "{integer(), atom()}" - assert union(tuple([integer(), atom()]), tuple([float(), atom()])) |> to_quoted_string() == + assert opt_union(tuple([integer(), atom()]), tuple([float(), atom()])) |> to_quoted_string() == "{float() or integer(), atom()}" - assert union(tuple([integer(), atom()]), tuple([float(), atom()])) - |> union(tuple([pid(), pid(), port()])) - |> union(tuple([pid(), pid(), atom()])) + assert opt_union(tuple([integer(), atom()]), tuple([float(), atom()])) + |> opt_union(tuple([pid(), pid(), port()])) + |> opt_union(tuple([pid(), pid(), atom()])) |> to_quoted_string() == "{float() or integer(), atom()} or {pid(), pid(), atom() or port()}" - assert union(open_tuple([integer()]), open_tuple([float()])) |> to_quoted_string() == + assert opt_union(open_tuple([integer()]), open_tuple([float()])) |> to_quoted_string() == "{float() or integer(), ...}" # {:ok, {term(), integer()}} or {:ok, {term(), float()}} or {:exit, :kill} or {:exit, :timeout} assert tuple([atom([:ok]), tuple([term(), empty_list()])]) - |> union(tuple([atom([:ok]), tuple([term(), open_map()])])) - |> union(tuple([atom([:exit]), atom([:kill])])) - |> union(tuple([atom([:exit]), atom([:timeout])])) + |> opt_union(tuple([atom([:ok]), tuple([term(), open_map()])])) + |> opt_union(tuple([atom([:exit]), atom([:kill])])) + |> opt_union(tuple([atom([:exit]), atom([:timeout])])) |> to_quoted_string() == "{:exit, :kill or :timeout} or {:ok, {term(), empty_list() or map()}}" # Detection of duplicates assert tuple([atom([:ok]), term()]) - |> union(tuple([atom([:ok]), term()])) + |> opt_union(tuple([atom([:ok]), term()])) |> to_quoted_string() == "{:ok, term()}" assert tuple([closed_map(a: integer(), b: atom()), open_map()]) - |> union(tuple([closed_map(a: integer(), b: atom()), open_map()])) + |> opt_union(tuple([closed_map(a: integer(), b: atom()), open_map()])) |> to_quoted_string() == "{%{a: integer(), b: atom()}, map()}" # Nested fusion assert tuple([closed_map(a: integer(), b: atom()), open_map()]) - |> union(tuple([closed_map(a: float(), b: atom()), open_map()])) + |> opt_union(tuple([closed_map(a: float(), b: atom()), open_map()])) |> to_quoted_string() == "{%{a: float() or integer(), b: atom()}, map()}" @@ -3365,13 +3510,15 @@ defmodule Module.Types.DescrTest do ) assert atom([:error]) - |> union( + |> opt_union( tuple([decimal_inf, binary()]) - |> union( + |> opt_union( tuple([decimal_nan, binary()]) - |> union( + |> opt_union( tuple([decimal_int, term()]) - |> union(tuple([union(decimal_inf, union(decimal_nan, decimal_int)), term()])) + |> opt_union( + tuple([opt_union(decimal_inf, opt_union(decimal_nan, decimal_int)), term()]) + ) ) ) ) @@ -3385,8 +3532,8 @@ defmodule Module.Types.DescrTest do end test "tuple (negation)" do - assert tuple([integer()]) |> negation() |> to_quoted_string() == "not {integer()}" - assert tuple([negation(integer())]) |> to_quoted_string() == "{not integer()}" + assert tuple([integer()]) |> opt_negation() |> to_quoted_string() == "not {integer()}" + assert tuple([opt_negation(integer())]) |> to_quoted_string() == "{not integer()}" end test "fun" do @@ -3394,44 +3541,44 @@ defmodule Module.Types.DescrTest do assert none_fun(1) |> to_quoted_string() == "(none() -> term())" assert none_fun(1) - |> intersection(none_fun(2)) + |> opt_intersection(none_fun(2)) |> to_quoted_string() == "none()" assert fun([integer(), float()], boolean()) |> to_quoted_string() == "(integer(), float() -> boolean())" assert fun([integer()], boolean()) - |> union(fun([float()], boolean())) + |> opt_union(fun([float()], boolean())) |> to_quoted_string() == "(float() -> boolean()) or (integer() -> boolean())" assert fun([integer()], boolean()) - |> intersection(fun([float()], boolean())) + |> opt_intersection(fun([float()], boolean())) |> to_quoted_string() == "(float() -> boolean()) and (integer() -> boolean())" # Thanks to lazy BDDs, consecutive union of functions come out as the original union assert fun([integer()], integer()) - |> union(fun([float()], float())) - |> union(fun([pid()], pid())) + |> opt_union(fun([float()], float())) + |> opt_union(fun([pid()], pid())) |> to_quoted_string() == "(integer() -> integer()) or (pid() -> pid()) or (float() -> float())" assert fun(3) |> to_quoted_string() == "(none(), none(), none() -> term())" - assert intersection(fun(), negation(fun())) |> to_quoted_string() == "none()" + assert opt_intersection(fun(), opt_negation(fun())) |> to_quoted_string() == "none()" - assert intersection(fun(), negation(fun(3))) |> to_quoted_string() == + assert opt_intersection(fun(), opt_negation(fun(3))) |> to_quoted_string() == "fun() and not (none(), none(), none() -> term())" end test "fun with optimized intersections" do - assert fun([integer()], atom()) |> intersection(none_fun(1)) |> to_quoted_string() == + assert fun([integer()], atom()) |> opt_intersection(none_fun(1)) |> to_quoted_string() == "(integer() -> atom())" assert fun([integer()], atom()) - |> difference(none_fun(2)) - |> intersection(none_fun(1)) + |> opt_difference(none_fun(2)) + |> opt_intersection(none_fun(1)) |> to_quoted_string() == "(integer() -> atom())" end @@ -3446,23 +3593,23 @@ defmodule Module.Types.DescrTest do assert fun([integer(), float()], dynamic(atom())) |> to_quoted_string() == "(integer(), float() -> dynamic(atom()))" - domain_part = fun([dynamic(atom()) |> union(integer()), binary()], float()) + domain_part = fun([dynamic(atom()) |> opt_union(integer()), binary()], float()) assert domain_part |> to_quoted_string() == "(dynamic(atom()) or integer(), binary() -> float())" - codomain_part = fun([pid(), float()], dynamic(atom()) |> union(integer())) + codomain_part = fun([pid(), float()], dynamic(atom()) |> opt_union(integer())) assert codomain_part |> to_quoted_string() == "(pid(), float() -> dynamic(atom()) or integer())" - assert union(domain_part, codomain_part) |> to_quoted_string() == + assert opt_union(domain_part, codomain_part) |> to_quoted_string() == """ (pid(), float() -> dynamic(atom()) or integer()) or (dynamic(atom()) or integer(), binary() -> float())\ """ - assert intersection(domain_part, codomain_part) |> to_quoted_string() == + assert opt_intersection(domain_part, codomain_part) |> to_quoted_string() == """ (pid(), float() -> dynamic(atom()) or integer()) and (dynamic(atom()) or integer(), binary() -> float())\ @@ -3470,7 +3617,7 @@ defmodule Module.Types.DescrTest do end test "fun (negation)" do - assert fun([integer()], atom()) |> negation() |> to_quoted_string() == + assert fun([integer()], atom()) |> opt_negation() |> to_quoted_string() == "not (integer() -> atom())" end @@ -3487,50 +3634,50 @@ defmodule Module.Types.DescrTest do assert open_map("Elixir.Foo.Bar": float()) |> to_quoted_string() == "%{..., Foo.Bar => float()}" - assert difference(open_map(), open_map(a: term())) |> to_quoted_string() == + assert opt_difference(open_map(), open_map(a: term())) |> to_quoted_string() == "%{..., a: not_set()}" assert closed_map(a: integer(), b: atom()) |> to_quoted_string() == "%{a: integer(), b: atom()}" assert open_map(a: float()) - |> difference(closed_map(a: float())) + |> opt_difference(closed_map(a: float())) |> to_quoted_string() == "%{..., a: float()} and not %{a: float()}" - assert difference(open_map(), empty_map()) |> to_quoted_string() == + assert opt_difference(open_map(), empty_map()) |> to_quoted_string() == "map() and not empty_map()" - assert closed_map(foo: union(integer(), not_set())) |> to_quoted_string() == + assert closed_map(foo: opt_union(integer(), not_set())) |> to_quoted_string() == "%{foo: if_set(integer())}" # Test normalization assert open_map(a: integer(), b: atom()) - |> difference(open_map(b: atom())) - |> union(open_map(a: integer())) + |> opt_difference(open_map(b: atom())) + |> opt_union(open_map(a: integer())) |> to_quoted_string() == "%{..., a: integer()}" - assert union(open_map(a: integer()), open_map(a: integer())) |> to_quoted_string() == + assert opt_union(open_map(a: integer()), open_map(a: integer())) |> to_quoted_string() == "%{..., a: integer()}" - assert difference(open_map(a: number(), b: atom()), open_map(a: integer())) + assert opt_difference(open_map(a: number(), b: atom()), open_map(a: integer())) |> to_quoted_string() == "%{..., a: float(), b: atom()}" # Basic map fusion - assert union(closed_map(a: integer()), closed_map(a: integer())) |> to_quoted_string() == + assert opt_union(closed_map(a: integer()), closed_map(a: integer())) |> to_quoted_string() == "%{a: integer()}" - assert union(closed_map(a: integer()), closed_map(a: float())) |> to_quoted_string() == + assert opt_union(closed_map(a: integer()), closed_map(a: float())) |> to_quoted_string() == "%{a: float() or integer()}" # Nested fusion - assert union(closed_map(a: integer(), b: atom()), closed_map(a: float(), b: atom())) - |> union(closed_map(x: pid(), y: pid(), z: port())) - |> union(closed_map(x: pid(), y: pid(), z: atom())) + assert opt_union(closed_map(a: integer(), b: atom()), closed_map(a: float(), b: atom())) + |> opt_union(closed_map(x: pid(), y: pid(), z: port())) + |> opt_union(closed_map(x: pid(), y: pid(), z: atom())) |> to_quoted_string() == "%{a: float() or integer(), b: atom()} or %{x: pid(), y: pid(), z: atom() or port()}" # Open map fusion - assert union(open_map(a: integer()), open_map(a: float())) |> to_quoted_string() == + assert opt_union(open_map(a: integer()), open_map(a: float())) |> to_quoted_string() == "%{..., a: float() or integer()}" # Fusing complex nested maps with unions @@ -3538,62 +3685,62 @@ defmodule Module.Types.DescrTest do status: atom([:ok]), data: closed_map(value: term(), count: empty_list()) ) - |> union( + |> opt_union( closed_map( status: atom([:ok]), data: closed_map(value: term(), count: open_map()) ) ) - |> union(closed_map(status: atom([:error]), reason: atom([:timeout]))) - |> union(closed_map(status: atom([:error]), reason: atom([:crash]))) + |> opt_union(closed_map(status: atom([:error]), reason: atom([:timeout]))) + |> opt_union(closed_map(status: atom([:error]), reason: atom([:crash]))) |> to_quoted_string() == "%{data: %{count: empty_list() or map(), value: term()}, status: :ok} or\n %{reason: :crash or :timeout, status: :error}" # Difference and union tests assert closed_map(status: atom([:ok]), value: term()) - |> difference(closed_map(status: atom([:ok]), value: float())) - |> union( + |> opt_difference(closed_map(status: atom([:ok]), value: float())) + |> opt_union( closed_map(status: atom([:ok]), value: term()) - |> difference(closed_map(status: atom([:ok]), value: integer())) + |> opt_difference(closed_map(status: atom([:ok]), value: integer())) ) |> to_quoted_string() == "%{status: :ok, value: term()}" # Nested map fusion assert closed_map(data: closed_map(x: integer(), y: atom()), meta: open_map()) - |> union(closed_map(data: closed_map(x: float(), y: atom()), meta: open_map())) + |> opt_union(closed_map(data: closed_map(x: float(), y: atom()), meta: open_map())) |> to_quoted_string() == "%{data: %{x: float() or integer(), y: atom()}, meta: map()}" # Test complex combinations - assert intersection( + assert opt_intersection( open_map(a: number(), b: atom()), open_map(a: integer(), c: boolean()) ) - |> union(difference(open_map(x: atom()), open_map(x: boolean()))) + |> opt_union(opt_difference(open_map(x: atom()), open_map(x: boolean()))) |> to_quoted_string() == "%{..., a: integer(), b: atom(), c: boolean()} or %{..., x: atom() and not boolean()}" assert closed_map(a: number(), b: atom(), c: pid()) - |> difference(closed_map(a: integer(), b: atom(), c: pid())) + |> opt_difference(closed_map(a: integer(), b: atom(), c: pid())) |> to_quoted_string() == "%{a: float(), b: atom(), c: pid()}" # No simplification compared to above, as it is an open map assert open_map(a: number(), b: atom()) - |> difference(closed_map(a: integer(), b: atom())) + |> opt_difference(closed_map(a: integer(), b: atom())) |> to_quoted_string() == "%{..., a: float() or integer(), b: atom()} and not %{a: integer(), b: atom()}" # Remark: this simplification is order dependent. Having the first difference # after the second gives a different result. - assert open_map(a: number(), b: atom(), c: union(pid(), port())) - |> difference(open_map(a: integer(), b: atom(), c: union(pid(), port()))) - |> difference(open_map(a: float(), b: atom(), c: pid())) + assert open_map(a: number(), b: atom(), c: opt_union(pid(), port())) + |> opt_difference(open_map(a: integer(), b: atom(), c: opt_union(pid(), port()))) + |> opt_difference(open_map(a: float(), b: atom(), c: pid())) |> to_quoted_string() == "%{..., a: float(), b: atom(), c: port()}" - assert open_map(a: number(), b: atom(), c: union(pid(), port())) - |> difference(open_map(a: float(), b: atom(), c: pid())) - |> difference(open_map(a: integer(), b: atom(), c: union(pid(), port()))) + assert open_map(a: number(), b: atom(), c: opt_union(pid(), port())) + |> opt_difference(open_map(a: float(), b: atom(), c: pid())) + |> opt_difference(open_map(a: integer(), b: atom(), c: opt_union(pid(), port()))) |> to_quoted_string() == "%{..., a: float(), b: atom(), c: port()}" end @@ -3606,16 +3753,16 @@ defmodule Module.Types.DescrTest do end test "map (negation)" do - assert open_map(a: integer()) |> negation() |> to_quoted_string() == - "not %{..., a: integer()}" + assert open_map(a: integer()) |> opt_negation() |> to_quoted_string() == + "not (map() and not %{..., a: if_set(not integer())})" - assert open_map(a: negation(integer())) |> to_quoted_string() == + assert open_map(a: opt_negation(integer())) |> to_quoted_string() == "%{..., a: not integer()}" - assert closed_map(a: integer()) |> negation() |> to_quoted_string() == + assert closed_map(a: integer()) |> opt_negation() |> to_quoted_string() == "not %{a: integer()}" - assert closed_map(a: negation(integer())) |> to_quoted_string() == + assert closed_map(a: opt_negation(integer())) |> to_quoted_string() == "%{a: not integer()}" end @@ -3648,12 +3795,12 @@ defmodule Module.Types.DescrTest do "%Decimal{sign: integer()}" # Does not fuse structs - assert union(closed_map(__struct__: atom([Foo])), closed_map(__struct__: atom([Bar]))) + assert opt_union(closed_map(__struct__: atom([Foo])), closed_map(__struct__: atom([Bar]))) |> to_quoted_string() == "%{__struct__: Bar} or %{__struct__: Foo}" # Properly format non_struct_map - assert open_map(__struct__: if_set(negation(atom()))) |> to_quoted_string() == + assert open_map(__struct__: if_set(opt_negation(atom()))) |> to_quoted_string() == "non_struct_map()" end end @@ -3662,21 +3809,21 @@ defmodule Module.Types.DescrTest do test "tuple difference" do # Large difference with no duplicates descr1 = - union( + opt_union( atom([:ignored, :reset]), tuple([atom([:font_style]), atom([:italic])]) ) descr2 = - union( + opt_union( atom([:ignored, :reset]), - union( + opt_union( tuple([atom([:font_style]), atom([:italic])]), Enum.reduce( for elem1 <- 1..5, elem2 <- 1..5 do tuple([atom([:"f#{elem1}"]), atom([:"s#{elem2}"])]) end, - &union/2 + &opt_union/2 ) ) ) @@ -3691,7 +3838,7 @@ defmodule Module.Types.DescrTest do open_map([ {:id, integer()}, {:name, binary()}, - {:age, union(integer(), atom())}, + {:age, opt_union(integer(), atom())}, {:email, binary()}, {:active, boolean()}, {:tags, list(atom())} @@ -3744,10 +3891,10 @@ defmodule Module.Types.DescrTest do name = :"name_#{i}" closed_map([__struct__: atom([name])] ++ [{name, binary()}]) end - |> Enum.reduce(&union/2) + |> Enum.reduce(&opt_union/2) - common = intersection(actual, expected) - difference(actual, common) + common = opt_intersection(actual, expected) + opt_difference(actual, common) end test "struct difference" do @@ -3838,7 +3985,7 @@ defmodule Module.Types.DescrTest do range = closed_map(__struct__: atom([Range]), first: integer(), last: integer(), step: integer()) - assert subtype?(range, Enum.reduce(entries, &union/2)) + assert subtype?(range, Enum.reduce(entries, &opt_union/2)) end end end diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 4114e81bbe5..5963b6b2364 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -34,7 +34,7 @@ defmodule Module.Types.ExprTest do describe "bitstrings" do test "integer alignment" do assert typecheck!(<>) == binary() - assert typecheck!(<>) == difference(bitstring(), binary()) + assert typecheck!(<>) == opt_difference(bitstring(), binary()) assert typecheck!(<>) == binary() assert typecheck!([size], <>) == bitstring() end @@ -58,11 +58,11 @@ defmodule Module.Types.ExprTest do assert typecheck!([1, 2 | 3]) == non_empty_list(integer(), integer()) assert typecheck!([1, 2 | [3, 4]]) == non_empty_list(integer()) - assert typecheck!([:ok, 123]) == non_empty_list(union(atom([:ok]), integer())) + assert typecheck!([:ok, 123]) == non_empty_list(opt_union(atom([:ok]), integer())) assert typecheck!([:ok | 123]) == non_empty_list(atom([:ok]), integer()) assert typecheck!([x], [:ok, x]) - |> equal?(union(non_empty_list(atom([:ok])), dynamic(non_empty_list(term())))) + |> equal?(opt_union(non_empty_list(atom([:ok])), dynamic(non_empty_list(term())))) assert typecheck!([x], [:ok | x]) == dynamic(non_empty_list(term(), term())) end @@ -78,7 +78,7 @@ defmodule Module.Types.ExprTest do end test "hd" do - assert typecheck!([x = [123, :foo]], hd(x)) == dynamic(union(atom([:foo]), integer())) + assert typecheck!([x = [123, :foo]], hd(x)) == dynamic(opt_union(atom([:foo]), integer())) assert typecheck!([x = [123 | :foo]], hd(x)) == dynamic(integer()) assert typeerror!(hd([])) |> strip_ansi() == @@ -113,10 +113,11 @@ defmodule Module.Types.ExprTest do end test "tl" do - assert typecheck!([x = [123, :foo]], tl(x)) == dynamic(list(union(atom([:foo]), integer()))) + assert typecheck!([x = [123, :foo]], tl(x)) == + dynamic(list(opt_union(atom([:foo]), integer()))) assert typecheck!([x = [123 | :foo]], tl(x)) == - dynamic(union(atom([:foo]), non_empty_list(integer(), atom([:foo])))) + dynamic(opt_union(atom([:foo]), non_empty_list(integer(), atom([:foo])))) assert typeerror!(tl([])) |> strip_ansi() == ~l""" @@ -172,7 +173,7 @@ defmodule Module.Types.ExprTest do [_ | _], %{} -> :list end) |> equal?( - intersection( + opt_intersection( fun( [non_empty_list(term(), term()), open_map()], dynamic(atom([:list])) @@ -424,7 +425,7 @@ defmodule Module.Types.ExprTest do {dynamic(), "URI.unknown/1 is undefined or private"} assert typewarn!(if(:rand.uniform() > 0.5, do: URI.unknown("foo"))) == - {dynamic() |> union(atom([nil])), "URI.unknown/1 is undefined or private"} + {dynamic() |> opt_union(atom([nil])), "URI.unknown/1 is undefined or private"} assert typewarn!(try(do: :ok, after: URI.unknown("foo"))) == {atom([:ok]), "URI.unknown/1 is undefined or private"} @@ -442,7 +443,7 @@ defmodule Module.Types.ExprTest do mod.to_string(:atom) ) ) == - {union(dynamic(), binary()), "GenServer.to_string/1 is undefined or private"} + {opt_union(dynamic(), binary()), "GenServer.to_string/1 is undefined or private"} end test "calling a function with none()" do @@ -559,7 +560,7 @@ defmodule Module.Types.ExprTest do res = mod.to_integer(arg) {arg, res} ) - ) == dynamic(tuple([union(binary(), non_empty_list(integer())), integer()])) + ) == dynamic(tuple([opt_union(binary(), non_empty_list(integer())), integer()])) assert typeerror!( [condition], @@ -663,7 +664,7 @@ defmodule Module.Types.ExprTest do <> {x, y} ) - ) == dynamic(tuple([union(float(), integer()), integer()])) + ) == dynamic(tuple([opt_union(float(), integer()), integer()])) end test "warnings" do @@ -1059,9 +1060,9 @@ defmodule Module.Types.ExprTest do ) |> 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]))) + |> opt_union(closed_map(bar: atom([:second]))) + |> opt_union(closed_map(foo: atom([:first]), bar: atom([:second]))) + |> opt_union(closed_map(bar: atom([:first]), foo: atom([:second]))) ) end @@ -1108,7 +1109,7 @@ defmodule Module.Types.ExprTest do %{foo_or_bar => :old, key => :new} ) ) == - union( + opt_union( closed_map([ {domain_key(:integer), atom([:new])}, {:foo, atom([:old])} @@ -1141,9 +1142,9 @@ defmodule Module.Types.ExprTest do ) |> 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]))) + |> opt_union(closed_map(key1: atom([:two!]), key2: atom([:one!]))) + |> opt_union(closed_map(key1: atom([:one!]), key2: atom([:two!]))) + |> opt_union(closed_map(key1: atom([:two!]), key2: atom([:two]))) ) assert typeerror!([x = :foo], %{x | x: :zero}) == ~l""" @@ -1558,7 +1559,7 @@ defmodule Module.Types.ExprTest do y ) - ) == union(atom([:foo]), dynamic()) + ) == opt_union(atom([:foo]), dynamic()) end test "in dynamic mode" do @@ -1572,9 +1573,9 @@ defmodule Module.Types.ExprTest do end test "min/max" do - assert typecheck!(min(123, 456.0)) == union(integer(), float()) + assert typecheck!(min(123, 456.0)) == opt_union(integer(), float()) # min/max uses parametric types, which will carry dynamic regardless of being a strong arrow - assert typecheck!([x = 123, y = 456.0], min(x, y)) == dynamic(union(integer(), float())) + assert typecheck!([x = 123, y = 456.0], min(x, y)) == dynamic(opt_union(integer(), float())) end test "warns when comparison is constant" do @@ -1830,7 +1831,7 @@ defmodule Module.Types.ExprTest do x ) ) == - dynamic(union(atom([:foo, :bar, :baz]), union(integer(), float()))) + dynamic(opt_union(atom([:foo, :bar, :baz]), opt_union(integer(), float()))) assert typecheck!( [x], @@ -1839,7 +1840,7 @@ defmodule Module.Types.ExprTest do x ) ) == - dynamic(negation(atom([:foo, :bar, :baz]))) + dynamic(opt_negation(atom([:foo, :bar, :baz]))) assert typecheck!( [x], @@ -1848,7 +1849,7 @@ defmodule Module.Types.ExprTest do x ) ) == - dynamic(negation(atom([:foo, :bar, :baz]))) + dynamic(opt_negation(atom([:foo, :bar, :baz]))) assert typecheck!( [x], @@ -1857,7 +1858,7 @@ defmodule Module.Types.ExprTest do x ) ) == - dynamic(union(atom([:foo, :bar, :baz]), union(integer(), float()))) + dynamic(opt_union(atom([:foo, :bar, :baz]), opt_union(integer(), float()))) assert typeerror!([x = :ok], true = x in [:foo, 1.0, :baz]) =~ ~l""" comparison between distinct types found: @@ -1970,7 +1971,7 @@ defmodule Module.Types.ExprTest do end ) == dynamic( - union( + opt_union( tuple([atom([:binary]), binary()]), tuple([atom([:bitstring]), bitstring_no_binary()]) ) @@ -2002,9 +2003,9 @@ defmodule Module.Types.ExprTest do dynamic( tuple([ term(), - union( + opt_union( tuple([atom([:first]), atom([:foo])]), - tuple([atom([:second]), union(binary(), atom([nil]))]) + tuple([atom([:second]), opt_union(binary(), atom([nil]))]) ) ]) ) @@ -2020,7 +2021,7 @@ defmodule Module.Types.ExprTest do x ) - ) == dynamic(negation(integer())) + ) == dynamic(opt_negation(integer())) assert typecheck!( [x], @@ -2031,7 +2032,7 @@ defmodule Module.Types.ExprTest do x ) - ) == dynamic(negation(atom([:foo]))) + ) == dynamic(opt_negation(atom([:foo]))) assert typecheck!( [x], @@ -2042,7 +2043,7 @@ defmodule Module.Types.ExprTest do x ) - ) == dynamic(negation(union(open_map(), integer()))) + ) == dynamic(opt_negation(opt_union(open_map(), integer()))) # When it is not precise enough, we don't filter assert typecheck!( @@ -2119,7 +2120,7 @@ defmodule Module.Types.ExprTest do x ) - ) == dynamic(union(open_map(), tuple([]))) + ) == dynamic(opt_union(open_map(), tuple([]))) end test "warns on redundant clauses" do @@ -2234,7 +2235,7 @@ defmodule Module.Types.ExprTest do end ) == dynamic( - union( + opt_union( tuple([atom([:ok]), binary()]), tuple([atom([:error]), atom([nil])]) ) @@ -2259,7 +2260,7 @@ defmodule Module.Types.ExprTest do end ) ) - |> equal?(union(dynamic(), integer())) + |> equal?(opt_union(dynamic(), integer())) assert typeerror!( [bin?, int?, bool?], @@ -2327,7 +2328,7 @@ defmodule Module.Types.ExprTest do end ) == dynamic( - union( + opt_union( tuple([atom([:ok]), binary()]), tuple([atom([:error]), atom([nil])]) ) @@ -2531,7 +2532,7 @@ defmodule Module.Types.ExprTest do after x -> x end - ) == dynamic(union(integer(), atom([:infinity]))) + ) == dynamic(opt_union(integer(), atom([:infinity]))) end test "resets branches" do @@ -2556,7 +2557,7 @@ defmodule Module.Types.ExprTest do x when is_binary(x) -> :ok y -> {:other, y} end - ) == union(atom([:ok]), dynamic(tuple([atom([:other]), negation(binary())]))) + ) == opt_union(atom([:ok]), dynamic(tuple([atom([:other]), opt_negation(binary())]))) end test "warns on redundant clauses" do @@ -2673,7 +2674,7 @@ defmodule Module.Types.ExprTest do x when is_binary(x) -> :ok y -> {:other, y} end - ) == union(atom([:ok]), dynamic(tuple([atom([:other]), negation(binary())]))) + ) == opt_union(atom([:ok]), dynamic(tuple([atom([:other]), opt_negation(binary())]))) end test "catch: warns on redundant clauses" do @@ -2707,7 +2708,10 @@ defmodule Module.Types.ExprTest do y -> {:other, y} end ) == - union(atom([:ok, :unused]), dynamic(tuple([atom([:other]), negation(binary())]))) + opt_union( + atom([:ok, :unused]), + dynamic(tuple([atom([:other]), opt_negation(binary())])) + ) end test "else: warns on redundant clauses" do @@ -2764,7 +2768,7 @@ defmodule Module.Types.ExprTest do end ) == dynamic( - union( + opt_union( closed_map( __struct__: atom([ArgumentError]), __exception__: term(), @@ -2962,9 +2966,9 @@ defmodule Module.Types.ExprTest do end ) == dynamic( - union( + opt_union( tuple([atom([:first]), binary()]), - tuple([atom([:second]), negation(binary())]) + tuple([atom([:second]), opt_negation(binary())]) ) ) @@ -2992,7 +2996,7 @@ defmodule Module.Types.ExprTest do x ) - ) == dynamic(negation(binary())) + ) == dynamic(opt_negation(binary())) assert typecheck!( [x], @@ -3018,8 +3022,8 @@ defmodule Module.Types.ExprTest do ) == dynamic( tuple([atom([:first]), binary()]) - |> union(tuple([atom([:second]), atom()])) - |> union(tuple([atom([:third]), negation(union(binary(), atom()))])) + |> opt_union(tuple([atom([:second]), atom()])) + |> opt_union(tuple([atom([:third]), opt_negation(opt_union(binary(), atom()))])) ) # Negated types do not leak through @@ -3077,7 +3081,7 @@ defmodule Module.Types.ExprTest do true -> :otherwise end - ) == union(boolean(), atom([:otherwise])) + ) == opt_union(boolean(), atom([:otherwise])) # Test with multiple clauses assert typecheck!( @@ -3095,7 +3099,7 @@ defmodule Module.Types.ExprTest do end) -> float? end - ) == union(boolean(), atom([:binary, :atom])) + ) == opt_union(boolean(), atom([:binary, :atom])) end end @@ -3169,10 +3173,10 @@ defmodule Module.Types.ExprTest do assert typecheck!([binary, other], for(<>, do: x, into: other)) == dynamic() assert typecheck!([enum], for(x <- enum, do: x)) == - union(list(dynamic()), empty_list()) + opt_union(list(dynamic()), empty_list()) assert typecheck!([enum], for(x <- enum, do: x, into: [])) == - union(list(dynamic()), empty_list()) + opt_union(list(dynamic()), empty_list()) assert typecheck!([enum], for(x <- enum, do: <>, into: "")) |> equal?(binary()) assert typecheck!([enum, other], for(x <- enum, do: x, into: other)) == dynamic() @@ -3183,7 +3187,7 @@ defmodule Module.Types.ExprTest do into = if :rand.uniform() > 0.5, do: [], else: "0" for(<>, do: x, into: into) ) - ) == union(bitstring(), list(term())) + ) == opt_union(bitstring(), list(term())) assert typecheck!( [binary, empty_list = []], @@ -3191,7 +3195,7 @@ defmodule Module.Types.ExprTest do into = if :rand.uniform() > 0.5, do: empty_list, else: "0" for(<>, do: x, into: into) ) - ) == union(bitstring(), list(term())) + ) == opt_union(bitstring(), list(term())) end test ":into inference" do @@ -3237,7 +3241,7 @@ defmodule Module.Types.ExprTest do :ok -> 1 _ -> 2.0 end - ) == union(atom([:ok]), union(integer(), float())) + ) == opt_union(atom([:ok]), opt_union(integer(), float())) end test ":reduce inference" do @@ -3281,7 +3285,7 @@ defmodule Module.Types.ExprTest do with y when is_binary(y) <- System.get_env(x) do {:ok, y} end - ) == dynamic(union(tuple([atom([:ok]), binary()]), atom([nil]))) + ) == dynamic(opt_union(tuple([atom([:ok]), binary()]), atom([nil]))) assert typecheck!( [x], @@ -3289,8 +3293,8 @@ defmodule Module.Types.ExprTest do :not_precise end ) == - dynamic(union(binary(), atom([nil, :not_precise]))) - |> union(atom([:not_precise])) + dynamic(opt_union(binary(), atom([nil, :not_precise]))) + |> opt_union(atom([:not_precise])) assert typecheck!( [x], @@ -3298,7 +3302,7 @@ defmodule Module.Types.ExprTest do {:ok, x} end ) == - dynamic(union(tuple([atom([:ok]), binary()]), atom([nil]))) + dynamic(opt_union(tuple([atom([:ok]), binary()]), atom([nil]))) assert typecheck!( [x], @@ -3306,8 +3310,8 @@ defmodule Module.Types.ExprTest do {:ok, x} end ) == - dynamic(union(tuple([atom([:ok]), binary()]), atom([nil]))) - |> union(atom([nil])) + dynamic(opt_union(tuple([atom([:ok]), binary()]), atom([nil]))) + |> opt_union(atom([nil])) end test "warns on non-matching generators" do @@ -3400,7 +3404,7 @@ defmodule Module.Types.ExprTest do list( closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())) ) - |> union(atom([nil])) + |> opt_union(atom([nil])) end test "behaviour_info/1" do diff --git a/lib/elixir/test/elixir/module/types/infer_test.exs b/lib/elixir/test/elixir/module/types/infer_test.exs index 20929ab6664..c3e69a39f96 100644 --- a/lib/elixir/test/elixir/module/types/infer_test.exs +++ b/lib/elixir/test/elixir/module/types/infer_test.exs @@ -62,7 +62,7 @@ defmodule Module.Types.InferTest do end assert types[{:fun, 1}] == - {:infer, [union(atom([:ok, :error]), binary())], + {:infer, [opt_union(atom([:ok, :error]), binary())], [ {[atom([:ok])], atom([:one])}, {[binary()], atom([:two, :three, :four])}, @@ -138,7 +138,7 @@ defmodule Module.Types.InferTest do end end - number = union(integer(), float()) + number = opt_union(integer(), float()) assert types[{:fun, 1}] == {:infer, nil, [{[open_map(foo: number, bar: number)], dynamic(number)}]} @@ -155,7 +155,7 @@ defmodule Module.Types.InferTest do assert types[{:parse, 1}] == {:infer, nil, - [{[term()], dynamic(union(atom([:error]), tuple([integer(), binary()])))}]} + [{[term()], dynamic(opt_union(atom([:error]), tuple([integer(), binary()])))}]} end test "from private functions", config do @@ -359,7 +359,7 @@ defmodule Module.Types.InferTest do assert signature == [ {[tuple([term(), term(), term()])], dynamic()}, - {[negation(tuple([term(), term(), term()]))], atom([nil])} + {[opt_negation(tuple([term(), term(), term()]))], atom([nil])} ] end end diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 6624bcd6cf9..c79e3373344 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -138,9 +138,11 @@ defmodule Module.Types.IntegrationTest do assert itself_arg.(Itself.Integer) == dynamic(integer()) assert itself_arg.(Itself.List) == - dynamic(union(empty_list(), non_empty_list(term(), term()))) + dynamic(opt_union(empty_list(), non_empty_list(term(), term()))) + + assert itself_arg.(Itself.Map) == + dynamic(open_map(__struct__: if_set(opt_negation(atom())))) - assert itself_arg.(Itself.Map) == dynamic(open_map(__struct__: if_set(negation(atom())))) assert itself_arg.(Itself.Port) == dynamic(port()) assert itself_arg.(Itself.PID) == dynamic(pid()) assert itself_arg.(Itself.Reference) == dynamic(reference()) diff --git a/lib/elixir/test/elixir/module/types/map_test.exs b/lib/elixir/test/elixir/module/types/map_test.exs index 9f706d9004b..d871af0a938 100644 --- a/lib/elixir/test/elixir/module/types/map_test.exs +++ b/lib/elixir/test/elixir/module/types/map_test.exs @@ -25,20 +25,20 @@ defmodule Module.Types.MapTest do describe ":maps.take/2" do test "checking" do assert typecheck!(:maps.take(:key, %{key: 123})) == - tuple([integer(), empty_map()]) |> union(atom([:error])) + tuple([integer(), empty_map()]) |> opt_union(atom([:error])) assert typecheck!([x], :maps.take(:key, x)) == - union( + opt_union( dynamic(tuple([term(), open_map(key: not_set())])), atom([:error]) ) assert typecheck!([condition?, x], :maps.take(if(condition?, do: :foo, else: :bar), x)) == - union( + opt_union( dynamic( tuple([ term(), - union( + opt_union( open_map(foo: not_set()), open_map(bar: not_set()) ) @@ -48,7 +48,7 @@ defmodule Module.Types.MapTest do ) assert typecheck!([x], :maps.take(123, x)) == - union( + opt_union( dynamic(tuple([term(), open_map()])), atom([:error]) ) @@ -105,7 +105,7 @@ defmodule Module.Types.MapTest do [condition?], Map.delete(%{foo: 123}, if(condition?, do: :foo, else: :bar)) ) == - union( + opt_union( empty_map(), closed_map(foo: integer()) ) @@ -140,22 +140,22 @@ defmodule Module.Types.MapTest do describe "Map.fetch/2" do test "checking" do assert typecheck!(Map.fetch(%{key: 123}, :key)) == - tuple([atom([:ok]), integer()]) |> union(atom([:error])) + tuple([atom([:ok]), integer()]) |> opt_union(atom([:error])) assert typecheck!(:maps.find(:key, %{key: 123})) == - tuple([atom([:ok]), integer()]) |> union(atom([:error])) + tuple([atom([:ok]), integer()]) |> opt_union(atom([:error])) assert typecheck!([x], Map.fetch(x, :key)) == - dynamic(tuple([atom([:ok]), term()])) |> union(atom([:error])) + dynamic(tuple([atom([:ok]), term()])) |> opt_union(atom([:error])) # If one of them succeeds, we are still fine! assert typecheck!( [condition?], Map.fetch(%{foo: 123}, if(condition?, do: :foo, else: :bar)) - ) == tuple([atom([:ok]), integer()]) |> union(atom([:error])) + ) == tuple([atom([:ok]), integer()]) |> opt_union(atom([:error])) assert typecheck!([x], Map.fetch(x, 123)) == - dynamic(tuple([atom([:ok]), term()])) |> union(atom([:error])) + dynamic(tuple([atom([:ok]), term()])) |> opt_union(atom([:error])) end test "inference" do @@ -279,7 +279,7 @@ defmodule Module.Types.MapTest do ) ) == closed_map(key1: if_set(integer()), key2: if_set(integer())) - |> difference(empty_map()) + |> opt_difference(empty_map()) assert typecheck!( [condition?], @@ -298,7 +298,7 @@ defmodule Module.Types.MapTest do value = typecheck!([condition?, x], if(condition?, do: :value, else: x)) optional = if_set(value) - assert equal?(optional, union(if_set(atom([:value])), dynamic(if_set(term())))) + assert equal?(optional, opt_union(if_set(atom([:value])), dynamic(if_set(term())))) refute equal?(optional, dynamic(if_set(term()))) end @@ -370,17 +370,17 @@ defmodule Module.Types.MapTest do describe "Map.get/2" do test "checking" do - assert typecheck!(Map.get(%{key: 123}, :key)) == integer() |> union(atom([nil])) + assert typecheck!(Map.get(%{key: 123}, :key)) == integer() |> opt_union(atom([nil])) - assert typecheck!([x], Map.get(x, :key)) == dynamic(term()) |> union(atom([nil])) + assert typecheck!([x], Map.get(x, :key)) == dynamic(term()) |> opt_union(atom([nil])) # If one of them succeeds, we are still fine! assert typecheck!( [condition?], Map.get(%{foo: 123}, if(condition?, do: :foo, else: :bar)) - ) == integer() |> union(atom([nil])) + ) == integer() |> opt_union(atom([nil])) - assert typecheck!([x], Map.get(x, 123)) == dynamic(term()) |> union(atom([nil])) + assert typecheck!([x], Map.get(x, 123)) == dynamic(term()) |> opt_union(atom([nil])) end test "inference" do @@ -417,7 +417,7 @@ defmodule Module.Types.MapTest do test "checking" do assert typecheck!(Map.get(%{key: 123}, :key, 123)) == integer() - assert typecheck!([x], Map.get(x, :key, 123)) == dynamic(term()) |> union(integer()) + assert typecheck!([x], Map.get(x, :key, 123)) == dynamic(term()) |> opt_union(integer()) # If one of them succeeds, we are still fine! assert typecheck!( @@ -425,7 +425,7 @@ defmodule Module.Types.MapTest do Map.get(%{foo: 123}, if(condition?, do: :foo, else: :bar), 123) ) == integer() - assert typecheck!([x], Map.get(x, 123, 123)) == dynamic(term()) |> union(integer()) + assert typecheck!([x], Map.get(x, 123, 123)) == dynamic(term()) |> opt_union(integer()) end test "inference" do @@ -543,7 +543,7 @@ defmodule Module.Types.MapTest do Map.keys(x) ) ) == - non_empty_list(union(atom([:a]), atom([:b]))) + non_empty_list(opt_union(atom([:a]), atom([:b]))) assert typecheck!( ( @@ -553,8 +553,8 @@ defmodule Module.Types.MapTest do ) == non_empty_list( atom([:a]) - |> union(atom([:b])) - |> union(binary()) + |> opt_union(atom([:b])) + |> opt_union(binary()) ) end @@ -576,7 +576,7 @@ defmodule Module.Types.MapTest do describe "Map.pop/2" do test "checking" do assert typecheck!(Map.pop(%{key: 123}, :key)) == - tuple([union(integer(), atom([nil])), empty_map()]) + tuple([opt_union(integer(), atom([nil])), empty_map()]) assert typecheck!([x], Map.pop(x, :key)) == dynamic(tuple([term(), open_map(key: not_set())])) @@ -585,7 +585,7 @@ defmodule Module.Types.MapTest do dynamic( tuple([ term(), - union( + opt_union( open_map(foo: not_set()), open_map(bar: not_set()) ) @@ -639,9 +639,9 @@ defmodule Module.Types.MapTest do describe "Map.pop_lazy/3" do test "checking" do assert typecheck!(Map.pop_lazy(%{key: 123}, :key, fn -> :error end)) == - union( + opt_union( tuple([integer(), empty_map()]), - dynamic(tuple([union(integer(), atom([:error])), empty_map()])) + dynamic(tuple([opt_union(integer(), atom([:error])), empty_map()])) ) assert typecheck!([x], Map.pop_lazy(x, :key, fn -> :error end)) == @@ -654,7 +654,7 @@ defmodule Module.Types.MapTest do dynamic( tuple([ term(), - union( + opt_union( open_map(foo: not_set()), open_map(bar: not_set()) ) @@ -671,7 +671,7 @@ defmodule Module.Types.MapTest do ) ) |> equal?( - union( + opt_union( tuple([atom([:before]), map]), dynamic(tuple([atom([:before, :after]), map])) ) @@ -723,7 +723,7 @@ defmodule Module.Types.MapTest do describe "Map.pop/3" do test "checking" do assert typecheck!(Map.pop(%{key: 123}, :key, :error)) == - tuple([union(integer(), atom([:error])), empty_map()]) + tuple([opt_union(integer(), atom([:error])), empty_map()]) assert typecheck!([x], Map.pop(x, :key, :error)) == dynamic(tuple([term(), open_map(key: not_set())])) @@ -732,7 +732,7 @@ defmodule Module.Types.MapTest do dynamic( tuple([ term(), - union( + opt_union( open_map(foo: not_set()), open_map(bar: not_set()) ) @@ -795,7 +795,7 @@ defmodule Module.Types.MapTest do dynamic( tuple([ term(), - union( + opt_union( open_map(foo: not_set()), open_map(bar: not_set()) ) @@ -857,7 +857,7 @@ defmodule Module.Types.MapTest do [condition?], Map.put(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123") ) == - union( + opt_union( closed_map(foo: binary()), closed_map(foo: integer(), bar: binary()) ) @@ -912,10 +912,10 @@ defmodule Module.Types.MapTest do assert typecheck!( [condition?], Map.put_new_lazy(%{foo: 123}, if(condition?, do: :foo, else: :bar), fn -> "123" end) - ) == union(closed_map(foo: integer()), closed_map(foo: integer(), bar: binary())) + ) == opt_union(closed_map(foo: integer()), closed_map(foo: integer(), bar: binary())) assert typecheck!([], Map.put_new_lazy(%{789 => "binary"}, 123, fn -> 456 end)) == - closed_map([{domain_key(:integer), union(binary(), integer())}]) + closed_map([{domain_key(:integer), opt_union(binary(), integer())}]) assert typecheck!([x], Map.put_new_lazy(x, 123, fn -> 456 end)) == dynamic(open_map()) end @@ -974,10 +974,10 @@ defmodule Module.Types.MapTest do assert typecheck!( [condition?], Map.put_new(%{foo: 123}, if(condition?, do: :foo, else: :bar), "123") - ) == union(closed_map(foo: integer()), closed_map(foo: integer(), bar: binary())) + ) == opt_union(closed_map(foo: integer()), closed_map(foo: integer(), bar: binary())) assert typecheck!([], Map.put_new(%{789 => "binary"}, 123, 456)) == - closed_map([{domain_key(:integer), union(binary(), integer())}]) + closed_map([{domain_key(:integer), opt_union(binary(), integer())}]) assert typecheck!([x], Map.put_new(x, 123, 456)) == dynamic(open_map()) end @@ -1086,7 +1086,7 @@ defmodule Module.Types.MapTest do ) ) == dynamic( - union( + opt_union( closed_map(key1: atom([:foo]), key2: float()), closed_map(key1: integer(), key2: atom([:bar])) ) @@ -1095,7 +1095,7 @@ defmodule Module.Types.MapTest do assert typecheck!([x], Map.replace_lazy(x, 123, fn _ -> 456 end)) == dynamic(open_map()) assert typecheck!([], Map.replace_lazy(%{123 => 456}, 123, fn x -> x * 1.0 end)) == - dynamic(closed_map([{domain_key(:integer), union(integer(), float())}])) + dynamic(closed_map([{domain_key(:integer), opt_union(integer(), float())}])) end test "inference" do @@ -1220,7 +1220,7 @@ defmodule Module.Types.MapTest do ) ) == non_empty_list( - union(tuple([atom([:a]), integer()]), tuple([atom([:b]), binary()])) + opt_union(tuple([atom([:a]), integer()]), tuple([atom([:b]), binary()])) ) assert typecheck!( @@ -1231,8 +1231,8 @@ defmodule Module.Types.MapTest do ) == non_empty_list( tuple([atom([:a]), integer()]) - |> union(tuple([atom([:b]), binary()])) - |> union(tuple([binary(), atom([:three])])) + |> opt_union(tuple([atom([:b]), binary()])) + |> opt_union(tuple([binary(), atom([:three])])) ) end @@ -1270,7 +1270,7 @@ defmodule Module.Types.MapTest do end) ) == dynamic( - union( + opt_union( closed_map(foo: binary()), closed_map(foo: integer(), bar: atom([:default])) ) @@ -1290,7 +1290,7 @@ defmodule Module.Types.MapTest do ) ) == dynamic( - union( + opt_union( closed_map(key1: atom([:foo]), key2: float()), closed_map(key1: integer(), key2: atom([:bar])) ) @@ -1301,7 +1301,7 @@ defmodule Module.Types.MapTest do integer_to_integer_float_atom = dynamic( closed_map([ - {domain_key(:integer), integer() |> union(float()) |> union(atom([:default]))} + {domain_key(:integer), integer() |> opt_union(float()) |> opt_union(atom([:default]))} ]) ) @@ -1364,7 +1364,7 @@ defmodule Module.Types.MapTest do end) ) == dynamic( - union( + opt_union( closed_map(key1: atom([:foo]), key2: float()), closed_map(key1: integer(), key2: atom([:bar])) ) @@ -1373,10 +1373,10 @@ defmodule Module.Types.MapTest do assert typecheck!([x], Map.update!(x, 123, fn _ -> 456 end)) == dynamic(open_map()) assert typecheck!([], Map.update!(%{123 => 456}, 123, fn x -> x * 1.0 end)) == - dynamic(closed_map([{domain_key(:integer), union(integer(), float())}])) + dynamic(closed_map([{domain_key(:integer), opt_union(integer(), float())}])) assert typecheck!([], Map.update!(%{123 => 456}, 456, fn x -> x * 1.0 end)) == - dynamic(closed_map([{domain_key(:integer), union(integer(), float())}])) + dynamic(closed_map([{domain_key(:integer), opt_union(integer(), float())}])) end test "inference" do @@ -1454,7 +1454,7 @@ defmodule Module.Types.MapTest do Map.values(x) ) ) == - non_empty_list(union(integer(), binary())) + non_empty_list(opt_union(integer(), binary())) assert typecheck!( ( @@ -1464,8 +1464,8 @@ defmodule Module.Types.MapTest do ) == non_empty_list( integer() - |> union(binary()) - |> union(atom([:three])) + |> opt_union(binary()) + |> opt_union(atom([:three])) ) end diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index b3cad4f891c..2cc61cfae8f 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -312,7 +312,7 @@ defmodule Module.Types.PatternTest do dynamic(non_empty_list(integer(), atom([:foo]))) assert typecheck!([x = [1, 2, 3 | y], y = [1.0, 2.0, 3.0]], x) == - dynamic(non_empty_list(union(integer(), float()))) + dynamic(non_empty_list(opt_union(integer(), float()))) assert typecheck!([x = [:ok | z]], {x, z}) == dynamic(tuple([non_empty_list(term(), term()), term()])) @@ -336,7 +336,7 @@ defmodule Module.Types.PatternTest do dynamic(non_empty_list(integer(), atom([:foo]))) assert typecheck!([x = [1, 2, 3] ++ y, y = [1.0, 2.0, 3.0]], x) == - dynamic(non_empty_list(union(integer(), float()))) + dynamic(non_empty_list(opt_union(integer(), float()))) end test "with lists inside tuples inside lists" do @@ -382,7 +382,7 @@ defmodule Module.Types.PatternTest do describe "bitstrings" do test "alignment" do assert typecheck!([<<_>> = x], x) == dynamic(binary()) - assert typecheck!([<<_::1>> = x], x) == dynamic(difference(bitstring(), binary())) + assert typecheck!([<<_::1>> = x], x) == dynamic(opt_difference(bitstring(), binary())) assert typecheck!([<<_::4, _::4>> = x], x) == dynamic(binary()) assert typecheck!([<> = x], x) == dynamic(bitstring()) end @@ -658,22 +658,22 @@ defmodule Module.Types.PatternTest do assert typecheck!([x], is_struct(x, URI), x) == dynamic(open_map(__struct__: atom([URI]))) assert typecheck!([x], not is_struct(x), x) - |> equal?(dynamic(negation(open_map(__struct__: atom())))) + |> equal?(dynamic(opt_negation(open_map(__struct__: atom())))) assert typecheck!([x], not is_struct(x, URI), x) == dynamic() end test "is_binary/1" do assert typecheck!([x], is_binary(x), x) == dynamic(binary()) - assert typecheck!([x], not is_binary(x), x) == dynamic(negation(binary())) + assert typecheck!([x], not is_binary(x), x) == dynamic(opt_negation(binary())) assert typecheck!([x], is_bitstring(x), x) == dynamic(bitstring()) - assert typecheck!([x], not is_bitstring(x), x) == dynamic(negation(bitstring())) + assert typecheck!([x], not is_bitstring(x), x) == dynamic(opt_negation(bitstring())) end test "is_function/2" do assert typecheck!([x], is_function(x, 3), x) == dynamic(fun(3)) - assert typecheck!([x], not is_function(x, 3), x) == dynamic(negation(fun(3))) + assert typecheck!([x], not is_function(x, 3), x) == dynamic(opt_negation(fun(3))) end test "is_map_key/2" do @@ -759,30 +759,32 @@ defmodule Module.Types.PatternTest do end test "when checks" do - assert typecheck!([x], is_binary(x) when is_atom(x), x) == dynamic(union(binary(), atom())) + assert typecheck!([x], is_binary(x) when is_atom(x), x) == + dynamic(opt_union(binary(), atom())) assert typecheck!([x], is_binary(x) when map_size(x) >= 0, x) == - dynamic(union(binary(), open_map())) + dynamic(opt_union(binary(), open_map())) assert typecheck!([x], tuple_size(x) >= 0 when map_size(x) >= 0, x) == - dynamic(union(tuple(), open_map())) + dynamic(opt_union(tuple(), open_map())) assert typecheck!([x, y], is_binary(x) when is_atom(y), {x, y}) == dynamic(tuple([term(), term()])) # with annotated hd/tl assert typecheck!([x], is_binary(x) when is_atom(hd(x)), x) == - dynamic(union(binary(), non_empty_list(term(), term()))) + dynamic(opt_union(binary(), non_empty_list(term(), term()))) assert typecheck!([x], is_binary(hd(x)) when is_atom(hd(x)), x) == dynamic(non_empty_list(term(), term())) end test "conditional checks (andalso/orelse)" do - assert typecheck!([x], is_binary(x) or is_atom(x), x) == dynamic(union(binary(), atom())) + assert typecheck!([x], is_binary(x) or is_atom(x), x) == + dynamic(opt_union(binary(), atom())) assert typecheck!([x], is_binary(x) or map_size(x) >= 0, x) == - dynamic(union(binary(), open_map())) + dynamic(opt_union(binary(), open_map())) assert typecheck!([x, y], is_binary(x) or is_atom(y), {x, y}) == dynamic(tuple([term(), term()])) @@ -834,7 +836,7 @@ defmodule Module.Types.PatternTest do dynamic(integer()) assert typecheck!([x, y], is_number(min(x, y)), min(x, y)) == - dynamic(union(integer(), float())) + dynamic(opt_union(integer(), float())) assert typecheck!([m], elem(m.pair, max(m.x, m.y)) > 0, m) == dynamic(open_map(pair: open_tuple([]), x: integer(), y: integer())) @@ -848,7 +850,7 @@ defmodule Module.Types.PatternTest do test "conditional checks (and/or)" do assert typecheck!([x], :erlang.or(is_binary(x), is_atom(x)), x) == - dynamic(union(binary(), atom())) + dynamic(opt_union(binary(), atom())) assert typecheck!([x], :erlang.or(is_binary(x), map_size(x) >= 0), x) == dynamic(open_map()) @@ -986,7 +988,7 @@ defmodule Module.Types.PatternTest do end test "with number literals" do - assert typecheck!([x], x == 1, x) == dynamic(union(integer(), float())) + assert typecheck!([x], x == 1, x) == dynamic(opt_union(integer(), float())) assert typecheck!([x], x === 1, x) == dynamic(integer()) assert typecheck!([x], x in [1, 2, 3], x) == dynamic(integer()) assert typecheck!([x], not (x == 1), x) == dynamic() @@ -995,10 +997,10 @@ defmodule Module.Types.PatternTest do assert typecheck!([x], x != 1, x) == dynamic() assert typecheck!([x], x !== 1, x) == dynamic() - assert typecheck!([x], not (x != 1), x) == dynamic(union(integer(), float())) + assert typecheck!([x], not (x != 1), x) == dynamic(opt_union(integer(), float())) assert typecheck!([x], not (x !== 1), x) == dynamic(integer()) - assert typecheck!([x], x == 1.0, x) == dynamic(union(integer(), float())) + assert typecheck!([x], x == 1.0, x) == dynamic(opt_union(integer(), float())) assert typecheck!([x], x === 1.0, x) == dynamic(float()) assert typecheck!([x], x in [1.0, 2.0, 3.0], x) == dynamic(float()) assert typecheck!([x], not (x == 1.0), x) == dynamic() @@ -1007,7 +1009,7 @@ defmodule Module.Types.PatternTest do assert typecheck!([x], x != 1.0, x) == dynamic() assert typecheck!([x], x !== 1.0, x) == dynamic() - assert typecheck!([x], not (x != 1.0), x) == dynamic(union(integer(), float())) + assert typecheck!([x], not (x != 1.0), x) == dynamic(opt_union(integer(), float())) assert typecheck!([x], not (x !== 1.0), x) == dynamic(float()) end @@ -1015,35 +1017,39 @@ defmodule Module.Types.PatternTest do assert typecheck!([x], x == :foo, x) == dynamic(atom([:foo])) assert typecheck!([x], x === :foo, x) == dynamic(atom([:foo])) assert typecheck!([x], x in [:foo, :bar, :baz], x) == dynamic(atom([:foo, :bar, :baz])) - assert typecheck!([x], not (x == :foo), x) == dynamic(negation(atom([:foo]))) - assert typecheck!([x], not (x === :foo), x) == dynamic(negation(atom([:foo]))) - assert typecheck!([x], x not in [:foo, :bar], x) == dynamic(negation(atom([:foo, :bar]))) + assert typecheck!([x], not (x == :foo), x) == dynamic(opt_negation(atom([:foo]))) + assert typecheck!([x], not (x === :foo), x) == dynamic(opt_negation(atom([:foo]))) - assert typecheck!([x], x != :foo, x) == dynamic(negation(atom([:foo]))) - assert typecheck!([x], x !== :foo, x) == dynamic(negation(atom([:foo]))) + assert typecheck!([x], x not in [:foo, :bar], x) == + dynamic(opt_negation(atom([:foo, :bar]))) + + assert typecheck!([x], x != :foo, x) == dynamic(opt_negation(atom([:foo]))) + assert typecheck!([x], x !== :foo, x) == dynamic(opt_negation(atom([:foo]))) assert typecheck!([x], not (x != :foo), x) == dynamic(atom([:foo])) assert typecheck!([x], not (x !== :foo), x) == dynamic(atom([:foo])) assert typecheck!([x], x == [], x) == dynamic(empty_list()) assert typecheck!([x], x === [], x) == dynamic(empty_list()) - assert typecheck!([x], not (x == []), x) == dynamic(negation(empty_list())) - assert typecheck!([x], not (x === []), x) == dynamic(negation(empty_list())) + assert typecheck!([x], not (x == []), x) == dynamic(opt_negation(empty_list())) + assert typecheck!([x], not (x === []), x) == dynamic(opt_negation(empty_list())) - assert typecheck!([x], x != [], x) == dynamic(negation(empty_list())) - assert typecheck!([x], x !== [], x) == dynamic(negation(empty_list())) + assert typecheck!([x], x != [], x) == dynamic(opt_negation(empty_list())) + assert typecheck!([x], x !== [], x) == dynamic(opt_negation(empty_list())) assert typecheck!([x], not (x != []), x) == dynamic(empty_list()) assert typecheck!([x], not (x !== []), x) == dynamic(empty_list()) - assert typecheck!([x], x != %{}, x) == dynamic(negation(empty_map())) - assert typecheck!([x = %{}], x != %{}, x) == dynamic(difference(open_map(), empty_map())) + assert typecheck!([x], x != %{}, x) == dynamic(opt_negation(empty_map())) + + assert typecheck!([x = %{}], x != %{}, x) == + dynamic(opt_difference(open_map(), empty_map())) end test "mixed-in" do assert typecheck!([x], x in [:foo, 1, :bar, 2.0, :baz], x) == - dynamic(union(atom([:foo, :bar, :baz]), union(integer(), float()))) + dynamic(opt_union(atom([:foo, :bar, :baz]), opt_union(integer(), float()))) assert typecheck!([x], x not in [:foo, 1, :bar, 2.0, :baz], x) == - dynamic(negation(atom([:foo, :bar, :baz]))) + dynamic(opt_negation(atom([:foo, :bar, :baz]))) end test "with singleton literals and composite types" do @@ -1055,10 +1061,10 @@ defmodule Module.Types.PatternTest do test "with expressions" do # With numbers assert typecheck!([x, y], x == y and y === 42, {x, y}) == - dynamic(tuple([union(integer(), float()), integer()])) + dynamic(tuple([opt_union(integer(), float()), integer()])) assert typecheck!([x, y], x == y and x === 42, {x, y}) == - dynamic(tuple([integer(), union(integer(), float())])) + dynamic(tuple([integer(), opt_union(integer(), float())])) assert typecheck!([x, y], x != y and y === 42, {x, y}) == dynamic(tuple([term(), integer()])) @@ -1094,13 +1100,13 @@ defmodule Module.Types.PatternTest do # With composite types assert typecheck!([x, y], x == {:ok, y} and y === 42, {x, y}) == - dynamic(tuple([tuple([atom([:ok]), union(integer(), float())]), integer()])) + dynamic(tuple([tuple([atom([:ok]), opt_union(integer(), float())]), integer()])) assert typecheck!([x, y], x != {:ok, y} and y === 42, {x, y}) == dynamic(tuple([term(), integer()])) assert typecheck!([x, y], x == elem(y, 0) and y === {1, :ok}, {x, y}) == - dynamic(tuple([union(integer(), float()), tuple([integer(), atom([:ok])])])) + dynamic(tuple([opt_union(integer(), float()), tuple([integer(), atom([:ok])])])) assert typecheck!([x, y], x != elem(y, 0) and y === {1, :ok}, {x, y}) == dynamic(tuple([term(), tuple([integer(), atom([:ok])])])) @@ -1208,7 +1214,7 @@ defmodule Module.Types.PatternTest do assert typecheck!([x], not (length(x) <= 2), x) == dynamic(non_empty_list(term())) end - @non_empty_map difference(open_map(), empty_map()) + @non_empty_map opt_difference(open_map(), empty_map()) test "map_size equality" do assert typecheck!([x], map_size(x) == 0, x) == dynamic(empty_map()) @@ -1270,13 +1276,13 @@ defmodule Module.Types.PatternTest do assert typecheck!([x], not (map_size(x) <= 2), x) == dynamic(@non_empty_map) end - @non_empty_tuple difference(open_tuple([]), tuple([])) - @non_binary_tuple difference(open_tuple([]), tuple([term(), term()])) + @non_empty_tuple opt_difference(open_tuple([]), tuple([])) + @non_binary_tuple opt_difference(open_tuple([]), tuple([term(), term()])) @open_binary_tuple open_tuple([term(), term()]) @open_ternary_tuple open_tuple([term(), term(), term()]) - @non_open_binary_tuple difference(open_tuple([]), open_tuple([term(), term()])) - @non_open_ternary_tuple difference(open_tuple([]), open_tuple([term(), term(), term()])) + @non_open_binary_tuple opt_difference(open_tuple([]), open_tuple([term(), term()])) + @non_open_ternary_tuple opt_difference(open_tuple([]), open_tuple([term(), term(), term()])) test "tuple_size equality" do assert typecheck!([x], tuple_size(x) == 0, x) == dynamic(tuple([])) @@ -1300,7 +1306,7 @@ defmodule Module.Types.PatternTest do assert typecheck!([x], not (2 != tuple_size(x)), x) == dynamic(tuple([term(), term()])) assert typecheck!([x], tuple_size(x) != 1, x) == - dynamic(difference(open_tuple([]), tuple([term()]))) + dynamic(opt_difference(open_tuple([]), tuple([term()]))) assert typecheck!( [x], diff --git a/lib/elixir/test/elixir/protocol/consolidation_test.exs b/lib/elixir/test/elixir/protocol/consolidation_test.exs index 49b5b36d51e..0d273433d66 100644 --- a/lib/elixir/test/elixir/protocol/consolidation_test.exs +++ b/lib/elixir/test/elixir/protocol/consolidation_test.exs @@ -185,7 +185,7 @@ defmodule Protocol.ConsolidationTest do assert clauses == [ {[Of.impl(ImplStruct, :open)], atom([Sample.Protocol.ConsolidationTest.ImplStruct])}, - {[negation(Of.impl(ImplStruct, :open))], atom([nil])} + {[opt_negation(Of.impl(ImplStruct, :open))], atom([nil])} ] assert %{{:impl_for!, 1} => %{sig: {:strong, domain, clauses}}} = exports @@ -217,7 +217,7 @@ defmodule Protocol.ConsolidationTest do {[Of.impl(Map, :open)], atom([WithAny.Map])}, {[Of.impl(ImplStruct, :open)], atom([WithAny.Protocol.ConsolidationTest.ImplStruct])}, - {[negation(union(Of.impl(ImplStruct, :open), Of.impl(Map, :open)))], + {[opt_negation(opt_union(Of.impl(ImplStruct, :open), Of.impl(Map, :open)))], atom([WithAny.Any])} ]