From 18ce15f3aee71debe3f35356c6a739943815da8a Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 12 Oct 2016 14:53:32 +0200 Subject: Initial work on testscript lexer/parser --- build2/buildfile | 147 +++++++++++----------- build2/test/script/lexer | 74 +++++++++++ build2/test/script/lexer.cxx | 249 +++++++++++++++++++++++++++++++++++++ build2/test/script/parser | 67 ++++++++++ build2/test/script/parser.cxx | 283 ++++++++++++++++++++++++++++++++++++++++++ build2/test/script/script | 66 ++++++++++ build2/test/script/script.cxx | 83 +++++++++++++ build2/test/script/token | 45 +++++++ 8 files changed, 942 insertions(+), 72 deletions(-) create mode 100644 build2/test/script/lexer create mode 100644 build2/test/script/lexer.cxx create mode 100644 build2/test/script/parser create mode 100644 build2/test/script/parser.cxx create mode 100644 build2/test/script/script create mode 100644 build2/test/script/script.cxx create mode 100644 build2/test/script/token (limited to 'build2') diff --git a/build2/buildfile b/build2/buildfile index 9c15468..6e71856 100644 --- a/build2/buildfile +++ b/build2/buildfile @@ -4,78 +4,81 @@ import libs = libbutl%lib{butl} -exe{b}: \ - {hxx ixx cxx}{ algorithm } \ - { cxx}{ b } \ - {hxx ixx cxx}{ b-options } \ - {hxx cxx}{ context } \ - {hxx cxx}{ depdb } \ - {hxx cxx}{ diagnostics } \ - {hxx cxx}{ dump } \ - {hxx ixx cxx}{ file } \ - {hxx txx cxx}{ filesystem } \ - {hxx cxx}{ lexer } \ - {hxx cxx}{ module } \ - {hxx ixx cxx}{ name } \ - {hxx cxx}{ operation } \ - {hxx cxx}{ parser } \ - {hxx cxx}{ prerequisite } \ - {hxx cxx}{ rule } \ - {hxx }{ rule-map } \ - {hxx cxx}{ scope } \ - {hxx cxx}{ search } \ - {hxx cxx}{ spec } \ - {hxx ixx txx cxx}{ target } \ - {hxx }{ target-key } \ - {hxx }{ target-type } \ - {hxx cxx}{ token } \ - {hxx }{ types } \ - {hxx cxx}{ types-parsers } \ - {hxx ixx txx cxx}{ utility } \ - {hxx ixx txx cxx}{ variable } \ - {hxx }{ version } \ - bin/{hxx cxx}{ guess } \ - bin/{hxx cxx}{ init } \ - bin/{hxx cxx}{ rule } \ - bin/{hxx cxx}{ target } \ - c/{hxx cxx}{ init } \ - c/{hxx }{ target } \ - cc/{hxx cxx}{ common } \ - cc/{hxx cxx}{ compile } \ - cc/{ cxx}{ gcc } \ - cc/{hxx cxx}{ guess } \ - cc/{hxx cxx}{ init } \ - cc/{hxx cxx}{ install } \ - cc/{hxx cxx}{ link } \ - cc/{hxx cxx}{ module } \ - cc/{ cxx}{ msvc } \ - cc/{ cxx}{ pkgconfig } \ - cc/{hxx cxx}{ target } \ - cc/{hxx }{ types } \ - cc/{hxx ixx cxx}{ utility } \ - cc/{ cxx}{ windows-manifest } \ - cc/{ cxx}{ windows-rpath } \ - cli/{hxx cxx}{ init } \ - cli/{hxx cxx}{ rule } \ - cli/{hxx cxx}{ target } \ - config/{hxx cxx}{ init } \ - config/{hxx }{ module } \ - config/{hxx cxx}{ operation } \ - config/{hxx txx cxx}{ utility } \ - cxx/{hxx cxx}{ init } \ - cxx/{hxx cxx}{ target } \ - dist/{hxx cxx}{ init } \ - dist/{hxx cxx}{ operation } \ - dist/{hxx cxx}{ rule } \ -pkgconfig/{hxx cxx}{ init } \ - install/{hxx cxx}{ init } \ - install/{hxx cxx}{ operation } \ - install/{hxx cxx}{ rule } \ - install/{hxx }{ utility } \ - test/{hxx cxx}{ init } \ - test/{hxx cxx}{ operation } \ - test/{hxx cxx}{ rule } \ - $libs +exe{b}: \ + {hxx ixx cxx}{ algorithm } \ + { cxx}{ b } \ + {hxx ixx cxx}{ b-options } \ + {hxx cxx}{ context } \ + {hxx cxx}{ depdb } \ + {hxx cxx}{ diagnostics } \ + {hxx cxx}{ dump } \ + {hxx ixx cxx}{ file } \ + {hxx txx cxx}{ filesystem } \ + {hxx cxx}{ lexer } \ + {hxx cxx}{ module } \ + {hxx ixx cxx}{ name } \ + {hxx cxx}{ operation } \ + {hxx cxx}{ parser } \ + {hxx cxx}{ prerequisite } \ + {hxx cxx}{ rule } \ + {hxx }{ rule-map } \ + {hxx cxx}{ scope } \ + {hxx cxx}{ search } \ + {hxx cxx}{ spec } \ + {hxx ixx txx cxx}{ target } \ + {hxx }{ target-key } \ + {hxx }{ target-type } \ + {hxx cxx}{ token } \ + {hxx }{ types } \ + {hxx cxx}{ types-parsers } \ + {hxx ixx txx cxx}{ utility } \ + {hxx ixx txx cxx}{ variable } \ + {hxx }{ version } \ + bin/{hxx cxx}{ guess } \ + bin/{hxx cxx}{ init } \ + bin/{hxx cxx}{ rule } \ + bin/{hxx cxx}{ target } \ + c/{hxx cxx}{ init } \ + c/{hxx }{ target } \ + cc/{hxx cxx}{ common } \ + cc/{hxx cxx}{ compile } \ + cc/{ cxx}{ gcc } \ + cc/{hxx cxx}{ guess } \ + cc/{hxx cxx}{ init } \ + cc/{hxx cxx}{ install } \ + cc/{hxx cxx}{ link } \ + cc/{hxx cxx}{ module } \ + cc/{ cxx}{ msvc } \ + cc/{ cxx}{ pkgconfig } \ + cc/{hxx cxx}{ target } \ + cc/{hxx }{ types } \ + cc/{hxx ixx cxx}{ utility } \ + cc/{ cxx}{ windows-manifest } \ + cc/{ cxx}{ windows-rpath } \ + cli/{hxx cxx}{ init } \ + cli/{hxx cxx}{ rule } \ + cli/{hxx cxx}{ target } \ + config/{hxx cxx}{ init } \ + config/{hxx }{ module } \ + config/{hxx cxx}{ operation } \ + config/{hxx txx cxx}{ utility } \ + cxx/{hxx cxx}{ init } \ + cxx/{hxx cxx}{ target } \ + dist/{hxx cxx}{ init } \ + dist/{hxx cxx}{ operation } \ + dist/{hxx cxx}{ rule } \ + pkgconfig/{hxx cxx}{ init } \ + install/{hxx cxx}{ init } \ + install/{hxx cxx}{ operation } \ + install/{hxx cxx}{ rule } \ + install/{hxx }{ utility } \ + test/{hxx cxx}{ init } \ + test/{hxx cxx}{ operation } \ + test/{hxx cxx}{ rule } \ +test/script/{hxx cxx}{ lexer } \ +test/script/{hxx cxx}{ parser } \ +test/script/{hxx cxx}{ script } \ + $libs # Pass our compiler target to be used as build2 host. # diff --git a/build2/test/script/lexer b/build2/test/script/lexer new file mode 100644 index 0000000..de4c84e --- /dev/null +++ b/build2/test/script/lexer @@ -0,0 +1,74 @@ +// file : build2/test/script/lexer -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_TEST_SCRIPT_LEXER +#define BUILD2_TEST_SCRIPT_LEXER + +#include +#include + +#include + +#include + +namespace build2 +{ + namespace test + { + namespace script + { + struct lexer_mode: build2::lexer_mode + { + using base_type = build2::lexer_mode; + + enum + { + script_line = base_type::value_next, + variable_line, + test_line, + command_line + }; + + using base_type::base_type; + }; + + class lexer: public build2::lexer + { + public: + using base_lexer = build2::lexer; + using base_mode = build2::lexer_mode; + + lexer (istream& is, const path& name, lexer_mode m) + : base_lexer (is, name, nullptr, nullptr, false) {mode (m);} + + virtual void + mode (base_mode, char = '\0') override; + + // Return true if we entered the quoted (double or single) mode since + // last reset. + // + bool + quoted () const {return quoted_;} + + void + reset_quoted (bool q) {quoted_ = q;} + + protected: + virtual token + next_impl () override; + + token + next_line (); + + token + name_line (bool separated); + + protected: + bool quoted_ = false; + }; + } + } +} + +#endif // BUILD2_TEST_SCRIPT_LEXER diff --git a/build2/test/script/lexer.cxx b/build2/test/script/lexer.cxx new file mode 100644 index 0000000..84be7c1 --- /dev/null +++ b/build2/test/script/lexer.cxx @@ -0,0 +1,249 @@ +// file : build2/test/script/lexer.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +using namespace std; + +namespace build2 +{ + namespace test + { + namespace script + { + using type = token_type; + + void lexer:: + mode (base_mode m, char) + { + const char* s1 (nullptr); + const char* s2 (nullptr); + bool s (true); + + switch (m) + { + case lexer_mode::script_line: + { + s1 = "=+!|&<> $()#\t\n"; + s2 = " == "; + break; + } + case lexer_mode::variable_line: + { + // Like value except we don't recognize {. + // + s1 = " $()[]#\t\n"; + s2 = " "; + break; + } + case lexer_mode::test_line: + { + // As script_line but without variable assignments. + // + s1 = "=!|&<> $()#\t\n"; + s2 = "== "; + break; + } + case lexer_mode::command_line: + { + // Note that whitespaces are not word separators in this mode. + // + s1 = "|&<>"; + s2 = " "; + s = false; + break; + } + case lexer_mode::single_quoted: + case lexer_mode::double_quoted: + quoted_ = true; + // Fall through. + default: + { + // Disable pair separator. + // + base_lexer::mode (m, '\0'); + } + } + + state_.push (state {m, '\0', s, s1, s2}); + } + + token lexer:: + next_impl () + { + switch (state_.top ().mode) + { + case lexer_mode::script_line: + case lexer_mode::variable_line: + case lexer_mode::test_line: + case lexer_mode::command_line: return next_line (); + default: return base_lexer::next_impl (); + } + } + + token lexer:: + next_line () + { + bool sep (skip_spaces ()); + + xchar c (get ()); + uint64_t ln (c.line), cn (c.column); + + if (eos (c)) + return token (type::eos, sep, ln, cn); + + lexer_mode m (state_.top ().mode); + + // NOTE: remember to update mode() if adding new special characters. + + if (m != lexer_mode::command_line) + { + switch (c) + { + case '\n': + { + return token (type::newline, sep, ln, cn); + } + + // Variable expansion, function call, and evaluation context. + // + case '$': return token (type::dollar, sep, ln, cn); + case '(': return token (type::lparen, sep, ln, cn); + case ')': return token (type::rparen, sep, ln, cn); + } + } + + if (m == lexer_mode::variable_line) + { + switch (c) + { + // Attributes. + // + case '[': return token (type::lsbrace, sep, ln, cn); + case ']': return token (type::rsbrace, sep, ln, cn); + } + } + + // Command line operator/separators. + // + if (m == lexer_mode::script_line || m == lexer_mode::test_line) + { + switch (c) + { + // Comparison (==, !=). + // + case '=': + case '!': + { + if (peek () == '=') + { + get (); + return token ( + c == '=' ? type::equal : type::not_equal, sep, ln, cn); + } + } + } + } + + // Command operators/separators. + // + if (m == lexer_mode::script_line || + m == lexer_mode::test_line || + m == lexer_mode::command_line) + { + switch (c) + { + // |, || + // + case '|': + { + if (peek () == '|') + { + get (); + return token (type::log_or, sep, ln, cn); + } + else + return token (type::pipe, sep, ln, cn); + } + // &, && + // + case '&': + { + if (peek () == '&') + { + get (); + return token (type::log_and, sep, ln, cn); + } + else + return token (type::clean, sep, ln, cn); + } + // < + // + case '<': + { + xchar p (peek ()); + + if (p == '!' || p == '<') + { + get (); + return token ( + p == '!' ? type::in_null : type::in_document, sep, ln, cn); + } + else + return token (type::in_string, sep, ln, cn); + + } + // > + // + case '>': + { + xchar p (peek ()); + + if (p == '!' || p == '>') + { + get (); + return token ( + p == '!' ? type::out_null : type::out_document, sep, ln, cn); + } + else + return token (type::out_string, sep, ln, cn); + } + } + } + + // Variable assignment (=, +=, =+). + // + if (m == lexer_mode::script_line) + { + switch (c) + { + case '=': + { + if (peek () == '+') + { + get (); + return token (type::prepend, sep, ln, cn); + } + else + return token (type::assign, sep, ln, cn); + } + case '+': + { + if (peek () == '=') + { + get (); + return token (type::append, sep, ln, cn); + } + } + } + } + + // Otherwise it is a name. + // + unget (c); + return name (sep); + } + } + } +} diff --git a/build2/test/script/parser b/build2/test/script/parser new file mode 100644 index 0000000..720a077 --- /dev/null +++ b/build2/test/script/parser @@ -0,0 +1,67 @@ +// file : build2/test/script/parser -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_TEST_SCRIPT_PARSER +#define BUILD2_TEST_SCRIPT_PARSER + +#include +#include + +#include +#include + +#include +#include + +namespace build2 +{ + namespace test + { + namespace script + { + class lexer; + + class parser: protected build2::parser + { + public: + using script_type = test::script::script; + + // Issue diagnostics and throw failed in case of an error. + // + script_type + parse (istream&, const path& name, target& test, target& script); + + // Recursive descent parser. + // + // Each parse function receives the token/type from which it should + // start consuming. On return the token/type should contain the first + // token that has not been consumed. + // + protected: + void + script (token&, token_type&); + + void + script_line (token&, token_type&); + + void + variable_line (token&, token_type&, string); + + void + test_line (token&, token_type&, names_type, location); + + void + command_exit (token&, token_type&); + + protected: + using base_parser = build2::parser; + + lexer* lexer_; + script_type* script_; + }; + } + } +} + +#endif // BUILD2_TEST_SCRIPT_PARSER diff --git a/build2/test/script/parser.cxx b/build2/test/script/parser.cxx new file mode 100644 index 0000000..aba9f9a --- /dev/null +++ b/build2/test/script/parser.cxx @@ -0,0 +1,283 @@ +// file : build2/test/script/parser.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include + +using namespace std; + +namespace build2 +{ + namespace test + { + namespace script + { + using type = token_type; + + script parser:: + parse (istream& is, const path& p, target& test_t, target& script_t) + { + path_ = &p; + + lexer l (is, *path_, lexer_mode::script_line); + lexer_ = &l; + base_parser::lexer_ = &l; + + script_type r (test_t, script_t); + script_ = &r; + + token t (type::eos, false, 0, 0); + type tt; + next (t, tt); + + script (t, tt); + + if (tt != type::eos) + fail (t) << "unexpected " << t; + + return r; + } + + void parser:: + script (token& t, token_type& tt) + { + while (tt != type::eos) + { + script_line (t, tt); + } + } + + void parser:: + script_line (token& t, token_type& tt) + { + // Parse first chunk. Keep track of whether anything in it was quoted. + // + names_type ns; + location nl (get_location (t)); + lexer_->reset_quoted (t.quoted); + names (t, tt, ns, true); + + // See if this is a variable assignment or a test command. + // + if (tt == type::assign || + tt == type::prepend || + tt == type::append) + { + // We need to strike a balance between recognizing command lines + // that contain the assignment operator and variable assignments. + // + // If we choose to treat these tokens literally (for example, if we + // have several names on the LHS), then we have the reversibility + // problem: we need to restore original whitespaces before and after + // the assignment operator (e.g., foo=bar vs foo = bar). + // + // To keep things simple we will start with the following rule: if + // the token after the first chunk of input is assignment, then it + // must be a variable assignment. After all, command lines like this + // are not expected to be common: + // + // $* =x + // + // It will also be easy to get the desired behavior with quoting: + // + // $* "=x" + // + // The only issue here is if $* above expands to a single, simple + // name (e.g., an executable name) in which case it will be treated + // as a variable name. One way to resolve it would be to detect + // "funny" variable names and require that they be quoted (this + // won't help with built-in commands; maybe we could warn if it's + // the same as built-in). Note that currently we have no way of + // knowing it's quoted. + // + // Or perhaps we should just let people learn that first assignment + // needs to be quoted? + // + if (ns.size () != 1 || !ns[0].simple () || ns[0].empty ()) + fail (nl) << "variable name expected instead of '" << ns << "'"; + + variable_line (t, tt, move (ns[0].value)); + } + else + test_line (t, tt, move (ns), move (nl)); + } + + void parser:: + variable_line (token& t, token_type& tt, string name) + { + type kind (tt); // Assignment kind. + const variable_type& var (script_->var_pool.insert (move (name))); + + // We cannot reuse the value mode since it will recognize { which + // we want to treat as a literal. + // + value rhs (variable_value (t, tt, lexer_mode::variable_line)); + + value& lhs (kind == type::assign + ? script_->assign (var) + : script_->append (var)); + + // @@ Need to adjust to make strings the default type. + // + value_attributes (&var, lhs, move (rhs), kind); + } + + void parser:: + test_line (token& t, token_type& tt, names_type ns, location nl) + { + // Stop recognizing variable assignments. + // + mode (lexer_mode::test_line); + + // Keep parsing chunks of the command line until we see the newline or + // the exit status comparison. + // + strings cmd; + + do + { + // Process words that we already have. + // + bool q (lexer_->quoted ()); + + for (name& n: ns) + { + string s; + + try + { + s = value_traits::convert (move (n), nullptr); + } + catch (const invalid_argument&) + { + fail (nl) << "invalid string value '" << n << "'"; + } + + // If it is a quoted chunk, then we add the word as is. Otherwise + // we re-lex it. But if the word doesn't contain any interesting + // characters (operators plus quotes/escapes), then no need to + // re-lex. + // + if (q || s.find_first_of ("|&<>\'\"\\") == string::npos) + cmd.push_back (move (s)); + else + { + // Come up with a "path" that contains both the original + // location as well as the expanded string. The resulting + // diagnostics will look like this: + // + // testscript:10:1 ('abc): unterminated single quote + // + path name; + { + string n (nl.file->string ()); + n += ':'; + n += to_string (nl.line); + n += ':'; + n += to_string (nl.column); + n += ": ("; + n += s; + n += ')'; + name = path (move (n)); + } + + istringstream is (s); + lexer lex (is, name, lexer_mode::command_line); + + string w; + bool f (true); // In case the whole thing is empty. + for (token t (lex.next ()); t.type != type::eos; t = lex.next ()) + { + // Note that this is not "our" token so we cannot do fail(t). + // Rather we should do fail(l). + // + location l (build2::get_location (t, lex.name ())); + + // Re-lexing double-quotes will recognize $, ( inside as + // tokens so we have to reverse them back. Since we don't + // treat spaces as separators we can be sure we will get it + // right. + // + switch (t.type) + { + case type::dollar: w += '$'; continue; + case type::lparen: w += '('; continue; + } + + // Retire the current word. We need to distinguish between + // empty and non-existent (e.g., > vs >""). + // + if (!w.empty () || f) + { + cmd.push_back (move (w)); + f = false; + } + + switch (t.type) + { + case type::name: w = move (t.value); f = true; break; + + // @@ TODO + // + case type::pipe: + case type::clean: + case type::log_and: + case type::log_or: + + case type::in_null: + case type::in_string: + case type::in_document: + + case type::out_null: + case type::out_string: + case type::out_document: + break; + } + } + + // Don't forget the last word. + // + if (!w.empty () || f) + cmd.push_back (move (w)); + } + } + + if (tt == type::newline || + tt == type::equal || + tt == type::not_equal) + break; + + // Parse the next chunk. + // + ns.clear (); + lexer_->reset_quoted (t.quoted); + names (t, tt, ns, true); + + } while (true); + + //@@ switch mode (we no longer want to recognize command operators)? + + if (tt == type::equal || tt == type::not_equal) + { + command_exit (t, tt); + } + + // here-document + } + + void parser:: + command_exit (token& t, token_type& tt) + { + // The next chunk should be the exit status. + // + next (t, tt); + names_type ns (names (t, tt, true)); + + //@@ TODO: validate to be single, simple, non-empty name that + // converts to integer (is exit status always non-negative). + } + } + } +} diff --git a/build2/test/script/script b/build2/test/script/script new file mode 100644 index 0000000..de81fa6 --- /dev/null +++ b/build2/test/script/script @@ -0,0 +1,66 @@ +// file : build2/test/script/script -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_TEST_SCRIPT_SCRIPT +#define BUILD2_TEST_SCRIPT_SCRIPT + +#include +#include + +#include + +namespace build2 +{ + class target; + + namespace test + { + namespace script + { + class script + { + public: + script (target& tt, target& st) + : test_target (tt), script_target (st) {} + + public: + target& test_target; // Target we are testing. + target& script_target; // Target of the testscript file. + + public: + // Note that if we pass the variable name as a string, then it will + // be looked up in the wrong pool. + // + variable_pool var_pool; + variable_map vars; + + // Lookup the variable starting from this scope, continuing with outer + // scopes, then the target being tested, then the testscript target, + // and then outer buildfile scopes (including testscript-type/pattern + // specific). + // + lookup + find (const variable&) const; + + // Return a value suitable for assignment. If the variable does not + // exist in this scope's map, then a new one with the NULL value is + // added and returned. Otherwise the existing value is returned. + // + value& + assign (const variable& var) {return vars.assign (var);} + + // Return a value suitable for append/prepend. If the variable does + // not exist in this scope's map, then outer scopes are searched for + // the same variable. If found then a new variable with the found + // value is added to this scope and returned. Otherwise this function + // proceeds as assign() above. + // + value& + append (const variable&); + }; + } + } +} + +#endif // BUILD2_TEST_SCRIPT_SCRIPT diff --git a/build2/test/script/script.cxx b/build2/test/script/script.cxx new file mode 100644 index 0000000..706c87d --- /dev/null +++ b/build2/test/script/script.cxx @@ -0,0 +1,83 @@ +// file : build2/test/script/script.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include + +using namespace std; + +namespace build2 +{ + namespace test + { + namespace script + { + lookup script:: + find (const variable& var) const + { + if (const value* v = vars.find (var)) + return lookup (v, &vars); + + // Switch to the corresponding buildfile variable. Note that we don't + // want to insert a new variable into the pool (we might be running + // concurrently). Plus, if there is no such variable, then we cannot + // possibly find any value. + // + const variable* pvar (build2::var_pool.find (var.name)); + + if (pvar == nullptr) + return lookup (); + + { + const variable& var (*pvar); + + // First check the target we are testing. + // + { + // Note that we skip applying the override if we did not find any + // value. In this case, presumably the override also affects the + // script target and we will pick it up there. A bit fuzzy. + // + auto p (test_target.find_original (var, true)); + + if (p.first) + { + if (var.override != nullptr) + p = test_target.base_scope ().find_override ( + var, move (p), true); + + return p.first; + } + } + + // Then the script target followed by the scopes it is in. Note that + // while unlikely it is possible the test and script targets will be + // in different scopes which brings the question of which scopes we + // should search. + // + return script_target[var]; + } + } + + value& script:: + append (const variable& var) + { + lookup l (find (var)); + + if (l.defined () && l.belongs (*this)) // Existing var in this scope. + return const_cast (*l); + + value& r (assign (var)); // NULL. + + //@@ I guess this is where we convert untyped value to strings? + // + if (l.defined ()) + r = *l; // Copy value (and type) from the outer scope. + + return r; + } + } + } +} diff --git a/build2/test/script/token b/build2/test/script/token new file mode 100644 index 0000000..51bf282 --- /dev/null +++ b/build2/test/script/token @@ -0,0 +1,45 @@ +// file : build2/test/script/token -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_TEST_SCRIPT_TOKEN +#define BUILD2_TEST_SCRIPT_TOKEN + +#include +#include + +#include + +namespace build2 +{ + namespace test + { + namespace script + { + struct token_type: build2::token_type + { + using base_type = build2::token_type; + + enum + { + pipe = base_type::value_next, // | + clean, // & + log_and, // && + log_or, // || + + in_null, //