Skip to content

Commit 9734788

Browse files
committed
Add support for zip generators
1 parent a8b9897 commit 9734788

3 files changed

Lines changed: 165 additions & 1 deletion

File tree

src/typechecker.erl

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2573,6 +2573,11 @@ type_check_comprehension(Env, Compr, Expr, [{BGenerateTag, _P, Pat, Gen} | Quals
25732573
{TyL, VarBinds2} =
25742574
type_check_comprehension(NewEnv, Compr, Expr, Quals),
25752575
{TyL, union_var_binds(VarBinds1, VarBinds2, Env)};
2576+
type_check_comprehension(Env, Compr, Expr, [{zip, _, Generators} | Quals]) ->
2577+
%% Zip generators iterate in lockstep. Each generator's expression is
2578+
%% evaluated in the original Env (variables don't leak between arms).
2579+
NewEnv = type_check_zip_generators(Env, Generators),
2580+
type_check_comprehension(NewEnv, Compr, Expr, Quals);
25762581
type_check_comprehension(Env, Compr, Expr, [{MGenerateTag, _, {map_field_exact, _, KeyPat, ValPat}, Gen} | Quals])
25772582
when MGenerateTag =:= m_generate; MGenerateTag =:= m_generate_strict ->
25782583
{Ty, _VB1} = type_check_expr(Env, Gen),
@@ -2605,6 +2610,78 @@ type_check_comprehension(Env, Compr, Expr, [Guard | Quals]) ->
26052610
{TyL, VarBinds2} = type_check_comprehension(NewEnv, Compr, Expr, Quals),
26062611
{TyL, union_var_binds(VarBinds1, VarBinds2, Env)}.
26072612

2613+
%% Type check generators in a zip group.
2614+
%% All generator expressions are type-checked in the original Env
2615+
%% (generators are independent - variables don't leak between zip arms).
2616+
%% Pattern variables from all generators are bound in the returned env.
2617+
-spec type_check_zip_generators(env(), [_]) -> env().
2618+
type_check_zip_generators(Env, Generators) ->
2619+
%% Remove all pattern variables from all generators
2620+
GenEnv = lists:foldl(fun zip_remove_pat_vars/2, Env, Generators),
2621+
%% Type-check each generator's expression in the ORIGINAL Env
2622+
%% and bind pattern variables in the accumulated env
2623+
lists:foldl(
2624+
fun(Gen, AccEnv) ->
2625+
type_check_single_zip_generator(Env, AccEnv, Gen)
2626+
end, GenEnv, Generators).
2627+
2628+
-spec zip_remove_pat_vars(_, env()) -> env().
2629+
zip_remove_pat_vars({Tag, _, Pat, _Gen}, Env)
2630+
when Tag =:= generate; Tag =:= generate_strict ->
2631+
remove_pat_vars(Pat, Env);
2632+
zip_remove_pat_vars({Tag, _, Pat, _Gen}, Env)
2633+
when Tag =:= b_generate; Tag =:= b_generate_strict ->
2634+
remove_pat_vars(Pat, Env);
2635+
zip_remove_pat_vars({Tag, _, {map_field_exact, _, KeyPat, ValPat}, _Gen}, Env)
2636+
when Tag =:= m_generate; Tag =:= m_generate_strict ->
2637+
remove_pat_vars(KeyPat, remove_pat_vars(ValPat, Env)).
2638+
2639+
-spec type_check_single_zip_generator(env(), env(), _) -> env().
2640+
type_check_single_zip_generator(OrigEnv, AccEnv, {Tag, _, Pat, Gen})
2641+
when Tag =:= generate; Tag =:= generate_strict ->
2642+
{Ty, _} = type_check_expr(OrigEnv, Gen),
2643+
case expect_list_type(Ty, allow_nil_type, OrigEnv) of
2644+
{elem_ty, ElemTy} ->
2645+
{_PatTys, _UBounds, NewEnv} =
2646+
add_types_pats([Pat], [ElemTy], AccEnv, capture_vars),
2647+
NewEnv;
2648+
any ->
2649+
add_any_types_pat(Pat, AccEnv);
2650+
{elem_tys, _ElemTys} ->
2651+
add_any_types_pat(Pat, AccEnv);
2652+
{type_error, BadTy} ->
2653+
throw(type_error(Gen, BadTy, type(list)))
2654+
end;
2655+
type_check_single_zip_generator(OrigEnv, AccEnv, {Tag, _, Pat, Gen})
2656+
when Tag =:= b_generate; Tag =:= b_generate_strict ->
2657+
BitStringTy = type(binary, [{integer, erl_anno:new(0), 0},
2658+
{integer, erl_anno:new(0), 1}]),
2659+
_VarBinds = type_check_expr_in(OrigEnv, BitStringTy, Gen),
2660+
{_PatTys, _UBounds, NewEnv} =
2661+
add_types_pats([Pat], [BitStringTy], AccEnv, capture_vars),
2662+
NewEnv;
2663+
type_check_single_zip_generator(OrigEnv, AccEnv, {Tag, _, {map_field_exact, _, KeyPat, ValPat}, Gen})
2664+
when Tag =:= m_generate; Tag =:= m_generate_strict ->
2665+
{Ty, _} = type_check_expr(OrigEnv, Gen),
2666+
case expect_map_type(normalize(Ty, OrigEnv), OrigEnv) of
2667+
{assoc_tys, AssocTys} when is_list(AssocTys) ->
2668+
{KeyTys, ValTys} = lists:foldl(
2669+
fun({type, _, _AssocTag, [KT, VT]}, {KAcc, VAcc}) ->
2670+
{[KT | KAcc], [VT | VAcc]}
2671+
end, {[], []}, AssocTys),
2672+
KeyTy = normalize(type(union, KeyTys), OrigEnv),
2673+
ValTy = normalize(type(union, ValTys), OrigEnv),
2674+
{_PatTys1, _UBounds1, Env1} =
2675+
add_types_pats([KeyPat], [KeyTy], AccEnv, capture_vars),
2676+
{_PatTys2, _UBounds2, NewEnv} =
2677+
add_types_pats([ValPat], [ValTy], Env1, capture_vars),
2678+
NewEnv;
2679+
any ->
2680+
add_any_types_pat(ValPat, add_any_types_pat(KeyPat, AccEnv));
2681+
{type_error, BadTy} ->
2682+
throw(type_error(Gen, BadTy, type(map)))
2683+
end.
2684+
26082685
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
26092686
%% Checking the type of an expression
26102687
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -3315,12 +3392,13 @@ unary_op_arg_type('-', Ty = {type, _, float, []}) ->
33153392
Compr :: lc | bc | mc,
33163393
Expr :: gradualizer_type:abstract_expr(),
33173394
Position :: erl_anno:anno(),
3318-
Qualifiers :: [ListGen | BinGen | MapGen | Filter]) ->
3395+
Qualifiers :: [ListGen | BinGen | MapGen | ZipGen | Filter]) ->
33193396
env()
33203397
when
33213398
ListGen :: {generate | generate_strict, erl_anno:anno(), gradualizer_type:abstract_expr(), gradualizer_type:abstract_expr()},
33223399
BinGen :: {b_generate | b_generate_strict, erl_anno:anno(), gradualizer_type:abstract_expr(), gradualizer_type:abstract_expr()},
33233400
MapGen :: {m_generate | m_generate_strict, erl_anno:anno(), gradualizer_type:abstract_expr(), gradualizer_type:abstract_expr()},
3401+
ZipGen :: {zip, erl_anno:anno(), [ListGen | BinGen | MapGen]},
33243402
Filter :: gradualizer_type:abstract_expr().
33253403
type_check_comprehension_in(Env, ResTy, OrigExpr, lc, Expr, _P, []) ->
33263404
case expect_list_type(ResTy, allow_nil_type, Env) of
@@ -3389,6 +3467,12 @@ type_check_comprehension_in(Env, ResTy, OrigExpr, mc,
33893467
{type_error, _} ->
33903468
throw(type_error(OrigExpr, type(map), ResTy))
33913469
end;
3470+
type_check_comprehension_in(Env, ResTy, OrigExpr, Compr, Expr, P,
3471+
[{zip, _, Generators} | Quals]) ->
3472+
%% Zip generators iterate in lockstep. Each generator's expression is
3473+
%% evaluated in the original Env (variables don't leak between arms).
3474+
NewEnv = type_check_zip_generators(Env, Generators),
3475+
type_check_comprehension_in(NewEnv, ResTy, OrigExpr, Compr, Expr, P, Quals);
33923476
type_check_comprehension_in(Env, ResTy, OrigExpr, Compr, Expr, P,
33933477
[{MGenerateTag, _, {map_field_exact, _, KeyPat, ValPat}, Gen} | Quals])
33943478
when MGenerateTag =:= m_generate; MGenerateTag =:= m_generate_strict ->
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-module(zip_generators_fail).
2+
3+
%% Each exported function should produce exactly one type error.
4+
5+
-export([zip_not_list/1, zip_wrong_return/0]).
6+
7+
%% Zip generator from non-list value
8+
-spec zip_not_list(integer()) -> [{integer(), integer()}].
9+
zip_not_list(N) ->
10+
[{X, Y} || X <- [1, 2, 3] && Y <- N].
11+
12+
%% Zip generator result type mismatch
13+
-spec zip_wrong_return() -> integer().
14+
zip_wrong_return() ->
15+
[{X, Y} || X <- [1, 2] && Y <- [a, b]].
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
-module(zip_generators_pass).
2+
3+
-compile([export_all, nowarn_export_all]).
4+
5+
%% Basic zip generator - two list generators in lockstep
6+
-spec zip_basic() -> [{integer(), atom()}].
7+
zip_basic() ->
8+
[{X, Y} || X <- [1, 2, 3] && Y <- [a, b, c]].
9+
10+
%% Zip generator with three lists
11+
-spec zip_three() -> [{integer(), atom(), float()}].
12+
zip_three() ->
13+
[{X, Y, Z} || X <- [1, 2] && Y <- [a, b] && Z <- [1.0, 2.0]].
14+
15+
%% Zip generator with arithmetic
16+
-spec zip_add() -> [integer()].
17+
zip_add() ->
18+
[X + Y || X <- [1, 2, 3] && Y <- [10, 20, 30]].
19+
20+
%% Zip generator with filter after zip
21+
-spec zip_with_filter() -> [{integer(), atom()}].
22+
zip_with_filter() ->
23+
[{X, Y} || X <- [1, 2, 3] && Y <- [a, b, c], X > 1].
24+
25+
%% Zip generator in binary comprehension
26+
-spec zip_in_bc() -> binary().
27+
zip_in_bc() ->
28+
<< <<(X + Y)>> || X <- [1, 2, 3] && Y <- [10, 20, 30] >>.
29+
30+
%% Zip generator in map comprehension
31+
-spec zip_in_mc() -> #{integer() => atom()}.
32+
zip_in_mc() ->
33+
#{X => Y || X <- [1, 2, 3] && Y <- [a, b, c]}.
34+
35+
%% Zip with strict generators
36+
-spec zip_strict() -> [{integer(), atom()}].
37+
zip_strict() ->
38+
[{X, Y} || X <:- [1, 2, 3] && Y <:- [a, b, c]].
39+
40+
%% Zip with mixed strict and non-strict
41+
-spec zip_mixed() -> [{integer(), atom()}].
42+
zip_mixed() ->
43+
[{X, Y} || X <- [1, 2, 3] && Y <:- [a, b, c]].
44+
45+
%% Zip scoping: variables from one arm don't leak into another arm's expression.
46+
%% X is boolean from the parameter. The first zip arm rebinds X to integer.
47+
%% The second arm's expression [X] must use the OUTER X (boolean), not the
48+
%% rebound X (integer). So Y :: boolean.
49+
%% With incorrect sequential scoping, Y would be integer() and this would fail.
50+
-spec zip_independent_scope(boolean()) -> [{integer(), boolean()}].
51+
zip_independent_scope(X) ->
52+
[{X, Y} || X <- [1, 2, 3] && Y <- [X, X, X]].
53+
54+
%% Zip scoping: each arm sees the pre-zip environment.
55+
%% N is an integer parameter. First arm binds X from a list of atoms.
56+
%% Second arm's expression uses N (outer scope), not X.
57+
-spec zip_outer_scope_in_arms(integer()) -> [{atom(), integer()}].
58+
zip_outer_scope_in_arms(N) ->
59+
[{X, Y} || X <- [a, b, c] && Y <- [N, N + 1, N + 2]].
60+
61+
%% Zip scoping: variable shadowing within a zip arm.
62+
%% The zip body sees the rebound X (integer), not the parameter (atom).
63+
-spec zip_body_sees_rebound(atom()) -> [integer()].
64+
zip_body_sees_rebound(X) ->
65+
[X || X <- [1, 2, 3] && _Y <- [a, b, c]].

0 commit comments

Comments
 (0)