File 2141-EEP-78-Implement-multi-valued-comprehensions.patch of Package erlang
From 0f89fa2be768427c0c364f5e4479d3a891875c29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20Muska=C5=82a?= <micmus@whatsapp.com>
Date: Thu, 9 Jan 2025 05:15:52 -0800
Subject: [PATCH] [EEP 78] Implement multi-valued comprehensions
Implement EEP 78 - Multi-valued comprehensions. This EEP proposes
enhancing the comprehension syntax to allow emitting multiple elements
in a single iteration of the comprehension loop - effectively enhancing
comprehensions to implement flatmap with a fixed number of elements.
An example of multi-valued comprehension:
````
[X + 1, X + 2, ... || X <- Xs]
````
---
erts/doc/guides/absform.md | 8 +++
lib/compiler/src/sys_coverage.erl | 19 +++--
lib/compiler/src/v3_core.erl | 44 +++++++-----
lib/compiler/test/lc_SUITE.erl | 10 ++-
lib/compiler/test/mc_SUITE.erl | 13 +++-
lib/debugger/src/dbg_ieval.erl | 71 +++++++++++--------
lib/debugger/src/dbg_iload.erl | 7 +-
lib/debugger/test/erl_eval_SUITE.erl | 24 +++++++
lib/stdlib/examples/erl_id_trans.erl | 12 +++-
lib/stdlib/src/erl_eval.erl | 58 ++++++++-------
lib/stdlib/src/erl_expand_records.erl | 9 ++-
lib/stdlib/src/erl_lint.erl | 15 +++-
lib/stdlib/src/erl_parse.yrl | 11 ++-
lib/stdlib/test/erl_eval_SUITE.erl | 26 ++++++-
lib/syntax_tools/src/erl_prettypr.erl | 14 +++-
lib/syntax_tools/src/erl_syntax.erl | 40 ++++++++---
lib/syntax_tools/src/erl_syntax_lib.erl | 10 ++-
lib/syntax_tools/test/syntax_tools_SUITE.erl | 2 +
.../syntax_tools_SUITE_test_module.erl | 12 ++++
.../list_comprehensions.md | 8 ++-
20 files changed, 308 insertions(+), 105 deletions(-)
diff --git a/erts/doc/guides/absform.md b/erts/doc/guides/absform.md
index 1cbaa93e2f..53cfd0405b 100644
--- a/erts/doc/guides/absform.md
+++ b/erts/doc/guides/absform.md
@@ -221,10 +221,18 @@ An expression E is one of the following:
- If E is a list comprehension `[E_0 || Q_1, ..., Q_k]`, where each `Q_i` is a
qualifier, then Rep(E) = `{lc,ANNO,Rep(E_0),[Rep(Q_1), ..., Rep(Q_k)]}`. For
Rep(Q), see below.
+- If E is a list comprehension with multiple expressions `[E_0, ..., E_k || Q_1, ..., Q_k]`,
+ where each `E_i` is an expression and each `Q_i` is a qualifier, then
+ Rep(E) = `{lc,ANNO,[Rep(E_0),...,Rep(E_k)],[Rep(Q_1),...Rep(Q_k)]}`. For `Rep(Q)`,
+ see below.
- If E is a map comprehension `#{E_0 || Q_1, ..., Q_k}`, where `E_0` is an
association `K => V` and each `Q_i` is a qualifier, then Rep(E) =
`{mc,ANNO,Rep(E_0),[Rep(Q_1), ..., Rep(Q_k)]}`. For Rep(E_0) and Rep(Q), see
below.
+- If E is a map comprehension with multiple expressions `#{E_0, ..., E_k || Q_1, ..., Q_k}`,
+ where each `E_i` is an association `K_i => V_i` and each `Q_i` is a qualifier, then
+ Rep(E) = `{mc,ANNO,[Rep(E_0),...,Rep(E_k)],[Rep(Q_1),...Rep(Q_k)]}`.
+ For `Rep(E)` and `Rep(Q)`, see below.
- If E is a map creation `#{A_1, ..., A_k}`, where each `A_i` is an association
`E_i_1 => E_i_2`, then Rep(E) = `{map,ANNO,[Rep(A_1), ..., Rep(A_k)]}`. For
Rep(A), see below.
diff --git a/lib/compiler/src/sys_coverage.erl b/lib/compiler/src/sys_coverage.erl
index 596b7a7e11..20bc0974da 100644
--- a/lib/compiler/src/sys_coverage.erl
+++ b/lib/compiler/src/sys_coverage.erl
@@ -511,16 +511,15 @@ munge_expr({call,Anno,Expr,Exprs}, Vars0) ->
{MungedExprs, Vars2} = munge_args(Exprs, Vars1),
{{call,Anno,MungedExpr,MungedExprs}, Vars2};
munge_expr({lc,Anno,Expr,Qs}, Vars0) ->
- {MungedExpr, Vars1} = munge_expr(?BLOCK1(Expr), Vars0),
+ {MungedExpr, Vars1} = munge_comprehension(Expr, Vars0),
{MungedQs, Vars2} = munge_qualifiers(Qs, Vars1),
{{lc,Anno,MungedExpr,MungedQs}, Vars2};
munge_expr({bc,Anno,Expr,Qs}, Vars0) ->
{MungedExpr,Vars1} = munge_expr(?BLOCK1(Expr), Vars0),
{MungedQs, Vars2} = munge_qualifiers(Qs, Vars1),
{{bc,Anno,MungedExpr,MungedQs}, Vars2};
-munge_expr({mc,Anno,{map_field_assoc,FAnno,K,V},Qs}, Vars0) ->
- Expr = {map_field_assoc,FAnno,?BLOCK1(K),?BLOCK1(V)},
- {MungedExpr, Vars1} = munge_expr(Expr, Vars0),
+munge_expr({mc,Anno,Expr,Qs}, Vars0) ->
+ {MungedExpr, Vars1} = munge_comprehension(Expr, Vars0),
{MungedQs, Vars2} = munge_qualifiers(Qs, Vars1),
{{mc,Anno,MungedExpr,MungedQs}, Vars2};
munge_expr({block,Anno,Body}, Vars0) ->
@@ -609,6 +608,18 @@ is_atomic({nil,_}) -> true;
is_atomic({var,_,_}) -> true;
is_atomic(_) -> false.
+munge_comprehension([Expr|Exprs], Vars0) ->
+ {MungedExpr, Vars1} = munge_comprehension(Expr, Vars0),
+ {MungedExprs, Vars2} = munge_comprehension(Exprs, Vars1),
+ {[MungedExpr|MungedExprs], Vars2};
+munge_comprehension([], Vars) ->
+ {[], Vars};
+munge_comprehension({map_field_assoc,Anno,K,V}, Vars0) ->
+ Expr = {map_field_assoc,Anno,?BLOCK(K),?BLOCK(V)},
+ munge_expr(Expr, Vars0);
+munge_comprehension(Expr, Vars0) ->
+ munge_expr(?BLOCK1(Expr), Vars0).
+
munge_exprs(Exprs, Vars) ->
munge_exprs(Exprs, Vars, []).
diff --git a/lib/compiler/src/v3_core.erl b/lib/compiler/src/v3_core.erl
index 48d1617af1..b372ff5e5e 100644
--- a/lib/compiler/src/v3_core.erl
+++ b/lib/compiler/src/v3_core.erl
@@ -712,12 +712,12 @@ expr({cons,L,H0,T0}, St0) ->
{annotate_cons(A, H1, T1, St1),Eps,St1};
expr({lc,L,E,Qs0}, St0) ->
{Qs1,St1} = preprocess_quals(L, Qs0, St0),
- lc_tq(L, E, Qs1, #c_literal{anno=lineno_anno(L, St1),val=[]}, St1);
+ lc_tq(L, wrap_list(E), Qs1, #c_literal{anno=lineno_anno(L, St1),val=[]}, St1);
expr({bc,L,E,Qs}, St) ->
bc_tq(L, E, Qs, St);
expr({mc,L,E,Qs0}, St0) ->
{Qs1,St1} = preprocess_quals(L, Qs0, St0),
- mc_tq(L, E, Qs1, #c_literal{anno=lineno_anno(L, St1),val=[]}, St1);
+ mc_tq(L, wrap_list(E), Qs1, #c_literal{anno=lineno_anno(L, St1),val=[]}, St1);
expr({tuple,L,Es0}, St0) ->
{Es1,Eps,St1} = safe_list(Es0, St0),
A = record_anno(L, St1),
@@ -969,7 +969,7 @@ expr({op,_,'++',{lc,Llc,E,Qs0},More}, St0) ->
%% number variables in the environment for letrec.
{Mc,Mps,St1} = safe(More, St0),
{Qs,St2} = preprocess_quals(Llc, Qs0, St1),
- {Y,Yps,St} = lc_tq(Llc, E, Qs, Mc, St2),
+ {Y,Yps,St} = lc_tq(Llc, wrap_list(E), Qs, Mc, St2),
{Y,Mps++Yps,St};
expr({op,_,'andalso',_,_}=E0, St0) ->
{op,L,'andalso',E1,E2} = right_assoc(E0, 'andalso'),
@@ -1626,21 +1626,29 @@ fun_tq(Cs0, L, St0, NameInfo) ->
vars=Args,clauses=Cs2,fc=Fc,name=NameInfo},
{Fun,[],St4}.
-%% lc_tq(Line, Exp, [Qualifier], Mc, State) -> {LetRec,[PreExp],State}.
+wrap_list(List) when is_list(List) -> List;
+wrap_list(Other) -> [Other].
+
+%% lc_tq(Line, Exprs, [Qualifier], Mc, State) -> {LetRec,[PreExp],State}.
%% This TQ from Simon PJ pp 127-138.
-lc_tq(Line, E, [#igen{}|_T] = Qs, Mc, St) ->
- lc_tq1(Line, E, Qs, Mc, St);
-lc_tq(Line, E, [#izip{}=Zip|Qs], Mc, St) ->
- zip_tq(Line, E, Zip, Mc, St, Qs);
-lc_tq(Line, E, [#ifilter{}=Filter|Qs], Mc, St) ->
- filter_tq(Line, E, Filter, Mc, St, Qs, fun lc_tq/5);
-lc_tq(Line, E0, [], Mc0, St0) ->
- {H1,Hps,St1} = safe(E0, St0),
+lc_tq(Line, Es, [#igen{}|_T] = Qs, Mc, St) ->
+ lc_tq1(Line, Es, Qs, Mc, St);
+lc_tq(Line, Es, [#izip{}=Zip|Qs], Mc, St) ->
+ zip_tq(Line, Es, Zip, Mc, St, Qs);
+lc_tq(Line, Es, [#ifilter{}=Filter|Qs], Mc, St) ->
+ filter_tq(Line, Es, Filter, Mc, St, Qs, fun lc_tq/5);
+lc_tq(Line, Es0, [], Mc0, St0) ->
+ {Hs1,Hps,St1} = safe_list(Es0, St0),
{T1,Tps,St} = force_safe(Mc0, St1),
- Anno = lineno_anno(Line, St),
- E = ann_c_cons(Anno, H1, T1),
- {set_anno(E, [compiler_generated|Anno]),Hps ++ Tps,St}.
+ Anno = lineno_anno(erl_anno:set_generated(true, Line), St),
+ E = ann_c_cons_all(Anno, Hs1, T1),
+ {E,Hps ++ Tps,St}.
+
+ann_c_cons_all(Anno, [H | Hs], T) ->
+ ann_c_cons(Anno, H, ann_c_cons_all(Anno, Hs, T));
+ann_c_cons_all(_Anno, [], T) ->
+ T.
lc_tq1(Line, E, [#igen{anno=#a{anno=GA}=GAnno,
acc_pat=AccPat,acc_guard=AccGuard,
@@ -1927,9 +1935,9 @@ bzip_tq1(Line, E, #izip{anno=#a{anno=_GA}=GAnno,
body=append(Pres) ++
[#iapply{anno=LAnno,op=F,args=Args++[Mc]}]},[],St4}.
-mc_tq(Line, {map_field_assoc,Lf,K,V}, Qs, Mc, St0) ->
- E = {tuple,Lf,[K,V]},
- {Lc,Pre0,St1} = lc_tq(Line, E, Qs, Mc, St0),
+mc_tq(Line, Es0, Qs, Mc, St0) ->
+ Es = map(fun({map_field_assoc,Lf,K,V}) -> {tuple,Lf,[K,V]} end, Es0),
+ {Lc,Pre0,St1} = lc_tq(Line, Es, Qs, Mc, St0),
{LcVar,St2} = new_var(St1),
Pre = Pre0 ++ [#iset{var=LcVar,arg=Lc}],
Call = #icall{module=#c_literal{val=maps},
diff --git a/lib/compiler/test/lc_SUITE.erl b/lib/compiler/test/lc_SUITE.erl
index 77bb3a8fb6..8406bbb297 100644
--- a/lib/compiler/test/lc_SUITE.erl
+++ b/lib/compiler/test/lc_SUITE.erl
@@ -28,7 +28,8 @@
init_per_testcase/2,end_per_testcase/2,
basic/1,deeply_nested/1,no_generator/1,
empty_generator/1,no_export/1,shadow/1,
- effect/1,singleton_generator/1,assignment_generator/1,gh10020/1]).
+ effect/1,singleton_generator/1,assignment_generator/1,gh10020/1,
+ multi/1]).
-include_lib("stdlib/include/assert.hrl").
-include_lib("common_test/include/ct.hrl").
@@ -51,7 +52,8 @@ groups() ->
effect,
singleton_generator,
assignment_generator,
- gh10020
+ gh10020,
+ multi
]}].
init_per_suite(Config) ->
@@ -407,6 +409,10 @@ gh10020(Config) when is_list(Config) ->
do_gh10020(L) ->
[] = [Rec || {_, Rec} <- L, is_record(L, L)].
+multi(Config) when is_list(Config) ->
+ [true, false] = [true, false || true],
+ [1, 2, 5, 6] = [X, X + 1 || X <- [1, 5]].
+
id(I) -> I.
-file("bad_lc.erl", 1).
diff --git a/lib/compiler/test/mc_SUITE.erl b/lib/compiler/test/mc_SUITE.erl
index 7f084f6fee..8e315eb395 100644
--- a/lib/compiler/test/mc_SUITE.erl
+++ b/lib/compiler/test/mc_SUITE.erl
@@ -27,7 +27,7 @@
-export([all/0,suite/0,groups/0,init_per_suite/1,end_per_suite/1,
init_per_group/2,end_per_group/2,
basic/1,duplicate_keys/1,mixed/1,
- shadow/1,bad_generators/1]).
+ shadow/1,bad_generators/1,multi/1]).
-include_lib("common_test/include/ct.hrl").
@@ -42,7 +42,8 @@ groups() ->
duplicate_keys,
mixed,
shadow,
- bad_generators]}].
+ bad_generators,
+ multi]}].
init_per_suite(Config) ->
test_lib:recompile(?MODULE),
@@ -300,6 +301,14 @@ bad_generators(_Config) ->
end,
ok.
+multi(_Config) ->
+ Exp = #{true => 1, false => 2},
+ Exp = #{true => 1, false => 2 || true},
+ Exp2 = #{1 => 1, 2 => 2, 5 => 5, 6 => 6},
+ Exp2 = #{X => X, X + 1 => X + 1 || X <- [1, 5]},
+ Exp3 = #{1 => 4, 5 => 8},
+ Exp3 = #{X => X+1, X => X+3 || X <- [1, 5]}.
+
id(I) -> I.
-file("bad_mc.erl", 1).
diff --git a/lib/debugger/src/dbg_ieval.erl b/lib/debugger/src/dbg_ieval.erl
index 4c70e9d9aa..ed4c9bf7e3 100644
--- a/lib/debugger/src/dbg_ieval.erl
+++ b/lib/debugger/src/dbg_ieval.erl
@@ -1107,33 +1107,35 @@ eval_fun(As, {Info,Bs,Cs}) ->
eval_named_fun(As, RF, {Info,Bs,Cs,FName}) ->
dbg_debugged:eval(?MODULE, eval_named_fun, [Cs,As,Bs,FName,RF,Info]).
-%% eval_lc(Expr,[Qualifier],Bindings,IevalState) ->
+%% eval_lc(ExprOrExprs,[Qualifier],Bindings,IevalState) ->
%% {value,Value,Bindings}.
%% This is evaluating list comprehensions "straight out of the book".
%% Copied from rv's implementation in erl_eval.
+eval_lc(Es, Qs, Bs, Ieval) when is_list(Es) ->
+ {value,eval_lc1(Es, Qs, Bs, Ieval),Bs};
eval_lc(E, Qs, Bs, Ieval) ->
- {value,eval_lc1(E, Qs, Bs, Ieval),Bs}.
+ eval_lc([E], Qs, Bs, Ieval).
-eval_lc1(E, [{zip, Anno, Gens}|Qs], Bs0, Ieval) ->
+eval_lc1(Es, [{zip, Anno, Gens}|Qs], Bs0, Ieval) ->
{VarList, Bs1} = convert_gen_values(Gens, [], Bs0, Ieval),
- eval_zip(E, [{zip, Anno, VarList}|Qs], Bs1, fun eval_lc1/4, Ieval);
-eval_lc1(E, [{generator,G}|Qs], Bs, Ieval) ->
- CompFun = fun(NewBs) -> eval_lc1(E, Qs, NewBs, Ieval) end,
+ eval_zip(Es, [{zip, Anno, VarList}|Qs], Bs1, fun eval_lc1/4, Ieval);
+eval_lc1(Es, [{generator,G}|Qs], Bs, Ieval) ->
+ CompFun = fun(NewBs) -> eval_lc1(Es, Qs, NewBs, Ieval) end,
eval_generator(G, Bs, CompFun, Ieval);
-eval_lc1(E, [{guard,Q}|Qs], Bs0, Ieval) ->
+eval_lc1(Es, [{guard,Q}|Qs], Bs0, Ieval) ->
case guard(Q, Bs0) of
- true -> eval_lc1(E, Qs, Bs0, Ieval);
+ true -> eval_lc1(Es, Qs, Bs0, Ieval);
false -> []
end;
-eval_lc1(E, [Q|Qs], Bs0, Ieval) ->
+eval_lc1(Es, [Q|Qs], Bs0, Ieval) ->
case expr(Q, Bs0, Ieval#ieval{top=false}) of
- {value,true,Bs} -> eval_lc1(E, Qs, Bs, Ieval);
+ {value,true,Bs} -> eval_lc1(Es, Qs, Bs, Ieval);
{value,false,_Bs} -> [];
{value,V,Bs} -> exception(error, {bad_filter,V}, Bs, Ieval)
end;
-eval_lc1(E, [], Bs, Ieval) ->
- {value,V,_} = expr(E, Bs, Ieval#ieval{top=false}),
- [V].
+eval_lc1(Es, [], Bs, Ieval) ->
+ {Vs, _Bs} = eval_list(Es, Bs, Ieval#ieval{top=false}),
+ Vs.
%% convert values for generator vars from abstract form to flattened lists
convert_gen_values([{generator,{Generate, Line, P, L0}}|Qs], Acc, Bs0, Ieval0)
@@ -1405,33 +1407,40 @@ eval_bc1(E, [], Bs, Ieval) ->
{value,V,_} = expr(E, Bs, Ieval#ieval{top=false}),
[V].
+eval_mc(Es, Qs, Bs, Ieval) when is_list(Es) ->
+ Map = eval_mc1(Es, Qs, Bs, Ieval),
+ {value,maps:from_list(Map),Bs};
eval_mc(E, Qs, Bs, Ieval) ->
- Map = eval_mc1(E, Qs, Bs, Ieval),
- {value,maps:from_list(Map),Bs}.
+ eval_mc([E], Qs, Bs, Ieval).
-eval_mc1(E, [{zip, Anno, Gens}|Qs], Bs0, Ieval) ->
+eval_mc1(Es, [{zip, Anno, Gens}|Qs], Bs0, Ieval) ->
{VarList, Bs1} = convert_gen_values(Gens, [], Bs0, Ieval),
- eval_zip(E, [{zip, Anno, VarList}|Qs], Bs1, fun eval_mc1/4, Ieval);
-eval_mc1(E, [{generator,G}|Qs], Bs, Ieval) ->
- CompFun = fun(NewBs) -> eval_mc1(E, Qs, NewBs, Ieval) end,
+ eval_zip(Es, [{zip, Anno, VarList}|Qs], Bs1, fun eval_mc1/4, Ieval);
+eval_mc1(Es, [{generator,G}|Qs], Bs, Ieval) ->
+ CompFun = fun(NewBs) -> eval_mc1(Es, Qs, NewBs, Ieval) end,
eval_generator(G, Bs, CompFun, Ieval);
-eval_mc1(E, [{guard,Q}|Qs], Bs0, Ieval) ->
+eval_mc1(Es, [{guard,Q}|Qs], Bs0, Ieval) ->
case guard(Q, Bs0) of
- true -> eval_mc1(E, Qs, Bs0, Ieval);
+ true -> eval_mc1(Es, Qs, Bs0, Ieval);
false -> []
end;
-eval_mc1(E, [Q|Qs], Bs0, Ieval) ->
+eval_mc1(Es, [Q|Qs], Bs0, Ieval) ->
case expr(Q, Bs0, Ieval#ieval{top=false}) of
- {value,true,Bs} -> eval_mc1(E, Qs, Bs, Ieval);
+ {value,true,Bs} -> eval_mc1(Es, Qs, Bs, Ieval);
{value,false,_Bs} -> [];
{value,V,Bs} -> exception(error, {bad_filter,V}, Bs, Ieval)
end;
-eval_mc1({map_field_assoc,_,K0,V0}, [], Bs, Ieval) ->
- {value,K,_} = expr(K0, Bs, Ieval#ieval{top=false}),
- {value,V,_} = expr(V0, Bs, Ieval#ieval{top=false}),
- [{K,V}].
+eval_mc1(Es, [], Bs, Ieval) ->
+ eval_mc2(Es, Bs, Ieval#ieval{top=false}).
+
+eval_mc2([{map_field_assoc,_,K0,V0}|Es], Bs, Ieval) ->
+ {value,K,_} = expr(K0, Bs, Ieval),
+ {value,V,_} = expr(V0, Bs, Ieval),
+ [{K,V}|eval_mc2(Es, Bs, Ieval)];
+eval_mc2([], _Bs, _Ieval) ->
+ [].
-eval_zip(E, [{zip, Anno, VarList}|Qs], Bs0, Fun, Ieval) ->
+eval_zip(Es, [{zip, Anno, VarList}|Qs], Bs0, Fun, Ieval) ->
Gens = case check_bad_generators(VarList, Bs0, []) of
{ok, Acc} -> Acc;
{error, Reason} ->
@@ -1445,10 +1454,10 @@ eval_zip(E, [{zip, Anno, VarList}|Qs], Bs0, Fun, Ieval) ->
{[], _, _} -> [];
{_,_,done} -> [];
{_, _, skip} ->
- eval_zip(E, [{zip, Anno, lists:reverse(Rest)}|Qs], Bs0, Fun, Ieval);
+ eval_zip(Es, [{zip, Anno, lists:reverse(Rest)}|Qs], Bs0, Fun, Ieval);
{_, _, _} ->
- Fun(E, Qs, add_bindings(Bs1, Bs0), Ieval) ++
- eval_zip(E, [{zip, Anno, lists:reverse(Rest)}|Qs], Bs0, Fun, Ieval)
+ Fun(Es, Qs, add_bindings(Bs1, Bs0), Ieval) ++
+ eval_zip(Es, [{zip, Anno, lists:reverse(Rest)}|Qs], Bs0, Fun, Ieval)
end.
eval_generator({Generate,Line,P,L0}, Bs0, CompFun, Ieval0) when Generate =:= generate;
diff --git a/lib/debugger/src/dbg_iload.erl b/lib/debugger/src/dbg_iload.erl
index 71a26837c6..5f4d328555 100644
--- a/lib/debugger/src/dbg_iload.erl
+++ b/lib/debugger/src/dbg_iload.erl
@@ -710,7 +710,7 @@ expr_comprehension({Tag,Anno,E0,Gs0}, St) ->
false -> expr(Expr, false, St)
end
end || G <- Gs0],
- {Tag,ln(Anno),expr(E0, false, St),Gs};
+ {Tag,ln(Anno),expr_or_exprs(E0, false, St),Gs};
expr_comprehension({zip,Anno,Gens}, St) ->
Gs = [case G of
({generate,L,P0,Qs}) ->
@@ -728,6 +728,11 @@ expr_comprehension({zip,Anno,Gens}, St) ->
end || G <- Gens],
{zip,ln(Anno),Gs}.
+expr_or_exprs(Es, Lc, St) when is_list(Es) ->
+ exprs(Es, Lc, St);
+expr_or_exprs(E, Lc, St) ->
+ expr(E, Lc, St).
+
mc_pattern({map_field_exact,L,KeyP0,ValP0}, St) ->
KeyP1 = pattern(KeyP0, St),
ValP1 = pattern(ValP0, St),
diff --git a/lib/debugger/test/erl_eval_SUITE.erl b/lib/debugger/test/erl_eval_SUITE.erl
index d2a523f810..ccff2c3e93 100644
--- a/lib/debugger/test/erl_eval_SUITE.erl
+++ b/lib/debugger/test/erl_eval_SUITE.erl
@@ -34,6 +34,8 @@
zlc/1,
zbc/1,
zmc/1,
+ multi_lc/1,
+ multi_mc/1,
simple_cases/1,
unary_plus/1,
apply_atom/1,
@@ -398,6 +400,28 @@ zmc(Config) when is_list(Config) ->
{bad_generators,{[1], a, #{b=>3}}}),
ok.
+%% EEP 78: multi-comprehensions
+multi_lc(Config) when is_list(Config) ->
+ check(fun() -> lists:append([[1, 2] || _ <- [1, 2]]) end,
+ "[1, 2 || _ <- [1, 2]].",
+ [1, 2, 1, 2]),
+ check(fun() -> lists:append([[one, two] || true]) end,
+ "[one, two || true].",
+ [one, two]),
+ error_check("[X = 1, X || true].", {unbound_var,'X'}).
+
+multi_mc(Config) when is_list(Config) ->
+ check(fun() -> #{A => B || X <- [1, 5], {A, B} <- [{X, X+1}, {X+2, X+3}]} end,
+ "#{X => X+1, X+2 => X+3 || X <- [1, 5]}.",
+ #{1 => 2, 3 => 4, 5 => 6, 7 => 8}),
+ check(fun() -> #{A => B || X <- [1, 5], {A, B} <- [{X, X+1}, {X, X+3}]} end,
+ "#{X => X+1, X => X+3 || X <- [1, 5]}.",
+ #{1 => 4, 5 => 8}),
+ error_check("#{1 := 2 || _ <- []}.", illegal_map_exact_in_comprehension),
+ error_check("#{1 => 2, 3 := 4 || _ <- []}.", illegal_map_exact_in_comprehension),
+ error_check("#{X = key => value, X => value2 || true}.", {unbound_var,'X'}),
+ error_check("#{key => x = value, key2 => X || true}.", {unbound_var,'X'}).
+
%% Simple cases, just to cover some code.
simple_cases(Config) when is_list(Config) ->
check(fun() -> A = $C end, "A = $C.", $C),
diff --git a/lib/stdlib/examples/erl_id_trans.erl b/lib/stdlib/examples/erl_id_trans.erl
index fa98cb4371..d86db71050 100644
--- a/lib/stdlib/examples/erl_id_trans.erl
+++ b/lib/stdlib/examples/erl_id_trans.erl
@@ -455,7 +455,7 @@ expr({cons,Anno,H0,T0}) ->
{cons,Anno,H1,T1};
expr({lc,Anno,E0,Qs0}) ->
Qs1 = comprehension_quals(Qs0),
- E1 = expr(E0),
+ E1 = expr_or_exprs(E0),
{lc,Anno,E1,Qs1};
expr({bc,Anno,E0,Qs0}) ->
Qs1 = comprehension_quals(Qs0),
@@ -463,7 +463,7 @@ expr({bc,Anno,E0,Qs0}) ->
{bc,Anno,E1,Qs1};
expr({mc,Anno,E0,Qs0}) ->
Qs1 = comprehension_quals(Qs0),
- E1 = expr(E0),
+ E1 = expr_or_exprs(E0),
{mc,Anno,E1,Qs1};
expr({tuple,Anno,Es0}) ->
Es1 = expr_list(Es0),
@@ -591,6 +591,14 @@ expr_list([E0|Es]) ->
[E1|expr_list(Es)];
expr_list([]) -> [].
+%% -type expr_or_exprs(Expression) -> Expression.
+%% -type expr_or_exprs([Expression]) -> [Expression].
+%% Comprehensions can have a single or multiple emitting expressions
+expr_or_exprs(Es) when is_list(Es) ->
+ expr_list(Es);
+expr_or_exprs(E) ->
+ expr(E).
+
%% -type record_inits([RecordInit]) -> [RecordInit].
%% N.B. Field names are full expressions here but only atoms are allowed
%% by the *linter*!.
diff --git a/lib/stdlib/src/erl_eval.erl b/lib/stdlib/src/erl_eval.erl
index 42867b8769..c6b3d60f78 100644
--- a/lib/stdlib/src/erl_eval.erl
+++ b/lib/stdlib/src/erl_eval.erl
@@ -930,28 +930,30 @@ do_apply(F, Anno, FunOrModFun, Args) when is_function(F, 3) ->
do_apply(F, _Anno, FunOrModFun, Args) when is_function(F, 2) ->
F(FunOrModFun, Args).
-%% eval_lc(Expr, [Qualifier], Bindings, LocalFunctionHandler,
+%% eval_lc(ExprOrExprs, [Qualifier], Bindings, LocalFunctionHandler,
%% ExternalFuncHandler, RetBindings) ->
%% {value,Value,Bindings} | Value
+eval_lc(Es, Qs, Bs, Lf, Ef, RBs, FUVs) when is_list(Es) ->
+ ret_expr(lists:reverse(eval_lc1(Es, Qs, Bs, Lf, Ef, FUVs, [])), Bs, RBs);
eval_lc(E, Qs, Bs, Lf, Ef, RBs, FUVs) ->
- ret_expr(lists:reverse(eval_lc1(E, Qs, Bs, Lf, Ef, FUVs, [])), Bs, RBs).
+ eval_lc([E], Qs, Bs, Lf, Ef, RBs, FUVs).
-eval_lc1(E, [{zip, Anno, Gens}|Qs], Bs0, Lf, Ef, FUVs, Acc0) ->
+eval_lc1(Es, [{zip, Anno, Gens}|Qs], Bs0, Lf, Ef, FUVs, Acc0) ->
{VarList, Bs1} = convert_gen_values(Gens, [], Bs0, Lf, Ef, FUVs),
- eval_zip(E, [{zip, Anno, VarList}|Qs], Bs1, Lf, Ef, FUVs, Acc0, fun eval_lc1/7);
-eval_lc1(E, [Q|Qs], Bs0, Lf, Ef, FUVs, Acc0) ->
+ eval_zip(Es, [{zip, Anno, VarList}|Qs], Bs1, Lf, Ef, FUVs, Acc0, fun eval_lc1/7);
+eval_lc1(Es, [Q|Qs], Bs0, Lf, Ef, FUVs, Acc0) ->
case is_generator(Q) of
true ->
- CF = fun(Bs, Acc) -> eval_lc1(E, Qs, Bs, Lf, Ef, FUVs, Acc) end,
+ CF = fun(Bs, Acc) -> eval_lc1(Es, Qs, Bs, Lf, Ef, FUVs, Acc) end,
eval_generator(Q, Bs0, Lf, Ef, FUVs, Acc0, CF);
false ->
- CF = fun(Bs) -> eval_lc1(E, Qs, Bs, Lf, Ef, FUVs, Acc0) end,
+ CF = fun(Bs) -> eval_lc1(Es, Qs, Bs, Lf, Ef, FUVs, Acc0) end,
eval_filter(Q, Bs0, Lf, Ef, CF, FUVs, Acc0)
end;
-eval_lc1(E, [], Bs, Lf, Ef, FUVs, Acc) ->
- {value,V,_} = expr(E, Bs, Lf, Ef, none, FUVs),
- [V|Acc].
+eval_lc1(Es, [], Bs, Lf, Ef, FUVs, Acc) ->
+ {Vs, _} = expr_list(Es, Acc, Bs, Bs, Lf, Ef, FUVs),
+ Vs.
%% convert values for generator vars from abstract form to flattened lists
convert_gen_values([{Generate, Anno, P, L0}|Qs], Acc, Bs0, Lf, Ef,FUVs)
@@ -1219,30 +1221,37 @@ eval_bc1(E, [], Bs, Lf, Ef, FUVs, Acc) ->
{value,V,_} = expr(E, Bs, Lf, Ef, none, FUVs),
<<Acc/bitstring,V/bitstring>>.
-%% eval_mc(Expr, [Qualifier], Bindings, LocalFunctionHandler,
+%% eval_mc(ExprOrExprs, [Qualifier], Bindings, LocalFunctionHandler,
%% ExternalFuncHandler, RetBindings) ->
%% {value,Value,Bindings} | Value
-eval_mc(E, Qs, Bs, Lf, Ef, RBs, FUVs) ->
- L = eval_mc1(E, Qs, Bs, Lf, Ef, FUVs, []),
+eval_mc(Es, Qs, Bs, Lf, Ef, RBs, FUVs) when is_list(Es) ->
+ L = eval_mc1(Es, Qs, Bs, Lf, Ef, FUVs, []),
Map = maps:from_list(reverse(L)),
- ret_expr(Map, Bs, RBs).
+ ret_expr(Map, Bs, RBs);
+eval_mc(E, Qs, Bs, Lf, Ef, RBs, FUVs) ->
+ eval_mc([E], Qs, Bs, Lf, Ef, RBs, FUVs).
-eval_mc1(E, [{zip, Anno, Gens}|Qs], Bs0, Lf, Ef, FUVs, Acc0) ->
+eval_mc1(Es, [{zip, Anno, Gens}|Qs], Bs0, Lf, Ef, FUVs, Acc0) ->
{VarList, Bs1} = convert_gen_values(Gens, [], Bs0, Lf, Ef, FUVs),
- eval_zip(E, [{zip, Anno, VarList}|Qs], Bs1, Lf, Ef, FUVs, Acc0, fun eval_mc1/7);
-eval_mc1(E, [Q|Qs], Bs0, Lf, Ef, FUVs, Acc0) ->
+ eval_zip(Es, [{zip, Anno, VarList}|Qs], Bs1, Lf, Ef, FUVs, Acc0, fun eval_mc1/7);
+eval_mc1(Es, [Q|Qs], Bs0, Lf, Ef, FUVs, Acc0) ->
case is_generator(Q) of
true ->
- CF = fun(Bs, Acc) -> eval_mc1(E, Qs, Bs, Lf, Ef, FUVs, Acc) end,
+ CF = fun(Bs, Acc) -> eval_mc1(Es, Qs, Bs, Lf, Ef, FUVs, Acc) end,
eval_generator(Q, Bs0, Lf, Ef, FUVs, Acc0, CF);
false ->
- CF = fun(Bs) -> eval_mc1(E, Qs, Bs, Lf, Ef, FUVs, Acc0) end,
+ CF = fun(Bs) -> eval_mc1(Es, Qs, Bs, Lf, Ef, FUVs, Acc0) end,
eval_filter(Q, Bs0, Lf, Ef, CF, FUVs, Acc0)
end;
-eval_mc1({map_field_assoc,Lfa,K0,V0}, [], Bs, Lf, Ef, FUVs, Acc) ->
- {value,KV,_} = expr({tuple,Lfa,[K0,V0]}, Bs, Lf, Ef, none, FUVs),
- [KV|Acc].
+eval_mc1(Es, [], Bs, Lf, Ef, FUVs, Acc) ->
+ eval_mc2(Es, Bs, Lf, Ef, FUVs, Acc).
+
+eval_mc2([{map_field_assoc,_,K0,V0}|Es], Bs, Lf, Ef, FUVs, Acc) ->
+ {[K,V],_} = expr_list([K0,V0], Bs, Lf, Ef, FUVs),
+ eval_mc2(Es, Bs, Lf, Ef, FUVs, [{K,V}|Acc]);
+eval_mc2([], _Bs, _Lf, _Ef, _FUVs, Acc) ->
+ Acc.
eval_zip(E, [{zip, Anno, VarList}|Qs], Bs0, Lf, Ef, FUVs, Acc0, Fun) ->
Gens = case check_bad_generators(VarList, {Bs0, Lf, Ef}, []) of
@@ -1502,13 +1511,14 @@ expr_list(Es, Bs, Lf, Ef) ->
expr_list(Es, Bs, Lf, Ef, empty_fun_used_vars()).
expr_list(Es, Bs, Lf, Ef, FUVs) ->
- expr_list(Es, [], Bs, Bs, Lf, Ef, FUVs).
+ {Vs, Bs1} = expr_list(Es, [], Bs, Bs, Lf, Ef, FUVs),
+ {reverse(Vs), Bs1}.
expr_list([E|Es], Vs, BsOrig, Bs0, Lf, Ef, FUVs) ->
{value,V,Bs1} = expr(E, BsOrig, Lf, Ef, none, FUVs),
expr_list(Es, [V|Vs], BsOrig, merge_bindings(Bs1, Bs0, element(2, E), Ef), Lf, Ef, FUVs);
expr_list([], Vs, _, Bs, _Lf, _Ef, _FUVs) ->
- {reverse(Vs),Bs}.
+ {Vs,Bs}.
eval_op(Op, Arg1, Arg2, Anno, Bs, Ef, RBs) ->
do_apply(erlang, Op, [Arg1,Arg2], Anno, Bs, Ef, RBs).
diff --git a/lib/stdlib/src/erl_expand_records.erl b/lib/stdlib/src/erl_expand_records.erl
index 5c80aef2d9..e16bfc3513 100644
--- a/lib/stdlib/src/erl_expand_records.erl
+++ b/lib/stdlib/src/erl_expand_records.erl
@@ -278,6 +278,11 @@ record_test_in_body(Anno, Expr, Name, St0) ->
{atom,NAnno,is_record}},
[Var,{atom,Anno,Name},{integer,Anno,length(Fs)+1}]}]}, St).
+expr_or_exprs(Es, St) when is_list(Es) ->
+ exprs(Es, St);
+expr_or_exprs(E, St) ->
+ expr(E, St).
+
exprs([E0 | Es0], St0) ->
{E,St1} = expr(E0, St0),
{Es,St2} = exprs(Es0, St1),
@@ -304,7 +309,7 @@ expr({cons,Anno,H0,T0}, St0) ->
{{cons,Anno,H,T},St2};
expr({lc,Anno,E0,Qs0}, St0) ->
{Qs1,St1} = lc_tq(Anno, Qs0, St0),
- {E1,St2} = expr(E0, St1),
+ {E1,St2} = expr_or_exprs(E0, St1),
{{lc,Anno,E1,Qs1},St2};
expr({bc,Anno,E0,Qs0}, St0) ->
{Qs1,St1} = lc_tq(Anno, Qs0, St0),
@@ -312,7 +317,7 @@ expr({bc,Anno,E0,Qs0}, St0) ->
{{bc,Anno,E1,Qs1},St2};
expr({mc,Anno,E0,Qs0}, St0) ->
{Qs1,St1} = lc_tq(Anno, Qs0, St0),
- {E1,St2} = expr(E0, St1),
+ {E1,St2} = expr_or_exprs(E0, St1),
{{mc,Anno,E1,Qs1},St2};
expr({tuple,Anno,Es0}, St0) ->
{Es1,St1} = expr_list(Es0, St0),
diff --git a/lib/stdlib/src/erl_lint.erl b/lib/stdlib/src/erl_lint.erl
index 003f139c32..f09a6457be 100644
--- a/lib/stdlib/src/erl_lint.erl
+++ b/lib/stdlib/src/erl_lint.erl
@@ -434,6 +434,8 @@ format_error_1(compr_assign) ->
With 'compr_assign' enabled, a match 'P = E' will behave as a
strict generator 'P <-:- [E]'."
""";
+format_error_1(illegal_map_exact_in_comprehension) ->
+ ~"illegal map association, did you mean to use `=>`?";
%% --- patterns and guards ---
format_error_1(illegal_map_assoc_in_pattern) -> ~"illegal pattern, did you mean to use `:=`?";
format_error_1(illegal_pattern) -> ~"illegal pattern";
@@ -4110,7 +4112,7 @@ icrt_export([], _, _, _, Acc) ->
handle_comprehension(E, Qs, Vt0, St0) ->
{Vt1, Uvt, St1} = lc_quals(Qs, Vt0, St0),
- {Evt,St2} = comprehension_expr(E, Vt1, St1),
+ {Evt,St2} = comprehension_exprs(E, Vt1, St1),
Vt2 = vtupdate(Evt, Vt1),
%% Shadowed global variables.
{_,St3} = check_old_unused_vars(Vt2, Uvt, St2),
@@ -4127,8 +4129,19 @@ handle_comprehension(E, Qs, Vt0, St0) ->
Vt = vt_no_unsafe(vt_no_unused(Vt4)),
{Vt, St}.
+comprehension_exprs(Es, Vt, St0) when is_list(Es) ->
+ foldl(fun (E, {Esvt, St1}) ->
+ {Evt, St2} = comprehension_expr(E, Vt, St1),
+ vtmerge_pat(Evt, Esvt, St2)
+ end, {[], St0}, Es);
+comprehension_exprs(E, Vt, St) ->
+ comprehension_expr(E, Vt, St).
+
comprehension_expr({map_field_assoc,_,K,V}, Vt0, St0) ->
expr_list([K,V], Vt0, St0);
+comprehension_expr({map_field_exact,A,K,V}, Vt0, St0) ->
+ St = add_error(A, illegal_map_exact_in_comprehension, St0),
+ expr_list([K,V], Vt0, St);
comprehension_expr(E, Vt, St) ->
expr(E, Vt, St).
diff --git a/lib/stdlib/src/erl_parse.yrl b/lib/stdlib/src/erl_parse.yrl
index c34bd26521..757f5135c0 100644
--- a/lib/stdlib/src/erl_parse.yrl
+++ b/lib/stdlib/src/erl_parse.yrl
@@ -360,10 +360,15 @@ sigil -> sigil_prefix string sigil_suffix : build_sigil('$1', '$2', '$3').
list_comprehension -> '[' expr '||' lc_exprs ']' :
{lc,?anno('$1'),'$2','$4'}.
-map_comprehension -> '#' '{' map_field_assoc '||' lc_exprs '}' :
+list_comprehension -> '[' expr ',' exprs '||' lc_exprs ']' :
+ {lc,?anno('$1'),['$2'|'$4'],'$6'}.
+map_comprehension -> '#' '{' map_field '||' lc_exprs '}' :
{mc,?anno('$1'),'$3','$5'}.
+map_comprehension -> '#' '{' map_field ',' map_fields '||' lc_exprs '}' :
+ {mc,?anno('$1'),['$3'|'$5'],'$7'}.
binary_comprehension -> '<<' expr_max '||' lc_exprs '>>' :
{bc,?anno('$1'),'$2','$4'}.
+
lc_exprs -> lc_expr : ['$1'].
lc_exprs -> lc_expr ',' lc_exprs : ['$1'|'$3'].
lc_exprs -> zc_exprs : [{zip, ?anno(hd('$1')), '$1'}].
@@ -930,10 +935,10 @@ processed (see section [Error Information](#module-error-information)).
{'remote', anno(), abstract_expr(), abstract_expr()}.
-type af_list_comprehension() ::
- {'lc', anno(), af_template(), af_qualifier_seq()}.
+ {'lc', anno(), af_template() | [af_template()], af_qualifier_seq()}.
-type af_map_comprehension() ::
- {'mc', anno(), af_assoc(abstract_expr()), af_qualifier_seq()}.
+ {'mc', anno(), af_assoc(abstract_expr()) | [af_assoc(abstract_expr())], af_qualifier_seq()}.
-type af_binary_comprehension() ::
{'bc', anno(), af_template(), af_qualifier_seq()}.
diff --git a/lib/stdlib/test/erl_eval_SUITE.erl b/lib/stdlib/test/erl_eval_SUITE.erl
index 1f2bb118f4..c943c727a8 100644
--- a/lib/stdlib/test/erl_eval_SUITE.erl
+++ b/lib/stdlib/test/erl_eval_SUITE.erl
@@ -39,6 +39,8 @@
zlc/1,
zbc/1,
zmc/1,
+ multi_lc/1,
+ multi_mc/1,
simple_cases/1,
unary_plus/1,
apply_atom/1,
@@ -112,7 +114,7 @@ all() ->
funs, custom_stacktrace, try_catch, eval_expr_5, zero_width,
eep37, eep43, otp_15035, otp_16439, otp_14708, otp_16545, otp_16865,
eep49, binary_and_map_aliases, eep58, strict_generators, binary_skip,
- assignment_generators, zlc, zbc, zmc].
+ assignment_generators, zlc, zbc, zmc, multi_lc, multi_mc].
groups() ->
[].
@@ -457,6 +459,28 @@ zmc(Config) when is_list(Config) ->
{bad_generators,{#{1 => 2},#{2 => 3}}}),
ok.
+%% EEP 78: multi-comprehensions
+multi_lc(Config) when is_list(Config) ->
+ check(fun() -> lists:append([[1, 2] || _ <- [1, 2]]) end,
+ "[1, 2 || _ <- [1, 2]].",
+ [1, 2, 1, 2]),
+ check(fun() -> lists:append([[one, two] || true]) end,
+ "[one, two || true].",
+ [one, two]),
+ error_check("[X = 1, X || true].", {unbound_var,'X'}).
+
+multi_mc(Config) when is_list(Config) ->
+ check(fun() -> #{A => B || X <- [1, 5], {A, B} <- [{X, X+1}, {X+2, X+3}]} end,
+ "#{X => X+1, X+2 => X+3 || X <- [1, 5]}.",
+ #{1 => 2, 3 => 4, 5 => 6, 7 => 8}),
+ check(fun() -> #{A => B || X <- [1, 5], {A, B} <- [{X, X+1}, {X, X+3}]} end,
+ "#{X => X+1, X => X+3 || X <- [1, 5]}.",
+ #{1 => 4, 5 => 8}),
+ error_check("#{1 := 2 || _ <- []}.", illegal_map_exact_in_comprehension),
+ error_check("#{1 => 2, 3 := 4 || _ <- []}.", illegal_map_exact_in_comprehension),
+ error_check("#{X = key => value, X => value2 || true}.", {unbound_var,'X'}),
+ error_check("#{key => x = value, key2 => X || true}.", {unbound_var,'X'}).
+
%% Simple cases, just to cover some code.
simple_cases(Config) when is_list(Config) ->
check(fun() -> A = $C end, "A = $C.", $C),
diff --git a/lib/syntax_tools/src/erl_prettypr.erl b/lib/syntax_tools/src/erl_prettypr.erl
index 88edc6f089..2a1e24cf2f 100644
--- a/lib/syntax_tools/src/erl_prettypr.erl
+++ b/lib/syntax_tools/src/erl_prettypr.erl
@@ -878,7 +878,12 @@ lay_2(Node, Ctxt) ->
list_comp ->
Ctxt1 = reset_prec(Ctxt),
- D1 = lay(erl_syntax:list_comp_template(Node), Ctxt1),
+ D1 = case erl_syntax:list_comp_template(Node) of
+ List when is_list(List) ->
+ par(seq(List, floating(text(",")), Ctxt1, fun lay/2));
+ Single ->
+ lay(Single, Ctxt1)
+ end,
D2 = par(seq(erl_syntax:list_comp_body(Node),
floating(text(",")), Ctxt1,
fun lay/2)),
@@ -898,7 +903,12 @@ lay_2(Node, Ctxt) ->
map_comp ->
Ctxt1 = set_prec(Ctxt, max_prec()),
- D1 = lay(erl_syntax:map_comp_template(Node), Ctxt1),
+ D1 = case erl_syntax:map_comp_template(Node) of
+ List when is_list(List) ->
+ par(seq(List, floating(text(",")), Ctxt1, fun lay/2));
+ Single ->
+ lay(Single, Ctxt1)
+ end,
D2 = par(seq(erl_syntax:map_comp_body(Node),
floating(text(",")), Ctxt1,
fun lay/2)),
diff --git a/lib/syntax_tools/src/erl_syntax.erl b/lib/syntax_tools/src/erl_syntax.erl
index ba4eafc9de..871453ff43 100644
--- a/lib/syntax_tools/src/erl_syntax.erl
+++ b/lib/syntax_tools/src/erl_syntax.erl
@@ -5624,7 +5624,7 @@ typed_record_field_type(Node) ->
%% =====================================================================
--record(list_comp, {template :: syntaxTree(), body :: [syntaxTree()]}).
+-record(list_comp, {template :: syntaxTree() | [syntaxTree()], body :: [syntaxTree()]}).
-doc """
Creates an abstract list comprehension.
@@ -5632,9 +5632,12 @@ Creates an abstract list comprehension.
If `Body` is `[E1, ..., En]`, the result represents "`[Template ||
E1, ..., En]`".
+Supports comprehensions with multiple emitted elements per iteration,
+from EEP 78 - in such cases, `Template` is a list of expressions.
+
_See also: _`generator/2`, `list_comp_body/1`, `list_comp_template/1`.
""".
--spec list_comp(syntaxTree(), [syntaxTree()]) -> syntaxTree().
+-spec list_comp(syntaxTree() | [syntaxTree()], [syntaxTree()]) -> syntaxTree().
%% `erl_parse' representation:
%%
@@ -5656,9 +5659,12 @@ revert_list_comp(Node) ->
-doc """
Returns the template subtree of a `list_comp` node.
+Supports comprehensions with multiple emitted elements per iteration,
+from EEP 78 - in such cases, template will be a list of expressions.
+
_See also: _`list_comp/2`.
""".
--spec list_comp_template(syntaxTree()) -> syntaxTree().
+-spec list_comp_template(syntaxTree()) -> syntaxTree() | [syntaxTree()].
list_comp_template(Node) ->
case unwrap(Node) of
@@ -5748,7 +5754,7 @@ binary_comp_body(Node) ->
%% =====================================================================
--record(map_comp, {template :: syntaxTree(), body :: [syntaxTree()]}).
+-record(map_comp, {template :: syntaxTree() | [syntaxTree()], body :: [syntaxTree()]}).
-doc """
Creates an abstract map comprehension.
@@ -5756,9 +5762,12 @@ Creates an abstract map comprehension.
If `Body` is `[E1, ..., En]`, the result represents "`#{Template ||
E1, ..., En}`".
+Supports comprehensions with multiple emitted elements per iteration,
+from EEP 78 - in such cases, `Template` is a list of key-value associations.
+
_See also: _`generator/2`, `map_comp_body/1`, `map_comp_template/1`.
""".
--spec map_comp(syntaxTree(), [syntaxTree()]) -> syntaxTree().
+-spec map_comp(syntaxTree() | [syntaxTree()], [syntaxTree()]) -> syntaxTree().
%% `erl_parse' representation:
%%
@@ -5780,9 +5789,12 @@ revert_map_comp(Node) ->
-doc """
Returns the template subtree of a `map_comp` node.
+Supports comprehensions with multiple emitted elements per iteration,
+from EEP 78 - in such cases, template will be list of key-value associations.
+
_See also: _`map_comp/2`.
""".
--spec map_comp_template(syntaxTree()) -> syntaxTree().
+-spec map_comp_template(syntaxTree()) -> syntaxTree() | [syntaxTree()].
map_comp_template(Node) ->
case unwrap(Node) of
@@ -7885,7 +7897,12 @@ subtrees(T) ->
[list_prefix(T), [S]]
end;
list_comp ->
- [[list_comp_template(T)], list_comp_body(T)];
+ case list_comp_template(T) of
+ Exprs when is_list(Exprs) ->
+ [Exprs, list_comp_body(T)];
+ Expr ->
+ [[Expr], list_comp_body(T)]
+ end;
macro ->
case macro_arguments(T) of
none ->
@@ -7894,7 +7911,12 @@ subtrees(T) ->
[[macro_name(T)], As]
end;
map_comp ->
- [[map_comp_template(T)], map_comp_body(T)];
+ case map_comp_template(T) of
+ Exprs when is_list(Exprs) ->
+ [Exprs, map_comp_body(T)];
+ Expr ->
+ [[Expr], map_comp_body(T)]
+ end;
map_expr ->
case map_expr_argument(T) of
none ->
@@ -8094,9 +8116,11 @@ make_tree(integer_range_type, [[L],[H]]) -> integer_range_type(L, H);
make_tree(list, [P]) -> list(P);
make_tree(list, [P, [S]]) -> list(P, S);
make_tree(list_comp, [[T], B]) -> list_comp(T, B);
+make_tree(list_comp, [Ts, B]) -> list_comp(Ts, B);
make_tree(macro, [[N]]) -> macro(N);
make_tree(macro, [[N], A]) -> macro(N, A);
make_tree(map_comp, [[T], B]) -> map_comp(T, B);
+make_tree(map_comp, [Ts, B]) -> map_comp(Ts, B);
make_tree(map_expr, [Fs]) -> map_expr(Fs);
make_tree(map_expr, [[E], Fs]) -> map_expr(E, Fs);
make_tree(map_field_assoc, [[K], [V]]) -> map_field_assoc(K, V);
diff --git a/lib/syntax_tools/src/erl_syntax_lib.erl b/lib/syntax_tools/src/erl_syntax_lib.erl
index ce66c8d7e5..cbd47388bb 100644
--- a/lib/syntax_tools/src/erl_syntax_lib.erl
+++ b/lib/syntax_tools/src/erl_syntax_lib.erl
@@ -542,6 +542,11 @@ vann_list_join(Env) ->
vann_list(Ts, Env) ->
lists:mapfoldl(vann_list_join(Env), {[], []}, Ts).
+vann_expr_or_list(Ts, Env) when is_list(Ts) ->
+ vann_list(Ts, Env);
+vann_expr_or_list(T, Env) ->
+ vann(T, Env).
+
vann_function(Tree, Env) ->
Cs = erl_syntax:function_clauses(Tree),
{Cs1, {_, Free}} = vann_clauses(Cs, Env),
@@ -677,7 +682,7 @@ vann_list_comp(Tree, Env) ->
{Es1, {Bound1, Free1}} = vann_comp_body(Es, Env),
Env1 = ordsets:union(Env, Bound1),
T = erl_syntax:list_comp_template(Tree),
- {T1, _, Free2} = vann(T, Env1),
+ {T1, _, Free2} = vann_expr_or_list(T, Env1),
Free = ordsets:union(Free1, ordsets:subtract(Free2, Bound1)),
Bound = [],
Tree1 = rewrite(Tree, erl_syntax:list_comp(T1, Es1)),
@@ -699,7 +704,7 @@ vann_map_comp(Tree, Env) ->
{Es1, {Bound1, Free1}} = vann_comp_body(Es, Env),
Env1 = ordsets:union(Env, Bound1),
T = erl_syntax:map_comp_template(Tree),
- {T1, _, Free2} = vann(T, Env1),
+ {T1, _, Free2} = vann_expr_or_list(T, Env1),
Free = ordsets:union(Free1, ordsets:subtract(Free2, Bound1)),
Bound = [],
Tree1 = rewrite(Tree, erl_syntax:map_comp(T1, Es1)),
@@ -2218,4 +2223,3 @@ push(N, C, Cs) when N > 0 ->
push(N - 1, C, [C | Cs]);
push(0, _, Cs) ->
Cs.
-
diff --git a/lib/syntax_tools/test/syntax_tools_SUITE.erl b/lib/syntax_tools/test/syntax_tools_SUITE.erl
index fb408e40ef..110fa44406 100644
--- a/lib/syntax_tools/test/syntax_tools_SUITE.erl
+++ b/lib/syntax_tools/test/syntax_tools_SUITE.erl
@@ -348,12 +348,14 @@ t_erl_parse_type(Config) when is_list(Config) ->
{"#{ a:=1, b:=2 }", map_expr,false},
{"M#{ a=>1, b=>2 }", map_expr,false},
{"[V||V <- Vs]", list_comp,false},
+ {"[V,V||V <- Vs]", list_comp,false},
{"[V||V <:- Vs]", list_comp,false},
{"[catch V||V <- Vs]", list_comp,false},
{"<< <<B>> || <<B>> <= Bs>>", binary_comp,false},
{"<< <<B>> || <<B>> <:= Bs>>", binary_comp,false},
{"<< (catch <<B>>) || <<B>> <= Bs>>", binary_comp,false},
{"#{K => V || {K,V} <- KVs}", map_comp,false},
+ {"#{K => V, K => V || {K,V} <- KVs}", map_comp,false},
{"#{K => V || {K,V} <:- KVs}", map_comp,false},
{"#{K => (catch V) || {K,V} <- KVs}", map_comp,false},
{"[V+W||V <- Vs && W <- Ws]", list_comp,false},
diff --git a/lib/syntax_tools/test/syntax_tools_SUITE_data/syntax_tools_SUITE_test_module.erl b/lib/syntax_tools/test/syntax_tools_SUITE_data/syntax_tools_SUITE_test_module.erl
index ddf06a7597..368de2de12 100644
--- a/lib/syntax_tools/test/syntax_tools_SUITE_data/syntax_tools_SUITE_test_module.erl
+++ b/lib/syntax_tools/test/syntax_tools_SUITE_data/syntax_tools_SUITE_test_module.erl
@@ -10,6 +10,7 @@
-export([to_upper/1, to_lower/1]).
-export([eep49/0, eep58/0]).
-export([strict_generators/0]).
+-export([eep73/0, eep78/0]).
-import(lists,[reverse/1,member/2]).
@@ -605,3 +606,14 @@ eep73() ->
[{X,Y}||X <- [1,2,3] && <<Y>> <= <<2,2,2>>],
[{K1,K2,V1,V2}|| K1 := V1 <- #{a=>1} && K2 := V2 <- #{b=>3}],
ok.
+
+%% EEP-78: Multi-comprehensions.
+eep78() ->
+ Seq = lists:seq(1, 10),
+
+ List = lists:flatten([[X, X + 100] || X <- Seq]),
+ List = [X, X + 100 || X <- Seq],
+
+ Map = maps:from_list([{X, X} || X <- List]),
+ Map = #{X => X, X + 100 => X + 100 || X <- Seq},
+ ok.
diff --git a/system/doc/programming_examples/list_comprehensions.md b/system/doc/programming_examples/list_comprehensions.md
index 11c8bcf9ec..04fe3c2308 100644
--- a/system/doc/programming_examples/list_comprehensions.md
+++ b/system/doc/programming_examples/list_comprehensions.md
@@ -60,6 +60,12 @@ follows:
[{1,a},{2,b},{3,c}]
```
+Finally, multiple elements can be emitted by the list comprehension in each iteration:
+```
+> [X, X + 100 || X <:- [1, 2, 3]].
+[1,101,2,102,3,103]
+```
+
> #### Change {: .info }
>
> Strict generators are used by default in the examples. More details and
@@ -329,4 +335,4 @@ left-hand side pattern of a generator is a fresh variable, pattern matching
cannot fail. Using either strict or relaxed generators leads to the same
behavior. While the preference and use cases might be individual, it is
recommended to use strict generators when either can be used. Using strict
-generators by default aligns with Erlang's "Let it crash" philosophy.
\ No newline at end of file
+generators by default aligns with Erlang's "Let it crash" philosophy.
--
2.51.0