Skip to content

Commit 349c770

Browse files
committed
Tuple of unions support
Dealing with tuples of unions vs unions of tuples is a hard problem in general, since we can get a combinatorial explosion of cases. This commit adds some support for it, but also has a limit on how many cases to consider so as not to take too much time. So this feature is incomplete.
1 parent 8144f60 commit 349c770

2 files changed

Lines changed: 94 additions & 4 deletions

File tree

src/typechecker.erl

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,18 @@ compat_ty({type, _, union, Tys1}, {type, _, union, Tys2} = Ty2, Seen, Env) ->
354354
end,
355355
{Seen2, constraints:combine(C1, C2, Env)}
356356
end, {Seen, constraints:empty()}, Tys1);
357-
compat_ty(Ty1, {type, _, union, Tys2}, Seen, Env) ->
358-
any_type(Ty1, Tys2, Seen, Env);
357+
compat_ty(Ty1, {type, _, union, Tys2} = Ty2, Seen, Env) ->
358+
try
359+
any_type(Ty1, Tys2, Seen, Env)
360+
catch
361+
nomatch ->
362+
case distribute_inner_unions(Ty1) of
363+
false ->
364+
throw(nomatch);
365+
{true, DistributedTy1} ->
366+
compat(DistributedTy1, Ty2, Seen, Env)
367+
end
368+
end;
359369
compat_ty({type, _, union, Tys1}, Ty2, Seen, Env) ->
360370
all_type(Tys1, Ty2, Seen, Env);
361371

@@ -590,6 +600,81 @@ all_type([Ty1|Tys], Ty, AIn, Css, Env) ->
590600
{AOut, Cs} = compat(Ty1, Ty, AIn, Env),
591601
all_type(Tys, Ty, AOut, [Cs|Css], Env).
592602

603+
%% Distribute unions inside compound types (tuples, maps) to create a union of
604+
%% compound types. For example:
605+
%% {t, a | b} -> {t, a} | {t, b}
606+
%% #{a => b | c} -> #{a => b} | #{a => c}
607+
%% Returns `false' if Ty contains no inner unions to distribute, or
608+
%% `{true, DistributedUnion}' after distribution.
609+
%% A size limit prevents exponential blowup for types with many union elements.
610+
-spec distribute_inner_unions(type()) -> false | {true, type()}.
611+
distribute_inner_unions({type, Ann, tuple, Args}) when is_list(Args) ->
612+
case has_inner_union(Args) of
613+
false -> false;
614+
true ->
615+
Distributed = distribute_tuple_args(Ann, Args),
616+
case length(Distributed) of
617+
N when N > 100 -> false;
618+
_ -> {true, type(union, Distributed)}
619+
end
620+
end;
621+
distribute_inner_unions({type, Ann, map, Assocs}) when is_list(Assocs) ->
622+
case has_inner_union_in_assocs(Assocs) of
623+
false -> false;
624+
true ->
625+
Distributed = distribute_map_assocs(Ann, Assocs),
626+
case length(Distributed) of
627+
N when N > 100 -> false;
628+
_ -> {true, type(union, Distributed)}
629+
end
630+
end;
631+
distribute_inner_unions(_) ->
632+
false.
633+
634+
-spec has_inner_union([type()]) -> boolean().
635+
has_inner_union([]) -> false;
636+
has_inner_union([{type, _, union, _} | _]) -> true;
637+
has_inner_union([_ | Rest]) -> has_inner_union(Rest).
638+
639+
-spec has_inner_union_in_assocs([type()]) -> boolean().
640+
has_inner_union_in_assocs([]) -> false;
641+
has_inner_union_in_assocs([{type, _, _, [_K, {type, _, union, _}]} | _]) -> true;
642+
has_inner_union_in_assocs([_ | Rest]) -> has_inner_union_in_assocs(Rest).
643+
644+
%% Distribute unions in tuple element positions.
645+
%% {A, B|C, D} -> [{A, B, D}, {A, C, D}]
646+
-spec distribute_tuple_args(erl_anno:anno(), [type()]) -> [type()].
647+
distribute_tuple_args(Ann, Args) ->
648+
[{type, Ann, tuple, Combo} || Combo <- cartesian_product(Args)].
649+
650+
%% Distribute unions in map association value positions.
651+
%% #{K1 => V1|V2, K2 => V3} -> [#{K1 => V1, K2 => V3}, #{K1 => V2, K2 => V3}]
652+
-spec distribute_map_assocs(erl_anno:anno(), [type()]) -> [type()].
653+
distribute_map_assocs(Ann, Assocs) ->
654+
ExpandedAssocLists = cartesian_product_assocs(Assocs),
655+
[{type, Ann, map, AssocList} || AssocList <- ExpandedAssocLists].
656+
657+
%% Cartesian product of type lists, expanding union elements.
658+
%% [a, b|c] -> [[a, b], [a, c]]
659+
-spec cartesian_product([type()]) -> [[type()]].
660+
cartesian_product([]) -> [[]];
661+
cartesian_product([{type, _, union, Tys} | Rest]) ->
662+
RestProduct = cartesian_product(Rest),
663+
[[Ty | R] || Ty <- Tys, R <- RestProduct];
664+
cartesian_product([Arg | Rest]) ->
665+
RestProduct = cartesian_product(Rest),
666+
[[Arg | R] || R <- RestProduct].
667+
668+
%% Cartesian product of map associations, expanding unions in value positions.
669+
-spec cartesian_product_assocs([type()]) -> [[type()]].
670+
cartesian_product_assocs([]) -> [[]];
671+
cartesian_product_assocs([{type, Ann, AssocType, [K, {type, _, union, Tys}]} | Rest]) ->
672+
RestProduct = cartesian_product_assocs(Rest),
673+
[[{type, Ann, AssocType, [K, Ty]} | R] || Ty <- Tys, R <- RestProduct];
674+
cartesian_product_assocs([Assoc | Rest]) ->
675+
RestProduct = cartesian_product_assocs(Rest),
676+
[[Assoc | R] || R <- RestProduct].
677+
593678
%% Looks up the fields of a record by name and, if present, by the module where
594679
%% it belongs if a filename is included in the Anno.
595680
-spec get_maybe_remote_record_fields(RecName :: atom(),
@@ -2892,8 +2977,13 @@ do_type_check_expr_in(Env, ResTy, {tuple, _, TS} = Tup) ->
28922977
{elem_tys, Tyss} ->
28932978
case type_check_tuple_union_in(Env, Tyss, TS) of
28942979
none ->
2895-
{Ty, _VB} = type_check_expr(Env, Tup),
2896-
throw(type_error(Tup, Ty, ResTy));
2980+
{Ty, VB} = type_check_expr(Env, Tup),
2981+
case subtype(Ty, ResTy, Env) of
2982+
true ->
2983+
VB;
2984+
false ->
2985+
throw(type_error(Tup, Ty, ResTy))
2986+
end;
28972987
VBs ->
28982988
union_var_binds(VBs, Env)
28992989
end;

test/known_problems/should_pass/inner_union_subtype_of_root_union.erl renamed to test/should_pass/inner_union_subtype_of_root_union.erl

File renamed without changes.

0 commit comments

Comments
 (0)