From ece4003beebd23082a5fd7a324de40c5572161d1 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Thu, 16 Mar 2017 10:35:32 +0200 Subject: Add support for passing parameters to (meta-) operations --- build2/b.cxx | 105 ++++++++++++++++++++------------- build2/config/operation.cxx | 49 ++++++++-------- build2/dist/operation.cxx | 37 +++++++----- build2/install/operation.cxx | 5 +- build2/lexer | 33 +++-------- build2/lexer.cxx | 53 ++++++++++++----- build2/operation | 51 +++++++++++----- build2/operation.cxx | 10 ++-- build2/parser | 2 +- build2/parser.cxx | 127 ++++++++++++++++++++++++++-------------- build2/spec | 4 ++ build2/spec.cxx | 28 ++++++++- build2/test/operation.cxx | 5 +- build2/test/script/lexer | 6 +- build2/test/script/lexer.cxx | 4 +- unit-tests/lexer/buildfile | 2 +- unit-tests/lexer/buildspec.test | 16 +++++ unit-tests/lexer/driver.cxx | 4 ++ 18 files changed, 349 insertions(+), 192 deletions(-) create mode 100644 unit-tests/lexer/buildspec.test diff --git a/build2/b.cxx b/build2/b.cxx index b13300e..d247958 100644 --- a/build2/b.cxx +++ b/build2/b.cxx @@ -359,7 +359,10 @@ main (int argc, char* argv[]) for (auto mit (bspec.begin ()); mit != bspec.end (); ) { vector_view opspecs; + const string& mname (lifted == nullptr ? mit->name : lifted->name); + const values& mparams (lifted == nullptr ? mit->params : lifted->params); + current_mname = &mname; if (lifted == nullptr) @@ -409,6 +412,8 @@ main (int argc, char* argv[]) // A lifted meta-operation will always have default operation. // const string& oname (lifted == nullptr ? os.name : string ()); + const values& oparams (lifted == nullptr ? os.params : values ()); + current_oname = &oname; if (lifted != nullptr) @@ -735,25 +740,6 @@ main (int argc, char* argv[]) skip = lifted - mit->data () + 1; break; // Out of targetspec loop. } - else - { - o = operation_table.find (oname); - - if (o == 0) - { - diag_record dr; - dr << fail (l) << "unknown operation " << oname; - - // If we guessed src_root and didn't load anything during - // bootstrap, then this is probably a meta-operation that - // would have been added by the module if src_root was - // correct. - // - if (guessing && !bootstrapped) - dr << info << "consider explicitly specifying src_base " - << "for " << tn; - } - } } if (!mname.empty ()) @@ -773,6 +759,26 @@ main (int argc, char* argv[]) } } + if (!oname.empty ()) + { + o = operation_table.find (oname); + + if (o == 0) + { + diag_record dr; + dr << fail (l) << "unknown operation " << oname; + + // If we guessed src_root and didn't load anything during + // bootstrap, then this is probably a meta-operation that + // would have been added by the module if src_root was + // correct. + // + if (guessing && !bootstrapped) + dr << info << "consider explicitly specifying src_base " + << "for " << tn; + } + } + // The default meta-operation is perform. The default // operation is assigned by the meta-operation below. // @@ -795,7 +801,10 @@ main (int argc, char* argv[]) << ", id " << static_cast (mid);}); if (mif->meta_operation_pre != nullptr) - mif->meta_operation_pre (); + mif->meta_operation_pre (mparams, l); + else if (!mparams.empty ()) + fail (l) << "unexpected parameters for meta-operation " + << mif->name; set_current_mif (*mif); dirty = true; @@ -844,7 +853,7 @@ main (int argc, char* argv[]) // Allow the meta-operation to translate the operation. // if (mif->operation_pre != nullptr) - oid = mif->operation_pre (o); + oid = mif->operation_pre (mparams, o); else // Otherwise translate default to update. oid = (o == default_id ? update_id : o); @@ -857,16 +866,25 @@ main (int argc, char* argv[]) // Handle pre/post operations. // - if (oif->pre != nullptr && (pre_oid = oif->pre (mid)) != 0) + if (oif->pre != nullptr) { - assert (pre_oid != default_id); - pre_oif = lookup (pre_oid); + if ((pre_oid = oif->pre (oparams, mid, l)) != 0) + { + assert (pre_oid != default_id); + pre_oif = lookup (pre_oid); + } } + else if (!oparams.empty ()) + fail (l) << "unexpected parameters for operation " + << oif->name; - if (oif->post != nullptr && (post_oid = oif->post (mid)) != 0) + if (oif->post != nullptr) { - assert (post_oid != default_id); - post_oif = lookup (post_oid); + if ((post_oid = oif->post (oparams, mid)) != 0) + { + assert (post_oid != default_id); + post_oif = lookup (post_oid); + } } } // @@ -1021,7 +1039,7 @@ main (int argc, char* argv[]) // Load the buildfile. // - mif->load (rs, ts.buildfile, ts.out_base, ts.src_base, l); + mif->load (mparams, rs, ts.buildfile, ts.out_base, ts.src_base, l); // Next search and match the targets. We don't want to start // building before we know how to for all the targets in this @@ -1055,7 +1073,8 @@ main (int argc, char* argv[]) ? out_src (d, rs) : dir_path ()); - mif->search (rs, bs, + mif->search (mparams, + rs, bs, target_key {ti, &d, &out, &tn.value, e}, l, tgs); @@ -1070,17 +1089,19 @@ main (int argc, char* argv[]) << ", id " << static_cast (pre_oid);}); if (mif->operation_pre != nullptr) - mif->operation_pre (pre_oid); // Cannot be translated. + mif->operation_pre (mparams, pre_oid); // Cannot be translated. set_current_oif (*pre_oif, oif); action a (mid, pre_oid, oid); - mif->match (a, tgs); - mif->execute (a, tgs, true); // Run quiet. + // Run quiet. + // + if (mif->match != nullptr) mif->match (mparams, a, tgs); + if (mif->execute != nullptr) mif->execute (mparams, a, tgs, true); if (mif->operation_post != nullptr) - mif->operation_post (pre_oid); + mif->operation_post (mparams, pre_oid); l5 ([&]{trace << "end pre-operation batch " << pre_oif->name << ", id " << static_cast (pre_oid);}); @@ -1090,8 +1111,8 @@ main (int argc, char* argv[]) action a (mid, oid, 0); - if (mif->match != nullptr) mif->match (a, tgs); - if (mif->execute != nullptr) mif->execute (a, tgs, verb == 0); + if (mif->match != nullptr) mif->match (mparams, a, tgs); + if (mif->execute != nullptr) mif->execute (mparams, a, tgs, verb == 0); if (post_oid != 0) { @@ -1099,24 +1120,26 @@ main (int argc, char* argv[]) << ", id " << static_cast (post_oid);}); if (mif->operation_pre != nullptr) - mif->operation_pre (post_oid); // Cannot be translated. + mif->operation_pre (mparams, post_oid); // Cannot be translated. set_current_oif (*post_oif, oif); action a (mid, post_oid, oid); - mif->match (a, tgs); - mif->execute (a, tgs, true); // Run quiet. + // Run quiet. + // + if (mif->match != nullptr) mif->match (mparams, a, tgs); + if (mif->execute != nullptr) mif->execute (mparams, a, tgs, true); if (mif->operation_post != nullptr) - mif->operation_post (post_oid); + mif->operation_post (mparams, post_oid); l5 ([&]{trace << "end post-operation batch " << post_oif->name << ", id " << static_cast (post_oid);}); } if (mif->operation_post != nullptr) - mif->operation_post (oid); + mif->operation_post (mparams, oid); l5 ([&]{trace << "end operation batch " << oif->name << ", id " << static_cast (oid);}); @@ -1125,7 +1148,7 @@ main (int argc, char* argv[]) if (mid != 0) { if (mif->meta_operation_post != nullptr) - mif->meta_operation_post (); + mif->meta_operation_post (mparams); l5 ([&]{trace << "end meta-operation batch " << mif->name << ", id " << static_cast (mid);}); diff --git a/build2/config/operation.cxx b/build2/config/operation.cxx index a029b60..a8874fd 100644 --- a/build2/config/operation.cxx +++ b/build2/config/operation.cxx @@ -26,15 +26,6 @@ namespace build2 { // configure // - static operation_id - configure_operation_pre (operation_id o) - { - // Don't translate default to update. In our case unspecified - // means configure everything. - // - return o; - } - static void save_src_root (const dir_path& out_root, const dir_path& src_root) { @@ -346,14 +337,23 @@ namespace build2 } } + static operation_id + configure_operation_pre (const values&, operation_id o) + { + // Don't translate default to update. In our case unspecified + // means configure everything. + // + return o; + } + static void - configure_match (action, action_targets&) + configure_match (const values&, action, action_targets&) { // Don't match anything -- see execute (). } static void - configure_execute (action a, action_targets& ts, bool) + configure_execute (const values&, action a, action_targets& ts, bool) { // Match rules to configure every operation supported by each // project. Note that we are not calling operation_pre/post() @@ -413,14 +413,6 @@ namespace build2 // disfigure // - static operation_id - disfigure_operation_pre (operation_id o) - { - // Don't translate default to update. In our case unspecified - // means disfigure everything. - // - return o; - } static void load_project (scope& root) @@ -460,8 +452,18 @@ namespace build2 } } + static operation_id + disfigure_operation_pre (const values&, operation_id o) + { + // Don't translate default to update. In our case unspecified + // means disfigure everything. + // + return o; + } + static void - disfigure_load (scope& root, + disfigure_load (const values&, + scope& root, const path& bf, const dir_path&, const dir_path&, @@ -478,7 +480,8 @@ namespace build2 } static void - disfigure_search (const scope& root, + disfigure_search (const values&, + const scope& root, const scope&, const target_key&, const location&, @@ -490,7 +493,7 @@ namespace build2 } static void - disfigure_match (action, action_targets&) + disfigure_match (const values&, action, action_targets&) { } @@ -588,7 +591,7 @@ namespace build2 } static void - disfigure_execute (action a, action_targets& ts, bool quiet) + disfigure_execute (const values&, action a, action_targets& ts, bool quiet) { tracer trace ("disfigure_execute"); diff --git a/build2/dist/operation.cxx b/build2/dist/operation.cxx index 5c08c50..accf2d5 100644 --- a/build2/dist/operation.cxx +++ b/build2/dist/operation.cxx @@ -39,22 +39,22 @@ namespace build2 const string& ext); static operation_id - dist_operation_pre (operation_id o) + dist_operation_pre (const values&, operation_id o) { if (o != default_id) - fail << "explicit operation specified for dist meta-operation"; + fail << "explicit operation specified for meta-operation dist"; return o; } static void - dist_match (action, action_targets&) + dist_match (const values&, action, action_targets&) { // Don't match anything -- see execute (). } static void - dist_execute (action, action_targets& ts, bool) + dist_execute (const values&, action, action_targets& ts, bool) { tracer trace ("dist_execute"); @@ -115,6 +115,11 @@ namespace build2 // Note that we are not calling operation_pre/post() callbacks here // since the meta operation is dist and we know what we are doing. // + + values params; + const path locf (""); + const location loc (&locf); // Dummy location. + for (operations::size_type id (default_id + 1); id < rs->operations.size (); ++id) @@ -125,19 +130,21 @@ namespace build2 // if (oif->pre != nullptr) { - const operation_info* poif (rs->operations[oif->pre (dist_id)]); + const operation_info* poif ( + rs->operations[oif->pre (params, dist_id, loc)]); set_current_oif (*poif, oif); - match (action (dist_id, poif->id, oif->id), ts); + match (params, action (dist_id, poif->id, oif->id), ts); } set_current_oif (*oif); - match (action (dist_id, oif->id), ts); + match (params, action (dist_id, oif->id), ts); if (oif->post != nullptr) { - const operation_info* poif (rs->operations[oif->post (dist_id)]); + const operation_info* poif ( + rs->operations[oif->post (params, dist_id)]); set_current_oif (*poif, oif); - match (action (dist_id, poif->id, oif->id), ts); + match (params, action (dist_id, poif->id, oif->id), ts); } } } @@ -239,7 +246,7 @@ namespace build2 // { if (perform.meta_operation_pre != nullptr) - perform.meta_operation_pre (); + perform.meta_operation_pre (params, loc); // This is a hack since according to the rules we need to completely // reset the state. We could have done that (i.e., saved target names @@ -252,20 +259,20 @@ namespace build2 current_on = on + 1; if (perform.operation_pre != nullptr) - perform.operation_pre (update_id); + perform.operation_pre (params, update_id); set_current_oif (update); action a (perform_id, update_id); - perform.match (a, files); - perform.execute (a, files, true); // Run quiet. + perform.match (params, a, files); + perform.execute (params, a, files, true); // Run quiet. if (perform.operation_post != nullptr) - perform.operation_post (update_id); + perform.operation_post (params, update_id); if (perform.meta_operation_post != nullptr) - perform.meta_operation_post (); + perform.meta_operation_post (params); } dir_path td (dist_root / dir_path (dist_package)); diff --git a/build2/install/operation.cxx b/build2/install/operation.cxx index 9873ac2..44d257f 100644 --- a/build2/install/operation.cxx +++ b/build2/install/operation.cxx @@ -12,8 +12,11 @@ namespace build2 namespace install { static operation_id - install_pre (meta_operation_id mo) + install_pre (const values& params, meta_operation_id mo, const location& l) { + if (!params.empty ()) + fail (l) << "unexpected parameters for operation install"; + // Run update as a pre-operation, unless we are disfiguring. // return mo != disfigure_id ? update_id : 0; diff --git a/build2/lexer b/build2/lexer index 8875892..aac1c40 100644 --- a/build2/lexer +++ b/build2/lexer @@ -56,6 +56,7 @@ namespace build2 eval, single_quoted, double_quoted, + buildspec, value_next }; @@ -72,11 +73,8 @@ namespace build2 // this string are considered "effective escapes" with all others passed // through as is. Note that the escape string is not copied. // - lexer (istream& is, - const path& name, - const char* escapes = nullptr, - void (*processor) (token&, const lexer&) = nullptr) - : lexer (is, name, escapes, processor, true) {} + lexer (istream& is, const path& name, const char* escapes = nullptr) + : lexer (is, name, escapes, true) {} const path& name () const {return name_;} @@ -104,7 +102,10 @@ namespace build2 // Scanner. Note that it is ok to call next() again after getting eos. // - token + // If you extend the lexer and add a custom lexer mode, then you must + // override next() and handle the custom mode there. + // + virtual token next (); // Peek at the first character of the next token. Return the character @@ -135,12 +136,6 @@ namespace build2 const char* sep_second; }; - // If you extend the lexer and add a custom lexer mode, then you must - // override next_impl() and handle the custom mode there. - // - virtual token - next_impl (); - token next_eval (); @@ -168,24 +163,14 @@ namespace build2 // Lexer state. // protected: - lexer (istream& is, - const path& n, - const char* e, - void (*p) (token&, const lexer&), - bool sm) - : char_scanner (is), - fail ("error", &name_), - name_ (n), - processor_ (p), - sep_ (false) + lexer (istream& is, const path& n, const char* e, bool sm) + : char_scanner (is), fail ("error", &name_), name_ (n), sep_ (false) { if (sm) mode (lexer_mode::normal, '@', e); } const path name_; - void (*processor_) (token&, const lexer&); - std::stack state_; bool sep_; // True if we skipped spaces in peek(). diff --git a/build2/lexer.cxx b/build2/lexer.cxx index ab8d96a..5989548 100644 --- a/build2/lexer.cxx +++ b/build2/lexer.cxx @@ -12,15 +12,6 @@ namespace build2 { using type = token_type; - token lexer:: - next () - { - token t (next_impl ()); - if (processor_ != nullptr) - processor_ (t, *this); - return t; - } - pair lexer:: peek_char () { @@ -69,6 +60,20 @@ namespace build2 s2 = " = &| "; break; } + case lexer_mode::buildspec: + { + // Like the value mode with these differences: + // + // 1. Returns '(' as a separated token provided the state stack depth + // is less than or equal to 3 (initial state plus two buildspec) + // (see parse_buildspec() for details). + // + // 2. Recognizes comma. + // + s1 = " $(){}[],\t\n"; + s2 = " "; + break; + } case lexer_mode::single_quoted: case lexer_mode::double_quoted: s = false; @@ -86,7 +91,7 @@ namespace build2 } token lexer:: - next_impl () + next () { const state& st (state_.top ()); lexer_mode m (st.mode); @@ -98,7 +103,8 @@ namespace build2 case lexer_mode::normal: case lexer_mode::value: case lexer_mode::attribute: - case lexer_mode::variable: break; + case lexer_mode::variable: + case lexer_mode::buildspec: break; case lexer_mode::eval: return next_eval (); case lexer_mode::double_quoted: return next_quoted (); default: assert (false); // Unhandled custom mode. @@ -150,14 +156,21 @@ namespace build2 return make_token (type::rsbrace); } case '$': return make_token (type::dollar); - case '(': return make_token (type::lparen); case ')': return make_token (type::rparen); + case '(': + { + // Left paren is always separated in the buildspec mode. + // + if (m == lexer_mode::buildspec && state_.size () <= 3) + sep = true; + + return make_token (type::lparen); + } } - // The following characters are not treated as special in the value and - // attribute modes. + // The following characters are special in the normal and variable modes. // - if (m != lexer_mode::value && m != lexer_mode::attribute) + if (m == lexer_mode::normal || m == lexer_mode::variable) { switch (c) { @@ -186,6 +199,16 @@ namespace build2 } } + // The following characters are special in the buildspec mode. + // + if (m == lexer_mode::buildspec) + { + switch (c) + { + case ',': return make_token (type::comma); + } + } + // Otherwise it is a word. // unget (c); diff --git a/build2/operation b/build2/operation index 39bb799..ca51ee5 100644 --- a/build2/operation +++ b/build2/operation @@ -10,6 +10,8 @@ #include #include +#include + namespace build2 { class location; @@ -188,35 +190,48 @@ namespace build2 const string name_did; // E.g., 'configured'. const string name_done; // E.g., 'is configured'. + // The first argument in all the callback is the meta-operation + // parameters. + // + // If the meta-operation expects parameters, then it should have a + // non-NULL meta_operation_pre(). Failed that, any parameters will be + // diagnosed as unexpected. + + // Start of meta-operation and operation batches. + // // If operation_pre() is not NULL, then it may translate default_id // (and only default_id) to some other operation. If not translated, // then default_id is used. If, however, operation_pre() is NULL, // then default_id is translated to update_id. // - void (*meta_operation_pre) (); // Start of meta-operation batch. - operation_id (*operation_pre) (operation_id); // Start of operation batch. + void (*meta_operation_pre) (const values&, const location&); + operation_id (*operation_pre) (const values&, operation_id); // Meta-operation-specific logic to load the buildfile, search and match // the targets, and execute the action on the targets. // - void (*load) (scope& root, + void (*load) (const values&, + scope& root, const path& buildfile, const dir_path& out_base, const dir_path& src_base, const location&); - void (*search) (const scope& root, + void (*search) (const values&, + const scope& root, const scope& base, const target_key&, const location&, action_targets&); - void (*match) (action, action_targets&); + void (*match) (const values&, action, action_targets&); - void (*execute) (action, action_targets&, bool quiet); + void (*execute) (const values&, action, action_targets&, bool quiet); - void (*operation_post) (operation_id); // End of operation batch. - void (*meta_operation_post) (); // End of meta-operation batch. + // Start of operation and meta-operation batches. + // + void (*operation_post) (const values&, operation_id); + void (*meta_operation_post) (const values&); }; // Built-in meta-operations. @@ -231,7 +246,8 @@ namespace build2 // scope. // void - load (scope&, + load (const values&, + scope&, const path&, const dir_path&, const dir_path&, @@ -241,21 +257,22 @@ namespace build2 // that does just that and adds a pointer to the target to the list. // void - search (const scope&, + search (const values&, + const scope&, const scope&, const target_key&, const location&, action_targets&); void - match (action, action_targets&); + match (const values&, action, action_targets&); // Execute the action on the list of targets. This is the default // implementation that does just that while issuing appropriate // diagnostics (unless quiet). // void - execute (action, const action_targets&, bool quiet); + execute (const values&, action, const action_targets&, bool quiet); extern const meta_operation_info noop; extern const meta_operation_info perform; @@ -285,12 +302,18 @@ namespace build2 // const size_t concurrency; + // The first argument in all the callback is the operation parameters. + // + // If the meta-operation expects parameters, then it should have a + // non-NULL pre(). Failed that, any parameters will be diagnosed as + // unexpected. + // If the returned operation_id's are not 0, then they are injected // as pre/post operations for this operation. Can be NULL if unused. // The returned operation_id shall not be default_id. // - operation_id (*pre) (meta_operation_id); - operation_id (*post) (meta_operation_id); + operation_id (*pre) (const values&, meta_operation_id, const location&); + operation_id (*post) (const values&, meta_operation_id); }; // Built-in operations. diff --git a/build2/operation.cxx b/build2/operation.cxx index b821b1d..9656e2a 100644 --- a/build2/operation.cxx +++ b/build2/operation.cxx @@ -44,7 +44,8 @@ namespace build2 // perform // void - load (scope& root, + load (const values&, + scope& root, const path& bf, const dir_path& out_base, const dir_path& src_base, @@ -67,7 +68,8 @@ namespace build2 } void - search (const scope&, + search (const values&, + const scope&, const scope& bs, const target_key& tk, const location& l, @@ -89,7 +91,7 @@ namespace build2 } void - match (action a, action_targets& ts) + match (const values&, action a, action_targets& ts) { tracer trace ("match"); @@ -191,7 +193,7 @@ namespace build2 } void - execute (action a, action_targets& ts, bool quiet) + execute (const values&, action a, action_targets& ts, bool quiet) { tracer trace ("execute"); diff --git a/build2/parser b/build2/parser index 0855e89..7ca9366 100644 --- a/build2/parser +++ b/build2/parser @@ -339,7 +339,7 @@ namespace build2 // Buildspec. // buildspec - parse_buildspec_clause (token&, token_type&, token_type end); + parse_buildspec_clause (token&, token_type&, size_t); // Customization hooks. // diff --git a/build2/parser.cxx b/build2/parser.cxx index b7f1930..27e1933 100644 --- a/build2/parser.cxx +++ b/build2/parser.cxx @@ -3712,27 +3712,27 @@ namespace build2 // parse_names() to parse names, get variable expansion/function calls, // quoting, etc. We just need to disable the eval context. The way this is // done has two parts: Firstly, we parse names in chunks and detect and - // handle the opening paren. In other words, a buildspec like 'clean (./)' - // is "chunked" as 'clean', '(', etc. While this is fairly straightforward, - // there is one snag: concatenating eval contexts, as in - // 'clean(./)'. Normally, this will be treated as a single chunk and we - // don't want that. So here comes the trick (or hack, if you like): we will - // make every opening paren token "separated" (i.e., as if it was proceeded - // by a space). This will disable concatenating eval. In fact, we will even - // go a step further and only do this if we are in the original value - // mode. This will allow us to still use eval contexts in buildspec, - // provided that we quote it: '"cle(an)"'. Note also that function calls - // still work as usual: '$filter (clean test)'. To disable a function call - // and make it instead a var that is expanded into operation name(s), we can - // use quoting: '"$ops"(./)'. + // handle the opening paren ourselves. In other words, a buildspec like + // 'clean (./)' is "chunked" as 'clean', '(', etc. While this is fairly + // straightforward, there is one snag: concatenating eval contexts, as in + // 'clean(./)'. Normally, this will be treated as a single chunk and we + // don't want that. So here comes the trick (or hack, if you like): the + // buildspec lexer mode makes every opening paren token "separated" (i.e., + // as if it was preceeded by a space). This will disable concatenating + // eval. + // + // In fact, because this is only done in the buildspec mode, we can still + // use eval contexts provided that we quote them: '"cle(an)"'. Note that + // function calls also need quoting (since a separated '(' is not treated as + // function call): '"$identity(update)"'. + // + // This poses a problem, though: if it's quoted then it is a concatenated + // expansion and therefore cannot contain multiple values, for example, + // $identity(foo/ bar/). So what we do is disable this chunking/separation + // after both meta-operation and operation were specified. So if we specify + // both explicitly, then we can use eval context, function calls, etc., + // normally: perform(update($identity(foo/ bar/))). // - static void - paren_processor (token& t, const lexer& l) - { - if (t.type == type::lparen && l.mode () == lexer_mode::value) - t.separated = true; - } - buildspec parser:: parse_buildspec (istream& is, const path& name) { @@ -3741,22 +3741,29 @@ namespace build2 // We do "effective escaping" and only for ['"\$(] (basically what's // necessary inside a double-quoted literal plus the single quote). // - lexer l (is, *path_, "\'\"\\$(", &paren_processor); + lexer l (is, *path_, "\'\"\\$("); lexer_ = &l; target_ = nullptr; scope_ = root_ = scope::global_; pbase_ = &work; // Use current working directory. - // Turn on the value mode/pairs recognition with '@' as the pair separator - // (e.g., src_root/@out_root/exe{foo bar}). + // Turn on the buildspec mode/pairs recognition with '@' as the pair + // separator (e.g., src_root/@out_root/exe{foo bar}). // - mode (lexer_mode::value, '@'); + mode (lexer_mode::buildspec, '@'); token t; type tt; next (t, tt); - return parse_buildspec_clause (t, tt, type::eos); + buildspec r (tt != type::eos + ? parse_buildspec_clause (t, tt, 0) + : buildspec ()); + + if (tt != type::eos) + fail (t) << "operation or target expected instead of " << t; + + return r; } static bool @@ -3780,11 +3787,11 @@ namespace build2 } buildspec parser:: - parse_buildspec_clause (token& t, type& tt, type tt_end) + parse_buildspec_clause (token& t, type& tt, size_t depth) { buildspec bs; - while (tt != tt_end) + for (bool first (true);; first = false) { // We always start with one or more names. Eval context (lparen) only // allowed if quoted. @@ -3794,23 +3801,28 @@ namespace build2 tt != type::dollar && // Variable expansion: '$foo ...' !(tt == type::lparen && mode () == lexer_mode::double_quoted) && tt != type::pair_separator) // Empty pair LHS: '@foo ...' - fail (t) << "operation or target expected instead of " << t; + { + if (first) + fail (t) << "operation or target expected instead of " << t; + + break; + } const location l (get_location (t)); // Start of names. // This call will parse the next chunk of output and produce zero or // more names. // - names ns (parse_names (t, tt, pattern_mode::expand, true)); + names ns (parse_names (t, tt, pattern_mode::expand, depth < 2)); if (ns.empty ()) // Can happen if pattern expansion. fail (l) << "operation or target expected"; - // What these names mean depends on what's next. If it is an - // opening paren, then they are operation/meta-operation names. - // Otherwise they are targets. + // What these names mean depends on what's next. If it is an opening + // paren, then they are operation/meta-operation names. Otherwise they + // are targets. // - if (tt == type::lparen) // Peeked into by parse_names(). + if (tt == type::lparen) // Got by parse_names(). { if (ns.empty ()) fail (t) << "operation name expected before '('"; @@ -3819,15 +3831,40 @@ namespace build2 if (!opname (n)) fail (l) << "operation name expected instead of '" << n << "'"; - // Inside '(' and ')' we have another, nested, buildspec. + // Inside '(' and ')' we have another, nested, buildspec. Push another + // mode to keep track of the depth (used in the lexer implementation + // to decide when to stop separating '('). // - next (t, tt); + mode (lexer_mode::buildspec, '@'); + + next (t, tt); // Get what's after '('. const location l (get_location (t)); // Start of nested names. - buildspec nbs (parse_buildspec_clause (t, tt, type::rparen)); + buildspec nbs (parse_buildspec_clause (t, tt, depth + 1)); + + // Parse additional operation/meta-operation parameters. + // + values params; + while (tt == type::comma) + { + next (t, tt); + + // Note that for now we don't expand patterns. If it turns out we + // need this, then will probably have to be (meta-) operation- + // specific (via pre-parse or some such). + // + params.push_back (tt != type::rparen + ? parse_value (t, tt, pattern_mode::ignore) + : value (names ())); + } - // Merge the nested buildspec into ours. But first determine - // if we are an operation or meta-operation and do some sanity - // checks. + if (tt != type::rparen) + fail (t) << "')' expected instead of " << t; + + expire_mode (); + next (t, tt); // Get what's after ')'. + + // Merge the nested buildspec into ours. But first determine if we are + // an operation or meta-operation and do some sanity checks. // bool meta (false); for (const metaopspec& nms: nbs) @@ -3839,8 +3876,8 @@ namespace build2 if (!meta) { - // If we have any operations in the nested spec, then this - // mean that our names are meta-operation names. + // If we have any operations in the nested spec, then this mean + // that our names are meta-operation names. // for (const opspec& nos: nms) { @@ -3865,12 +3902,13 @@ namespace build2 { bs.push_back (nmo); bs.back ().name = move (n.value); + bs.back ().params = params; } } else { - // Since we are not a meta-operation, the nested buildspec - // should be just a bunch of targets. + // Since we are not a meta-operation, the nested buildspec should be + // just a bunch of targets. // assert (nmo.size () == 1); const opspec& nos (nmo.back ()); @@ -3882,10 +3920,9 @@ namespace build2 { bs.back ().push_back (nos); bs.back ().back ().name = move (n.value); + bs.back ().back ().params = params; } } - - next (t, tt); // Done with '('. } else if (!ns.empty ()) { diff --git a/build2/spec b/build2/spec index 92b56ec..3ec4689 100644 --- a/build2/spec +++ b/build2/spec @@ -8,6 +8,8 @@ #include #include +#include + namespace build2 { class scope; @@ -37,6 +39,7 @@ namespace build2 opspec (string n): name (move (n)) {} string name; + values params; }; struct metaopspec: vector @@ -45,6 +48,7 @@ namespace build2 metaopspec (string n): name (move (n)) {} string name; + values params; }; typedef vector buildspec; diff --git a/build2/spec.cxx b/build2/spec.cxx index c10d966..c6f386c 100644 --- a/build2/spec.cxx +++ b/build2/spec.cxx @@ -40,7 +40,6 @@ namespace build2 bool hn (!s.name.empty ()); bool ht (!s.empty ()); - //os << s.name; os << (hn ? "\"" : "") << s.name << (hn ? "\"" : ""); if (hn && ht) @@ -49,6 +48,19 @@ namespace build2 for (auto b (s.begin ()), i (b); i != s.end (); ++i) os << (i != b ? " " : "") << *i; + for (const value& v: s.params) + { + os << ", "; + + if (v) + { + names storage; + os << reverse (v, storage); + } + else + os << "[null]"; + } + if (hn && ht) os << ')'; @@ -61,7 +73,6 @@ namespace build2 bool hn (!s.name.empty ()); bool ho (!s.empty ()); - //os << s.name; os << (hn ? "\'" : "") << s.name << (hn ? "\'" : ""); if (hn && ho) @@ -70,6 +81,19 @@ namespace build2 for (auto b (s.begin ()), i (b); i != s.end (); ++i) os << (i != b ? " " : "") << *i; + for (const value& v: s.params) + { + os << ", "; + + if (v) + { + names storage; + os << reverse (v, storage); + } + else + os << "[null]"; + } + if (hn && ho) os << ')'; diff --git a/build2/test/operation.cxx b/build2/test/operation.cxx index 87e3083..1f59fc1 100644 --- a/build2/test/operation.cxx +++ b/build2/test/operation.cxx @@ -12,8 +12,11 @@ namespace build2 namespace test { static operation_id - test_pre (meta_operation_id mo) + test_pre (const values& params, meta_operation_id mo, const location& l) { + if (!params.empty ()) + fail (l) << "unexpected parameters for operation test"; + // Run update as a pre-operation, unless we are disfiguring. // return mo != disfigure_id ? update_id : 0; diff --git a/build2/test/script/lexer b/build2/test/script/lexer index 207cfef..4851e13 100644 --- a/build2/test/script/lexer +++ b/build2/test/script/lexer @@ -49,7 +49,7 @@ namespace build2 const path& name, lexer_mode m, const char* escapes = nullptr) - : base_lexer (is, name, nullptr, nullptr, false) + : base_lexer (is, name, nullptr, false) { mode (m, '\0', escapes); } @@ -67,10 +67,10 @@ namespace build2 void reset_quoted (size_t q) {quoted_ = q;} - protected: virtual token - next_impl () override; + next () override; + protected: token next_line (); diff --git a/build2/test/script/lexer.cxx b/build2/test/script/lexer.cxx index e060869..c7f9193 100644 --- a/build2/test/script/lexer.cxx +++ b/build2/test/script/lexer.cxx @@ -139,7 +139,7 @@ namespace build2 } token lexer:: - next_impl () + next () { token r; @@ -158,7 +158,7 @@ namespace build2 r = next_description (); break; default: - r = base_lexer::next_impl (); + r = base_lexer::next (); break; } diff --git a/unit-tests/lexer/buildfile b/unit-tests/lexer/buildfile index 520d17c..47c9e45 100644 --- a/unit-tests/lexer/buildfile +++ b/unit-tests/lexer/buildfile @@ -12,6 +12,6 @@ functions-target-triplet algorithm search dump filesystem scheduler \ config/{utility init operation} exe{driver}: cxx{driver} ../../build2/cxx{$src} ../../build2/liba{b} $libs \ -test{comment eval quoting} +test{*} include ../../build2/ diff --git a/unit-tests/lexer/buildspec.test b/unit-tests/lexer/buildspec.test new file mode 100644 index 0000000..81eeb00 --- /dev/null +++ b/unit-tests/lexer/buildspec.test @@ -0,0 +1,16 @@ +# file : unit-tests/lexer/buildspec.test +# copyright : Copyright (c) 2014-2017 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +test.arguments = buildspec + +: punctuation +: +$* <:'x,x(x)' >>EOO +'x' +, +'x' + ( +'x' +) +EOO diff --git a/unit-tests/lexer/driver.cxx b/unit-tests/lexer/driver.cxx index e9ccd63..41366ec 100644 --- a/unit-tests/lexer/driver.cxx +++ b/unit-tests/lexer/driver.cxx @@ -36,6 +36,7 @@ namespace build2 else if (a == "value") m = lexer_mode::value; else if (a == "attribute") m = lexer_mode::attribute; else if (a == "eval") m = lexer_mode::eval; + else if (a == "buildspec") m = lexer_mode::buildspec; else assert (false); break; } @@ -56,6 +57,9 @@ namespace build2 // for (token t (l.next ()); t.type != token_type::eos; t = l.next ()) { + if (t.separated && t.type != token_type::newline) + cout << ' '; + // Print each token on a separate line without quoting operators. // t.printer (cout, t, false); -- cgit v1.1