From 1c60c97b6b05cbee7e106fae6d8582382cbe4b7c Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Fri, 23 Sep 2022 20:08:47 +0300 Subject: Add support for 'for' loop first form (for x:...) in script --- libbuild2/build/script/parser+for.test.testscript | 184 +++++++++++++ libbuild2/build/script/parser.cxx | 119 +++++--- libbuild2/build/script/parser.hxx | 2 +- libbuild2/parser.hxx | 10 + libbuild2/script/parser.cxx | 190 ++++++++++++- libbuild2/script/parser.hxx | 17 +- libbuild2/script/script.cxx | 4 +- libbuild2/script/script.hxx | 1 + libbuild2/test/script/parser+for.test.testscript | 315 ++++++++++++++++++++++ libbuild2/test/script/parser.cxx | 157 +++++++---- libbuild2/test/script/parser.hxx | 7 +- tests/recipe/buildscript/testscript | 122 +++++++++ tests/test/script/runner/for.testscript | 42 +++ 13 files changed, 1067 insertions(+), 103 deletions(-) create mode 100644 libbuild2/build/script/parser+for.test.testscript create mode 100644 libbuild2/test/script/parser+for.test.testscript create mode 100644 tests/test/script/runner/for.testscript diff --git a/libbuild2/build/script/parser+for.test.testscript b/libbuild2/build/script/parser+for.test.testscript new file mode 100644 index 0000000..f4ebf3c --- /dev/null +++ b/libbuild2/build/script/parser+for.test.testscript @@ -0,0 +1,184 @@ +# file : libbuild2/build/script/parser+for.test.testscript +# license : MIT; see accompanying LICENSE file + +: form-1 +: +: for x: ... +: +{ + : for + : + { + : no-var + : + $* <>EOE != 0 + for + cmd + end + EOI + buildfile:11:4: error: expected variable name instead of + EOE + + : untyped + : + $* <>EOO + for x: a b + cmd $x + end + EOI + cmd a + cmd b + EOO + + : null + : + $* <:'' + for x: [null] + cmd $x + end + EOI + + : empty + : + $* <:'' + for x: + cmd $x + end + EOI + + : expansion + : + $* <>EOO + vs = a b + for x: $vs + cmd $x + end + EOI + cmd a + cmd b + EOO + + : typed-value + : + $* <>~%EOO% + for x: [dir_paths] a b + cmd $x + end + EOI + %cmd (a/|'a\\')% + %cmd (b/|'b\\')% + EOO + + : typed-var + : + $* <>~%EOO% + for [dir_path] x: a b + cmd $x + end + EOI + %cmd (a/|'a\\')% + %cmd (b/|'b\\')% + EOO + + : type-mismatch + : + $* <>EOE != 0 + for [string] x: [dir_paths] a b + cmd $x + end + EOI + error: type mismatch in variable x + info: value type is dir_path + info: variable type is string + EOE + + : defined-var + : + $* <>EOO + x = x + + for x: a b + cmd $x + end + + cmd $x + EOI + cmd a + cmd b + cmd b + EOO + } + + : end + : + { + : without-end + : + $* <>EOE != 0 + for x: a b + cmd + EOI + buildfile:13:1: error: expected closing 'end' + EOE + } + + : elif + : + { + : without-if + : + $* <>EOE != 0 + for x: a b + elif true + cmd + end + end + EOI + buildfile:12:3: error: 'elif' without preceding 'if' + EOE + } + + : nested + : + { + $* -l -r <>EOO + for x: a b + cmd1 $x # 1 + if ($x == "a") # 2 + cmd2 # 3 + for y: x y + cmd3 # 4 + end + else + cmd4 # 5 + end + cmd5 # 6 + end + cmd6 # 7 + EOI + cmd1 a # 1 i1 + ? true # 2 i1 + cmd2 # 3 i1 + cmd3 # 4 i1 i1 + cmd3 # 4 i1 i2 + cmd5 # 6 i1 + cmd1 b # 1 i2 + ? false # 2 i2 + cmd4 # 5 i2 + cmd5 # 6 i2 + cmd6 # 7 + EOO + } + + : contained + : + { + : eos + : + $* <>EOE != 0 + for x: + EOI + buildfile:12:1: error: expected closing 'end' + EOE + } +} diff --git a/libbuild2/build/script/parser.cxx b/libbuild2/build/script/parser.cxx index 1a7f4e1..cb0dbbb 100644 --- a/libbuild2/build/script/parser.cxx +++ b/libbuild2/build/script/parser.cxx @@ -205,9 +205,10 @@ namespace build2 // enter: next token is peeked at (type in tt) // leave: newline - assert (!fct || - *fct == line_type::cmd_if || - *fct == line_type::cmd_while); + assert (!fct || + *fct == line_type::cmd_if || + *fct == line_type::cmd_while || + *fct == line_type::cmd_for); // Determine the line type/start token. // @@ -245,6 +246,52 @@ namespace build2 break; } + case line_type::cmd_for: + { + // First take care of the variable name. + // + mode (lexer_mode::normal); + + next_with_attributes (t, tt); + attributes_push (t, tt); + + if (tt != type::word || t.qtype != quote_type::unquoted) + fail (t) << "expected variable name instead of " << t; + + const string& n (t.value); + + if (special_variable (n)) + fail (t) << "attempt to set '" << n << "' variable directly"; + + // We don't pre-enter variables. + // + ln.var = nullptr; + + next (t, tt); + + if (tt != type::colon) + { + // @@ TMP We will need to fallback to parsing the 'for x <...' + // form instead. + // + fail (t) << "expected ':' instead of " << t + << " after variable name"; + } + + expire_mode (); // Expire the normal lexer mode. + + // Parse the value similar to the var line type (see above). + // + mode (lexer_mode::variable_line); + parse_variable_line (t, tt); + + if (tt != type::newline) + fail (t) << "expected newline instead of " << t << " after for"; + + ++level_; + + break; + } case line_type::cmd_elif: case line_type::cmd_elifn: case line_type::cmd_else: @@ -300,17 +347,25 @@ namespace build2 *save_line_ = move (ln); } - if (lt == line_type::cmd_if || lt == line_type::cmd_ifn) + switch (lt) { - tt = peek (lexer_mode::first_token); + case line_type::cmd_if: + case line_type::cmd_ifn: + { + tt = peek (lexer_mode::first_token); - pre_parse_if_else (t, tt); - } - else if (lt == line_type::cmd_while) - { - tt = peek (lexer_mode::first_token); + pre_parse_if_else (t, tt); + break; + } + case line_type::cmd_while: + case line_type::cmd_for: + { + tt = peek (lexer_mode::first_token); - pre_parse_while (t, tt); + pre_parse_loop (t, tt, lt); + break; + } + default: break; } } @@ -341,8 +396,9 @@ namespace build2 break; } case line_type::cmd_while: + case line_type::cmd_for: { - fct = line_type::cmd_while; + fct = bt; break; } default: assert(false); @@ -405,18 +461,20 @@ namespace build2 } void parser:: - pre_parse_while (token& t, type& tt) + pre_parse_loop (token& t, type& tt, line_type lt) { // enter: peeked first token of next line (type in tt) // leave: newline + assert (lt == line_type::cmd_while || lt == line_type::cmd_for); + // Parse lines until we see closing 'end'. // for (;; tt = peek (lexer_mode::first_token)) { size_t i (script_->body.size ()); - pre_parse_block_line (t, tt, line_type::cmd_while); + pre_parse_block_line (t, tt, lt); if (script_->body[i].type == line_type::cmd_end) break; @@ -1247,25 +1305,19 @@ namespace build2 // Note that we rely on "small function object" optimization for the // exec_*() lambdas. // - auto exec_set = [this] (const variable& var, - token& t, build2::script::token_type& tt, - const location&) + auto exec_assign = [this] (const variable& var, + value&& val, + type kind, + const location& l) { - next (t, tt); - type kind (tt); // Assignment kind. - - mode (lexer_mode::variable_line); - value rhs (parse_variable_line (t, tt)); - - assert (tt == type::newline); - - // Assign. - // value& lhs (kind == type::assign ? environment_->assign (var) : environment_->append (var)); - apply_value_attributes (&var, lhs, move (rhs), kind); + if (kind == type::assign) + lhs = move (val); + else + append_value (&var, lhs, move (val), l); }; auto exec_cond = [this] (token& t, build2::script::token_type& tt, @@ -1281,11 +1333,12 @@ namespace build2 return runner_->run_cond (*environment_, ce, ii, li, ll); }; - build2::script::parser::exec_lines (begin, end, - exec_set, exec_cmd, exec_cond, - nullptr /* iteration_index */, - environment_->exec_line, - &environment_->var_pool); + build2::script::parser::exec_lines ( + begin, end, + exec_assign, exec_cmd, exec_cond, + nullptr /* iteration_index */, + environment_->exec_line, + &environment_->var_pool); } names parser:: diff --git a/libbuild2/build/script/parser.hxx b/libbuild2/build/script/parser.hxx index 987ed1f..a81583c 100644 --- a/libbuild2/build/script/parser.hxx +++ b/libbuild2/build/script/parser.hxx @@ -75,7 +75,7 @@ namespace build2 pre_parse_if_else (token&, token_type&); void - pre_parse_while (token&, token_type&); + pre_parse_loop (token&, token_type&, line_type); command_expr parse_command_line (token&, token_type&); diff --git a/libbuild2/parser.hxx b/libbuild2/parser.hxx index 5c9d16a..0d1e9e2 100644 --- a/libbuild2/parser.hxx +++ b/libbuild2/parser.hxx @@ -759,6 +759,16 @@ namespace build2 } void + replay_pop () + { + assert (replay_ == replay::save); + + assert (!peeked_ && !replay_data_.empty ()); + + replay_data_.pop_back (); + } + + void replay_play () { assert ((replay_ == replay::save && !replay_data_.empty ()) || diff --git a/libbuild2/script/parser.cxx b/libbuild2/script/parser.cxx index c371e5b..aa186ff 100644 --- a/libbuild2/script/parser.cxx +++ b/libbuild2/script/parser.cxx @@ -2035,6 +2035,37 @@ namespace build2 build2::parser::apply_value_attributes (var, lhs, move (rhs), kind); } + void parser:: + append_value (const variable* var, + value& v, + value&& rhs, + const location& l) + { + if (rhs) // Don't append/prepent NULL. + { + // Perform the type conversion (see + // build2::parser::apply_value_attributes() for the approach + // reasoning). + // + if (rhs.type != nullptr) + { + if (!v) + v.type = rhs.type; + else if (v.type == nullptr) + typify (v, *rhs.type, var); + else if (v.type != rhs.type) + fail (l) << "conflicting original value type " << v.type->name + << " and append value type " << rhs.type->name; + + // Reduce this to the untyped value case. + // + untypify (rhs); + } + + v.append (move (rhs).as (), var); + } + } + line_type parser:: pre_parse_line_start (token& t, token_type& tt, lexer_mode stm) { @@ -2062,6 +2093,7 @@ namespace build2 else if (n == "elif!") r = line_type::cmd_elifn; else if (n == "else") r = line_type::cmd_else; else if (n == "while") r = line_type::cmd_while; + else if (n == "for") r = line_type::cmd_for; else if (n == "end") r = line_type::cmd_end; else { @@ -2090,7 +2122,7 @@ namespace build2 bool parser:: exec_lines (lines::const_iterator i, lines::const_iterator e, - const function& exec_set, + const function& exec_assign, const function& exec_cmd, const function& exec_cond, const iteration_index* ii, size_t& li, @@ -2134,9 +2166,10 @@ namespace build2 { line_type lt (j->type); - if (lt == line_type::cmd_if || - lt == line_type::cmd_ifn || - lt == line_type::cmd_while) + if (lt == line_type::cmd_if || + lt == line_type::cmd_ifn || + lt == line_type::cmd_while || + lt == line_type::cmd_for) ++n; // If we are nested then we just wait until we get back @@ -2163,6 +2196,9 @@ namespace build2 { // Note that we don't count else and end as commands. // + // @@ Note that for the for-loop's second and third forms + // will probably need to increment li. + // switch (lt) { case line_type::cmd: @@ -2197,7 +2233,25 @@ namespace build2 var = &var_pool->insert (t.value); } - exec_set (*var, t, tt, ll); + next (t, tt); + type kind (tt); // Assignment kind. + + assert (kind == type::assign || kind == type::append); + + // Parse the value with the potential attributes. + // + // Note that we don't really need to change the mode since we + // are replaying the tokens. + // + value val; + apply_value_attributes (var, + val, + parse_variable_line (t, tt), + kind); + + assert (tt == type::newline); + + exec_assign (*var, move (val), kind, ll); replay_stop (); break; @@ -2258,7 +2312,7 @@ namespace build2 lines::const_iterator j (fcend (i, false, false)); if (!exec_lines (i + 1, j, - exec_set, exec_cmd, exec_cond, + exec_assign, exec_cmd, exec_cond, ii, li, var_pool)) return false; @@ -2281,6 +2335,10 @@ namespace build2 } case line_type::cmd_while: { + // The while-loop construct end. Set on the first iteration. + // + lines::const_iterator we (e); + size_t wli (li); for (iteration_index wi {1, ii};; wi.index++) @@ -2300,12 +2358,13 @@ namespace build2 // if (exec) { - // Find construct end. + // Find the construct end, if it is not found yet. // - lines::const_iterator j (fcend (i, true, false)); + if (we == e) + we = fcend (i, true, false); - if (!exec_lines (i + 1, j, - exec_set, exec_cmd, exec_cond, + if (!exec_lines (i + 1, we, + exec_assign, exec_cmd, exec_cond, &wi, li, var_pool)) return false; @@ -2318,7 +2377,8 @@ namespace build2 } else { - // Find construct end. + // Position to the construct end, always incrementing the + // line index (skip is true). // i = fcend (i, true, true); break; // Bail out from the while-loop. @@ -2327,9 +2387,117 @@ namespace build2 break; } + case line_type::cmd_for: + { + // Parse the variable name with the potential attributes. + // + next_with_attributes (t, tt); + attributes_push (t, tt); + + assert (tt == type::word && t.qtype == quote_type::unquoted); + + string vn (move (t.value)); + + // Enter the variable into the pool if this is not done during + // the script parsing (see the var line type handling for + // details). + // + const variable* var (ln.var); + + if (var == nullptr) + { + assert (var_pool != nullptr); + + var = &var_pool->insert (move (vn)); + } + + apply_variable_attributes (*var); + + next (t, tt); // Skip the colon. + assert (tt == type::colon); + + // Parse the value with the potential attributes. + // + // Note that we don't really need to change the mode since we + // are replaying the tokens. + // + value val; + apply_value_attributes (nullptr /* variable */, + val, + parse_variable_line (t, tt), + type::assign); + + replay_stop (); + + // If the value is not NULL then iterate over its elements, + // assigning them to the for-loop variable, and parsing all the + // construct lines afterwards. Then position to the end line of + // the construct and continue parsing. + + // The for-loop construct end. Set on the first iteration. + // + lines::const_iterator fe (e); + + if (val) + { + // If this value is a vector, then save its element type so + // that we can typify each element below. + // + const value_type* etype (nullptr); + + if (val.type != nullptr) + { + etype = val.type->element_type; + untypify (val); + } + + size_t fli (li); + iteration_index fi {1, ii}; + + // @@ Handle pairs. + // + // Do we need to always lex the variable values (for-loop + // and var lines) pair-character aware? + // + // Can there be any harm if a value with pairs is + // substituted into the command line? + // + for (name& n: val.as ()) + { + li = fli; + + value v (names {move (n)}); // Untyped. + + if (etype != nullptr) + typify (v, *etype, var); + + exec_assign (*var, move (v), type::assign, ll); + + // Find the construct end, if it is not found yet. + // + if (fe == e) + fe = fcend (i, true, false); + + if (!exec_lines (i + 1, fe, + exec_assign, exec_cmd, exec_cond, + &fi, li, + var_pool)) + return false; + + fi.index++; + } + } + + // Position to construct end. + // + i = (fe != e ? fe : fcend (i, true, true)); + + break; + } case line_type::cmd_end: { assert (false); + break; } } } diff --git a/libbuild2/script/parser.hxx b/libbuild2/script/parser.hxx index 9edb6ca..0f276e1 100644 --- a/libbuild2/script/parser.hxx +++ b/libbuild2/script/parser.hxx @@ -42,6 +42,11 @@ namespace build2 using build2::parser::apply_value_attributes; + // The variable is optional and is only used for diagnostics. + // + void + append_value (const variable*, value& lhs, value&& rhs, const location&); + // Return true if a command line element needs to be re-lexed. // // Specifically, it needs to be re-lexed if it contains any of the @@ -159,11 +164,13 @@ namespace build2 protected: // Return false if the execution of the script should be terminated with // the success status (e.g., as a result of encountering the exit - // builtin). For unsuccessful termination the failed exception is thrown. + // builtin). For unsuccessful termination the failed exception is + // thrown. // - using exec_set_function = void (const variable&, - token&, token_type&, - const location&); + using exec_assign_function = void (const variable&, + value&&, + token_type kind, + const location&); using exec_cmd_function = void (token&, token_type&, const iteration_index*, size_t li, @@ -181,7 +188,7 @@ namespace build2 // bool exec_lines (lines::const_iterator b, lines::const_iterator e, - const function&, + const function&, const function&, const function&, const iteration_index*, size_t& li, diff --git a/libbuild2/script/script.cxx b/libbuild2/script/script.cxx index d4096cf..33c4c30 100644 --- a/libbuild2/script/script.cxx +++ b/libbuild2/script/script.cxx @@ -28,6 +28,7 @@ namespace build2 case line_type::cmd_elifn: s = "'elif!'"; break; case line_type::cmd_else: s = "'else'"; break; case line_type::cmd_while: s = "'while'"; break; + case line_type::cmd_for: s = "'for'"; break; case line_type::cmd_end: s = "'end'"; break; } @@ -225,7 +226,8 @@ namespace build2 case line_type::cmd_elif: case line_type::cmd_elifn: case line_type::cmd_else: - case line_type::cmd_while: fc_ind += " "; break; + case line_type::cmd_while: + case line_type::cmd_for: fc_ind += " "; break; default: break; } diff --git a/libbuild2/script/script.hxx b/libbuild2/script/script.hxx index d6018f0..5eb4ee9 100644 --- a/libbuild2/script/script.hxx +++ b/libbuild2/script/script.hxx @@ -28,6 +28,7 @@ namespace build2 cmd_elifn, cmd_else, cmd_while, + cmd_for, cmd_end }; diff --git a/libbuild2/test/script/parser+for.test.testscript b/libbuild2/test/script/parser+for.test.testscript new file mode 100644 index 0000000..92fc714 --- /dev/null +++ b/libbuild2/test/script/parser+for.test.testscript @@ -0,0 +1,315 @@ +# file : libbuild2/test/script/parser+for.test.testscript +# license : MIT; see accompanying LICENSE file + +: form-1 +: +: for x: ... +: +{ + : for + : + { + : no-var + : + $* <>EOE != 0 + for + cmd + end + EOI + testscript:1:4: error: expected variable name instead of + EOE + + : untyped + : + $* <>EOO + for x: a b + cmd $x + end + EOI + cmd a + cmd b + EOO + + : null + : + $* <:'' + for x: [null] + cmd $x + end + EOI + + : empty + : + $* <:'' + for x: + cmd $x + end + EOI + + : expansion + : + $* <>EOO + vs = a b + for x: $vs + cmd $x + end + EOI + cmd a + cmd b + EOO + + : typed-value + : + $* <>~%EOO% + for x: [dir_paths] a b + cmd $x + end + EOI + %cmd (a/|'a\\')% + %cmd (b/|'b\\')% + EOO + + : typed-var + : + $* <>~%EOO% + for [dir_path] x: a b + cmd $x + end + EOI + %cmd (a/|'a\\')% + %cmd (b/|'b\\')% + EOO + + : type-mismatch + : + $* <>EOE != 0 + for [string] x: [dir_paths] a b + cmd $x + end + EOI + error: type mismatch in variable x + info: value type is dir_path + info: variable type is string + EOE + + : scope-var + : + $* <>EOO + x = x + + for x: a b + cmd $x + end + + -cmd $x + EOI + cmd a + cmd b + -cmd x + EOO + } + + : after-semi + : + $* -s <>EOO + cmd1; + for x: a b + cmd2 $x + end + EOI + { + { + cmd1 + cmd2 a + cmd2 b + } + } + EOO + + : setup + : + $* -s <>EOO + +for x: a b + cmd $x + end + EOI + { + +cmd a + +cmd b + } + EOO + + : tdown + : + $* -s <>EOO + -for x: a b + cmd $x + end + EOI + { + -cmd a + -cmd b + } + EOO + + : end + : + { + : without-end + : + $* <>EOE != 0 + for x: a b + cmd + EOI + testscript:3:1: error: expected closing 'end' + EOE + } + + : elif + : + { + : without-if + : + $* <>EOE != 0 + for x: a b + elif true + cmd + end + end + EOI + testscript:2:3: error: 'elif' without preceding 'if' + EOE + } + + : nested + : + { + $* -l -r <>EOO + for x: a b + cmd1 $x # 1 + if ($x == "a") # 2 + cmd2 # 3 + for y: x y + cmd3 # 4 + end + else + cmd4 # 5 + end + cmd5 # 6 + end; + cmd6 # 7 + EOI + cmd1 a # 1 i1 + ? true # 2 i1 + cmd2 # 3 i1 + cmd3 # 4 i1 i1 + cmd3 # 4 i1 i2 + cmd5 # 6 i1 + cmd1 b # 1 i2 + ? false # 2 i2 + cmd4 # 5 i2 + cmd5 # 6 i2 + cmd6 # 7 + EOO + } + + : contained + : + { + : semi + : + $* <>EOE != 0 + for x: + cmd; + cmd + end + EOI + testscript:2:3: error: ';' inside 'for' + EOE + + : colon-leading + : + $* <>EOE != 0 + for x: + : foo + cmd + end + EOI + testscript:2:3: error: description inside 'for' + EOE + + : colon-trailing + : + $* <>EOE != 0 + for x: + cmd : foo + end + EOI + testscript:2:3: error: description inside 'for' + EOE + + : eos + : + $* <>EOE != 0 + for x: + EOI + testscript:2:1: error: expected closing 'end' + EOE + + : scope + : + $* <>EOE != 0 + for x: + cmd + { + } + end + EOI + testscript:3:3: error: expected closing 'end' + EOE + + : setup + : + $* <>EOE != 0 + for x: + +cmd + end + EOI + testscript:2:3: error: setup command inside 'for' + EOE + + : tdown + : + $* <>EOE != 0 + for x: + -cmd + end + EOI + testscript:2:3: error: teardown command inside 'for' + EOE + } + + : var + : + $* <>EOO + for x: a b + cmd1 $x + end; + cmd2 $x + EOI + cmd1 a + cmd1 b + cmd2 b + EOO + + : leading-and-trailing-description + : + $* <>EOE != 0 + : foo + for x: a b + cmd + end : bar + EOI + testscript:4:1: error: both leading and trailing descriptions + EOE +} diff --git a/libbuild2/test/script/parser.cxx b/libbuild2/test/script/parser.cxx index a99f80a..0ba5a79 100644 --- a/libbuild2/test/script/parser.cxx +++ b/libbuild2/test/script/parser.cxx @@ -311,9 +311,10 @@ namespace build2 // enter: next token is peeked at (type in tt) // leave: newline - assert (!fct || - *fct == line_type::cmd_if || - *fct == line_type::cmd_while); + assert (!fct || + *fct == line_type::cmd_if || + *fct == line_type::cmd_while || + *fct == line_type::cmd_for); // Note: token is only peeked at. // @@ -373,6 +374,7 @@ namespace build2 if (n == "if") lt = line_type::cmd_if; else if (n == "if!") lt = line_type::cmd_ifn; else if (n == "while") lt = line_type::cmd_while; + else if (n == "for") lt = line_type::cmd_for; } break; @@ -414,16 +416,70 @@ namespace build2 mode (lexer_mode::variable_line); parse_variable_line (t, tt); + // Note that the semicolon token is only required during + // pre-parsing to decide which line list the current line should + // go to and provides no additional semantics during the + // execution. Moreover, build2::script::parser::exec_lines() + // doesn't expect this token to be present. Thus, we just drop + // this token from the saved tokens. + // semi = (tt == type::semi); - if (tt == type::semi) + if (semi) + { + replay_pop (); next (t, tt); + } if (tt != type::newline) fail (t) << "expected newline instead of " << t; break; } + case line_type::cmd_for: + { + // First take care of the variable name. There is no reason not to + // support variable attributes. + // + mode (lexer_mode::normal); + + next_with_attributes (t, tt); + attributes_push (t, tt); + + if (tt != type::word || t.qtype != quote_type::unquoted) + fail (t) << "expected variable name instead of " << t; + + string& n (t.value); + + if (special_variable (n)) + fail (t) << "attempt to set '" << n << "' variable directly"; + + ln.var = &script_->var_pool.insert (move (n)); + + next (t, tt); + + if (tt != type::colon) + { + // @@ TMP We will need to fallback to parsing the 'for x <...' + // form instead. + // + fail (t) << "expected ':' instead of " << t + << " after variable name"; + } + + expire_mode (); // Expire the normal lexer mode. + + // Parse the value similar to the var line type (see above), + // except for the fact that we don't expect a trailing semicolon. + // + mode (lexer_mode::variable_line); + parse_variable_line (t, tt); + + if (tt != type::newline) + fail (t) << "expected newline instead of " << t << " after for"; + + break; + } case line_type::cmd_elif: case line_type::cmd_elifn: case line_type::cmd_else: @@ -480,7 +536,8 @@ namespace build2 case type::semi: { semi = true; - next (t, tt); // Get newline. + replay_pop (); // See above for the reasoning. + next (t, tt); // Get newline. break; } } @@ -506,18 +563,29 @@ namespace build2 ln.tokens = replay_data (); ls->push_back (move (ln)); - if (lt == line_type::cmd_if || lt == line_type::cmd_ifn) + switch (lt) { - semi = pre_parse_if_else (t, tt, d, *ls); + case line_type::cmd_if: + case line_type::cmd_ifn: + { + semi = pre_parse_if_else (t, tt, d, *ls); - // If this turned out to be scope-if, then ls is empty, semi is - // false, and none of the below logic applies. - // - if (ls->empty ()) - return semi; + // If this turned out to be scope-if, then ls is empty, semi is + // false, and none of the below logic applies. + // + if (ls->empty ()) + return semi; + + break; + } + case line_type::cmd_while: + case line_type::cmd_for: + { + semi = pre_parse_loop (t, tt, lt, d, *ls); + break; + } + default: break; } - else if (lt == line_type::cmd_while) - semi = pre_parse_while (t, tt, d, *ls); // Unless we were told where to put it, decide where it actually goes. // @@ -540,6 +608,7 @@ namespace build2 case line_type::cmd_if: case line_type::cmd_ifn: case line_type::cmd_while: + case line_type::cmd_for: { // See if this is a variable-only flow control construct. // @@ -896,8 +965,9 @@ namespace build2 break; } case line_type::cmd_while: + case line_type::cmd_for: { - fct = line_type::cmd_while; + fct = bt; break; } default: assert(false); @@ -990,13 +1060,16 @@ namespace build2 } bool parser:: - pre_parse_while (token& t, type& tt, - optional& d, - lines& ls) + pre_parse_loop (token& t, type& tt, + line_type lt, + optional& d, + lines& ls) { // enter: (previous line) // leave: + assert (lt == line_type::cmd_while || lt == line_type::cmd_for); + tt = peek (lexer_mode::first_token); // Parse lines until we see closing 'end'. @@ -1005,8 +1078,7 @@ namespace build2 { size_t i (ls.size ()); - bool semi ( - pre_parse_block_line (t, tt, line_type::cmd_while, d, ls)); + bool semi (pre_parse_block_line (t, tt, lt, d, ls)); if (ls[i].type == line_type::cmd_end) return semi; @@ -1359,11 +1431,8 @@ namespace build2 pair p ( parse_command_expr (t, tt, lexer::redirect_aliases)); - switch (tt) - { - case type::colon: parse_trailing_description (t, tt); break; - case type::semi: next (t, tt); break; // Get newline. - } + if (tt == type::colon) + parse_trailing_description (t, tt); assert (tt == type::newline); @@ -1480,30 +1549,19 @@ namespace build2 // Note that we rely on "small function object" optimization for the // exec_*() lambdas. // - auto exec_set = [this] (const variable& var, - token& t, build2::script::token_type& tt, - const location&) + auto exec_assign = [this] (const variable& var, + value&& val, + type kind, + const location& l) { - next (t, tt); - type kind (tt); // Assignment kind. - - // We cannot reuse the value mode (see above for details). - // - mode (lexer_mode::variable_line); - value rhs (parse_variable_line (t, tt)); - - if (tt == type::semi) - next (t, tt); - - assert (tt == type::newline); - - // Assign. - // value& lhs (kind == type::assign ? scope_->assign (var) : scope_->append (var)); - apply_value_attributes (&var, lhs, move (rhs), kind); + if (kind == type::assign) + lhs = move (val); + else + append_value (&var, lhs, move (val), l); if (script_->test_command_var (var.name)) scope_->reset_special (); @@ -1550,16 +1608,17 @@ namespace build2 ct = command_type::test; exec_lines (t->tests_.begin (), t->tests_.end (), - exec_set, exec_cmd, exec_cond, + exec_assign, exec_cmd, exec_cond, nullptr /* iteration_index */, li); } else if (group* g = dynamic_cast (scope_)) { ct = command_type::setup; - bool exec_scope (exec_lines (g->setup_.begin (), g->setup_.end (), - exec_set, exec_cmd, exec_cond, - nullptr /* iteration_index */, li)); + bool exec_scope ( + exec_lines (g->setup_.begin (), g->setup_.end (), + exec_assign, exec_cmd, exec_cond, + nullptr /* iteration_index */, li)); if (exec_scope) { @@ -1729,7 +1788,7 @@ namespace build2 ct = command_type::teardown; exec_lines (g->tdown_.begin (), g->tdown_.end (), - exec_set, exec_cmd, exec_cond, + exec_assign, exec_cmd, exec_cond, nullptr /* iteration_index */, li); } else diff --git a/libbuild2/test/script/parser.hxx b/libbuild2/test/script/parser.hxx index 31dd41d..6fe46e2 100644 --- a/libbuild2/test/script/parser.hxx +++ b/libbuild2/test/script/parser.hxx @@ -86,9 +86,10 @@ namespace build2 lines&); bool - pre_parse_while (token&, token_type&, - optional&, - lines&); + pre_parse_loop (token&, token_type&, + line_type, + optional&, + lines&); void pre_parse_directive (token&, token_type&); diff --git a/tests/recipe/buildscript/testscript b/tests/recipe/buildscript/testscript index 54c3bbe..0ac5d5a 100644 --- a/tests/recipe/buildscript/testscript +++ b/tests/recipe/buildscript/testscript @@ -910,4 +910,126 @@ if $posix $* clean 2>- } + + : for + : + { + : form-1 + : + : for x: ... + : + { + : basics + : + { + echo 'bar' >=bar; + echo 'baz' >=baz; + + cat <=buildfile; + foo: bar baz + {{ + p = $path($>) + rm -f $p + + for f: $< + cat $path($f) >>$p + end + }} + EOI + + $* 2>'cat file{foo}'; + + cat <<>EOO; + bar + baz + EOO + + $* clean 2>- + } + + : depdb + : + { + : inside + : + { + echo 'bar' >=bar; + + cat <=buildfile; + foo: bar + {{ + for f: $< + depdb hash $f + end + + p = $path($>) + rm -f $p + + for f: $< + cat $path($f) >>$p + end + }} + EOI + + $* 2>>EOE != 0 + buildfile:4:5: error: 'depdb' call inside flow control construct + EOE + } + + : after-commands + : + { + echo 'bar' >=bar; + + cat <=buildfile; + foo: bar + {{ + for f: $< + echo $path($f) >- + end + + depdb hash a + }} + EOI + + $* 2>>~%EOE% != 0; + buildfile:4:5: error: disallowed command in depdb preamble + info: only variable assignments are allowed in depdb preamble + buildfile:7:3: info: depdb preamble ends here + %.{3} + EOE + + $* clean 2>- + } + + : after-vars + : + { + echo 'bar' >=bar; + + cat <=buildfile; + foo: bar + {{ + h = + for f: $< + h += $path($f) + end + + depdb hash $h + + p = $path($>) + rm -f $p + + for f: $< + cat $path($f) >>$p + end + }} + EOI + + $* 2>'cat file{foo}'; + $* clean 2>- + } + } + } + } } diff --git a/tests/test/script/runner/for.testscript b/tests/test/script/runner/for.testscript new file mode 100644 index 0000000..21042e5 --- /dev/null +++ b/tests/test/script/runner/for.testscript @@ -0,0 +1,42 @@ +# File : tests/test/script/runner/for.testscript +# license : MIT; see accompanying LICENSE file + +.include ../common.testscript + +: form-1 +: +: for x: ... +: +{ + : basics + : + $c <>EOO + for x: a b + echo "$x" >| + end + EOI + a + b + EOO + + : test-options + : + $c <>~%EOO% + for test.options: -a -b + echo $* >| + end + EOI + %.+ -a% + %.+ -b% + EOO + + : special-var + : + $c <>EOE != 0 + for *: -a -b + echo $* >| + end + EOI + testscript:1:5: error: attempt to set '*' variable directly + EOE +} -- cgit v1.1