From ad90a17bf4f21953ea43acec861d875dd2ac510b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Szoboszlay?= Date: Thu, 13 Jun 2024 12:09:11 +0200 Subject: [PATCH] [WIP] Implement non-skipping generators as an experimental feature. Currently existing generators are "skipping": they ignore terms in the right-hand side expression that do not match the left-hand side pattern. Non-skipping generators on the other hand fail with exception badmatch. The motivation for non-skipping generators is that skipping generators can hide the presence of unexpected elements in the input data of a comprehension. For example consider the below snippet: [{User, Email} || #{user := User, email := Email} <- all_users()] This list comprehension would skip users that don't have an email address. This may be an issue if we suspect potentionally incorrect input data, like in case all_users/0 would read the users from a JSON file. Therefore caucious code that would prefer crashing instead of silently skipping incorrect input would have to use a more verbose map function: lists:map(fun(#{user := User, email := Email}) -> {User, Email} end, all_users()) Unlike the generator, the anonymous function would crash on a user without an email address. Non-skipping generators would allow similar semantics in comprehensions too: [{User, Email} || #{user := User, email := Email} <:- all_users()] This generator would crash (with a badmatch error) if the pattern wouldn't match an element of the list. Syntactically non-skipping generators use <:- (for lists and maps) and <:= (for binaries) instead of <- and <=. This syntax was chosen because `<:-` and `<:=` somewhat resemble the `=:=` operator that tests whether two terms match, and at the same time keep the operators short and easy to type. Having the two types of operators differ by a single character, `:`, also makes the operators easy to remember as "`:` means non-skipping." Nevertheless, non-skipping generators are added as an experimental feature, that has to be explicitly enabled: -feature(non_skipping_generators,enable). change operators --- erts/doc/guides/absform.md | 7 + lib/compiler/src/sys_coverage.erl | 12 + lib/compiler/src/v3_core.erl | 241 ++++++++++++---- lib/compiler/test/Makefile | 5 +- lib/compiler/test/bs_bincomp_SUITE.erl | 38 ++- lib/compiler/test/lc_SUITE.erl | 17 +- lib/compiler/test/mc_SUITE.erl | 15 + lib/debugger/src/dbg_ieval.erl | 125 +++++---- lib/debugger/src/dbg_iload.erl | 6 + lib/debugger/test/Makefile | 3 +- lib/debugger/test/bs_bincomp_SUITE.erl | 32 ++- lib/debugger/test/lc_SUITE.erl | 19 +- lib/debugger/test/mc_SUITE.erl | 15 + lib/stdlib/examples/erl_id_trans.erl | 12 + lib/stdlib/src/erl_eval.erl | 64 +++-- lib/stdlib/src/erl_expand_records.erl | 18 ++ lib/stdlib/src/erl_features.erl | 9 + lib/stdlib/src/erl_lint.erl | 10 + lib/stdlib/src/erl_parse.yrl | 12 +- lib/stdlib/src/erl_pp.erl | 9 + lib/stdlib/src/erl_scan.erl | 10 +- lib/stdlib/src/qlc_pt.erl | 9 +- lib/stdlib/src/shell.erl | 6 + lib/stdlib/test/Makefile | 6 +- lib/stdlib/test/erl_eval_SUITE.erl | 260 ++++++++++++------ lib/stdlib/test/erl_scan_SUITE.erl | 6 +- lib/syntax_tools/src/erl_prettypr.erl | 50 ++-- lib/syntax_tools/src/erl_syntax.erl | 216 +++++++++++++++ lib/syntax_tools/src/erl_syntax_lib.erl | 48 +++- lib/syntax_tools/test/syntax_tools_SUITE.erl | 11 +- .../syntax_tools_SUITE_test_module.erl | 14 +- lib/tools/emacs/erlang.el | 4 +- system/doc/reference_manual/expressions.md | 39 ++- system/doc/reference_manual/features.md | 4 + 34 files changed, 1066 insertions(+), 286 deletions(-) diff --git a/erts/doc/guides/absform.md b/erts/doc/guides/absform.md index 6f15630dd98b..1528615358cc 100644 --- a/erts/doc/guides/absform.md +++ b/erts/doc/guides/absform.md @@ -294,11 +294,18 @@ A qualifier Q is one of the following: - If Q is a filter `E`, where `E` is an expression, then Rep(Q) = `Rep(E)`. - If Q is a list generator `P <- E`, where `P` is a pattern and `E` is an expression, then Rep(Q) = `{generate,ANNO,Rep(P),Rep(E)}`. +- If Q is a list generator `P <:- E`, where `P` is a pattern and `E` is an + expression, then Rep(Q) = `{generate_ns,ANNO,Rep(P),Rep(E)}`. - If Q is a bitstring generator `P <= E`, where `P` is a pattern and `E` is an expression, then Rep(Q) = `{b_generate,ANNO,Rep(P),Rep(E)}`. +- If Q is a bitstring generator `P <:= E`, where `P` is a pattern and `E` is an + expression, then Rep(Q) = `{b_generate_ns,ANNO,Rep(P),Rep(E)}`. - If Q is a map generator `P <- E`, where `P` is an association pattern `P_1 := P_2` and `E` is an expression, then Rep(Q) = `{m_generate,ANNO,Rep(P),Rep(E)}`. For Rep(P), see below. +- If Q is a map generator `P <:- E`, where `P` is an association pattern + `P_1 := P_2` and `E` is an expression, then Rep(Q) = + `{m_generate_ns,ANNO,Rep(P),Rep(E)}`. ### Bitstring Element Type Specifiers diff --git a/lib/compiler/src/sys_coverage.erl b/lib/compiler/src/sys_coverage.erl index 2c0539a37c34..523bfa6e7c70 100644 --- a/lib/compiler/src/sys_coverage.erl +++ b/lib/compiler/src/sys_coverage.erl @@ -553,14 +553,26 @@ munge_qs([{generate,Anno,Pattern,Expr}|Qs], Vars0, MQs) -> A = element(2, Expr), {MungedExpr, Vars1} = munge_expr(Expr, Vars0), munge_qs1(Qs, A, {generate,Anno,Pattern,MungedExpr}, Vars0, Vars1, MQs); +munge_qs([{generate_ns,Anno,Pattern,Expr}|Qs], Vars0, MQs) -> + A = element(2, Expr), + {MungedExpr, Vars1} = munge_expr(Expr, Vars0), + munge_qs1(Qs, A, {generate_ns,Anno,Pattern,MungedExpr}, Vars0, Vars1, MQs); munge_qs([{b_generate,Anno,Pattern,Expr}|Qs], Vars0, MQs) -> A = element(2, Expr), {MExpr, Vars1} = munge_expr(Expr, Vars0), munge_qs1(Qs, A, {b_generate,Anno,Pattern,MExpr}, Vars0, Vars1, MQs); +munge_qs([{b_generate_ns,Anno,Pattern,Expr}|Qs], Vars0, MQs) -> + A = element(2, Expr), + {MExpr, Vars1} = munge_expr(Expr, Vars0), + munge_qs1(Qs, A, {b_generate_ns,Anno,Pattern,MExpr}, Vars0, Vars1, MQs); munge_qs([{m_generate,Anno,Pattern,Expr}|Qs], Vars0, MQs) -> A = element(2, Expr), {MExpr, Vars1} = munge_expr(Expr, Vars0), munge_qs1(Qs, A, {m_generate,Anno,Pattern,MExpr}, Vars0, Vars1, MQs); +munge_qs([{m_generate_ns,Anno,Pattern,Expr}|Qs], Vars0, MQs) -> + A = element(2, Expr), + {MExpr, Vars1} = munge_expr(Expr, Vars0), + munge_qs1(Qs, A, {m_generate_ns,Anno,Pattern,MExpr}, Vars0, Vars1, MQs); munge_qs([Expr|Qs], Vars0, MQs) -> A = element(2, Expr), {MungedExpr, Vars1} = munge_expr(Expr, Vars0), diff --git a/lib/compiler/src/v3_core.erl b/lib/compiler/src/v3_core.erl index 5a6ec26b1b8d..b1fb198d5cae 100644 --- a/lib/compiler/src/v3_core.erl +++ b/lib/compiler/src/v3_core.erl @@ -64,7 +64,7 @@ %% atom 'compiler_generated', to indicate that the compiler has generated %% them and that no warning should be generated if they are optimized %% away. -%% +%% %% %% In this translation: %% @@ -121,7 +121,8 @@ -record(itry, {anno=#a{},args,vars,body,evars,handler}). -record(ifilter, {anno=#a{},arg}). -record(igen, {anno=#a{},acc_pat,acc_guard, - skip_pat,tail,tail_pat,arg, + nomatch_pat,nomatch_guard,nomatch_mode, + tail,tail_pat,arg, refill={nomatch,ignore}}). -record(isimple, {anno=#a{},term :: cerl:cerl()}). @@ -343,7 +344,7 @@ guard(Gs0, St0) -> end, guard_tests(last(Gs0)), droplast(Gs0)), {Gs,St} = gexpr_top(Gs1, St0#core{in_guard=true}), {Gs,St#core{in_guard=false}}. - + guard_tests(Gs) -> L = element(2, hd(Gs)), {protect,L,foldr(fun (G, Rhs) -> {op,L,'and',G,Rhs} end, last(Gs), droplast(Gs))}. @@ -1515,7 +1516,7 @@ constant_bin_1(Es) -> end. %% verify_suitable_fields([{bin_element,_,Sz,Opts}=E|Es]) -> - + verify_suitable_fields([{bin_element,_,Val,SzTerm,Opts}|Es]) -> case member(big, Opts) orelse member(little, Opts) of true -> ok; @@ -1603,22 +1604,39 @@ fun_tq(Cs0, L, St0, NameInfo) -> lc_tq(Line, E, [#igen{anno=#a{anno=GA}=GAnno, acc_pat=AccPat,acc_guard=AccGuard, - skip_pat=SkipPat,tail=Tail,tail_pat=TailPat, + nomatch_pat=NomatchPat, + nomatch_guard=NomatchGuard, + nomatch_mode=NomatchMode, + tail=Tail,tail_pat=TailPat, refill={RefillPat,RefillAction}, arg={Pre,Arg}}|Qs], Mc, St0) -> {Name,St1} = new_fun_name("lc", St0), LA = lineno_anno(Line, St1), F = #c_var{anno=LA,name={Name,1}}, - Nc = #iapply{anno=GAnno,op=F,args=[Tail]}, + Sc = #iapply{anno=GAnno,op=F,args=[Tail]}, + Nc = case NomatchMode of + skip -> + Sc; + BM -> + #icall{anno=GAnno, + module=#c_literal{anno=GA,val=erlang}, + name=#c_literal{anno=GA,val=error}, + args=[ann_c_tuple(GA, [#c_literal{val=badmatch},BM])]} + end, {[FcVar,Var],St2} = new_vars(2, St1), Fc = bad_generator([FcVar], FcVar, Arg), - SkipClause = make_clause([skip_clause,compiler_generated|LA], - SkipPat, [], [], [Nc]), + NomatchClause = make_clause([nomatch_clause,compiler_generated|LA], + NomatchPat, [], NomatchGuard, [Nc]), TailClause = make_clause(LA, TailPat, [], [], [Mc]), - {Lc,Lps,St3} = lc_tq(Line, E, Qs, Nc, St2), + {Lc,Lps,St3} = lc_tq(Line, E, Qs, Sc, St2), AccClause = make_clause(LA, AccPat, [], AccGuard, Lps ++ [Lc]), - RefillClause = make_clause(LA, RefillPat, [], [], [RefillAction,Nc]), - Cs0 = [AccClause,SkipClause,TailClause,RefillClause], + AccClauseNoGuards = if AccGuard =:= [] -> nomatch; + NomatchMode =:= skip -> nomatch; + true -> make_clause([compiler_generated|LA], + AccPat, [], [], [Sc]) + end, + RefillClause = make_clause(LA, RefillPat, [], [], [RefillAction,Sc]), + Cs0 = [AccClause,AccClauseNoGuards,NomatchClause,TailClause,RefillClause], Cs = [C || C <- Cs0, C =/= nomatch], Fun = #ifun{anno=GAnno,id=[],vars=[Var],clauses=Cs,fc=Fc}, {#iletrec{anno=GAnno#a{anno=[list_comprehension|GA]},defs=[{{Name,1},Fun}], @@ -1634,7 +1652,7 @@ lc_tq(Line, E0, [], Mc0, St0) -> {set_anno(E, [compiler_generated|Anno]),Hps ++ Tps,St}. %% bc_tq(Line, Exp, [Qualifier], More, State) -> {LetRec,[PreExp],State}. -%% This TQ from Gustafsson ERLANG'05. +%% This TQ from Gustafsson ERLANG'05. %% More could be transformed before calling bc_tq. bc_tq(Line, Exp, Qs0, St0) -> @@ -1655,9 +1673,12 @@ bc_tq(Line, Exp, Qs0, St0) -> args=[InitialSize]}}] ++ BcPre, {E,Pre,St}. -bc_tq1(Line, E, [#igen{anno=GAnno, +bc_tq1(Line, E, [#igen{anno=#a{anno=GA}=GAnno, acc_pat=AccPat,acc_guard=AccGuard, - skip_pat=SkipPat,tail=Tail,tail_pat=TailPat, + nomatch_pat=NomatchPat, + nomatch_guard=NomatchGuard, + nomatch_mode=NomatchMode, + tail=Tail,tail_pat=TailPat, refill={RefillPat,RefillAction}, arg={Pre,Arg}}|Qs], Mc, St0) -> {Name,St1} = new_fun_name("lbc", St0), @@ -1667,16 +1688,30 @@ bc_tq1(Line, E, [#igen{anno=GAnno, {[_,_]=FcVars,St3} = new_vars(LA, 2, St2), {IgnoreVar,St4} = new_var(LA, St3), F = #c_var{anno=LA,name={Name,2}}, - Nc = #iapply{anno=GAnno,op=F,args=[Tail,AccVar]}, + Sc = #iapply{anno=GAnno,op=F,args=[Tail,AccVar]}, + Nc = case NomatchMode of + skip -> + Sc; + BM -> + #icall{anno=GAnno, + module=#c_literal{anno=GA,val=erlang}, + name=#c_literal{anno=GA,val=error}, + args=[ann_c_tuple(GA, [#c_literal{val=badmatch},BM])]} + end, Fc = bad_generator(FcVars, hd(FcVars), Arg), - SkipClause = make_clause([compiler_generated,skip_clause|LA], - SkipPat, [IgnoreVar], [], [Nc]), + NomatchClause = make_clause([compiler_generated,nomatch_clause|LA], + NomatchPat, [IgnoreVar], NomatchGuard, [Nc]), TailClause = make_clause(LA, TailPat, [IgnoreVar], [], [AccVar]), {Bc,Bps,St5} = bc_tq1(Line, E, Qs, AccVar, St4), - Body = Bps ++ [#iset{var=AccVar,arg=Bc},Nc], + Body = Bps ++ [#iset{var=AccVar,arg=Bc},Sc], AccClause = make_clause(LA, AccPat, [IgnoreVar], AccGuard, Body), - RefillClause = make_clause(LA, RefillPat, [AccVar], [], [RefillAction,Nc]), - Cs0 = [AccClause,SkipClause,TailClause,RefillClause], + AccClauseNoGuards = if AccGuard =:= [] -> nomatch; + NomatchMode =:= skip -> nomatch; + true -> make_clause([compiler_generated|LA], + AccPat, [IgnoreVar], [], [Sc]) + end, + RefillClause = make_clause(LA, RefillPat, [AccVar], [], [RefillAction,Sc]), + Cs0 = [AccClause,AccClauseNoGuards,NomatchClause,TailClause,RefillClause], Cs = [C || C <- Cs0, C =/= nomatch], Fun = #ifun{anno=GAnno,id=[],vars=Vars,clauses=Cs,fc=Fc}, @@ -1811,8 +1846,11 @@ preprocess_quals(_, [], St, Acc) -> {reverse(Acc),St}. is_generator({generate,_,_,_}) -> true; +is_generator({generate_ns,_,_,_}) -> true; is_generator({b_generate,_,_,_}) -> true; +is_generator({b_generate_ns,_,_,_}) -> true; is_generator({m_generate,_,_,_}) -> true; +is_generator({m_generate_ns,_,_,_}) -> true; is_generator(_) -> false. %% Retrieve the annotation from an Erlang AST form. @@ -1825,10 +1863,15 @@ get_qual_anno(Abstract) -> element(2, Abstract). %% - acc_pat is the accumulator pattern, e.g. [Pat|Tail] for Pat <- Expr. %% - acc_guard is the list of guards immediately following the current %% generator in the qualifier list input. -%% - skip_pat is the skip pattern, e.g. <> for -%% <> <= Expr. -%% - tail is the variable used in AccPat and SkipPat bound to the rest of the -%% generator input. +%% - nomatch_pat is the no-match pattern, e.g. <> +%% for <> <= Expr. +%% - nomatch_guard is the list of guards to add to the no-match clause. +%% - nomatch_mode is either skip (not matching elements of the generator +%% have to be silently skipped by the comprehension) or a value X +%% (to be used in the {badmatch, X} error a non-skipping generator +%% shall raise). +%% - tail is the variable used in AccPat and NomatchPat bound to the +%% rest of the generator input. %% - tail_pat is the tail pattern, respectively [] and <<_/bitstring>> for list %% and bit string generators. %% - refill is a pair {RefillPat,RefillAction}, used to refill the iterator @@ -1841,11 +1884,12 @@ get_qual_anno(Abstract) -> element(2, Abstract). %% generator(Line, Generator, Guard, State) -> {Generator',State}. %% Transform a given generator into its #igen{} representation. -generator(Line, {generate,Lg,P0,E}, Gs, St0) -> +generator(Line, {Generate,Lg,P0,E}, Gs, St0) when Generate =:= generate; + Generate =:= generate_ns -> LA = lineno_anno(Line, St0), GA = lineno_anno(Lg, St0), {Head,St1} = list_gen_pattern(P0, Line, St0), - {[Tail,Skip],St2} = new_vars(2, St1), + {[Tail,Nomatch],St2} = new_vars(2, St1), {Cg,St3} = lc_guard_tests(Gs, St2), AccPat = case Head of nomatch -> @@ -1853,17 +1897,56 @@ generator(Line, {generate,Lg,P0,E}, Gs, St0) -> _ -> ann_c_cons(LA, Head, Tail) end, - SkipPat = ann_c_cons(LA, Skip, Tail), + NomatchPat = ann_c_cons(LA, Nomatch, Tail), + NomatchMode = case Generate of + generate -> + skip; + generate_ns -> + Nomatch + end, {Ce,Pre,St4} = safe(E, St3), - Gen = #igen{anno=#a{anno=GA}, - acc_pat=AccPat,acc_guard=Cg,skip_pat=SkipPat, + Gen = #igen{anno=#a{anno=GA},acc_pat=AccPat,acc_guard=Cg, + nomatch_pat=NomatchPat,nomatch_guard=[],nomatch_mode=NomatchMode, tail=Tail,tail_pat=#c_literal{anno=LA,val=[]},arg={Pre,Ce}}, {Gen,St4}; -generator(Line, {b_generate,Lg,P,E}, Gs, St0) -> +generator(Line, {Generate,Lg,P,E}, Gs, St0) when Generate =:= b_generate; + Generate =:= b_generate_ns -> LA = lineno_anno(Line, St0), GA = lineno_anno(Lg, St0), + GAnno = #a{anno=GA}, + %% No-match patterns are very different between skipping and + %% non-skipping generators. Consider a skipping generator like + %% <<0:1, X:15>> <= Expr. If the pattern doesn't match the beginning + %% of the bitstring Expr evaluates to, the non-matching part has to + %% be skipped. But, unlike in case of list or map generators, + %% bitstrings don't have a natural "first element" that can be + %% skipped (would it be 1 byte? or 1 bit?), so how many bits to + %% skip? + %% + %% In this example the pattern is always 16 bits long, so we will + %% skip 16 bits, unless the remaining bitstring is less than 16 bits + %% long, in which case no more elements are generated. + %% + %% However, patterns can be variable length too, like + %% <<0:1, X:15, Y:X>>. In this case we geenerate a skip pattern that + %% still attempts to parse the field lengths: + %% <<_:1, X:16, _:X, Tail/bitstring>>. If it matches, we can + %% continue the generator on the tail, otherwise we can stop. + %% + %% But for a non-skipping generator there are only three possible + %% scenarios to consider depending on what kind of bitstring Expr + %% evaluates to: + %% - In case of an empty binary, we can stop. + %% - If the pattern matches the beginning of the bitstring, we can + %% continue. + %% - Otherwise the generator must fail. + %% + %% This means the skip pattern could be "any non-empty bitstring": + %% <<_:1, _/bitstring>>. However, to simplify raising the badmatch + %% exception, the code will instead use a variable for the pattern + %% and add the non-empty bitstring constraint as a guard. try pattern(P, St0) of - {#ibinary{segments=Segs}=Cp,St1} -> + {#ibinary{segments=Segs}=Cp,St1} when Generate =:= b_generate -> %% The function append_tail_segment/2 keeps variable %% patterns as-is, making it possible to have the same %% skip clause removal as with list generators. @@ -1871,26 +1954,64 @@ generator(Line, {b_generate,Lg,P,E}, Gs, St0) -> AccPat = Cp#ibinary{segments=AccSegs}, {Cg,St3} = lc_guard_tests(Gs, St2), {SkipSegs,St4} = skip_segments(AccSegs, St3, []), - SkipPat = Cp#ibinary{segments=SkipSegs}, + NomatchPat = Cp#ibinary{segments=SkipSegs}, {Ce,Pre,St5} = safe(E, St4), - Gen = #igen{anno=#a{anno=GA},acc_pat=AccPat,acc_guard=Cg, - skip_pat=SkipPat,tail=Tail, - tail_pat=#ibinary{anno=#a{anno=LA},segments=[TailSeg]}, + Gen = #igen{anno=GAnno,acc_pat=AccPat,acc_guard=Cg, + nomatch_pat=NomatchPat,nomatch_guard=[],nomatch_mode=skip, + tail=Tail,tail_pat=#ibinary{anno=#a{anno=LA},segments=[TailSeg]}, + arg={Pre,Ce}}, + {Gen,St5}; + {#ibinary{segments=Segs}=Cp,St1} when Generate =:= b_generate_ns -> + {AccSegs,Tail,TailSeg,St2} = append_tail_segment(Segs, St1), + AccPat = Cp#ibinary{segments=AccSegs}, + {Cg,St3} = lc_guard_tests(Gs, St2), + {[BitStr,BitSize],St4} = new_vars(2, St3), + %% The non-empty bistring guard could be implemented either + %% as + %% + %% bit_size(BitStr) > 0 + %% + %% or as + %% + %% is_bitstring(BitStr), BitStr =/= <<>> + %% + %% However, in the second case the compiler incorrectly + %% decides the is_bitstring/1 call had no effect and optimises + %% it away. So we stick to the first option instead. + BitSizeCall = #icall{anno=GAnno, + module=#c_literal{anno=GA,val=erlang}, + name=#c_literal{anno=GA,val=bit_size}, + args=[BitStr]}, + CompCall = #icall{anno=GAnno, + module=#c_literal{anno=GA,val=erlang}, + name=#c_literal{anno=GA,val='>'}, + args=[BitSize,#c_literal{anno=GA,val=0}]}, + Guard = #iprotect{anno=GAnno, + body=[#iset{anno=GAnno,var=BitSize,arg=BitSizeCall}, + CompCall]}, + {Ce,Pre,St5} = safe(E, St4), + Gen = #igen{anno=GAnno,acc_pat=AccPat,acc_guard=Cg, + nomatch_pat=BitStr, + nomatch_guard=[Guard], + nomatch_mode=BitStr, + tail=Tail,tail_pat=#ibinary{anno=#a{anno=LA},segments=[TailSeg]}, arg={Pre,Ce}}, {Gen,St5} catch throw:nomatch -> {Ce,Pre,St1} = safe(E, St0), Gen = #igen{anno=#a{anno=GA},acc_pat=nomatch,acc_guard=[], - skip_pat=nomatch, + nomatch_pat=nomatch,nomatch_guard=[],nomatch_mode=skip, tail_pat=#c_var{name='_'}, arg={Pre,Ce}}, {Gen,St1} end; -generator(Line, {m_generate,Lg,{map_field_exact,_,K0,V0},E}, Gs, St0) -> +generator(Line, {Generate,Lg,{map_field_exact,_,K0,V0},E}, Gs, St0) when + Generate =:= m_generate; + Generate =:= m_generate_ns -> %% Consider this example: %% - %% [{K,V} || K := V <- L]. + %% [{K,V} || K := V <:- L, is_integer(K)]. %% %% The following Core Erlang code will be generated: %% @@ -1898,14 +2019,20 @@ generator(Line, {m_generate,Lg,{map_field_exact,_,K0,V0},E}, Gs, St0) -> %% 'lc$^0'/1 = %% fun (Iter0) -> %% case Iter0 of - %% <{K,V,NextIter}> when 'true' -> + %% <{K,V,NextIter}> when call 'erlang':'is_integer' (K) -> %% let = %% apply 'lc$^0'/1(NextIter) %% in [{K,V}|Tail] %% <{_K,_V,NextIter}> when 'true' -> - %% %% Skip clause; will be optimized away later - %% %% since there are no filters. + %% %% Match clause without guards; this is always + %% %% a skip, even for non-skipping generators + %% %% (since the generator does match in this case, + %% %% the skip happens due to the subsequent guard). %% apply 'lc$^0'/1(NextIter) + %% <{K,V,_NextIter}> when 'true' -> + %% %% Nomatch clause; will be optimized away later + %% %% since the left hand side pattern always matches. + %% call 'erlang':'error'({'badmatch',{K,V}}) %% <'none'> when 'true' -> %% [] %% when 'true' -> @@ -1929,7 +2056,7 @@ generator(Line, {m_generate,Lg,{map_field_exact,_,K0,V0},E}, Gs, St0) -> LA = lineno_anno(Line, St0), GA = lineno_anno(Lg, St0), {Pat,St1} = list_gen_pattern({cons,Lg,K0,V0}, Line, St0), - {[SkipK,SkipV,IterVar,OuterIterVar,_BadGenVar],St2} = new_vars(5, St1), + {[NomatchK,NomatchV,IterVar,OuterIterVar,_BadGenVar],St2} = new_vars(5, St1), {Cg,St3} = lc_guard_tests(Gs, St2), {Ce,Pre0,St4} = safe(E, St3), AccPat = case Pat of @@ -1940,14 +2067,19 @@ generator(Line, {m_generate,Lg,{map_field_exact,_,K0,V0},E}, Gs, St0) -> V = cons_tl(Pat), #c_tuple{es=[K,V,IterVar]} end, - SkipPat = #c_tuple{es=[SkipK,SkipV,IterVar]}, - - Refill = {SkipK, + NomatchPat = #c_tuple{es=[NomatchK,NomatchV,IterVar]}, + NomatchMode = case Generate of + m_generate -> + skip; + m_generate_ns -> + #c_tuple{es=[NomatchK,NomatchV]} + end, + Refill = {NomatchK, #iset{var=IterVar, arg=#icall{anno=#a{anno=GA}, module=#c_literal{val=erts_internal}, name=#c_literal{val=mc_refill}, - args=[SkipK]}}}, + args=[NomatchK]}}}, InitIter = #icall{anno=#a{anno=GA}, module=#c_literal{val=erts_internal}, @@ -1967,7 +2099,10 @@ generator(Line, {m_generate,Lg,{map_field_exact,_,K0,V0},E}, Gs, St0) -> Pre = Pre0 ++ [Before], Gen = #igen{anno=#a{anno=GA}, - acc_pat=AccPat,acc_guard=Cg,skip_pat=SkipPat, + acc_pat=AccPat,acc_guard=Cg, + nomatch_pat=NomatchPat, + nomatch_guard=[], + nomatch_mode=NomatchMode, tail=IterVar,tail_pat=#c_literal{anno=LA,val=none}, refill=Refill, arg={Pre,OuterIterVar}}, @@ -2656,16 +2791,16 @@ uclause(Cl0, Ks, St0) -> do_uclause(#iclause{anno=A0,pats=Ps0,guard=G0,body=B0}, Ks0, St0) -> {Ps1,Pg0,Pvs,Pus,St1} = upattern_list(Ps0, Ks0, St0), Anno = A0#a.anno, - {Pg,A} = case member(skip_clause, Anno) of + {Pg,A} = case member(nomatch_clause, Anno) of true -> - %% This is the skip clause for a binary generator. + %% This is the no-match clause for a binary generator. %% To ensure that it will properly skip the nonmatching %% patterns in generators such as: %% %% <> <= Gen %% %% we must remove any generated pre guard. - {[],A0#a{anno=Anno -- [skip_clause]}}; + {[],A0#a{anno=Anno -- [nomatch_clause]}}; false -> {Pg0,A0} end, @@ -2993,7 +3128,7 @@ upattern_list([P0|Ps0], Ks, St0) -> {P1,Pg,Pv,Pu,St1} = upattern(P0, Ks, St0), {Ps1,Psg,Psv,Psu,St2} = upattern_list(Ps0, known_union(Ks, Pv), St1), {[P1|Ps1],Pg ++ Psg,union(Pv, Psv),union(Pu, Psu),St2}; -upattern_list([], _, St) -> {[],[],[],[],St}. +upattern_list([], _, St) -> {[],[],[],[],St}. %% upat_bin([Pat], [KnownVar], State) -> %% {[Pat],[GuardTest],[NewVar],[UsedVar],State}. @@ -3020,7 +3155,7 @@ upat_bin([P0|Ps0], Ks, Bs, St0) -> {P1,Pg,Pv,Pu,Bs1,St1} = upat_element(P0, Ks, Bs, St0), {Ps1,Psg,Psv,Psu,St2} = upat_bin(Ps0, known_union(Ks, Pv), Bs1, St1), {[P1|Ps1],Pg ++ Psg,union(Pv, Psv),union(Pu, Psu),St2}; -upat_bin([], _, _, St) -> {[],[],[],[],St}. +upat_bin([], _, _, St) -> {[],[],[],[],St}. %% upat_element(Segment, [KnownVar], [LocalVar], State) -> @@ -4024,7 +4159,7 @@ lit_vars(#c_cons{hd=H,tl=T}, Vs) -> lit_vars(H, lit_vars(T, Vs)); lit_vars(#c_tuple{es=Es}, Vs) -> lit_list_vars(Es, Vs); lit_vars(#c_map{arg=V,es=Es}, Vs) -> lit_vars(V, lit_list_vars(Es, Vs)); lit_vars(#c_map_pair{key=K,val=V}, Vs) -> lit_vars(K, lit_vars(V, Vs)); -lit_vars(#c_var{name=V}, Vs) -> add_element(V, Vs); +lit_vars(#c_var{name=V}, Vs) -> add_element(V, Vs); lit_vars(_, Vs) -> Vs. %These are atomic lit_list_vars(Ls) -> lit_list_vars(Ls, []). diff --git a/lib/compiler/test/Makefile b/lib/compiler/test/Makefile index c6e5a23a3215..48514f7b84c8 100644 --- a/lib/compiler/test/Makefile +++ b/lib/compiler/test/Makefile @@ -173,8 +173,9 @@ RELSYSDIR = $(RELEASE_PATH)/compiler_test # FLAGS # ---------------------------------------------------- +NON_SKIPPING_GENERATORS_OPT = '+{enable_feature,non_skipping_generators}' ERL_MAKE_FLAGS += -ERL_COMPILE_FLAGS += +clint +clint0 +ssalint +nowarn_missing_spec_documented +ERL_COMPILE_FLAGS += +clint +clint0 +ssalint +nowarn_missing_spec_documented ${NON_SKIPPING_GENERATORS_OPT} ERL_COMPILE_FLAGS := $(filter-out +deterministic,$(ERL_COMPILE_FLAGS)) EBIN = . @@ -265,7 +266,7 @@ docs: # ---------------------------------------------------- # Release Target -# ---------------------------------------------------- +# ---------------------------------------------------- include $(ERL_TOP)/make/otp_release_targets.mk release_spec: opt diff --git a/lib/compiler/test/bs_bincomp_SUITE.erl b/lib/compiler/test/bs_bincomp_SUITE.erl index d97209e2820d..94e00d4ac68d 100644 --- a/lib/compiler/test/bs_bincomp_SUITE.erl +++ b/lib/compiler/test/bs_bincomp_SUITE.erl @@ -1,8 +1,8 @@ %% %% %CopyrightBegin% -%% +%% %% Copyright Ericsson AB 2006-2024. All Rights Reserved. -%% +%% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at @@ -14,7 +14,7 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -%% +%% %% %CopyrightEnd% %% %% Originally based on Per Gustafsson's test suite. @@ -22,14 +22,14 @@ -module(bs_bincomp_SUITE). --export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, +-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, init_per_group/2,end_per_group/2, verify_highest_opcode/1, byte_aligned/1,bit_aligned/1,extended_byte_aligned/1, extended_bit_aligned/1,mixed/1,filters/1,trim_coverage/1, nomatch/1,sizes/1,general_expressions/1, no_generator/1,zero_pattern/1,multiple_segments/1, - grab_bag/1]). + grab_bag/1, non_skipping_generators/1]). -include_lib("common_test/include/ct.hrl"). @@ -41,7 +41,7 @@ all() -> extended_bit_aligned, mixed, filters, trim_coverage, nomatch, sizes, general_expressions, no_generator, zero_pattern, multiple_segments, - grab_bag]. + grab_bag, non_skipping_generators]. groups() -> []. @@ -322,7 +322,7 @@ trim_coverage(Config) when is_list(Config) -> <<0,0,0,2,0,0,5,48,0,11,219,174,0,0,0,0>> = coverage_materialiv(a, b, {1328,777134}), <<67,40,0,0,66,152,0,0,69,66,64,0>> = coverage_trimmer([42,19,777]), <<0,0,2,43,0,0,3,9,0,0,0,3,64,8,0,0,0,0,0,0, - 64,68,0,0,0,0,0,0,192,171,198,0,0,0,0,0>> = + 64,68,0,0,0,0,0,0,192,171,198,0,0,0,0,0>> = coverage_lightfv(555, 777, {3.0,40.0,-3555.0}), <<"abcabc">> = coverage_strange(0, <<"abc">>), ok. @@ -680,6 +680,28 @@ grab_bag_gh_8617(Bin) -> [0 || <<_:0, _:(tuple_size({self()}))>> <= Bin, is_pid(id(self()))]. +non_skipping_generators(_Config) -> + %% Basic non-skipping generators (each generator type) + <<2,3,4>> = << <<(X+1)>> || X <:- [1,2,3]>>, + <<2,3,4>> = << <<(X+1)>> || <> <:= <<1,2,3>> >>, + <<2,12>> = << <<(X*Y)>> || X := Y <:- #{1 => 2, 3 => 4} >>, + + %% A failing guard following a non-skipping generator is ok + <<3,4>> = << <<(X+1)>> || X <:- [1,2,3], X > 1>>, + <<3,4>> = << <<(X+1)>> || <> <:= <<1,2,3>>, X > 1 >>, + <<12>> = << <<(X*Y)>> || X := Y <:- #{1 => 2, 3 => 4}, X > 1 >>, + + %% Non-matching elements cause a badmatch error for non-skipping generators + {'EXIT',{{badmatch,2},_}} = (catch << <> || {ok, X} <:- [{ok,1},2,{ok,3}] >>), + {'EXIT',{{badmatch,<<128,2>>},_}} = (catch << <> || <<0:1, X:7>> <:= <<1,128,2>> >>), + {'EXIT',{{badmatch,{2,error}},_}} = (catch << <> || X := ok <:- #{1 => ok, 2 => error, 3 => ok} >>), + + %% Extra bits cannot be skipped at the end of the binary either + {'EXIT',{{badmatch,<<0:2>>},_}} = (catch [X || <> <:= <<0>>]), + {'EXIT',{{badmatch,<<9,2>>},_}} = (catch [Y || <> <:= <<8,1,9,2>>]), + + ok. + cs_init() -> erts_debug:set_internal_state(available_internal_state, true), ok. @@ -717,7 +739,7 @@ cs(Bin) -> %% Verify that the allocated size of the binary is the default size. cs_default(Bin) -> ByteSize = byte_size(Bin), - {refc_binary,ByteSize,{binary,256},_} = + {refc_binary,ByteSize,{binary,256},_} = erts_debug:get_internal_state({binary_info,Bin}), Bin. diff --git a/lib/compiler/test/lc_SUITE.erl b/lib/compiler/test/lc_SUITE.erl index 5de383f92800..965bbec7afa3 100644 --- a/lib/compiler/test/lc_SUITE.erl +++ b/lib/compiler/test/lc_SUITE.erl @@ -101,6 +101,16 @@ basic(Config) when is_list(Config) -> %% Not matching. [] = [3 || {3=4} <- []], + %% Non-skipping generators (each generator type) + [2,3,4] = [X+1 || X <:- [1,2,3]], + [2,3,4] = [X+1 || <> <:= <<1,2,3>>], + [2,12] = [X*Y || X := Y <:- #{1 => 2, 3 => 4}], + + %% A failing guard following a non-skipping generator is ok + [3,4] = [X+1 || X <:- [1,2,3], X > 1], + [3,4] = [X+1 || <> <:= <<1,2,3>>, X > 1], + [12] = [X*Y || X := Y <:- #{1 => 2, 3 => 4}, X > 1], + %% Error cases. [] = [{xx,X} || X <- L0, element(2, X) == no_no_no], {'EXIT',_} = (catch [X || X <- L1, list_to_atom(X) == dum]), @@ -109,6 +119,11 @@ basic(Config) when is_list(Config) -> {'EXIT',{{bad_generator,x},_}} = (catch [E || E <- id(x)]), {'EXIT',{{bad_filter,not_bool},_}} = (catch [E || E <- [1,2], id(not_bool)]), + %% Non-matching elements cause a badmatch error for non-skipping generators + {'EXIT',{{badmatch,2},_}} = (catch [X || {ok, X} <:- [{ok,1},2,{ok,3}]]), + {'EXIT',{{badmatch,<<128,2>>},_}} = (catch [X || <<0:1, X:7>> <:= <<1,128,2>>]), + {'EXIT',{{badmatch,{2,error}},_}} = (catch [X || X := ok <:- #{1 => ok, 2 => error, 3 => ok}]), + %% Make sure that line numbers point out the generator. case ?MODULE of lc_inline_SUITE -> @@ -173,7 +188,7 @@ no_generator(Config) when is_list(Config) -> [a,b,c] = [a || true] ++ [b,c], ok. -no_gen(A, B) -> +no_gen(A, B) -> [{A,B} || A+B =:= 0] ++ [{A,B} || A*B =:= 0] ++ [{A,B} || A rem B =:= 3] ++ diff --git a/lib/compiler/test/mc_SUITE.erl b/lib/compiler/test/mc_SUITE.erl index 9f2628255723..fa332767d968 100644 --- a/lib/compiler/test/mc_SUITE.erl +++ b/lib/compiler/test/mc_SUITE.erl @@ -122,6 +122,21 @@ basic(_Config) -> N rem 2 =:= 1]), Odd = lists:sort([V || #foo{a=N} := V <- RecordMap, N rem 2 =:= 1]), + %% Non-skipping generators (each generator type) + #{1 := 2, 2 := 3, 3 := 4} = #{X => X+1 || X <:- [1,2,3]}, + #{1 := 2, 2 := 3, 3 := 4} = #{X => X+1 || <> <:= <<1,2,3>>}, + #{2 := 4, 4 := 8} = #{X+1 => Y*2 || X := Y <:- #{1 => 2, 3 => 4}}, + + %% A failing guard following a non-skipping generator is ok + #{2 := 3, 3 := 4} = #{X => X+1 || X <:- [1,2,3], X > 1}, + #{2 := 3, 3 := 4} = #{X => X+1 || <> <:= <<1,2,3>>, X > 1}, + #{4 := 8} = #{X+1 => Y*2 || X := Y <:- #{1 => 2, 3 => 4}, X > 1}, + + %% Non-matching elements cause a badmatch error for non-skipping generators + {'EXIT',{{badmatch,2},_}} = (catch #{X => X+1 || {ok, X} <:- [{ok,1},2,{ok,3}]}), + {'EXIT',{{badmatch,<<128,2>>},_}} = (catch #{X => X+1 || <<0:1, X:7>> <:= <<1,128,2>>}), + {'EXIT',{{badmatch,{2,error}},_}} = (catch #{X => X+1 || X := ok <:-#{1 => ok, 2 => error, 3 => ok}}), + ok. mc_double(Size) -> diff --git a/lib/debugger/src/dbg_ieval.erl b/lib/debugger/src/dbg_ieval.erl index 60a3dc85c8a5..03293d2a4f88 100644 --- a/lib/debugger/src/dbg_ieval.erl +++ b/lib/debugger/src/dbg_ieval.erl @@ -1,8 +1,8 @@ %% %% %CopyrightBegin% -%% +%% %% Copyright Ericsson AB 1998-2024. All Rights Reserved. -%% +%% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at @@ -14,7 +14,7 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -%% +%% %% %CopyrightEnd% %% -module(dbg_ieval). @@ -69,7 +69,7 @@ exit_info(Int, AttPid, OrigPid, Reason, ExitInfo) -> put(breakpoints, dbg_iserver:call(Int, all_breaks)), put(self, OrigPid), put(exit_info, ExitInfo), - + case ExitInfo of {{Mod,Line},Bs,S} -> dbg_istk:from_external(S), @@ -216,7 +216,7 @@ meta(Int, Debugged, M, F, As) -> {M, F, As} end, Status = dbg_iserver:call(Int, {new_process,Debugged,self(),Pargs}), - + %% Initiate process dictionary put(int, Int), % pid() dbg_iserver put(attached, undefined),% pid() attached process @@ -283,7 +283,7 @@ meta_loop(Debugged, Bs, #ieval{level=Le} = Ieval) -> dbg_istk:init(), put(stacktrace, []), put(exit_info, undefined), - + dbg_iserver:cast(get(int), {set_status,self(),running,{}}), dbg_icmd:tell_attached(running), @@ -357,7 +357,7 @@ format_trace(What, Args, P) -> call -> {Called, {Le,Li,M,F,As}} = Args, case Called of - extern -> + extern -> io_lib:format("++ (~w) <~w> ~w:~tw~ts~n", [Le,Li,M,F,format_args(As, P)]); local -> @@ -404,7 +404,7 @@ catch_value(throw, Reason) -> %%--Code interpretation----------------------------------------------- %%-------------------------------------------------------------------- -%% Top level function of meta evaluator. +%% Top level function of meta evaluator. %% Return message to be replied to the target process. %%-------------------------------------------------------------------- eval_mfa(Debugged, M, F, As, #ieval{level=Le}=Ieval0) -> @@ -483,11 +483,11 @@ do_eval_function(Mod, Name, As0, Bs0, Called, Ieval0) -> lambda(eval_fun, [Cs,As,Bs,{Mod,Name}=F]) -> %% Fun defined in interpreted code, called from outside - if + if length(element(3,hd(Cs))) =:= length(As) -> db_ref(Mod), %% Adds ref between module and process {Cs,Mod,Name,As,Bs}; - true -> + true -> {error,{badarity,{F,As}}} end; lambda(eval_named_fun, [Cs,As,Bs0,FName,RF,{Mod,Name}=F]) -> @@ -514,7 +514,7 @@ lambda(Fun, As) when is_function(Fun) -> {M,F,add_binding(FName, Fun, Bs0), Cs0} end, {arity, Arity} = erlang:fun_info(Fun, arity), - if + if length(As) =:= Arity -> db_ref(Mod), %% Adds ref between module and process {Cs,Mod,Name,As,Bs}; @@ -586,7 +586,7 @@ db_ref(Mod) -> cache(Key, Data) -> put(cache, lists:sublist([{Key,Data}|get(cache)], 5)). - + cached(Key) -> case lists:keyfind(Key, 1, get(cache)) of {Key,Data} -> Data; @@ -806,7 +806,7 @@ expr({maybe_match,Line,Lhs,Rhs0}, Bs0, Ieval0) -> expr({make_fun,Line,Name,Cs}, Bs, #ieval{module=Module}=Ieval) -> Arity = length(element(3,hd(Cs))), Info = {{Module,Name},Bs,Cs}, - Fun = + Fun = case Arity of 0 -> fun() -> eval_fun([], Info) end; 1 -> fun(A) -> eval_fun([A], Info) end; @@ -815,33 +815,33 @@ expr({make_fun,Line,Name,Cs}, Bs, #ieval{module=Module}=Ieval) -> 4 -> fun(A,B,C,D) -> eval_fun([A,B,C,D], Info) end; 5 -> fun(A,B,C,D,E) -> eval_fun([A,B,C,D,E], Info) end; 6 -> fun(A,B,C,D,E,F) -> eval_fun([A,B,C,D,E,F], Info) end; - 7 -> fun(A,B,C,D,E,F,G) -> + 7 -> fun(A,B,C,D,E,F,G) -> eval_fun([A,B,C,D,E,F,G], Info) end; - 8 -> fun(A,B,C,D,E,F,G,H) -> + 8 -> fun(A,B,C,D,E,F,G,H) -> eval_fun([A,B,C,D,E,F,G,H], Info) end; - 9 -> fun(A,B,C,D,E,F,G,H,I) -> + 9 -> fun(A,B,C,D,E,F,G,H,I) -> eval_fun([A,B,C,D,E,F,G,H,I], Info) end; - 10 -> fun(A,B,C,D,E,F,G,H,I,J) -> + 10 -> fun(A,B,C,D,E,F,G,H,I,J) -> eval_fun([A,B,C,D,E,F,G,H,I,J], Info) end; - 11 -> fun(A,B,C,D,E,F,G,H,I,J,K) -> + 11 -> fun(A,B,C,D,E,F,G,H,I,J,K) -> eval_fun([A,B,C,D,E,F,G,H,I,J,K], Info) end; - 12 -> fun(A,B,C,D,E,F,G,H,I,J,K,L) -> + 12 -> fun(A,B,C,D,E,F,G,H,I,J,K,L) -> eval_fun([A,B,C,D,E,F,G,H,I,J,K,L], Info) end; - 13 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M) -> + 13 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M) -> eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M], Info) end; - 14 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N) -> + 14 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N) -> eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N], Info) end; - 15 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O) -> + 15 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O) -> eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O], Info) end; - 16 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P) -> + 16 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P) -> eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P], Info) end; - 17 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q) -> + 17 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q) -> eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q], Info) end; - 18 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R) -> + 18 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R) -> eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R], Info) end; - 19 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S) -> + 19 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S) -> eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S],Info) end; - 20 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T) -> + 20 -> fun(A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T) -> eval_fun([A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T],Info) end; _Other -> exception(error, {'argument_limit',{'fun',Cs}}, Bs, @@ -1049,7 +1049,7 @@ expr({apply,Line,As0,Lc}, Bs0, Ieval0) -> Ieval = Ieval0#ieval{line=Line}, {[M,F,As],Bs} = eval_list(As0, Bs0, Ieval), eval_function(M, F, As, Bs, extern, Ieval, Lc); - + %% Receive statement expr({'receive',Line,Cs}, Bs0, #ieval{level=Le}=Ieval) -> trace(receivex, {Le,false}), @@ -1179,15 +1179,18 @@ eval_mc1({map_field_assoc,_,K0,V0}, [], Bs, Ieval) -> {value,V,_} = expr(V0, Bs, Ieval#ieval{top=false}), [{K,V}]. -eval_generator({generate,Line,P,L0}, Bs0, CompFun, Ieval0) -> +eval_generator({Generate,Line,P,L0}, Bs0, CompFun, Ieval0) when Generate =:= generate; + Generate =:= generate_ns -> Ieval = Ieval0#ieval{line=Line}, {value,L1,Bs1} = expr(L0, Bs0, Ieval#ieval{top=false}), - eval_generate(L1, P, Bs1, CompFun, Ieval); -eval_generator({b_generate,Line,P,Bin0}, Bs0, CompFun, Ieval0) -> + eval_generate(L1, P, Bs1, CompFun, Generate =:= generate, Ieval); +eval_generator({Generate,Line,P,Bin0}, Bs0, CompFun, Ieval0) when Generate =:= b_generate; + Generate =:= b_generate_ns -> Ieval = Ieval0#ieval{line=Line}, {value,Bin,Bs1} = expr(Bin0, Bs0, Ieval#ieval{top=false}), - eval_b_generate(Bin, P, Bs1, CompFun, Ieval); -eval_generator({m_generate,Line,P,Map0}, Bs0, CompFun, Ieval0) -> + eval_b_generate(Bin, P, Bs1, CompFun, Generate =:= b_generate, Ieval); +eval_generator({Generate,Line,P,Map0}, Bs0, CompFun, Ieval0) when Generate =:= m_generate; + Generate =:= m_generate_ns -> Ieval = Ieval0#ieval{line=Line}, {map_field_exact,_,K,V} = P, {value,Map,_Bs1} = expr(Map0, Bs0, Ieval), @@ -1204,45 +1207,53 @@ eval_generator({m_generate,Line,P,Map0}, Bs0, CompFun, Ieval0) -> exception(error, {bad_generator,Map}, Bs0, Ieval) end end, - eval_m_generate(Iter, {tuple,Line,[K,V]}, Bs0, CompFun, Ieval). + eval_m_generate(Iter, {tuple,Line,[K,V]}, Bs0, CompFun, Generate =:= m_generate, Ieval). -eval_generate([V|Rest], P, Bs0, CompFun, Ieval) -> +eval_generate([V|Rest], P, Bs0, CompFun, AllowSkipping, Ieval) -> case catch match1(P, V, erl_eval:new_bindings(), Bs0) of {match,Bsn} -> Bs2 = add_bindings(Bsn, Bs0), - CompFun(Bs2) ++ eval_generate(Rest, P, Bs0, CompFun, Ieval); - nomatch -> - eval_generate(Rest, P, Bs0, CompFun, Ieval) + CompFun(Bs2) ++ eval_generate(Rest, P, Bs0, CompFun, AllowSkipping, Ieval); + nomatch when AllowSkipping -> + eval_generate(Rest, P, Bs0, CompFun, AllowSkipping, Ieval); + nomatch -> + exception(error, {badmatch, V}, Bs0, Ieval) end; -eval_generate([], _P, _Bs0, _CompFun, _Ieval) -> +eval_generate([], _P, _Bs0, _CompFun, _AllowSkipping, _Ieval) -> []; -eval_generate(Term, _P, Bs, _CompFun, Ieval) -> +eval_generate(Term, _P, Bs, _CompFun, _AllowSkipping, Ieval) -> exception(error, {bad_generator,Term}, Bs, Ieval). -eval_b_generate(<<_/bitstring>>=Bin, P, Bs0, CompFun, Ieval) -> +eval_b_generate(<<_/bitstring>>=Bin, P, Bs0, CompFun, AllowSkipping, Ieval) -> Mfun = match_fun(Bs0), Efun = fun(Exp, Bs) -> expr(Exp, Bs, #ieval{}) end, case eval_bits:bin_gen(P, Bin, erl_eval:new_bindings(), Bs0, Mfun, Efun) of {match,Rest,Bs1} -> Bs2 = add_bindings(Bs1, Bs0), - CompFun(Bs2) ++ eval_b_generate(Rest, P, Bs0, CompFun, Ieval); - {nomatch,Rest} -> - eval_b_generate(Rest, P, Bs0, CompFun, Ieval); - done -> - [] + CompFun(Bs2) ++ eval_b_generate(Rest, P, Bs0, CompFun, AllowSkipping, Ieval); + {nomatch,Rest} when AllowSkipping -> + eval_b_generate(Rest, P, Bs0, CompFun, AllowSkipping, Ieval); + {nomatch,_Rest} -> + exception(error, {badmatch, Bin}, Bs0, Ieval); + done when not AllowSkipping, Bin =/= <<>> -> + exception(error, {badmatch, Bin}, Bs0, Ieval); + done -> + [] end; -eval_b_generate(Term, _P, Bs, _CompFun, Ieval) -> +eval_b_generate(Term, _P, Bs, _CompFun, _AllowSkipping, Ieval) -> exception(error, {bad_generator,Term}, Bs, Ieval). -eval_m_generate(Iter0, P, Bs0, CompFun, Ieval) -> +eval_m_generate(Iter0, P, Bs0, CompFun, AllowSkipping, Ieval) -> case maps:next(Iter0) of {K,V,Iter} -> case catch match1(P, {K,V}, erl_eval:new_bindings(), Bs0) of {match,Bsn} -> Bs2 = add_bindings(Bsn, Bs0), - CompFun(Bs2) ++ eval_m_generate(Iter, P, Bs0, CompFun, Ieval); + CompFun(Bs2) ++ eval_m_generate(Iter, P, Bs0, CompFun, AllowSkipping, Ieval); + nomatch when AllowSkipping -> + eval_m_generate(Iter, P, Bs0, CompFun, AllowSkipping, Ieval); nomatch -> - eval_m_generate(Iter, P, Bs0, CompFun, Ieval) + exception(error, {badmatch, {K,V}}, Bs0, Ieval) end; none -> [] @@ -1264,7 +1275,7 @@ safe_bif(M, F, As, Bs, Ieval0) -> eval_send(To, Msg, Bs, Ieval) -> try To ! Msg of - Msg -> + Msg -> trace(send, {To,Msg}), {value,Msg,Bs} catch @@ -1273,7 +1284,7 @@ eval_send(To, Msg, Bs, Ieval) -> end. %% Start tracing of messages before fetching current messages in -%% the queue to make sure that no messages are lost. +%% the queue to make sure that no messages are lost. eval_receive(Debugged, Cs, Bs0, #ieval{module=M,line=Line,level=Le}=Ieval) -> %% To avoid private message passing protocol between META @@ -1514,7 +1525,7 @@ rec_clauses([], _, _) -> %% Evaluate a list of guards. guard([], _) -> true; guard(Gs, Bs) -> or_guard(Gs, Bs). - + or_guard([G|Gs], Bs) -> %% Short-circuit OR. and_guard(G, Bs) orelse or_guard(Gs, Bs); @@ -1588,7 +1599,7 @@ guard_expr({map,_,E0,Fs0}, Bs) -> E, Fs), {value,Value}; guard_expr({bin,_,Flds}, Bs) -> - {value,V,_Bs} = + {value,V,_Bs} = eval_bits:expr_grp(Flds, Bs, fun(E,B) -> {value,V} = guard_expr(E,B), @@ -1678,7 +1689,7 @@ match1({match,_,Pat1,Pat2}, Term, Bs0, BBs) -> match1({cons,_,H,T}, [H1|T1], Bs0, BBs) -> {match,Bs} = match1(H, H1, Bs0, BBs), match1(T, T1, Bs, BBs); -match1({tuple,_,Elts}, Tuple, Bs, BBs) +match1({tuple,_,Elts}, Tuple, Bs, BBs) when length(Elts) =:= tuple_size(Tuple) -> match_tuple(Elts, Tuple, 1, Bs, BBs); match1({map,_,Fields}, Map, Bs, BBs) when is_map(Map) -> @@ -1724,7 +1735,7 @@ match_map([], _, Bs, _BBs) -> head_match([Par|Pars], [Arg|Args], Bs0, BBs) -> try match1(Par, Arg, Bs0, BBs) of {match,Bs} -> head_match(Pars, Args, Bs, BBs) - catch + catch Result -> Result end; head_match([],[],Bs,_) -> {match,Bs}. @@ -1787,7 +1798,7 @@ add_anon(Val,[]) -> [{'_',Val}]. %% merge_bindings(Bindings1, Bindings2, Ieval) -%% Merge bindings detecting bad matches. +%% Merge bindings detecting bad matches. %% Special case '_',save the new one !!! %% Bindings1 is the newest bindings. merge_bindings(Bs, Bs, _Ieval) -> diff --git a/lib/debugger/src/dbg_iload.erl b/lib/debugger/src/dbg_iload.erl index abb3529f5b36..9cd269188944 100644 --- a/lib/debugger/src/dbg_iload.erl +++ b/lib/debugger/src/dbg_iload.erl @@ -687,10 +687,16 @@ expr_comprehension({Tag,Anno,E0,Gs0}, St) -> Gs = [case G of ({generate,L,P0,Qs}) -> {generator,{generate,L,pattern(P0, St),expr(Qs, false, St)}}; + ({generate_ns,L,P0,Qs}) -> + {generator,{generate_ns,L,pattern(P0, St),expr(Qs, false, St)}}; ({b_generate,L,P0,Qs}) -> %R12. {generator,{b_generate,L,pattern(P0, St),expr(Qs, false, St)}}; + ({b_generate_ns,L,P0,Qs}) -> %R12. + {generator,{b_generate_ns,L,pattern(P0, St),expr(Qs, false, St)}}; ({m_generate,L,P0,Qs}) -> %OTP 26 {generator,{m_generate,L,mc_pattern(P0, St),expr(Qs, false, St)}}; + ({m_generate_ns,L,P0,Qs}) -> %OTP 26 + {generator,{m_generate_ns,L,mc_pattern(P0, St),expr(Qs, false, St)}}; (Expr) -> case is_guard_test(Expr, St) of true -> {guard,guard([[Expr]], St)}; diff --git a/lib/debugger/test/Makefile b/lib/debugger/test/Makefile index bf464f8d76d7..6e1033e0fffe 100644 --- a/lib/debugger/test/Makefile +++ b/lib/debugger/test/Makefile @@ -71,8 +71,9 @@ RELSYSDIR = $(RELEASE_PATH)/debugger_test # FLAGS # ---------------------------------------------------- +NON_SKIPPING_GENERATORS_OPT = '+{enable_feature,non_skipping_generators}' ERL_MAKE_FLAGS += -ERL_COMPILE_FLAGS += +nowarn_missing_spec_documented +ERL_COMPILE_FLAGS += +nowarn_missing_spec_documented ${NON_SKIPPING_GENERATORS_OPT} ERL_COMPILE_FLAGS := $(filter-out +deterministic,$(ERL_COMPILE_FLAGS)) EBIN = . diff --git a/lib/debugger/test/bs_bincomp_SUITE.erl b/lib/debugger/test/bs_bincomp_SUITE.erl index 064e9567b356..ca87d0c0dc65 100644 --- a/lib/debugger/test/bs_bincomp_SUITE.erl +++ b/lib/debugger/test/bs_bincomp_SUITE.erl @@ -24,11 +24,11 @@ -module(bs_bincomp_SUITE). --export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, +-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, init_per_group/2,end_per_group/2, init_per_testcase/2,end_per_testcase/2, byte_aligned/1,bit_aligned/1,extended_byte_aligned/1, - extended_bit_aligned/1,mixed/1]). + extended_bit_aligned/1,mixed/1,non_skipping_generators/1]). -include_lib("common_test/include/ct.hrl"). @@ -43,11 +43,11 @@ suite() -> [{ct_hooks,[ts_install_cth]}, {timetrap,{minutes,1}}]. -all() -> +all() -> [byte_aligned, bit_aligned, extended_byte_aligned, - extended_bit_aligned, mixed]. + extended_bit_aligned, mixed, non_skipping_generators]. -groups() -> +groups() -> []. init_per_suite(Config) -> @@ -125,3 +125,25 @@ mixed(Config) when is_list(Config) -> [2,3,3,4,4,5,5,6] = [(X+Y) || <> <= <<1:3,2:3,3:3,4:3>>, Y <- [1,2]], ok. + +non_skipping_generators(Config) when is_list(Config) -> + %% Basic non-skipping generators (each generator type) + <<2,3,4>> = << <<(X+1)>> || X <:- [1,2,3]>>, + <<2,3,4>> = << <<(X+1)>> || <> <:= <<1,2,3>> >>, + <<2,12>> = << <<(X*Y)>> || X := Y <:- #{1 => 2, 3 => 4} >>, + + %% A failing guard following a non-skipping generator is ok + <<3,4>> = << <<(X+1)>> || X <:- [1,2,3], X > 1>>, + <<3,4>> = << <<(X+1)>> || <> <:= <<1,2,3>>, X > 1 >>, + <<12>> = << <<(X*Y)>> || X := Y <:- #{1 => 2, 3 => 4}, X > 1 >>, + + %% Non-matching elements cause a badmatch error for non-skipping generators + {'EXIT',{{badmatch,2},_}} = (catch << <> || {ok, X} <:- [{ok,1},2,{ok,3}] >>), + {'EXIT',{{badmatch,<<128,2>>},_}} = (catch << <> || <<0:1, X:7>> <:= <<1,128,2>> >>), + {'EXIT',{{badmatch,{2,error}},_}} = (catch << <> || X := ok <:- #{1 => ok, 2 => error, 3 => ok} >>), + + %% Extra bits cannot be skipped at the end of the binary either + {'EXIT',{{badmatch,<<0:2>>},_}} = (catch [X || <> <:= <<0>>]), + {'EXIT',{{badmatch,<<9,2>>},_}} = (catch [Y || <> <:= <<8,1,9,2>>]), + + ok. diff --git a/lib/debugger/test/lc_SUITE.erl b/lib/debugger/test/lc_SUITE.erl index 28415b412bcd..a7e59b671321 100644 --- a/lib/debugger/test/lc_SUITE.erl +++ b/lib/debugger/test/lc_SUITE.erl @@ -34,10 +34,10 @@ suite() -> [{ct_hooks,[ts_install_cth]}, {timetrap,{minutes,1}}]. -all() -> +all() -> [basic, deeply_nested, no_generator, empty_generator]. -groups() -> +groups() -> []. init_per_group(_GroupName, Config) -> @@ -84,12 +84,27 @@ basic(Config) when is_list(Config) -> %% Filter expressions with andalso/orelse. "abc123" = alphanum("?abc123.;"), + %% Non-skipping generators (each generator type) + [2,3,4] = [X+1 || X <:- [1,2,3]], + [2,3,4] = [X+1 || <> <:= <<1,2,3>>], + [2,12] = [X*Y || X := Y <:- #{1 => 2, 3 => 4}], + + %% A failing guard following a non-skipping generator is ok + [3,4] = [X+1 || X <:- [1,2,3], X > 1], + [3,4] = [X+1 || <> <:= <<1,2,3>>, X > 1], + [12] = [X*Y || X := Y <:- #{1 => 2, 3 => 4}, X > 1], + %% Error cases. [] = [{xx,X} || X <- L0, element(2, X) == no_no_no], {'EXIT',_} = (catch [X || X <- L1, list_to_atom(X) == dum]), [] = [X || X <- L1, X+1 < 2], {'EXIT',_} = (catch [X || X <- L1, odd(X)]), + %% Non-matching elements cause a badmatch error for non-skipping generators + {'EXIT',{{badmatch,2},_}} = (catch [X || {ok, X} <:- [{ok,1},2,{ok,3}]]), + {'EXIT',{{badmatch,<<128,2>>},_}} = (catch [X || <<0:1, X:7>> <:= <<1,128,2>>]), + {'EXIT',{{badmatch,{2,error}},_}} = (catch [X || X := ok <:- #{1 => ok, 2 => error, 3 => ok}]), + %% A bad generator has a different exception compared to BEAM. {'EXIT',{{bad_generator,x},_}} = (catch [E || E <- id(x)]), ok. diff --git a/lib/debugger/test/mc_SUITE.erl b/lib/debugger/test/mc_SUITE.erl index 0a374cb51ab7..948d9964ffa8 100644 --- a/lib/debugger/test/mc_SUITE.erl +++ b/lib/debugger/test/mc_SUITE.erl @@ -125,6 +125,21 @@ basic(_Config) -> N rem 2 =:= 1]), Odd = lists:sort([V || #foo{a=N} := V <- RecordMap, N rem 2 =:= 1]), + %% Non-skipping generators (each generator type) + #{1 := 2, 2 := 3, 3 := 4} = #{X => X+1 || X <:- [1,2,3]}, + #{1 := 2, 2 := 3, 3 := 4} = #{X => X+1 || <> <:= <<1,2,3>>}, + #{2 := 4, 4 := 8} = #{X+1 => Y*2 || X := Y <:- #{1 => 2, 3 => 4}}, + + %% A failing guard following a non-skipping generator is ok + #{2 := 3, 3 := 4} = #{X => X+1 || X <:- [1,2,3], X > 1}, + #{2 := 3, 3 := 4} = #{X => X+1 || <> <:= <<1,2,3>>, X > 1}, + #{4 := 8} = #{X+1 => Y*2 || X := Y <:- #{1 => 2, 3 => 4}, X > 1}, + + %% Non-matching elements cause a badmatch error for non-skipping generators + {'EXIT',{{badmatch,2},_}} = (catch #{X => X+1 || {ok, X} <:- [{ok,1},2,{ok,3}]}), + {'EXIT',{{badmatch,<<128,2>>},_}} = (catch #{X => X+1 || <<0:1, X:7>> <:= <<1,128,2>>}), + {'EXIT',{{badmatch,{2,error}},_}} = (catch #{X => X+1 || X := ok <:- #{1 => ok, 2 => error, 3 => ok}}), + ok. mc_double(Size) -> diff --git a/lib/stdlib/examples/erl_id_trans.erl b/lib/stdlib/examples/erl_id_trans.erl index c50a6ae75e3d..12c51d36925b 100644 --- a/lib/stdlib/examples/erl_id_trans.erl +++ b/lib/stdlib/examples/erl_id_trans.erl @@ -623,14 +623,26 @@ comprehension_quals([{generate,Anno,P0,E0}|Qs]) -> E1 = expr(E0), P1 = pattern(P0), [{generate,Anno,P1,E1}|comprehension_quals(Qs)]; +comprehension_quals([{generate_ns,Anno,P0,E0}|Qs]) -> + E1 = expr(E0), + P1 = pattern(P0), + [{generate_ns,Anno,P1,E1}|comprehension_quals(Qs)]; comprehension_quals([{b_generate,Anno,P0,E0}|Qs]) -> E1 = expr(E0), P1 = pattern(P0), [{b_generate,Anno,P1,E1}|comprehension_quals(Qs)]; +comprehension_quals([{b_generate_ns,Anno,P0,E0}|Qs]) -> + E1 = expr(E0), + P1 = pattern(P0), + [{b_generate_ns,Anno,P1,E1}|comprehension_quals(Qs)]; comprehension_quals([{m_generate,Anno,P0,E0}|Qs]) -> E1 = expr(E0), P1 = pattern(P0), [{m_generate,Anno,P1,E1}|comprehension_quals(Qs)]; +comprehension_quals([{m_generate_ns,Anno,P0,E0}|Qs]) -> + E1 = expr(E0), + P1 = pattern(P0), + [{m_generate_ns,Anno,P1,E1}|comprehension_quals(Qs)]; comprehension_quals([E0|Qs]) -> E1 = expr(E0), [E1|comprehension_quals(Qs)]; diff --git a/lib/stdlib/src/erl_eval.erl b/lib/stdlib/src/erl_eval.erl index 0989f43ed9c7..c213fc1e652e 100644 --- a/lib/stdlib/src/erl_eval.erl +++ b/lib/stdlib/src/erl_eval.erl @@ -1,8 +1,8 @@ %% %% %CopyrightBegin% -%% +%% %% Copyright Ericsson AB 1996-2024. All Rights Reserved. -%% +%% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at @@ -14,7 +14,7 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -%% +%% %% %CopyrightEnd% %% -module(erl_eval). @@ -971,13 +971,16 @@ 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_generator({generate,Anno,P,L0}, Bs0, Lf, Ef, FUVs, Acc0, CompFun) -> +eval_generator({Generate,Anno,P,L0}, Bs0, Lf, Ef, FUVs, Acc0, CompFun) when Generate =:= generate; + Generate =:= generate_ns -> {value,L1,_Bs1} = expr(L0, Bs0, Lf, Ef, none, FUVs), - eval_generate(L1, P, Anno, Bs0, Lf, Ef, CompFun, Acc0); -eval_generator({b_generate,Anno,P,Bin0}, Bs0, Lf, Ef, FUVs, Acc0, CompFun) -> + eval_generate(L1, P, Anno, Bs0, Lf, Ef, CompFun, Generate =:= generate, Acc0); +eval_generator({Generate,Anno,P,Bin0}, Bs0, Lf, Ef, FUVs, Acc0, CompFun) when Generate =:= b_generate; + Generate =:= b_generate_ns -> {value,Bin,_Bs1} = expr(Bin0, Bs0, Lf, Ef, none, FUVs), - eval_b_generate(Bin, P, Anno, Bs0, Lf, Ef, CompFun, Acc0); -eval_generator({m_generate,Anno,P,Map0}, Bs0, Lf, Ef, FUVs, Acc0, CompFun) -> + eval_b_generate(Bin, P, Anno, Bs0, Lf, Ef, CompFun, Generate =:= b_generate, Acc0); +eval_generator({Generate,Anno,P,Map0}, Bs0, Lf, Ef, FUVs, Acc0, CompFun) when Generate =:= m_generate; + Generate =:= m_generate_ns -> {map_field_exact,_,K,V} = P, {value,Map,_Bs1} = expr(Map0, Bs0, Lf, Ef, none, FUVs), Iter = case is_map(Map) of @@ -994,23 +997,25 @@ eval_generator({m_generate,Anno,P,Map0}, Bs0, Lf, Ef, FUVs, Acc0, CompFun) -> Anno, Bs0, Ef, none) end end, - eval_m_generate(Iter, {tuple,Anno,[K,V]}, Anno, Bs0, Lf, Ef, CompFun, Acc0). + eval_m_generate(Iter, {tuple,Anno,[K,V]}, Anno, Bs0, Lf, Ef, CompFun, Generate =:= m_generate, Acc0). -eval_generate([V|Rest], P, Anno, Bs0, Lf, Ef, CompFun, Acc) -> +eval_generate([V|Rest], P, Anno, Bs0, Lf, Ef, CompFun, AllowSkipping, Acc) -> case match(P, V, Anno, new_bindings(Bs0), Bs0, Ef) of {match,Bsn} -> Bs2 = add_bindings(Bsn, Bs0), NewAcc = CompFun(Bs2, Acc), - eval_generate(Rest, P, Anno, Bs0, Lf, Ef, CompFun, NewAcc); - nomatch -> - eval_generate(Rest, P, Anno, Bs0, Lf, Ef, CompFun, Acc) + eval_generate(Rest, P, Anno, Bs0, Lf, Ef, CompFun, AllowSkipping, NewAcc); + nomatch when AllowSkipping -> + eval_generate(Rest, P, Anno, Bs0, Lf, Ef, CompFun, AllowSkipping, Acc); + nomatch -> + apply_error({badmatch, V}, ?STACKTRACE, Anno, Bs0, Ef, none) end; -eval_generate([], _P, _Anno, _Bs0, _Lf, _Ef, _CompFun, Acc) -> +eval_generate([], _P, _Anno, _Bs0, _Lf, _Ef, _CompFun, _AllowSkipping, Acc) -> Acc; -eval_generate(Term, _P, Anno, Bs0, _Lf, Ef, _CompFun, _Acc) -> +eval_generate(Term, _P, Anno, Bs0, _Lf, Ef, _CompFun, _AllowSkipping, _Acc) -> apply_error({bad_generator,Term}, ?STACKTRACE, Anno, Bs0, Ef, none). -eval_b_generate(<<_/bitstring>>=Bin, P, Anno, Bs0, Lf, Ef, CompFun, Acc) -> +eval_b_generate(<<_/bitstring>>=Bin, P, Anno, Bs0, Lf, Ef, CompFun, AllowSkipping, Acc) -> Mfun = match_fun(Bs0, Ef), Efun = fun(Exp, Bs) -> expr(Exp, Bs, Lf, Ef, none) end, ErrorFun = fun(A, R, S) -> apply_error(R, S, A, Bs0, Ef, none) end, @@ -1018,25 +1023,31 @@ eval_b_generate(<<_/bitstring>>=Bin, P, Anno, Bs0, Lf, Ef, CompFun, Acc) -> {match, Rest, Bs1} -> Bs2 = add_bindings(Bs1, Bs0), NewAcc = CompFun(Bs2, Acc), - eval_b_generate(Rest, P, Anno, Bs0, Lf, Ef, CompFun, NewAcc); - {nomatch, Rest} -> - eval_b_generate(Rest, P, Anno, Bs0, Lf, Ef, CompFun, Acc); - done -> + eval_b_generate(Rest, P, Anno, Bs0, Lf, Ef, CompFun, AllowSkipping, NewAcc); + {nomatch, Rest} when AllowSkipping -> + eval_b_generate(Rest, P, Anno, Bs0, Lf, Ef, CompFun, AllowSkipping, Acc); + {nomatch, _Rest} -> + apply_error({badmatch, Bin}, ?STACKTRACE, Anno, Bs0, Ef, none); + done when not AllowSkipping, Bin =/= <<>> -> + apply_error({badmatch, Bin}, ?STACKTRACE, Anno, Bs0, Ef, none); + done -> Acc end; -eval_b_generate(Term, _P, Anno, Bs0, _Lf, Ef, _CompFun, _Acc) -> +eval_b_generate(Term, _P, Anno, Bs0, _Lf, Ef, _CompFun, _AllowSkipping, _Acc) -> apply_error({bad_generator,Term}, ?STACKTRACE, Anno, Bs0, Ef, none). -eval_m_generate(Iter0, P, Anno, Bs0, Lf, Ef, CompFun, Acc0) -> +eval_m_generate(Iter0, P, Anno, Bs0, Lf, Ef, CompFun, AllowSkipping, Acc0) -> case maps:next(Iter0) of {K,V,Iter} -> case match(P, {K,V}, Anno, new_bindings(Bs0), Bs0, Ef) of {match,Bsn} -> Bs2 = add_bindings(Bsn, Bs0), Acc = CompFun(Bs2, Acc0), - eval_m_generate(Iter, P, Anno, Bs0, Lf, Ef, CompFun, Acc); + eval_m_generate(Iter, P, Anno, Bs0, Lf, Ef, CompFun, AllowSkipping, Acc); + nomatch when AllowSkipping -> + eval_m_generate(Iter, P, Anno, Bs0, Lf, Ef, CompFun, AllowSkipping, Acc0); nomatch -> - eval_m_generate(Iter, P, Anno, Bs0, Lf, Ef, CompFun, Acc0) + apply_error({badmatch, {K,V}}, ?STACKTRACE, Anno, Bs0, Ef, none) end; none -> Acc0 @@ -1059,8 +1070,11 @@ eval_filter(F, Bs0, Lf, Ef, CompFun, FUVs, Acc) -> end. is_generator({generate,_,_,_}) -> true; +is_generator({generate_ns,_,_,_}) -> true; is_generator({b_generate,_,_,_}) -> true; +is_generator({b_generate_ns,_,_,_}) -> true; is_generator({m_generate,_,_,_}) -> true; +is_generator({m_generate_ns,_,_,_}) -> true; is_generator(_) -> false. %% eval_map_fields([Field], Bindings, LocalFunctionHandler, @@ -1828,7 +1842,7 @@ normalise_list([]) -> %%---------------------------------------------------------------------------- %% %% Evaluate expressions: -%% constants and +%% constants and %% op A %% L op R %% Things that evaluate to constants are accepted diff --git a/lib/stdlib/src/erl_expand_records.erl b/lib/stdlib/src/erl_expand_records.erl index 2b09006faf24..636fcd76ce47 100644 --- a/lib/stdlib/src/erl_expand_records.erl +++ b/lib/stdlib/src/erl_expand_records.erl @@ -545,11 +545,21 @@ lc_tq(Anno, [{generate,AnnoG,P0,G0} | Qs0], St0) -> {P1,St2} = pattern(P0, St1), {Qs1,St3} = lc_tq(Anno, Qs0, St2), {[{generate,AnnoG,P1,G1} | Qs1],St3}; +lc_tq(Anno, [{generate_ns,AnnoG,P0,G0} | Qs0], St0) -> + {G1,St1} = expr(G0, St0), + {P1,St2} = pattern(P0, St1), + {Qs1,St3} = lc_tq(Anno, Qs0, St2), + {[{generate_ns,AnnoG,P1,G1} | Qs1],St3}; lc_tq(Anno, [{b_generate,AnnoG,P0,G0} | Qs0], St0) -> {G1,St1} = expr(G0, St0), {P1,St2} = pattern(P0, St1), {Qs1,St3} = lc_tq(Anno, Qs0, St2), {[{b_generate,AnnoG,P1,G1} | Qs1],St3}; +lc_tq(Anno, [{b_generate_ns,AnnoG,P0,G0} | Qs0], St0) -> + {G1,St1} = expr(G0, St0), + {P1,St2} = pattern(P0, St1), + {Qs1,St3} = lc_tq(Anno, Qs0, St2), + {[{b_generate_ns,AnnoG,P1,G1} | Qs1],St3}; lc_tq(Anno, [{m_generate,AnnoG,P0,G0} | Qs0], St0) -> {G1,St1} = expr(G0, St0), {map_field_exact,AnnoMFE,KeyP0,ValP0} = P0, @@ -558,6 +568,14 @@ lc_tq(Anno, [{m_generate,AnnoG,P0,G0} | Qs0], St0) -> {Qs1,St4} = lc_tq(Anno, Qs0, St3), P1 = {map_field_exact,AnnoMFE,KeyP1,ValP1}, {[{m_generate,AnnoG,P1,G1} | Qs1],St4}; +lc_tq(Anno, [{m_generate_ns,AnnoG,P0,G0} | Qs0], St0) -> + {G1,St1} = expr(G0, St0), + {map_field_exact,AnnoMFE,KeyP0,ValP0} = P0, + {KeyP1,St2} = pattern(KeyP0, St1), + {ValP1,St3} = pattern(ValP0, St2), + {Qs1,St4} = lc_tq(Anno, Qs0, St3), + P1 = {map_field_exact,AnnoMFE,KeyP1,ValP1}, + {[{m_generate_ns,AnnoG,P1,G1} | Qs1],St4}; lc_tq(Anno, [F0 | Qs0], #exprec{calltype=Calltype,raw_records=Records}=St0) -> %% Allow record/2 and expand out as guard test. IsOverriden = fun(FA) -> diff --git a/lib/stdlib/src/erl_features.erl b/lib/stdlib/src/erl_features.erl index 3f99c648c031..b3b538e02a89 100644 --- a/lib/stdlib/src/erl_features.erl +++ b/lib/stdlib/src/erl_features.erl @@ -74,6 +74,15 @@ feature_specs() -> experimental => 25, approved => 27, keywords => ['maybe', 'else'], + type => extension}, + non_skipping_generators => + #{short => "Non-skipping generators", + description => + "Introduction of non-skipping variant of generators " + "to be used in comprehensions.", + status => experimental, + experimental => 28, + keywords => [], type => extension}}. %% Return all currently known features. diff --git a/lib/stdlib/src/erl_lint.erl b/lib/stdlib/src/erl_lint.erl index 5672b702ae49..9a6f858eff00 100644 --- a/lib/stdlib/src/erl_lint.erl +++ b/lib/stdlib/src/erl_lint.erl @@ -3883,13 +3883,23 @@ lc_quals(Qs, Vt0, St0) -> lc_quals([{generate,_Anno,P,E} | Qs], Vt0, Uvt0, St0) -> {Vt,Uvt,St} = handle_generator(P,E,Vt0,Uvt0,St0), lc_quals(Qs, Vt, Uvt, St); +lc_quals([{generate_ns,_Anno,P,E} | Qs], Vt0, Uvt0, St0) -> + {Vt,Uvt,St} = handle_generator(P,E,Vt0,Uvt0,St0), + lc_quals(Qs, Vt, Uvt, St); lc_quals([{b_generate,_Anno,P,E} | Qs], Vt0, Uvt0, St0) -> St1 = handle_bitstring_gen_pat(P,St0), {Vt,Uvt,St} = handle_generator(P,E,Vt0,Uvt0,St1), lc_quals(Qs, Vt, Uvt, St); +lc_quals([{b_generate_ns,_Anno,P,E} | Qs], Vt0, Uvt0, St0) -> + St1 = handle_bitstring_gen_pat(P,St0), + {Vt,Uvt,St} = handle_generator(P,E,Vt0,Uvt0,St1), + lc_quals(Qs, Vt, Uvt, St); lc_quals([{m_generate,_Anno,P,E} | Qs], Vt0, Uvt0, St0) -> {Vt,Uvt,St} = handle_generator(P,E,Vt0,Uvt0,St0), lc_quals(Qs, Vt, Uvt, St); +lc_quals([{m_generate_ns,_Anno,P,E} | Qs], Vt0, Uvt0, St0) -> + {Vt,Uvt,St} = handle_generator(P,E,Vt0,Uvt0,St0), + lc_quals(Qs, Vt, Uvt, St); lc_quals([F|Qs], Vt, Uvt, St0) -> Info = is_guard_test2_info(St0), {Fvt,St1} = case is_guard_test2(F, Info) of diff --git a/lib/stdlib/src/erl_parse.yrl b/lib/stdlib/src/erl_parse.yrl index dd99245d336a..9f46cb553186 100644 --- a/lib/stdlib/src/erl_parse.yrl +++ b/lib/stdlib/src/erl_parse.yrl @@ -79,7 +79,7 @@ ssa_check_when_clauses. Terminals char integer float atom sigil_prefix string sigil_suffix var -'(' ')' ',' '->' '{' '}' '[' ']' '|' '||' '<-' ';' ':' '#' '.' +'(' ')' ',' '->' '{' '}' '[' ']' '|' '||' '<-' '<:-' ';' ':' '#' '.' 'after' 'begin' 'case' 'try' 'catch' 'end' 'fun' 'if' 'of' 'receive' 'when' 'maybe' 'else' 'andalso' 'orelse' @@ -87,7 +87,7 @@ char integer float atom sigil_prefix string sigil_suffix var '*' '/' 'div' 'rem' 'band' 'and' '+' '-' 'bor' 'bxor' 'bsl' 'bsr' 'or' 'xor' '++' '--' -'==' '/=' '=<' '<' '>=' '>' '=:=' '=/=' '<=' '=>' ':=' +'==' '/=' '=<' '<' '>=' '>' '=:=' '=/=' '<=' '<:=' '=>' ':=' '<<' '>>' '!' '=' '::' '..' '...' '?=' @@ -365,8 +365,11 @@ lc_exprs -> lc_expr ',' lc_exprs : ['$1'|'$3']. lc_expr -> expr : '$1'. lc_expr -> map_field_exact '<-' expr : {m_generate,?anno('$2'),'$1','$3'}. +lc_expr -> map_field_exact '<:-' expr : {m_generate_ns,?anno('$2'),'$1','$3'}. lc_expr -> expr '<-' expr : {generate,?anno('$2'),'$1','$3'}. +lc_expr -> expr '<:-' expr : {generate_ns,?anno('$2'),'$1','$3'}. lc_expr -> binary '<=' expr : {b_generate,?anno('$2'),'$1','$3'}. +lc_expr -> binary '<:=' expr : {b_generate_ns,?anno('$2'),'$1','$3'}. tuple -> '{' '}' : {tuple,?anno('$1'),[]}. tuple -> '{' exprs '}' : {tuple,?anno('$1'),'$2'}. @@ -935,8 +938,11 @@ processed (see section [Error Information](#module-error-information)). -doc "Abstract representation of a list, bitstring or map generator.". -type af_generator() :: {'generate', anno(), af_pattern(), abstract_expr()} + | {'generate_ns', anno(), af_pattern(), abstract_expr()} | {'m_generate', anno(), af_assoc_exact(af_pattern()), abstract_expr()} - | {'b_generate', anno(), af_pattern(), abstract_expr()}. + | {'m_generate_ns', anno(), af_assoc_exact(af_pattern()), abstract_expr()} + | {'b_generate', anno(), af_pattern(), abstract_expr()} + | {'b_generate_ns', anno(), af_pattern(), abstract_expr()}. -type af_filter() :: abstract_expr(). diff --git a/lib/stdlib/src/erl_pp.erl b/lib/stdlib/src/erl_pp.erl index 8cc2c24781d7..1bf768e78564 100644 --- a/lib/stdlib/src/erl_pp.erl +++ b/lib/stdlib/src/erl_pp.erl @@ -1050,12 +1050,21 @@ lc_quals(Qs, Opts) -> lc_qual({m_generate,_,Pat,E}, Opts) -> Pl = map_field(Pat, Opts), {list,[{step,[Pl,leaf(" <-")],lexpr(E, 0, Opts)}]}; +lc_qual({m_generate_ns,_,Pat,E}, Opts) -> + Pl = map_field(Pat, Opts), + {list,[{step,[Pl,leaf(" <:-")],lexpr(E, 0, Opts)}]}; lc_qual({b_generate,_,Pat,E}, Opts) -> Pl = lexpr(Pat, 0, Opts), {list,[{step,[Pl,leaf(" <=")],lexpr(E, 0, Opts)}]}; +lc_qual({b_generate_ns,_,Pat,E}, Opts) -> + Pl = lexpr(Pat, 0, Opts), + {list,[{step,[Pl,leaf(" <:=")],lexpr(E, 0, Opts)}]}; lc_qual({generate,_,Pat,E}, Opts) -> Pl = lexpr(Pat, 0, Opts), {list,[{step,[Pl,leaf(" <-")],lexpr(E, 0, Opts)}]}; +lc_qual({generate_ns,_,Pat,E}, Opts) -> + Pl = lexpr(Pat, 0, Opts), + {list,[{step,[Pl,leaf(" <:-")],lexpr(E, 0, Opts)}]}; lc_qual(Q, Opts) -> lexpr(Q, 0, Opts). diff --git a/lib/stdlib/src/erl_scan.erl b/lib/stdlib/src/erl_scan.erl index 899785ae3d8a..f340fa1d5d79 100644 --- a/lib/stdlib/src/erl_scan.erl +++ b/lib/stdlib/src/erl_scan.erl @@ -637,13 +637,21 @@ scan1("?="++Cs, St, Line, Col, Toks) -> tok2(Cs, St, Line, Col, Toks, "?=", '?=', 2); scan1("?"=Cs, St, Line, Col, Toks) -> {more,{Cs,St,Col,Toks,Line,[],fun scan/6}}; -%% << <- <= +%% << <:- <- <:= <= scan1("<<"++Cs, St, Line, Col, Toks) -> tok2(Cs, St, Line, Col, Toks, "<<", '<<', 2); +scan1("<:-"++Cs, St, Line, Col, Toks) -> + %% fixme: reject the new operator unless the non_skipping_generators feature is enabled + tok2(Cs, St, Line, Col, Toks, "<:-", '<:-', 3); scan1("<-"++Cs, St, Line, Col, Toks) -> tok2(Cs, St, Line, Col, Toks, "<-", '<-', 2); +scan1("<:="++Cs, St, Line, Col, Toks) -> + %% fixme: reject the new operator unless the non_skipping_generators feature is enabled + tok2(Cs, St, Line, Col, Toks, "<:=", '<:=', 3); scan1("<="++Cs, St, Line, Col, Toks) -> tok2(Cs, St, Line, Col, Toks, "<=", '<=', 2); +scan1("<:"=Cs, St, Line, Col, Toks) -> + {more,{Cs,St,Col,Toks,Line,[],fun scan/6}}; scan1("<"=Cs, St, Line, Col, Toks) -> {more,{Cs,St,Col,Toks,Line,[],fun scan/6}}; %% >> >= diff --git a/lib/stdlib/src/qlc_pt.erl b/lib/stdlib/src/qlc_pt.erl index d34b139781ae..0a1147664405 100644 --- a/lib/stdlib/src/qlc_pt.erl +++ b/lib/stdlib/src/qlc_pt.erl @@ -292,13 +292,16 @@ record_attributes(Forms) -> %% transformation. %% compile_messages(Forms, FormsNoShadows, Options, State) -> - %% The qlc module can only handle list generators. - BGenF = fun(_QId,{b_generate,Anno,_P,_LE}=BGen, GA, A) -> + %% The qlc module can only handle skipping list generators. + BGenF = fun(_QId,{Type,Anno,_P,_LE}=BGen, GA, A) when Type =:= b_generate; Type =:= b_generate_ns -> M = {loc(Anno),?APIMOD,binary_generator}, {BGen,[{get(?QLC_FILE),[M]}|GA],A}; - (_QId,{m_generate,Anno,_P,_LE}=BGen, GA, A) -> + (_QId,{Type,Anno,_P,_LE}=BGen, GA, A) when Type =:= m_generate; Type =:= m_generate_ns -> M = {loc(Anno),?APIMOD,map_generator}, {BGen,[{get(?QLC_FILE),[M]}|GA],A}; + (_QId,{generate_ns,Anno,_P,_LE}=BGen, GA, A) -> + M = {loc(Anno),?APIMOD,non_skipping_generator}, + {BGen,[{get(?QLC_FILE),[M]}|GA],A}; (_QId, Q, GA, A) -> {Q,GA,A} end, diff --git a/lib/stdlib/src/shell.erl b/lib/stdlib/src/shell.erl index 59044a54d8d2..4eac65a29dc3 100644 --- a/lib/stdlib/src/shell.erl +++ b/lib/stdlib/src/shell.erl @@ -635,10 +635,16 @@ expand_fields([], _C) -> []. expand_quals([{generate,A,P,E}|Qs], C) -> [{generate,A,P,expand_expr(E, C)}|expand_quals(Qs, C)]; +expand_quals([{generate_ns,A,P,E}|Qs], C) -> + [{generate_ns,A,P,expand_expr(E, C)}|expand_quals(Qs, C)]; expand_quals([{b_generate,A,P,E}|Qs], C) -> [{b_generate,A,P,expand_expr(E, C)}|expand_quals(Qs, C)]; +expand_quals([{b_generate_ns,A,P,E}|Qs], C) -> + [{b_generate_ns,A,P,expand_expr(E, C)}|expand_quals(Qs, C)]; expand_quals([{m_generate,A,P,E}|Qs], C) -> [{m_generate,A,P,expand_expr(E, C)}|expand_quals(Qs, C)]; +expand_quals([{m_generate_ns,A,P,E}|Qs], C) -> + [{m_generate_ns,A,P,expand_expr(E, C)}|expand_quals(Qs, C)]; expand_quals([E|Qs], C) -> [expand_expr(E, C)|expand_quals(Qs, C)]; expand_quals([], _C) -> []. diff --git a/lib/stdlib/test/Makefile b/lib/stdlib/test/Makefile index 55d9acbdb7e3..b4828b2d0a81 100644 --- a/lib/stdlib/test/Makefile +++ b/lib/stdlib/test/Makefile @@ -132,9 +132,11 @@ RELSYSDIR = $(RELEASE_PATH)/stdlib_test # FLAGS # ---------------------------------------------------- +NON_SKIPPING_GENERATORS_OPT = '+{enable_feature,non_skipping_generators}' ERL_COMPILE_FLAGS += -I$(ERL_TOP)/lib/kernel/include \ +nowarn_missing_spec_documented \ - -I$(ERL_TOP)/lib/stdlib/include + -I$(ERL_TOP)/lib/stdlib/include \ + ${NON_SKIPPING_GENERATORS_OPT} ERL_COMPILE_FLAGS := $(filter-out +deterministic,$(ERL_COMPILE_FLAGS)) EBIN = . @@ -163,7 +165,7 @@ docs: # ---------------------------------------------------- # Release Target -# ---------------------------------------------------- +# ---------------------------------------------------- include $(ERL_TOP)/make/otp_release_targets.mk release_spec: opt diff --git a/lib/stdlib/test/erl_eval_SUITE.erl b/lib/stdlib/test/erl_eval_SUITE.erl index e28dd788bb9b..f1e61cc6e830 100644 --- a/lib/stdlib/test/erl_eval_SUITE.erl +++ b/lib/stdlib/test/erl_eval_SUITE.erl @@ -1,8 +1,8 @@ %% %% %CopyrightBegin% -%% +%% %% Copyright Ericsson AB 1998-2024. All Rights Reserved. -%% +%% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at @@ -14,11 +14,11 @@ %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. -%% +%% %% %CopyrightEnd% -module(erl_eval_SUITE). --export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, +-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, init_per_testcase/2, end_per_testcase/2, init_per_group/2,end_per_group/2]). @@ -56,7 +56,8 @@ otp_16865/1, eep49/1, binary_and_map_aliases/1, - eep58/1]). + eep58/1, + non_skipping_generators/1]). %% %% Define to run outside of test server @@ -65,7 +66,7 @@ -import(lists,[concat/1, sort/1]). --export([count_down/2, count_down_fun/0, do_apply/2, +-export([count_down/2, count_down_fun/0, do_apply/2, local_func/3, local_func_value/2]). -export([simple/0]). @@ -89,7 +90,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}, {timetrap,{minutes,1}}]. -all() -> +all() -> [guard_1, guard_2, match_pattern, string_plusplus, pattern_expr, match_bin, guard_3, guard_4, guard_5, lc, simple_cases, unary_plus, apply_atom, otp_5269, @@ -97,9 +98,9 @@ all() -> otp_8133, otp_10622, otp_13228, otp_14826, 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]. + eep49, binary_and_map_aliases, eep58, non_skipping_generators]. -groups() -> +groups() -> []. init_per_suite(Config) -> @@ -384,7 +385,7 @@ simple_cases(Config) when is_list(Config) -> (X) when X == 2 -> zwei end, ett = F(1), zwei = F(2) end, "begin F = fun(X) when X == 1 -> ett; - (X) when X == 2 -> zwei end, + (X) when X == 2 -> zwei end, ett = F(1), zwei = F(2) end.", zwei), error_check("begin F = fun(1) -> ett end, zwei = F(2) end.", @@ -515,7 +516,7 @@ unary_plus(Config) when is_list(Config) -> %% OTP-5064. Can no longer apply atoms. apply_atom(Config) when is_list(Config) -> error_check("[X || X <- [[1],[2]], - begin L = length, L(X) =:= 1 end].", + begin L = length, L(X) =:= 1 end].", {badfun,length}), ok. @@ -525,7 +526,7 @@ otp_5269(Config) when is_list(Config) -> F = fun(<>) -> B end, F(<<16:8, 7:16>>) end, - "begin + "begin L = 8, F = fun(<>) -> B end, F(<<16:8, 7:16>>) end.", 7), @@ -533,7 +534,7 @@ otp_5269(Config) when is_list(Config) -> F = fun(<>) -> B end, F(<<16:8, 7:16>>) end, - "begin + "begin L = 8, F = fun(<>) -> B end, F(<<16:8, 7:16>>) end.", 7), @@ -542,7 +543,7 @@ otp_5269(Config) when is_list(Config) -> 7), error_check("begin L = 8, <> = <<16:8, 7:16>> end.", {badmatch,<<16:8,7:16>>}), - + error_check("begin <> = <<16:16,8:16>>, L end.", {badmatch, <<16:16,8:16>>}), check(fun() -> U = 8, (fun(<>) -> U end)(<<32:8>>) end, @@ -577,18 +578,18 @@ otp_5269(Config) when is_list(Config) -> %% OTP-6539. try/catch bugs. otp_6539(Config) when is_list(Config) -> check(fun() -> - F = fun(A,B) -> - try A+B - catch _:_ -> dontthinkso - end + F = fun(A,B) -> + try A+B + catch _:_ -> dontthinkso + end end, lists:zipwith(F, [1,2], [2,3]) end, - "begin - F = fun(A,B) -> - try A+B - catch _:_ -> dontthinkso - end + "begin + F = fun(A,B) -> + try A+B + catch _:_ -> dontthinkso + end end, lists:zipwith(F, [1,2], [2,3]) end.", @@ -613,10 +614,10 @@ otp_6543(Config) when is_list(Config) -> "<< <> || <> <= <<\"hej\">> >>.", <<1,2,2,0,1,2,1,1,1,2,2,2>>), check(fun() -> - << <> || + << <> || <<65,X:4>> <= <<65,7:4,65,3:4,66,8:4>> >> end, - "<< <> || + "<< <> || <<65,X:4>> <= <<65,7:4,65,3:4,66,8:4>> >>.", <<7,3>>), check(fun() -> <<34:18/big>> end, @@ -690,61 +691,61 @@ otp_6543(Config) when is_list(Config) -> 34), check(fun() -> <> = <<34:18/little-signed>>, X end, - "begin <> = <<34:18/little-signed>>, + "begin <> = <<34:18/little-signed>>, X end.", 34), check(fun() -> <> = <<34:18/native-signed>>, X end, - "begin <> = <<34:18/native-signed>>, + "begin <> = <<34:18/native-signed>>, X end.", 34), check(fun() -> <> = <<34:18/big-unsigned>>, X end, - "begin <> = <<34:18/big-unsigned>>, + "begin <> = <<34:18/big-unsigned>>, X end.", 34), check(fun() -> - <> = <<34:18/little-unsigned>>, + <> = <<34:18/little-unsigned>>, X end, - "begin <> = <<34:18/little-unsigned>>, + "begin <> = <<34:18/little-unsigned>>, X end.", 34), check(fun() -> - <> = <<34:18/native-unsigned>>, + <> = <<34:18/native-unsigned>>, X end, - "begin <> = <<34:18/native-unsigned>>, + "begin <> = <<34:18/native-unsigned>>, X end.", 34), check(fun() -> <> = <<2.0:32/float-big>>, X end, - "begin <> = <<2.0:32/float-big>>, + "begin <> = <<2.0:32/float-big>>, X end.", 2.0), check(fun() -> <> = <<2.0:32/float-little>>, X end, - "begin <> = <<2.0:32/float-little>>, + "begin <> = <<2.0:32/float-little>>, X end.", 2.0), check(fun() -> <> = <<2.0:32/float-native>>, X end, - "begin <> = <<2.0:32/float-native>>, + "begin <> = <<2.0:32/float-native>>, X end.", 2.0), check( - fun() -> + fun() -> [X || <<"hej",X:8>> <= <<"hej",8,"san",9,"hej",17,"hej">>] end, - "[X || <<\"hej\",X:8>> <= + "[X || <<\"hej\",X:8>> <= <<\"hej\",8,\"san\",9,\"hej\",17,\"hej\">>].", [8,17]), check( fun() -> L = 8, << <> || <> <= <<16:8, 7:16>> >> end, - "begin L = 8, << <> || <> <= <<16:8, 7:16>> >> + "begin L = 8, << <> || <> <= <<16:8, 7:16>> >> end.", <<0,0,0,7>>), - %% Test the Value part of a binary segment. + %% Test the Value part of a binary segment. %% "Old" bugs have been fixed (partial_eval is called on Value). check(fun() -> [ 3 || <<17/float>> <= <<17.0/float>>] end, "[ 3 || <<17/float>> <= <<17.0/float>>].", @@ -771,28 +772,28 @@ otp_6543(Config) when is_list(Config) -> check(fun() -> [ foo || <<(1 bsl 1024)/float>> <- [<<(1 bsl 1023)/float>>]] end, - "[ foo || <<(1 bsl 1024)/float>> <- + "[ foo || <<(1 bsl 1024)/float>> <- [<<(1 bsl 1023)/float>>]].", []), check(fun() -> [ foo || <<(1 bsl 1024)/float>> <= <<(1 bsl 1023)/float>>] end, - "[ foo || <<(1 bsl 1024)/float>> <= + "[ foo || <<(1 bsl 1024)/float>> <= <<(1 bsl 1023)/float>>].", []), check(fun() -> - L = 8, + L = 8, [{L,B} || <> <= <<32:8,7:32/float>>] end, - "begin L = 8, + "begin L = 8, [{L,B} || <> <= <<32:8,7:32/float>>] end.", [{32,7.0}]), check(fun() -> - L = 8, + L = 8, [{L,B} || <> <- [<<32:8,7:32/float>>]] end, - "begin L = 8, + "begin L = 8, [{L,B} || <> <- [<<32:8,7:32/float>>]] end.", [{32,7.0}]), @@ -908,47 +909,47 @@ otp_7550(Config) when is_list(Config) -> otp_8133(Config) when is_list(Config) -> check( fun() -> - E = fun(N) -> - if - is_integer(N) -> <>; - true -> throw(foo) - end + E = fun(N) -> + if + is_integer(N) -> <>; + true -> throw(foo) + end end, - try << << (E(V))/binary >> || V <- [1,2,3,a] >> + try << << (E(V))/binary >> || V <- [1,2,3,a] >> catch foo -> ok end end, "begin - E = fun(N) -> - if is_integer(N) -> <>; - true -> throw(foo) - end + E = fun(N) -> + if is_integer(N) -> <>; + true -> throw(foo) + end end, - try << << (E(V))/binary >> || V <- [1,2,3,a] >> + try << << (E(V))/binary >> || V <- [1,2,3,a] >> catch foo -> ok end end.", ok), check( fun() -> - E = fun(N) -> - if - is_integer(N) -> <>; + E = fun(N) -> + if + is_integer(N) -> <>; - true -> erlang:error(foo) - end + true -> erlang:error(foo) + end end, - try << << (E(V))/binary >> || V <- [1,2,3,a] >> + try << << (E(V))/binary >> || V <- [1,2,3,a] >> catch error:foo -> ok end end, "begin - E = fun(N) -> - if is_integer(N) -> <>; - true -> erlang:error(foo) - end + E = fun(N) -> + if is_integer(N) -> <>; + true -> erlang:error(foo) + end end, - try << << (E(V))/binary >> || V <- [1,2,3,a] >> + try << << (E(V))/binary >> || V <- [1,2,3,a] >> catch error:foo -> ok end end.", @@ -1092,13 +1093,13 @@ otp_14826(_Config) -> ?MODULE]), backtrace_check("[A || A <- a].", {bad_generator, a}, - [{erl_eval,eval_generate,8}, {erl_eval, eval_lc, 7}]), + [{erl_eval,eval_generate,9}, {erl_eval, eval_lc, 7}]), backtrace_check("<< <> || <> <= a>>.", {bad_generator, a}, - [{erl_eval,eval_b_generate,8}, {erl_eval, eval_bc, 7}]), + [{erl_eval,eval_b_generate,9}, {erl_eval, eval_bc, 7}]), backtrace_check("[A || A <- [1], begin a end].", {bad_filter, a}, - [{erl_eval,eval_filter,7}, {erl_eval, eval_generate, 8}]), + [{erl_eval,eval_filter,7}, {erl_eval, eval_generate, 9}]), fun() -> {'EXIT', {{badarity, {_Fun, []}}, BT}} = (catch parse_and_run("fun(A) -> A end().")), @@ -1396,7 +1397,7 @@ many_args(N) -> [many_args1(I) || I <- lists:seq(1, N)]. many_args1(N) -> - F = fun(L, P) -> + F = fun(L, P) -> tl(lists:flatten([","++P++integer_to_list(E) || E <- L])) end, L = lists:seq(1, N), @@ -1414,16 +1415,16 @@ do_funs(LFH, EFH) -> M = atom_to_list(?MODULE), check(fun() -> F1 = fun(F,N) -> ?MODULE:count_down(F, N) end, F1(F1, 1000) end, - concat(["begin F1 = fun(F,N) -> ", M, + concat(["begin F1 = fun(F,N) -> ", M, ":count_down(F, N) end, F1(F1,1000) end."]), 0, ['F1'], LFH, EFH), check(fun() -> F1 = fun(F,N) -> apply(?MODULE,count_down,[F,N]) end, F1(F1, 1000) end, - concat(["begin F1 = fun(F,N) -> apply(", M, + concat(["begin F1 = fun(F,N) -> apply(", M, ",count_down,[F, N]) end, F1(F1,1000) end."]), 0, ['F1'], LFH, EFH), check(fun() -> F = fun(F,N) when N > 0 -> apply(F,[F,N-1]); - (_F,0) -> ok end, + (_F,0) -> ok end, F(F, 1000) end, "begin F = fun(F,N) when N > 0 -> apply(F,[F,N-1]);" @@ -1432,7 +1433,7 @@ do_funs(LFH, EFH) -> ok, ['F'], LFH, EFH), check(fun() -> F = fun(F,N) when N > 0 -> apply(erlang,apply,[F,[F,N-1]]); - (_F,0) -> ok end, + (_F,0) -> ok end, F(F, 1000) end, "begin F = fun(F,N) when N > 0 ->" @@ -1451,7 +1452,7 @@ do_funs(LFH, EFH) -> check(fun() -> F = fun(X) -> A = 1+X, {X,A} end, true = {2,3} == F(2) end, - "begin F = fun(X) -> A = 1+X, {X,A} end, + "begin F = fun(X) -> A = 1+X, {X,A} end, true = {2,3} == F(2) end.", true, ['F'], LFH, EFH), check(fun() -> F = fun(X) -> erlang:'+'(X,2) end, true = 3 == F(1) end, @@ -1459,7 +1460,7 @@ do_funs(LFH, EFH) -> " true = 3 == F(1) end.", true, ['F'], LFH, EFH), check(fun() -> F = fun(X) -> byte_size(X) end, - ?MODULE:do_apply(F,<<"hej">>) end, + ?MODULE:do_apply(F,<<"hej">>) end, concat(["begin F = fun(X) -> size(X) end,", M,":do_apply(F,<<\"hej\">>) end."]), 3, ['F'], LFH, EFH), @@ -1468,22 +1469,22 @@ do_funs(LFH, EFH) -> Z = 5, F2 = fun(X, Y) -> F1(Z,{X,Y}) end, F3 = fun(X, Y) -> {a,F1(Z,{X,Y})} end, - {5,{x,y}} = F2(x,y), - {a,{5,{y,x}}} = F3(y,x), - {5,{5,y}} = F2(Z,y), + {5,{x,y}} = F2(x,y), + {a,{5,{y,x}}} = F3(y,x), + {5,{5,y}} = F2(Z,y), true = {5,{x,5}} == F2(x,Z) end, "begin F1 = fun(X, Z) -> {X,Z} end, Z = 5, F2 = fun(X, Y) -> F1(Z,{X,Y}) end, F3 = fun(X, Y) -> {a,F1(Z,{X,Y})} end, - {5,{x,y}} = F2(x,y), - {a,{5,{y,x}}} = F3(y,x), - {5,{5,y}} = F2(Z,y), + {5,{x,y}} = F2(x,y), + {a,{5,{y,x}}} = F3(y,x), + {5,{5,y}} = F2(Z,y), true = {5,{x,5}} == F2(x,Z) end.", true, ['F1','Z','F2','F3'], LFH, EFH), check(fun() -> F = fun(X) -> byte_size(X) end, F2 = fun(Y) -> F(Y) end, - ?MODULE:do_apply(F2,<<"hej">>) end, + ?MODULE:do_apply(F2,<<"hej">>) end, concat(["begin F = fun(X) -> size(X) end,", "F2 = fun(Y) -> F(Y) end,", M,":do_apply(F2,<<\"hej\">>) end."]), @@ -1498,11 +1499,11 @@ do_funs(LFH, EFH) -> {1,1} = F2(1), Z = 7, Z end, "begin F = fun(Z) -> Z end, F2 = fun(X) -> F(X), Z = {X,X}, Z end, - {1,1} = F2(1), Z = 7, Z end.", 7, ['F','F2','Z'], + {1,1} = F2(1), Z = 7, Z end.", 7, ['F','F2','Z'], LFH, EFH), check(fun() -> F = fun(F, N) -> [?MODULE:count_down(F,N) || X <-[1]] end, F(F,2) end, - concat(["begin F = fun(F, N) -> [", M, + concat(["begin F = fun(F, N) -> [", M, ":count_down(F,N) || X <-[1]] end, F(F,2) end."]), [[[0]]], ['F'], LFH, EFH), ok. @@ -1636,7 +1637,7 @@ try_catch(Config) when is_list(Config) -> {badmatch,2}), %% Uncaught exception with after check(fun () -> {'EXIT',{{badmatch,2},_}} = - begin catch try 1=2 + begin catch try 1=2 after put(try_catch, 3) end end, get(try_catch) end, "begin {'EXIT',{{badmatch,2},_}} = " @@ -2030,12 +2031,95 @@ eep58(Config) when is_list(Config) -> ok. +non_skipping_generators(Config) when is_list(Config) -> + %% Basic scenario for each comprehension and generator type + check(fun() -> [X+1 || X <:- [1,2,3]] end, + "[X+1 || X <:- [1,2,3]].", + [2,3,4]), + check(fun() -> [X+1 || <> <:= <<1,2,3>>] end, + "[X+1 || <> <:= <<1,2,3>>].", + [2,3,4]), + check(fun() -> [X*Y || X := Y <:- #{1 => 2, 3 => 4}] end, + "[X*Y || X := Y <:- #{1 => 2, 3 => 4}].", + [2,12]), + check(fun() -> << <<(X+1)>> || X <:- [1,2,3]>> end, + "<< <<(X+1)>> || X <:- [1,2,3]>>.", + <<2,3,4>>), + check(fun() -> << <<(X+1)>> || <> <:= <<1,2,3>> >> end, + "<< <<(X+1)>> || <> <:= <<1,2,3>> >>.", + <<2,3,4>>), + check(fun() -> << <<(X*Y)>> || X := Y <:- #{1 => 2, 3 => 4} >> end, + "<< <<(X*Y)>> || X := Y <:- #{1 => 2, 3 => 4} >>.", + <<2,12>>), + check(fun() -> #{X => X+1 || X <:- [1,2,3]} end, + "#{X => X+1 || X <:- [1,2,3]}.", + #{1 => 2, 2 => 3, 3 => 4}), + check(fun() -> #{X => X+1 || <> <:= <<1,2,3>>} end, + "#{X => X+1 || <> <:= <<1,2,3>>}.", + #{1 => 2, 2 => 3, 3 => 4}), + check(fun() -> #{X+1 => Y*2 || X := Y <:- #{1 => 2, 3 => 4}} end, + "#{X+1 => Y*2 || X := Y <:- #{1 => 2, 3 => 4}}.", + #{2 => 4, 4 => 8}), + %% A failing guard following a non-skipping generator is ok + check(fun() -> [X+1 || X <:- [1,2,3], X > 1] end, + "[X+1 || X <:- [1,2,3], X > 1].", + [3,4]), + check(fun() -> [X+1 || <> <:= <<1,2,3>>, X > 1] end, + "[X+1 || <> <:= <<1,2,3>>, X > 1].", + [3,4]), + check(fun() -> [X*Y || X := Y <:- #{1 => 2, 3 => 4}, X > 1] end, + "[X*Y || X := Y <:- #{1 => 2, 3 => 4}, X > 1].", + [12]), + check(fun() -> << <<(X+1)>> || X <:- [1,2,3], X > 1>> end, + "<< <<(X+1)>> || X <:- [1,2,3], X > 1>>.", + <<3,4>>), + check(fun() -> << <<(X+1)>> || <> <:= <<1,2,3>>, X > 1 >> end, + "<< <<(X+1)>> || <> <:= <<1,2,3>>, X > 1 >>.", + <<3,4>>), + check(fun() -> << <<(X*Y)>> || X := Y <:- #{1 => 2, 3 => 4}, X > 1 >> end, + "<< <<(X*Y)>> || X := Y <:- #{1 => 2, 3 => 4}, X > 1 >>.", + <<12>>), + check(fun() -> #{X => X+1 || X <:- [1,2,3], X > 1} end, + "#{X => X+1 || X <:- [1,2,3], X > 1}.", + #{2 => 3, 3 => 4}), + check(fun() -> #{X => X+1 || <> <:= <<1,2,3>>, X > 1} end, + "#{X => X+1 || <> <:= <<1,2,3>>, X > 1}.", + #{2 => 3, 3 => 4}), + check(fun() -> #{X+1 => Y*2 || X := Y <:- #{1 => 2, 3 => 4}, X > 1} end, + "#{X+1 => Y*2 || X := Y <:- #{1 => 2, 3 => 4}, X > 1}.", + #{4 => 8}), + %% Non-matching elements cause a badmatch error + error_check("[X || {ok, X} <:- [{ok,1},2,{ok,3}]].", + {badmatch,2}), + error_check("[X || <<0:1, X:7>> <:= <<1,128,2>>].", + {badmatch,<<128,2>>}), + error_check("[X || X := ok <:- #{1 => ok, 2 => error, 3 => ok}].", + {badmatch,{2,error}}), + error_check("<< <> || {ok, X} <:- [{ok,1},2,{ok,3}] >>.", + {badmatch,2}), + error_check("<< <> || <<0:1, X:7>> <:= <<1,128,2>> >>.", + {badmatch,<<128,2>>}), + error_check("<< <> || X := ok <:- #{1 => ok, 2 => error, 3 => ok} >>.", + {badmatch,{2,error}}), + error_check("#{X => X+1 || {ok, X} <:- [{ok,1},2,{ok,3}]}.", + {badmatch,2}), + error_check("#{X => X+1 || <<0:1, X:7>> <:= <<1,128,2>>}.", + {badmatch,<<128,2>>}), + error_check("#{X => X+1 || X := ok <:- #{1 => ok, 2 => error, 3 => ok}}.", + {badmatch,{2,error}}), + %% Binary generators don't skip unused bits at the end either + error_check("[X || <> <:= <<0>>].", + {badmatch,<<0:2>>}), + error_check("[Y || <> <:= <<8,1,9,2>>].", + {badmatch,<<9,2>>}), + ok. + %% Check the string in different contexts: as is; in fun; from compiled code. check(F, String, Result) -> check1(F, String, Result), FunString = concat(["fun() -> ", no_final_dot(String), " end(). "]), check1(F, FunString, Result), - CompileString = concat(["hd(lists:map(fun(_) -> ", no_final_dot(String), + CompileString = concat(["hd(lists:map(fun(_) -> ", no_final_dot(String), " end, [foo])). "]), check1(F, CompileString, Result). diff --git a/lib/stdlib/test/erl_scan_SUITE.erl b/lib/stdlib/test/erl_scan_SUITE.erl index 771ffe19a00b..656e33ea2008 100644 --- a/lib/stdlib/test/erl_scan_SUITE.erl +++ b/lib/stdlib/test/erl_scan_SUITE.erl @@ -57,11 +57,11 @@ suite() -> [{ct_hooks,[ts_install_cth]}, {timetrap,{minutes,20}}]. -all() -> +all() -> [{group, error}, iso88591, otp_7810, otp_10302, otp_10990, otp_10992, otp_11807, otp_16480, otp_17024, text_fun, triple_quoted_string]. -groups() -> +groups() -> [{error, [], [error_1, error_2]}]. init_per_suite(Config) -> @@ -223,7 +223,7 @@ punctuations() -> Ts = [{W,{1,1}}], test_string(S, Ts) end || S <- L], - Three = ["/=:=", "<=:=", "==:=", ">=:="], % three tokens... + Three = ["/=:=", "<:=", "==:=", ">=:="], % three tokens... No = Three ++ L, SL0 = [{S1++S2,{-length(S1),S1,S2}} || S1 <- L, diff --git a/lib/syntax_tools/src/erl_prettypr.erl b/lib/syntax_tools/src/erl_prettypr.erl index ff01d0f4e60a..142be049b691 100644 --- a/lib/syntax_tools/src/erl_prettypr.erl +++ b/lib/syntax_tools/src/erl_prettypr.erl @@ -423,22 +423,22 @@ add_comment_prefix(S) -> lay_2(Node, Ctxt) -> case erl_syntax:type(Node) of %% We list literals and other common cases first. - + variable -> text(erl_syntax:variable_literal(Node)); - + atom -> text(erl_syntax:atom_literal(Node, Ctxt#ctxt.encoding)); - + integer -> text(erl_syntax:integer_literal(Node)); float -> text(tidy_float(erl_syntax:float_literal(Node))); - + char -> text(erl_syntax:char_literal(Node, Ctxt#ctxt.encoding)); - + string -> lay_string(erl_syntax:string_literal(Node, Ctxt#ctxt.encoding), Ctxt); @@ -452,7 +452,7 @@ lay_2(Node, Ctxt) -> beside(floating(text("{")), beside(sep(Es), floating(text("}")))); - + list -> Ctxt1 = reset_prec(Ctxt), Node1 = erl_syntax:compact_list(Node), @@ -473,7 +473,7 @@ lay_2(Node, Ctxt) -> operator -> floating(text(erl_syntax:operator_literal(Node))); - + infix_expr -> Operator = erl_syntax:infix_expr_operator(Node), {PrecL, Prec, PrecR} = @@ -491,7 +491,7 @@ lay_2(Node, Ctxt) -> set_prec(Ctxt, PrecR)), D4 = par([D1, D2, D3], Ctxt#ctxt.break_indent), maybe_parentheses(D4, Prec, Ctxt); - + prefix_expr -> Operator = erl_syntax:prefix_expr_operator(Node), {{Prec, PrecR}, Name} = @@ -514,7 +514,7 @@ lay_2(Node, Ctxt) -> par([D1, D2], Ctxt#ctxt.break_indent) end, maybe_parentheses(D3, Prec, Ctxt); - + application -> {PrecL, Prec} = func_prec(), D = lay(erl_syntax:application_operator(Node), @@ -526,7 +526,7 @@ lay_2(Node, Ctxt) -> beside(sep(As), floating(text(")"))))), maybe_parentheses(D1, Prec, Ctxt); - + match_expr -> {PrecL, Prec, PrecR} = inop_prec('='), D1 = lay(erl_syntax:match_expr_pattern(Node), @@ -593,7 +593,7 @@ lay_2(Node, Ctxt) -> Ctxt1#ctxt.break_indent), nest(Ctxt1#ctxt.break_indent, D2), text("end")]); - + if_expr -> Ctxt1 = reset_prec(Ctxt), D = lay_clauses(erl_syntax:if_expr_clauses(Node), @@ -814,7 +814,7 @@ lay_2(Node, Ctxt) -> eof_marker -> empty(); - + form_list -> Es = seq(erl_syntax:form_list_elements(Node), none, reset_prec(Ctxt), fun lay/2), @@ -826,18 +826,36 @@ lay_2(Node, Ctxt) -> D2 = lay(erl_syntax:generator_body(Node), Ctxt1), par([D1, beside(text("<- "), D2)], Ctxt1#ctxt.break_indent); + non_skipping_generator -> + Ctxt1 = reset_prec(Ctxt), + D1 = lay(erl_syntax:non_skipping_generator_pattern(Node), Ctxt1), + D2 = lay(erl_syntax:non_skipping_generator_body(Node), Ctxt1), + par([D1, beside(text("<:- "), D2)], Ctxt1#ctxt.break_indent); + binary_generator -> Ctxt1 = reset_prec(Ctxt), D1 = lay(erl_syntax:binary_generator_pattern(Node), Ctxt1), D2 = lay(erl_syntax:binary_generator_body(Node), Ctxt1), par([D1, beside(text("<= "), D2)], Ctxt1#ctxt.break_indent); + non_skipping_binary_generator -> + Ctxt1 = reset_prec(Ctxt), + D1 = lay(erl_syntax:non_skipping_binary_generator_pattern(Node), Ctxt1), + D2 = lay(erl_syntax:non_skipping_binary_generator_body(Node), Ctxt1), + par([D1, beside(text("<:= "), D2)], Ctxt1#ctxt.break_indent); + map_generator -> Ctxt1 = reset_prec(Ctxt), D1 = lay(erl_syntax:map_generator_pattern(Node), Ctxt1), D2 = lay(erl_syntax:map_generator_body(Node), Ctxt1), par([D1, beside(text("<- "), D2)], Ctxt1#ctxt.break_indent); + non_skipping_map_generator -> + Ctxt1 = reset_prec(Ctxt), + D1 = lay(erl_syntax:non_skipping_map_generator_pattern(Node), Ctxt1), + D2 = lay(erl_syntax:non_skipping_map_generator_body(Node), Ctxt1), + par([D1, beside(text("<:- "), D2)], Ctxt1#ctxt.break_indent); + implicit_fun -> D = lay(erl_syntax:implicit_fun_name(Node), reset_prec(Ctxt)), @@ -933,7 +951,7 @@ lay_2(Node, Ctxt) -> maybe_parentheses(beside(D1, D3), Prec, Ctxt); record_expr -> - {PrecL, Prec, _} = inop_prec('#'), + {PrecL, Prec, _} = inop_prec('#'), Ctxt1 = reset_prec(Ctxt), D1 = lay(erl_syntax:record_expr_type(Node), Ctxt1), D2 = par(seq(erl_syntax:record_expr_fields(Node), @@ -949,7 +967,7 @@ lay_2(Node, Ctxt) -> beside(lay(A, set_prec(Ctxt, PrecL)), D3) end, maybe_parentheses(D4, Prec, Ctxt); - + record_field -> Ctxt1 = reset_prec(Ctxt), D1 = lay(erl_syntax:record_field_name(Node), Ctxt1), @@ -962,7 +980,7 @@ lay_2(Node, Ctxt) -> end; record_index_expr -> - {Prec, PrecR} = preop_prec('#'), + {Prec, PrecR} = preop_prec('#'), D1 = lay(erl_syntax:record_index_expr_type(Node), reset_prec(Ctxt)), D2 = lay(erl_syntax:record_index_expr_field(Node), @@ -1342,7 +1360,7 @@ split_string_2([$x, ${ | Xs], N, L, As) -> split_string_2([X1, X2, X3 | Xs], N, L, As) when X1 >= $0, X1 =< $7, X2 >= $0, X2 =< $7, X3 >= $0, X3 =< $7 -> split_string_1(Xs, N - 3, L - 3, [X3, X2, X1 | As]); -split_string_2([X1, X2 | Xs], N, L, As) when +split_string_2([X1, X2 | Xs], N, L, As) when X1 >= $0, X1 =< $7, X2 >= $0, X2 =< $7 -> split_string_1(Xs, N - 2, L - 2, [X2, X1 | As]); split_string_2([X | Xs], N, L, As) -> diff --git a/lib/syntax_tools/src/erl_syntax.erl b/lib/syntax_tools/src/erl_syntax.erl index 84605345dfc4..3012d91dc0d1 100644 --- a/lib/syntax_tools/src/erl_syntax.erl +++ b/lib/syntax_tools/src/erl_syntax.erl @@ -269,6 +269,15 @@ trees. named_fun_expr_clauses/1, named_fun_expr_name/1, nil/0, + non_skipping_binary_generator/2, + non_skipping_binary_generator_body/1, + non_skipping_binary_generator_pattern/1, + non_skipping_generator/2, + non_skipping_generator_body/1, + non_skipping_generator_pattern/1, + non_skipping_map_generator/2, + non_skipping_map_generator_body/1, + non_skipping_map_generator_pattern/1, operator/1, operator_literal/1, operator_name/1, @@ -621,8 +630,11 @@ type(Node) -> {cons, _, _, _} -> list; {function, _, _, _, _} -> function; {b_generate, _, _, _} -> binary_generator; + {b_generate_ns, _, _, _} -> non_skipping_binary_generator; {generate, _, _, _} -> generator; + {generate_ns, _, _, _} -> non_skipping_generator; {m_generate, _, _, _} -> map_generator; + {m_generate_ns, _, _, _} -> non_skipping_map_generator; {lc, _, _, _} -> list_comp; {bc, _, _, _} -> binary_comp; {mc, _, _, _} -> map_comp; @@ -5836,6 +5848,68 @@ generator_body(Node) -> end. +%% ===================================================================== + +-record(non_skipping_generator, {pattern :: syntaxTree(), body :: syntaxTree()}). + +-doc """ +Creates an abstract non-skipping list generator. + +The result represents "`*Pattern*<:- *Body*`". + +_See also: _`binary_comp/2`, `non_skipping_generator_body/1`, +`non_skipping_generator_pattern/1`, `list_comp/2`. +""". +-spec non_skipping_generator(syntaxTree(), syntaxTree()) -> syntaxTree(). + +%% `erl_parse' representation: +%% +%% {generate_ns, Pos, Pattern, Body} +%% +%% Pattern = Body = erl_parse() + +non_skipping_generator(Pattern, Body) -> + tree(non_skipping_generator, #non_skipping_generator{pattern = Pattern, body = Body}). + +revert_non_skipping_generator(Node) -> + Pos = get_pos(Node), + Pattern = non_skipping_generator_pattern(Node), + Body = non_skipping_generator_body(Node), + {generate_ns, Pos, Pattern, Body}. + + +-doc """ +Returns the pattern subtree of a `generator` node. + +_See also: _`non_skipping_generator/2`. +""". +-spec non_skipping_generator_pattern(syntaxTree()) -> syntaxTree(). + +non_skipping_generator_pattern(Node) -> + case unwrap(Node) of + {generate_ns, _, Pattern, _} -> + Pattern; + Node1 -> + (data(Node1))#non_skipping_generator.pattern + end. + + +-doc """ +Returns the body subtree of a `generator` node. + +_See also: _`non_skipping_generator/2`. +""". +-spec non_skipping_generator_body(syntaxTree()) -> syntaxTree(). + +non_skipping_generator_body(Node) -> + case unwrap(Node) of + {generate_ns, _, _, Body} -> + Body; + Node1 -> + (data(Node1))#non_skipping_generator.body + end. + + %% ===================================================================== -record(binary_generator, {pattern :: syntaxTree(), body :: syntaxTree()}). @@ -5898,6 +5972,68 @@ binary_generator_body(Node) -> end. +%% ===================================================================== + +-record(non_skipping_binary_generator, {pattern :: syntaxTree(), body :: syntaxTree()}). + +-doc """ +Creates an abstract non-skipping binary_generator. + +The result represents "`*Pattern*<:- *Body*`". + +_See also: _`binary_comp/2`, `non_skipping_binary_generator_body/1`, +`non_skipping_binary_generator_pattern/1`, `list_comp/2`. +""". +-spec non_skipping_binary_generator(syntaxTree(), syntaxTree()) -> syntaxTree(). + +%% `erl_parse' representation: +%% +%% {b_generate_ns, Pos, Pattern, Body} +%% +%% Pattern = Body = erl_parse() + +non_skipping_binary_generator(Pattern, Body) -> + tree(non_skipping_binary_generator, #non_skipping_binary_generator{pattern = Pattern, body = Body}). + +revert_non_skipping_binary_generator(Node) -> + Pos = get_pos(Node), + Pattern = non_skipping_binary_generator_pattern(Node), + Body = non_skipping_binary_generator_body(Node), + {b_generate_ns, Pos, Pattern, Body}. + + +-doc """ +Returns the pattern subtree of a `generator` node. + +_See also: _`non_skipping_binary_generator/2`. +""". +-spec non_skipping_binary_generator_pattern(syntaxTree()) -> syntaxTree(). + +non_skipping_binary_generator_pattern(Node) -> + case unwrap(Node) of + {b_generate_ns, _, Pattern, _} -> + Pattern; + Node1 -> + (data(Node1))#non_skipping_binary_generator.pattern + end. + + +-doc """ +Returns the body subtree of a `generator` node. + +_See also: _`non_skipping_binary_generator/2`. +""". +-spec non_skipping_binary_generator_body(syntaxTree()) -> syntaxTree(). + +non_skipping_binary_generator_body(Node) -> + case unwrap(Node) of + {b_generate_ns, _, _, Body} -> + Body; + Node1 -> + (data(Node1))#non_skipping_binary_generator.body + end. + + %% ===================================================================== -record(map_generator, {pattern :: syntaxTree(), body :: syntaxTree()}). @@ -5960,6 +6096,68 @@ map_generator_body(Node) -> end. +%% ===================================================================== + +-record(non_skipping_map_generator, {pattern :: syntaxTree(), body :: syntaxTree()}). + +-doc """ +Creates an abstract non-skipping map_generator. The result represents +"`*Pattern*<- *Body*`". + +_See also: _`list_comp/2`, `map_comp/2`, +`non_skipping_map_generator_body/1`, +`non_skipping_map_generator_pattern/1`. +""". +-spec non_skipping_map_generator(syntaxTree(), syntaxTree()) -> syntaxTree(). + +%% `erl_parse' representation: +%% +%% {m_generate_ns, Pos, Pattern, Body} +%% +%% Pattern = Body = erl_parse() + +non_skipping_map_generator(Pattern, Body) -> + tree(non_skipping_map_generator, #non_skipping_map_generator{pattern = Pattern, body = Body}). + +revert_non_skipping_map_generator(Node) -> + Pos = get_pos(Node), + Pattern = non_skipping_map_generator_pattern(Node), + Body = non_skipping_map_generator_body(Node), + {m_generate_ns, Pos, Pattern, Body}. + + +-doc """ +Returns the pattern subtree of a `generator` node. + +_See also: _`non_skipping_map_generator/2`. +""". +-spec non_skipping_map_generator_pattern(syntaxTree()) -> syntaxTree(). + +non_skipping_map_generator_pattern(Node) -> + case unwrap(Node) of + {m_generate_ns, _, Pattern, _} -> + Pattern; + Node1 -> + (data(Node1))#non_skipping_map_generator.pattern + end. + + +-doc """ +Returns the body subtree of a `generator` node. + +_See also: _`non_skipping_map_generator/2`. +""". +-spec non_skipping_map_generator_body(syntaxTree()) -> syntaxTree(). + +non_skipping_map_generator_body(Node) -> + case unwrap(Node) of + {m_generate_ns, _, _, Body} -> + Body; + Node1 -> + (data(Node1))#non_skipping_map_generator.body + end. + + %% ===================================================================== -doc """ @@ -7337,6 +7535,12 @@ revert_root(Node) -> revert_named_fun_expr(Node); nil -> revert_nil(Node); + non_skipping_binary_generator -> + revert_non_skipping_binary_generator(Node); + non_skipping_generator -> + revert_non_skipping_generator(Node); + non_skipping_map_generator -> + revert_non_skipping_map_generator(Node); parentheses -> revert_parentheses(Node); prefix_expr -> @@ -7655,6 +7859,15 @@ subtrees(T) -> named_fun_expr -> [[named_fun_expr_name(T)], named_fun_expr_clauses(T)]; + non_skipping_binary_generator -> + [[non_skipping_binary_generator_pattern(T)], + [non_skipping_binary_generator_body(T)]]; + non_skipping_generator -> + [[non_skipping_generator_pattern(T)], + [non_skipping_generator_body(T)]]; + non_skipping_map_generator -> + [[non_skipping_map_generator_pattern(T)], + [non_skipping_map_generator_body(T)]]; parentheses -> [[parentheses_body(T)]]; prefix_expr -> @@ -7816,6 +8029,9 @@ make_tree(maybe_expr, [Body, [Else]]) -> maybe_expr(Body, Else); make_tree(maybe_match_expr, [[P], [E]]) -> maybe_match_expr(P, E); make_tree(named_fun_expr, [[N], C]) -> named_fun_expr(N, C); make_tree(module_qualifier, [[M], [N]]) -> module_qualifier(M, N); +make_tree(non_skipping_binary_generator, [[P], [E]]) -> non_skipping_binary_generator(P, E); +make_tree(non_skipping_generator, [[P], [E]]) -> non_skipping_generator(P, E); +make_tree(non_skipping_map_generator, [[P], [E]]) -> non_skipping_map_generator(P, E); make_tree(parentheses, [[E]]) -> parentheses(E); make_tree(prefix_expr, [[F], [A]]) -> prefix_expr(F, A); make_tree(receive_expr, [C]) -> receive_expr(C); diff --git a/lib/syntax_tools/src/erl_syntax_lib.erl b/lib/syntax_tools/src/erl_syntax_lib.erl index 8b0e1126d6fb..9f7d12789b45 100644 --- a/lib/syntax_tools/src/erl_syntax_lib.erl +++ b/lib/syntax_tools/src/erl_syntax_lib.erl @@ -492,10 +492,16 @@ vann(Tree, Env) -> vann_binary_comp(Tree, Env); generator -> vann_generator(Tree, Env); + non_skipping_generator -> + vann_non_skipping_generator(Tree, Env); binary_generator -> vann_binary_generator(Tree, Env); + non_skipping_binary_generator -> + vann_non_skipping_binary_generator(Tree, Env); map_generator -> vann_map_generator(Tree, Env); + non_skipping_map_generator -> + vann_non_skipping_map_generator(Tree, Env); block_expr -> vann_block_expr(Tree, Env); macro -> @@ -630,10 +636,16 @@ vann_list_comp_body_join() -> {T1, Bound1, Free1} = case erl_syntax:type(T) of generator -> vann_generator(T, Env); + non_skipping_generator -> + vann_non_skipping_generator(T, Env); binary_generator -> vann_binary_generator(T,Env); + non_skipping_binary_generator -> + vann_non_skipping_binary_generator(T,Env); map_generator -> vann_map_generator(T,Env); + non_skipping_map_generator -> + vann_non_skipping_map_generator(T,Env); _ -> %% Bindings in filters are not %% exported to the rest of the @@ -643,7 +655,7 @@ vann_list_comp_body_join() -> end, Env1 = ordsets:union(Env, Bound1), {T1, {Env1, ordsets:union(Bound, Bound1), - ordsets:union(Free, + ordsets:union(Free, ordsets:subtract(Free1, Bound))}} end. @@ -668,10 +680,16 @@ vann_binary_comp_body_join() -> {T1, Bound1, Free1} = case erl_syntax:type(T) of generator -> vann_generator(T, Env); + non_skipping_generator -> + vann_non_skipping_generator(T, Env); binary_generator -> vann_binary_generator(T,Env); + non_skipping_binary_generator -> + vann_non_skipping_binary_generator(T,Env); map_generator -> vann_map_generator(T,Env); + non_skipping_map_generator -> + vann_non_skipping_map_generator(T,Env); _ -> %% Bindings in filters are not %% exported to the rest of the @@ -681,7 +699,7 @@ vann_binary_comp_body_join() -> end, Env1 = ordsets:union(Env, Bound1), {T1, {Env1, ordsets:union(Bound, Bound1), - ordsets:union(Free, + ordsets:union(Free, ordsets:subtract(Free1, Bound))}} end. @@ -703,6 +721,14 @@ vann_generator(Tree, Env) -> Tree1 = rewrite(Tree, erl_syntax:generator(P1, E1)), {ann_bindings(Tree1, Env, Bound, Free), Bound, Free}. +vann_non_skipping_generator(Tree, Env) -> + P = erl_syntax:non_skipping_generator_pattern(Tree), + {P1, Bound, _} = vann_pattern(P, []), + E = erl_syntax:non_skipping_generator_body(Tree), + {E1, _, Free} = vann(E, Env), + Tree1 = rewrite(Tree, erl_syntax:non_skipping_generator(P1, E1)), + {ann_bindings(Tree1, Env, Bound, Free), Bound, Free}. + vann_binary_generator(Tree, Env) -> P = erl_syntax:binary_generator_pattern(Tree), {P1, Bound, _} = vann_pattern(P, Env), @@ -711,6 +737,14 @@ vann_binary_generator(Tree, Env) -> Tree1 = rewrite(Tree, erl_syntax:binary_generator(P1, E1)), {ann_bindings(Tree1, Env, Bound, Free), Bound, Free}. +vann_non_skipping_binary_generator(Tree, Env) -> + P = erl_syntax:non_skipping_binary_generator_pattern(Tree), + {P1, Bound, _} = vann_pattern(P, Env), + E = erl_syntax:non_skipping_binary_generator_body(Tree), + {E1, _, Free} = vann(E, Env), + Tree1 = rewrite(Tree, erl_syntax:non_skipping_binary_generator(P1, E1)), + {ann_bindings(Tree1, Env, Bound, Free), Bound, Free}. + vann_map_generator(Tree, Env) -> P = erl_syntax:map_generator_pattern(Tree), {P1, Bound, _} = vann_pattern(P, []), @@ -719,6 +753,14 @@ vann_map_generator(Tree, Env) -> Tree1 = rewrite(Tree, erl_syntax:map_generator(P1, E1)), {ann_bindings(Tree1, Env, Bound, Free), Bound, Free}. +vann_non_skipping_map_generator(Tree, Env) -> + P = erl_syntax:non_skipping_map_generator_pattern(Tree), + {P1, Bound, _} = vann_pattern(P, []), + E = erl_syntax:non_skipping_map_generator_body(Tree), + {E1, _, Free} = vann(E, Env), + Tree1 = rewrite(Tree, erl_syntax:non_skipping_map_generator(P1, E1)), + {ann_bindings(Tree1, Env, Bound, Free), Bound, Free}. + vann_block_expr(Tree, Env) -> Es = erl_syntax:block_expr_body(Tree), {Es1, {Bound, Free}} = vann_body(Es, Env), @@ -877,7 +919,7 @@ _See also: _[//erts/erlang:error/1](`erlang:error/1`), """. -spec is_fail_expr(syntaxTree()) -> boolean(). -is_fail_expr(E) -> +is_fail_expr(E) -> case erl_syntax:type(E) of application -> N = length(erl_syntax:application_arguments(E)), diff --git a/lib/syntax_tools/test/syntax_tools_SUITE.erl b/lib/syntax_tools/test/syntax_tools_SUITE.erl index bb6b36174832..87aa5cc810c5 100644 --- a/lib/syntax_tools/test/syntax_tools_SUITE.erl +++ b/lib/syntax_tools/test/syntax_tools_SUITE.erl @@ -19,7 +19,7 @@ -include_lib("common_test/include/ct.hrl"). %% Test server specific exports --export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, +-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1, init_per_group/2,end_per_group/2]). %% Test cases @@ -32,7 +32,7 @@ suite() -> [{ct_hooks,[ts_install_cth]}]. -all() -> +all() -> [app_test,appup_test,smoke_test,revert,revert_map,revert_map_type, revert_preserve_pos_changes, wrapped_subtrees, @@ -40,7 +40,7 @@ all() -> t_epp_dodger,t_epp_dodger_clever, t_comment_scan,t_prettypr,test_named_fun_bind_ann]. -groups() -> +groups() -> []. init_per_suite(Config) -> @@ -91,7 +91,7 @@ print_error_markers(F, File) -> _ -> ok end. - + %% Read with erl_parse, wrap and revert with erl_syntax and check for equality. revert(Config) when is_list(Config) -> @@ -332,10 +332,13 @@ 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 <:- Vs]", list_comp,false}, {"[catch V||V <- Vs]", list_comp,false}, {"<< <> || <> <= Bs>>", binary_comp,false}, + {"<< <> || <> <:= Bs>>", binary_comp,false}, {"<< (catch <>) || <> <= Bs>>", binary_comp,false}, {"#{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}, {"#state{ a = A, b = B}", record_expr,false}, {"#state{}", record_expr,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 daa95b6a259e..9ac20736dbfc 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 @@ -9,6 +9,7 @@ sub_string/2,sub_string/3,centre/2,centre/3, join/2]). -export([to_upper/1, to_lower/1]). -export([eep49/0, eep58/0]). +-export([non_skipping_generators/0]). -import(lists,[reverse/1,member/2]). @@ -216,7 +217,7 @@ cspan([], _Cs, I) -> I. SubString :: string(), Start :: pos_integer(). -substr(String, 1) when is_list(String) -> +substr(String, 1) when is_list(String) -> String; substr(String, S) when is_integer(S), S > 1 -> substr2(String, S). @@ -344,9 +345,9 @@ sub_word(String, Index, Char) when is_integer(Index), is_integer(Char) -> s_word([], _, _, _,Res) -> reverse(Res); s_word([Char|_],Index,Char,Index,Res) -> reverse(Res); s_word([H|T],Index,Char,Index,Res) -> s_word(T,Index,Char,Index,[H|Res]); -s_word([Char|T],Stop,Char,Index,Res) when Index < Stop -> +s_word([Char|T],Stop,Char,Index,Res) when Index < Stop -> s_word(strip(T,left,Char),Stop,Char,Index+1,Res); -s_word([_|T],Stop,Char,Index,Res) when Index < Stop -> +s_word([_|T],Stop,Char,Index,Res) when Index < Stop -> s_word(T,Stop,Char,Index,Res). %%% STRIP %%% @@ -590,3 +591,10 @@ eep58() -> MapDouble = maps:from_list([{{key,I}, 2 * I} || I <- Seq]), ok. + +non_skipping_generators() -> + [X+1 || X <:- [1,2,3]], + [X+1 || <> <:= <<1,2,3>>], + [X*Y || X := Y <:- #{1 => 2, 3 => 4}], + + ok. \ No newline at end of file diff --git a/lib/tools/emacs/erlang.el b/lib/tools/emacs/erlang.el index fa041a8e5558..e83e1a5adca0 100644 --- a/lib/tools/emacs/erlang.el +++ b/lib/tools/emacs/erlang.el @@ -1183,7 +1183,7 @@ behaviour.") (defvar erlang-font-lock-keywords-lc (list - (list "\\(<-\\|<=\\|||\\)\\(\\s \\|$\\)" 1 'font-lock-keyword-face)) + (list "\\(<-\\|<:-\\|<=\\|<:=\\|||\\)\\(\\s \\|$\\)" 1 'font-lock-keyword-face)) "Font lock keyword highlighting list comprehension operators.") (defvar erlang-font-lock-keywords-keywords @@ -1510,7 +1510,7 @@ Other commands: )) (add-to-list 'align-rules-list `(erlang-generator-arrows - (regexp . ,(concat space-group "\\(<-\\|<=\\)" space-group)) + (regexp . ,(concat space-group "\\(<-\\|<:-\\|<=\\|<:=\\)" space-group)) (group . (1 3)) (separate . ,(concat "\\(||\\|" erl-sep-forms "\\|" erl-sep-symbols "\\)")) (repeat . t) diff --git a/system/doc/reference_manual/expressions.md b/system/doc/reference_manual/expressions.md index e6d0e6bc1e51..2adfa3abeccb 100644 --- a/system/doc/reference_manual/expressions.md +++ b/system/doc/reference_manual/expressions.md @@ -1958,30 +1958,61 @@ is either a **generator** or a **filter**. > > Map comprehensions and map generators were introduced in Erlang/OTP 26. -There are three kinds of generators. +There are three kinds of generators, each with a skipping and a +non-skipping variant. -A _list generator_ has the following syntax: +> #### Change {: .info } +> +> Non-skipping generators are an experimental +> [feature](`e:system:features.md#features`) introduced in Erlang/OTP 28. +> By default, they are disabled. To enable the feature, either use the +> `-feature(non_skipping_generators,enable).` directive (from within the +> source code), or the compiler option `{feature,non_skipping_generators,enable}`. + +Skipping generators ignore terms in the right-hand side expression that +do not match the left-hand side pattern. Non-skipping generators on the +other hand fail with excaption `badmatch`. + +A _list generator_ has the following syntax for skipping: ``` Pattern <- ListExpr ``` +and non-skipping variant: + +``` +Pattern <:- ListExpr +``` + where `ListExpr` is an expression that evaluates to a list of terms. -A _bit string generator_ has the following syntax: +A _bit string generator_ has the following syntax for skipping: ``` BitstringPattern <= BitStringExpr ``` +and non-skipping variant: + +``` +BitstringPattern <:= BitStringExpr +``` + where `BitStringExpr` is an expression that evaluates to a bit string. -A _map generator_ has the following syntax: +A _map generator_ has the following syntax for skipping: ``` KeyPattern := ValuePattern <- MapExpression ``` +and non-skipping variant: + +``` +KeyPattern := ValuePattern <:- MapExpression +``` + where `MapExpr` is an expression that evaluates to a map, or a map iterator obtained by calling `maps:iterator/1` or `maps:iterator/2`. diff --git a/system/doc/reference_manual/features.md b/system/doc/reference_manual/features.md index fa4e6c8e47e4..04d11d78d2b8 100644 --- a/system/doc/reference_manual/features.md +++ b/system/doc/reference_manual/features.md @@ -131,3 +131,7 @@ The following configurable features exist: [`maybe`](expressions.md#maybe) expression proposed in [EEP 49](https://www.erlang.org/eeps/eep-0049). It was approved in Erlang/OTP 27. + +- **`non_skipping_generators` (experimental)** - Introduction of + non-skipping variant of generators to be used in + [comprehensions](expressions.md#comprehensions).