From 6b7075adc71104c5f6ad652b99fb753565eb67d8 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 18 Nov 2016 17:28:46 +0200 Subject: Add function machinery, implement path.normalize() Note that multi-argument functions are not yet "callable" since there is no support for value packs. --- build2/buildfile | 4 +- build2/function | 534 ++++++++++++++++++++++++++++++++ build2/function.cxx | 231 ++++++++++++++ build2/functions-path.cxx | 53 ++++ build2/parser | 5 +- build2/parser.cxx | 46 ++- build2/utility | 2 +- build2/variable | 39 ++- build2/variable.cxx | 107 +++++-- build2/variable.ixx | 9 +- build2/variable.txx | 29 ++ tests/buildfile | 2 +- tests/function/buildfile | 7 + tests/function/call/buildfile | 17 - tests/function/call/test.out | 9 - tests/function/call/test.sh | 3 - tests/function/path/buildfile | 7 + tests/function/path/testscript | 39 +++ tests/test.sh | 1 - unit-tests/buildfile | 2 +- unit-tests/function/buildfile | 14 + unit-tests/function/call.test | 131 ++++++++ unit-tests/function/driver.cxx | 110 +++++++ unit-tests/function/syntax.test | 29 ++ unit-tests/test/script/parser/buildfile | 4 +- 25 files changed, 1330 insertions(+), 104 deletions(-) create mode 100644 build2/function create mode 100644 build2/function.cxx create mode 100644 build2/functions-path.cxx create mode 100644 tests/function/buildfile delete mode 100644 tests/function/call/buildfile delete mode 100644 tests/function/call/test.out delete mode 100755 tests/function/call/test.sh create mode 100644 tests/function/path/buildfile create mode 100644 tests/function/path/testscript create mode 100644 unit-tests/function/buildfile create mode 100644 unit-tests/function/call.test create mode 100644 unit-tests/function/driver.cxx create mode 100644 unit-tests/function/syntax.test diff --git a/build2/buildfile b/build2/buildfile index cd04ce7..b87a3dc 100644 --- a/build2/buildfile +++ b/build2/buildfile @@ -13,7 +13,9 @@ exe{b}: \ {hxx cxx}{ diagnostics } \ {hxx cxx}{ dump } \ {hxx ixx cxx}{ file } \ - {hxx txx cxx}{ filesystem } \ + {hxx txx cxx}{ filesystem } \ + {hxx cxx}{ function } \ + { cxx}{ functions-path } \ {hxx cxx}{ lexer } \ {hxx cxx}{ module } \ {hxx ixx cxx}{ name } \ diff --git a/build2/function b/build2/function new file mode 100644 index 0000000..7a7b43e --- /dev/null +++ b/build2/function @@ -0,0 +1,534 @@ +// file : build2/function -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BUILD2_FUNCTION +#define BUILD2_FUNCTION + +#include // index_sequence +#include // aligned_storage +#include + +#include +#include + +#include +#include + +namespace build2 +{ + // Functions can be overloaded based on types of their arguments but + // arguments can be untyped and a function can elect to accept an argument + // of any type. + // + // Functions can be qualified (e.g, string.length(), path.directory()) and + // unqualified (e.g., length(), directory()). Only functions overloaded on + // static types can be unqualified plus they should also define a qualified + // alias. + // + // Low-level function implementation would be called with a list of values + // as arguments. There is also higher-level, more convenient support for + // defining functions as pointers to functions (including capture-less + // lambdas), pointers to member functions (e.g., string::size()), or + // pointers to data members (e.g., name::type). In this case the build2 + // function types are automatically matched to C++ function types according + // to these rules: + // + // T - statically-typed (value_traits must be defined) + // names - untyped + // value - any type + // T* - NULL-able argument (here T can be names, value). + // optional - optional argument (here T can be T*, names, value) + // + // Optional arguments must be last. In case of a failure the function is + // expected to issue diagnostics and throw failed. Note that the arguments + // are conceptually "moved" and can be reused by the implementation. + // + // Normally functions come in families that share a common qualification + // (e.g., string. or path.). The function_family class is a "registrar" + // that simplifies handling of function families. For example: + // + // function_family f ("string"); + // + // // Register length() and string.length(). + // // + // f["length"] = &string::size; + // + // // Register string.max_size(). + // // + // f[".max_size"] = []() {return string ().max_size ();}; + // + // For more examples/ideas, study the existing function families (reside + // in the functions-*.cxx files). + // + struct function_overload; + + using function_impl = value (vector_view, const function_overload&); + + struct function_overload + { + const char* name; // Set to point to key by insert() below. + const char* qual_name; // Qualified name, NULL if none. + + // Arguments. + // + // A function can have a number of optional arguments. Arguments can also + // be typed. A non-existent entry in arg_types means a value of any type. + // A NULL entry means an untyped value. + // + // If arg_max equals to arg_variadic, then the function takes an unlimited + // number of arguments. In this case the semantics of arg_min and + // arg_types is unchanged. + // + static const size_t arg_variadic = size_t (~0); + + using types = vector_view>; + + const size_t arg_min; + const size_t arg_max; + const types arg_types; + + // Function implementation. + // + function_impl* const impl; + + // Auxiliary data storage. Note that it is assumed to be POD (no + // destructors, bitwise copy, etc). + // + std::aligned_storage::type data; + static const size_t data_size = sizeof (decltype (data)); + + function_overload () = default; + + function_overload (const char* qn, + size_t mi, size_t ma, types ts, + function_impl* im) + : qual_name (qn), + arg_min (mi), arg_max (ma), arg_types (move (ts)), + impl (im) {} + + template + function_overload (const char* qn, + size_t mi, size_t ma, types ts, + function_impl* im, + D d) + : function_overload (qn, mi, ma, move (ts), im) + { + // std::is_pod appears to be broken in VC15. + // +#if !defined(_MSC_VER) || _MSC_VER > 1900 + static_assert (std::is_pod::value, "type is not POD"); +#endif + static_assert (sizeof (D) <= data_size, "insufficient space"); + new (&data) D (move (d)); + } + }; + + class function_map + { + public: + using map_type = std::unordered_multimap; + using iterator = map_type::iterator; + + iterator + insert (string name, function_overload); + + void + erase (iterator i) {map_.erase (i);} + + value + call (const string& name, vector_view args, const location&); + + private: + map_type map_; + }; + + extern function_map functions; + + class function_family + { + public: + // The default thunk catches invalid_argument and issues diagnostics + // by assuming it is related to function arguments and contains useful + // description. + // + // In order to implement a custom thunk (e.g., to catch additional extra + // exceptions), you would normally call the default implementation. + // + static value + default_thunk (vector_view, const function_overload&); + + // A function family uses a common qualification (though you can pass + // empty string to supress it). For an unqualified name (doesn't not + // contain a dot) the qualified version is added automatically. A name + // containing a leading dot is a shortcut notation for a qualified-only + // name. + // + explicit + function_family (string qual, function_impl* thunk = &default_thunk) + : qual_ (qual), thunk_ (thunk) {} + + struct entry; + + entry + operator[] (string name) const; + + private: + const string qual_; + function_impl* thunk_; + }; + + // Implementation details. If you can understand and explain all of this, + // then you are hired ;-)! + // + + template + struct function_arg + { + static const bool null = false; + static const bool opt = false; + + static optional + type () {return &value_traits::value_type;} + + static T&& + cast (value* v) + { + if (v->null) + throw invalid_argument ("null value"); + + // Use fast but unchecked cast since the caller matched the types. + // + return move (v->as ()); + } + }; + + template <> + struct function_arg // Untyped. + { + static const bool null = false; + static const bool opt = false; + + static optional + type () {return nullptr;} + + static names&& + cast (value* v) + { + if (v->null) + throw invalid_argument ("null value"); + + return move (v->as ()); + } + }; + + template <> + struct function_arg // Anytyped. + { + static const bool null = false; + static const bool opt = false; + + static optional + type () {return nullopt;} + + static value&& + cast (value* v) + { + if (v->null) + throw invalid_argument ("null value"); + + return move (*v); + } + }; + + template + struct function_arg: function_arg + { + static const bool null = true; + + static T* + cast (value* v) + { + if (v->null) + return nullptr; + + // This looks bizarre but makes sense. The cast() that we are calling + // returns an r-value reference to (what's inside) v. And it has to + // return an r-value reference to that the value is moved into by-value + // arguments. + // + T&& r (function_arg::cast (v)); + return &r; + } + }; + + template + struct function_arg>: function_arg + { + static const bool opt = true; + + static optional + cast (value* v) + { + return v != nullptr ? optional (function_arg::cast (v)) : nullopt; + } + }; + + // Number of optional arguments. Note that we currently don't check that + // they are all at the end. + // + template + struct function_args_opt + { + static const size_t count = (function_arg::opt ? 1 : 0) + + function_args_opt::count; + }; + + template + struct function_args_opt + { + static const size_t count = (function_arg::opt ? 1 : 0); + }; + + // Argument counts/types. + // + template + struct function_args + { + static const size_t max = sizeof...(A); + static const size_t min = max - function_args_opt::count; + + static const optional types[max]; + }; + + template + const optional + function_args::types[function_args::max] = { + function_arg::type ()...}; + + // Specialization for no arguments. + // + template <> + struct function_args<> + { + static const size_t max = 0; + static const size_t min = 0; + static const optional* const types; // NULL + }; + + // Cast data/thunk. + // + template + struct function_cast + { + // A pointer to a standard layout struct is a pointer to its first data + // member, which in our case is the cast thunk. + // + struct data + { + value (*const thunk) (vector_view, const void*); + R (*const impl) (A...); + }; + + static value + thunk (vector_view args, const void* d) + { + return thunk (move (args), + static_cast (d)->impl, + std::index_sequence_for ()); + } + + template + static value + thunk (vector_view args, + R (*impl) (A...), + std::index_sequence) + { + return value ( + impl ( + function_arg::cast ( + i < args.size () ? &args[i] : nullptr)...)); + } + }; + + // Specialization for void return type. In this case we return NULL value. + // + template + struct function_cast + { + struct data + { + value (*const thunk) (vector_view, const void*); + void (*const impl) (A...); + }; + + static value + thunk (vector_view args, const void* d) + { + thunk (move (args), + static_cast (d)->impl, + std::index_sequence_for ()); + return value (nullptr); + } + + template + static void + thunk (vector_view args, + void (*impl) (A...), + std::index_sequence) + { + impl (function_arg::cast (i < args.size () ? &args[i] : nullptr)...); + } + }; + + // Customization for member functions. + // + template + struct function_cast_memf + { + struct data + { + value (*const thunk) (vector_view, const void*); + R (T::*const impl) () const; + }; + + static value + thunk (vector_view args, const void* d) + { + auto mf (static_cast (d)->impl); + return value ((function_arg::cast (&args[0]).*mf) ()); + } + }; + + template + struct function_cast_memf + { + struct data + { + value (*const thunk) (vector_view, const void*); + void (T::*const impl) () const; + }; + + static value + thunk (vector_view args, const void* d) + { + auto mf (static_cast (d)->impl); + (function_arg::cast (args[0]).*mf) (); + return value (nullptr); + } + }; + + // Customization for data members. + // + template + struct function_cast_memd + { + struct data + { + value (*const thunk) (vector_view, const void*); + R T::*const impl; + }; + + static value + thunk (vector_view args, const void* d) + { + auto dm (static_cast (d)->impl); + return value (move (function_arg::cast (&args[0]).*dm)); + } + }; + + struct function_family::entry + { + string name; + const string& qual; + function_impl* thunk; + + template + void + operator= (R (*impl) (A...)) && + { + using args = function_args; + using cast = function_cast; + + insert (move (name), + function_overload ( + nullptr, + args::min, + args::max, + function_overload::types (args::types, args::max), + thunk, + typename cast::data {&cast::thunk, impl})); + } + + // Support for assigning a (capture-less) lambda. + // + template + void + operator= (const L& l) && + { + move (*this).operator= (decay_lambda (&L::operator(), l)); + } + + template + static auto + decay_lambda (R (L::*) (A...) const, const L& l) -> R (*) (A...) + { + return static_cast (l); + } + + // Support for assigning a pointer to member function (e.g. an accessor). + // + // For now we don't support passing additional (to this) arguments though + // we could probably do that. The issues would be the argument passing + // semantics (e.g., what if it's const&) and the optional/default argument + // handling. + // + template + void + operator= (R (T::*mf) () const) && + { + using args = function_args; + using cast = function_cast_memf; + + insert (move (name), + function_overload ( + nullptr, + args::min, + args::max, + function_overload::types (args::types, args::max), + thunk, + typename cast::data {&cast::thunk, mf})); + } + + // Support for assigning a pointer to data member. + // + template + void + operator= (R T::*dm) && + { + using args = function_args; + using cast = function_cast_memd; + + insert (move (name), + function_overload ( + nullptr, + args::min, + args::max, + function_overload::types (args::types, args::max), + thunk, + typename cast::data {&cast::thunk, dm})); + } + + private: + void + insert (string, function_overload) const; + }; + + inline auto function_family:: + operator[] (string name) const -> entry + { + return entry {move (name), qual_, thunk_}; + } +} + +#endif // BUILD2_FUNCTION diff --git a/build2/function.cxx b/build2/function.cxx new file mode 100644 index 0000000..91c5b3e --- /dev/null +++ b/build2/function.cxx @@ -0,0 +1,231 @@ +// file : build2/function.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +using namespace std; + +namespace build2 +{ + auto function_map:: + insert (string name, function_overload f) -> iterator + { + // Sanity checks. + // + assert (f.arg_min <= f.arg_max && + f.arg_types.size () <= f.arg_max && + f.impl != nullptr); + + auto i (map_.emplace (move (name), move (f))); + + i->second.name = i->first.c_str (); + return i; + } + + value function_map:: + call (const string& name, vector_view args, const location& loc) + { + auto print_call = [&name, &args] (ostream& os) + { + os << name << '('; + + for (size_t i (0); i != args.size (); ++i) + { + const value_type* t (args[i].type); + os << (i != 0 ? ", " : "") << (t != nullptr ? t->name : ""); + } + + os << ')'; + }; + + auto print_fovl = [&name] (ostream& os, const function_overload& f) + { + os << name << '('; + + bool v (f.arg_max == function_overload::arg_variadic); + size_t n (v ? max (f.arg_min, f.arg_types.size ()): f.arg_max); + + // Handle variadic tail as the last pseudo-argument. + // + for (size_t i (0); i != n + (v ? 1 : 0); ++i) + { + if (i == f.arg_min) + os << (i != 0 ? " [" : "["); + + os << (i != 0 ? ", " : ""); + + if (i == n) // Variadic tail (last). + os << "..."; + else + { + // If count is greater than f.arg_typed, then we assume the rest are + // valid but untyped. + // + const optional t ( + i < f.arg_types.size () ? f.arg_types[i] : nullopt); + + os << (t ? (*t != nullptr ? (*t)->name : "") : ""); + } + } + + if (n + (v ? 1 : 0) > f.arg_min) + os << ']'; + + os << ')'; + + if (f.qual_name) + os << ", qualified name " << f.qual_name; + }; + + // Overload resolution. + // + const function_overload* r (nullptr); + + size_t count (args.size ()); + auto ip (map_.equal_range (name)); + + for (auto it (ip.first); it != ip.second; ++it) + { + const function_overload& f (it->second); + + // Argument count match. + // + if (count < f.arg_min || count > f.arg_max) + continue; + + // Argument types match. + // + { + size_t i (0), n (min (count, f.arg_types.size ())); + for (; i != n; ++i) + if (f.arg_types[i] && *f.arg_types[i] != args[i].type) + break; + + if (i != n) + continue; + } + + if (r != nullptr) + { + diag_record dr (fail (loc)); + + dr << "ambiguous call to "; print_call (dr.os); + dr << info << " candidate: "; print_fovl (dr.os, *r); + dr << info << " candidate: "; print_fovl (dr.os, f); + } + + r = &f; // Continue looking to detect ambiguities. + } + + if (r == nullptr) + { + diag_record dr (fail (loc)); + + dr << "unmatched call to "; print_call (dr.os); + + for (auto it (ip.first); it != ip.second; ++it) + { + const function_overload& f (it->second); + + dr << info << " candidate: "; print_fovl (dr.os, f); + } + } + + // Print the call location if the function fails. + // + auto g ( + make_exception_guard ( + [&loc, &print_call] () + { + if (verb != 0) + { + diag_record dr (info (loc)); + dr << "while calling "; print_call (dr.os); + } + })); + + return r->impl (move (args), *r); + } + + value function_family:: + default_thunk (vector_view args, const function_overload& f) + try + { + // Call the cast thunk. + // + struct cast_data // Prefix of function_cast::data. + { + value (*const thunk) (vector_view, const void*); + }; + + auto d (reinterpret_cast (&f.data)); + return d->thunk (move (args), d); + } + catch (const invalid_argument& e) + { + { + diag_record dr (error); + dr << "invalid argument"; + + const char* w (e.what ()); + if (*w != '\0') + dr << ": " << w; + } + + throw failed (); + } + + const optional* const function_args<>::types = nullptr; + + void function_family::entry:: + insert (string n, function_overload f) const + { + // Figure out qualification. + // + string qn; + size_t p (n.find ('.')); + + if (p == string::npos) + { + if (!qual.empty ()) + { + qn = qual; + qn += '.'; + qn += n; + } + } + else if (p == 0) + { + assert (!qual.empty ()); + n.insert (0, qual); + } + + // First insert the qualified name and use its key for f.qual_name. + // + if (!qn.empty ()) + { + auto i (functions.insert (move (qn), f)); + f.qual_name = i->first.c_str (); + } + + functions.insert (move (n), move (f)); + } + + // Static-initialize the function map and populate with builtin functions. + // + function_map functions; + + void + path_functions (); // functions-path.cxx + + struct functions_init + { + functions_init () + { + path_functions (); + } + }; + + static const functions_init init_; +} diff --git a/build2/functions-path.cxx b/build2/functions-path.cxx new file mode 100644 index 0000000..b76b277 --- /dev/null +++ b/build2/functions-path.cxx @@ -0,0 +1,53 @@ +// file : build2/functions-path.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include +#include + +using namespace std; + +namespace build2 +{ + static value + path_thunk (vector_view args, const function_overload& f) + try + { + return function_family::default_thunk (move (args), f); + } + catch (const invalid_path& e) + { + error << "invalid path: '" << e.path << "'"; + throw failed (); + } + + void + path_functions () + { + function_family f ("path", &path_thunk); + + // normalize + // + f["normalize"] = [](path p) {p.normalize (); return p;}; + f["normalize"] = [](paths v) {for (auto& p: v) p.normalize (); return v;}; + + f["normalize"] = [](dir_path p) {p.normalize (); return p;}; + f["normalize"] = [](dir_paths v) {for (auto& p: v) p.normalize (); return v;}; + + f[".normalize"] = [](names ns) + { + // For each path decide based on the presence of a trailing slash + // whether it is a directory. Return as untyped list of (potentially + // mixed) paths. + // + for (name& n: ns) + { + if (n.directory ()) + n.dir.normalize (); + else + n.value = convert (move (n)).normalize ().string (); + } + return ns; + }; + } +} diff --git a/build2/parser b/build2/parser index 3e6fc00..954a706 100644 --- a/build2/parser +++ b/build2/parser @@ -107,7 +107,10 @@ namespace build2 value&& rhs, token_type kind); - value + // Return the value as well as the indication of whether this is a non- + // empty eval context (i.e., '()' potentially with whitespace in between). + // + pair parse_eval (token&, token_type&); value diff --git a/build2/parser.cxx b/build2/parser.cxx index d095047..4d2b933 100644 --- a/build2/parser.cxx +++ b/build2/parser.cxx @@ -8,14 +8,15 @@ #include +#include #include +#include #include -#include +#include +#include #include -#include -#include #include -#include +#include using namespace std; @@ -1658,12 +1659,14 @@ namespace build2 } } - value parser:: + pair parser:: parse_eval (token& t, type& tt) { mode (lexer_mode::eval, '@'); next (t, tt); - return parse_eval_trailer (t, tt); + return tt != type::rparen + ? make_pair (parse_eval_trailer (t, tt), true) + : make_pair (value (names ()), false); } value parser:: @@ -2306,7 +2309,7 @@ namespace build2 else if (tt == type::lparen) { expire_mode (); - value v (parse_eval (t, tt)); //@@ OUT will parse @-pair and do well? + value v (parse_eval (t, tt).first); //@@ OUT will parse @-pair and do well? if (!pre_parse_) { @@ -2357,31 +2360,22 @@ namespace build2 next (t, tt); // Get '('. - // Just a stub for now. - // - // Should we use (target/scope) qualification (of name) as the + // @@ Should we use (target/scope) qualification (of name) as the // context in which to call the function? // - value a (parse_eval (t, tt)); + auto args (parse_eval (t, tt)); tt = peek (); if (pre_parse_) continue; // As if empty result. - cout << name << "("; - - if (a) - { - if (!a.empty ()) - cout << reverse (a, lv_storage); - } - else - cout << "[null]"; - - - cout << ")" << endl; - - result = value (); // NULL for now. + // Note that we move args to call(). + // + result = functions.call (name, + vector_view ( + args.second ? &args.first : nullptr, + args.second ? 1 : 0), + loc); // See if we should propagate the NULL indicator. // @@ -2428,7 +2422,7 @@ namespace build2 else { loc = get_location (t); - result = parse_eval (t, tt); + result = parse_eval (t, tt).first; tt = peek (); diff --git a/build2/utility b/build2/utility index deefc43..4b83844 100644 --- a/build2/utility +++ b/build2/utility @@ -485,7 +485,7 @@ namespace build2 find (const std::string& s) {return *emplace (s).first;} }; - // Initialize build2 global state (verbosity, home/work dirrectories, etc). + // Initialize build2 global state (verbosity, home/work directories, etc). // Should be called early in main() once. // void diff --git a/build2/variable b/build2/variable index ab5586d..a3f050d 100644 --- a/build2/variable +++ b/build2/variable @@ -171,7 +171,7 @@ namespace build2 value (const value_type* t): type (t), null (true), extra (0) {} explicit - value (names&&); // Create untyped value. + value (names); // Create untyped value. template explicit @@ -372,15 +372,17 @@ namespace build2 // // - Specialization for vector (if used and becomes critical). // - // + template + struct value_traits_specialization; // enable_if'able specialization support. + template - struct value_traits; + struct value_traits: value_traits_specialization {}; // { // static_assert (sizeof (T) <= value::size_, "insufficient space"); // // // Convert name to T. If rhs is not NULL, then it is the second half // // of a pair. Only needs to be provided by simple types. Throw - // // invalid_argument (without a message) if the name is not a valid + // // invalid_argument (with a message) if the name is not a valid // // representation of value (in which case the name should remain // // unchanged for diagnostics). // // @@ -416,14 +418,19 @@ namespace build2 // static const build2::value_type value_type; // }; - // Convert name to a simple value. Throw invalid_argument (without any - // message) if the name is not a valid representation of value (in which - // case the name remains unchanged for diagnostics). The second version is - // called for a pair. + // Convert name to a simple value. Throw invalid_argument (with a message) + // if the name is not a valid representation of value (in which case the + // name remains unchanged for diagnostics). The second version is called for + // a pair. // template T convert (name&&); template T convert (name&&, name&&); + // As above but for container types. Note that in case of invalid_argument + // the names are not guaranteed to be unchanged. + // + template T convert (names&&); + // Default implementations of the dtor/copy_ctor/copy_assing callbacks for // types that are stored directly in value::data_ and the provide all the // necessary functions (copy/move ctor and assignment operator). @@ -515,6 +522,16 @@ namespace build2 static const build2::value_type value_type; }; + // Treat unsigned integral types as uint64. Note that bool is handled + // differently at an earlier stage. + // + template + struct value_traits_specialization::value && + std::is_unsigned::value>::type>: + value_traits {}; + // string // template <> @@ -534,6 +551,11 @@ namespace build2 static const build2::value_type value_type; }; + // Treat const char* as string. + // + template <> + struct value_traits: value_traits {}; + // path // template <> @@ -637,6 +659,7 @@ namespace build2 { static_assert (sizeof (vector) <= value::size_, "insufficient space"); + static vector convert (names&&); static void assign (value&, vector&&); static void append (value&, vector&&); static void prepend (value&, vector&&); diff --git a/build2/variable.cxx b/build2/variable.cxx index 3fc9835..39fdb5d 100644 --- a/build2/variable.cxx +++ b/build2/variable.cxx @@ -336,6 +336,32 @@ namespace build2 } } + // Throw invalid_argument for an invalid simple value. + // + [[noreturn]] static void + throw_invalid_argument (const name& n, const name* r, const char* type) + { + string m; + string t (type); + + if (r != nullptr) + m = "pair in " + t + " value"; + else + { + m = "invalid " + t + " value: "; + + if (n.simple ()) + m += "'" + n.value + "'"; + else if (n.directory ()) + m += "'" + + (n.original ? n.dir.representation () : n.dir.string ()) + "'"; + else + m += "complex name"; + } + + throw invalid_argument (m); + } + // bool value // bool value_traits:: @@ -354,7 +380,7 @@ namespace build2 // Fall through. } - throw invalid_argument (string ()); + throw_invalid_argument (n, r, "bool"); } const char* const value_traits::type_name = "bool"; @@ -389,13 +415,13 @@ namespace build2 // return stoull (n.value); } - catch (const out_of_range&) + catch (const std::exception&) { // Fall through. } } - throw invalid_argument (string ()); + throw_invalid_argument (n, r, "uint64"); } const char* const value_traits::type_name = "uint64"; @@ -431,7 +457,7 @@ namespace build2 // if (!(n.simple (true) || n.directory (true)) || !(r == nullptr || r->simple (true) || r->directory (true))) - throw invalid_argument (string ()); + throw_invalid_argument (n, r, "string"); string s; @@ -525,7 +551,7 @@ namespace build2 // Fall through. } - throw invalid_argument (string ()); + throw_invalid_argument (n, r, "path"); } const char* const value_traits::type_name = "path"; @@ -569,7 +595,7 @@ namespace build2 // Fall through. } - throw invalid_argument (string ()); + throw_invalid_argument (n, r, "dir_path"); } const char* const value_traits::type_name = "dir_path"; @@ -596,17 +622,26 @@ namespace build2 abs_dir_path value_traits:: convert (name&& n, name* r) { - dir_path d (value_traits::convert (move (n), r)); - - if (!d.empty ()) + if (r == nullptr && (n.simple () || n.directory ())) { - if (d.relative ()) - d.complete (); + try + { + dir_path d (n.simple () ? dir_path (move (n.value)) : move (n.dir)); - d.normalize (true); // Actualize. + if (!d.empty ()) + { + if (d.relative ()) + d.complete (); + + d.normalize (true); // Actualize. + } + + return abs_dir_path (move (d)); + } + catch (const invalid_path&) {} // Fall through. } - return abs_dir_path (move (d)); + throw_invalid_argument (n, r, "abs_dir_path"); } const char* const value_traits::type_name = "abs_dir_path"; @@ -633,11 +668,13 @@ namespace build2 name value_traits:: convert (name&& n, name* r) { - if (r != nullptr) - throw invalid_argument (string ()); + if (r == nullptr) + { + n.original = false; + return move (n); + } - n.original = false; - return move (n); + throw_invalid_argument (n, r, "name"); } static names_view @@ -671,25 +708,31 @@ namespace build2 process_path value_traits:: convert (name&& n, name* r) { - path rp (move (n.dir)); - if (rp.empty ()) - rp = path (move (n.value)); - else - rp /= n.value; - - path ep; - if (r != nullptr) + if ( n.untyped () && n.unqualified () && !n.empty () && + (r == nullptr || (r->untyped () && r->unqualified () && !r->empty ()))) { - ep = move (r->dir); - if (ep.empty ()) - ep = path (move (r->value)); + path rp (move (n.dir)); + if (rp.empty ()) + rp = path (move (n.value)); else - ep /= r->value; + rp /= n.value; + + path ep; + if (r != nullptr) + { + ep = move (r->dir); + if (ep.empty ()) + ep = path (move (r->value)); + else + ep /= r->value; + } + + process_path pp (nullptr, move (rp), move (ep)); + pp.initial = pp.recall.string ().c_str (); + return pp; } - process_path pp (nullptr, move (rp), move (ep)); - pp.initial = pp.recall.string ().c_str (); - return pp; + throw_invalid_argument (n, r, "process_path"); } void diff --git a/build2/variable.ixx b/build2/variable.ixx index d054dd4..1e9dfe6 100644 --- a/build2/variable.ixx +++ b/build2/variable.ixx @@ -18,7 +18,7 @@ namespace build2 } inline value:: - value (names&& ns) + value (names ns) : type (nullptr), null (false), extra (0) { new (&data_) names (move (ns)); @@ -227,6 +227,13 @@ namespace build2 return value_traits::convert (move (l), &r); } + template + inline T + convert (names&& ns) + { + return value_traits::convert (move (ns)); + } + // bool value // inline void value_traits:: diff --git a/build2/variable.txx b/build2/variable.txx index 95e4f0a..7bb30e9 100644 --- a/build2/variable.txx +++ b/build2/variable.txx @@ -177,6 +177,35 @@ namespace build2 // template + vector value_traits>:: + convert (names&& ns) + { + vector v; + + // Similar to vector_append() below except we throw instead of issuing + // diagnostics. + // + for (auto i (ns.begin ()); i != ns.end (); ++i) + { + name& n (*i); + name* r (nullptr); + + if (n.pair) + { + r = &*++i; + + if (n.pair != '@') + throw invalid_argument ( + string ("invalid pair character: '") + n.pair + "'"); + } + + v.push_back (value_traits::convert (move (n), r)); + } + + return v; + } + + template void vector_append (value& v, names&& ns, const variable* var) { diff --git a/tests/buildfile b/tests/buildfile index b6799d5..cd5177e 100644 --- a/tests/buildfile +++ b/tests/buildfile @@ -2,6 +2,6 @@ # copyright : Copyright (c) 2014-2016 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -d = test/ +d = function/ test/ ./: $d include $d diff --git a/tests/function/buildfile b/tests/function/buildfile new file mode 100644 index 0000000..52288ca --- /dev/null +++ b/tests/function/buildfile @@ -0,0 +1,7 @@ +# file : tests/function/buildfile +# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +d = path/ +./: $d +include $d diff --git a/tests/function/call/buildfile b/tests/function/call/buildfile deleted file mode 100644 index 136f652..0000000 --- a/tests/function/call/buildfile +++ /dev/null @@ -1,17 +0,0 @@ -$identity() -$identity (a) -$identity (a b c) -$identity(sub/dir{x y z}) - -print a$identity (b)c - -# Verify we can inhibit function call with quoting. -# -foo = FOO -bar = BAR - -print $foo"($bar)" -print "$foo"($bar) -print "$foo""($bar)" - -./: diff --git a/tests/function/call/test.out b/tests/function/call/test.out deleted file mode 100644 index 88a852b..0000000 --- a/tests/function/call/test.out +++ /dev/null @@ -1,9 +0,0 @@ -identity() -identity(a) -identity(a b c) -identity(sub/dir{x} sub/dir{y} sub/dir{z}) -identity(b) -ac -FOOBAR -FOOBAR -FOOBAR diff --git a/tests/function/call/test.sh b/tests/function/call/test.sh deleted file mode 100755 index c745b76..0000000 --- a/tests/function/call/test.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -b -q | diff --strip-trailing-cr -u test.out - diff --git a/tests/function/path/buildfile b/tests/function/path/buildfile new file mode 100644 index 0000000..db40358 --- /dev/null +++ b/tests/function/path/buildfile @@ -0,0 +1,7 @@ +# file : tests/function/path/buildfile +# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +./: test{testscript} + +test{*}: test = $build.driver diff --git a/tests/function/path/testscript b/tests/function/path/testscript new file mode 100644 index 0000000..5656446 --- /dev/null +++ b/tests/function/path/testscript @@ -0,0 +1,39 @@ +# file : tests/function/path/testscript +# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + ++mkdir build ++cat <>>build/boostrap.build +project = test +amalgamation = +EOI + +test.options += -q --buildfile - noop + ++if ($cxx.target.class != windows) # @@ TMP ternarry + s = '/' +else + s = '\' +end + +: normalize +: +{ + $* <'print $normalize([path] a/../b/)' >"b" : path + $* <'print $normalize([paths] a/../b/ a/../c)' >"b c" : paths + $* <'print $normalize([dir_path] a/../b)' >"b$s" : dir-path + $* <'print $normalize([dir_paths] a/../b a/../c/)' >"b$s c$s" : dir-paths + $* <'print $path.normalize(a/../b)' >"b" : untyped + $* <'print $path.normalize(a/../b/ a/../b)' >"b$s b" : mixed +} + +: invalid-path +: +if ($cxx.target.class != windows) # @@ TMP ternarry + p = /../foo +else + p = c:/../foo +end; +$* <"\$path.normalize\($p)" 2>>EOE != 0 +error: invalid path: '$p' +EOE diff --git a/tests/test.sh b/tests/test.sh index 9ce7152..de93322 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -15,7 +15,6 @@ function test () test "amalgam/unnamed" test "escaping" test "eval" -test "function/call" test "if-else" test "keyword" test "names" diff --git a/unit-tests/buildfile b/unit-tests/buildfile index 6e8b5c2..5d06ec7 100644 --- a/unit-tests/buildfile +++ b/unit-tests/buildfile @@ -2,6 +2,6 @@ # copyright : Copyright (c) 2014-2016 Code Synthesis Ltd # license : MIT; see accompanying LICENSE file -d = test/script/ +d = function/ test/script/ ./: $d include $d diff --git a/unit-tests/function/buildfile b/unit-tests/function/buildfile new file mode 100644 index 0000000..6c1d346 --- /dev/null +++ b/unit-tests/function/buildfile @@ -0,0 +1,14 @@ +# file : unit-tests/function/buildfile +# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +#@@ Temporary until we get utility library support. +# +import libs = libbutl%lib{butl} +src = token lexer diagnostics utility variable name b-options types-parsers \ +context scope parser target operation rule prerequisite file module function \ +functions-path algorithm search dump filesystem config/{utility init operation} + +exe{driver}: cxx{driver} ../../build2/cxx{$src} $libs test{call syntax} + +include ../../build2/ diff --git a/unit-tests/function/call.test b/unit-tests/function/call.test new file mode 100644 index 0000000..d459300 --- /dev/null +++ b/unit-tests/function/call.test @@ -0,0 +1,131 @@ +# file : unit-tests/function/call.test +# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +: qual-implicit +: +$* <'print $dummy.dummy0()' >'abc' + +: qual-explicit +: +$* <'print $dummy.qual()' >'abc' + +: qual-fail +: +$* <'print $qual()' 2>>EOE != 0 +buildfile:1:8: error: unmatched call to qual\() +EOE + +: variadic +: +# @@ TMP: add some args +$* <'print $variadic([bool] true)' >'1' + +: fail +: +$* <'$fail()' 2>>EOE != 0 +error: failed +buildfile:1:2: info: while calling fail\() +EOE + +: fail-invalid-arg +: +$* <'$fail_arg(abc)' 2>>EOE != 0 +error: invalid argument: invalid uint64 value: 'abc' +buildfile:1:2: info: while calling fail_arg\() +EOE + +: no-match-name +: +$* <'$bogus()' 2>>EOE != 0 +buildfile:1:2: error: unmatched call to bogus\() +EOE + +: no-match-count +: +$* <'$dummy0(abc)' 2>>EOE != 0 +buildfile:1:2: error: unmatched call to dummy0\() + info: candidate: dummy0\(), qualified name dummy.dummy0 +EOE + +: no-match-type +: +$* <'$dummy1([uint64] 123)' 2>>EOE != 0 +buildfile:1:2: error: unmatched call to dummy1\(uint64) + info: candidate: dummy1\(string), qualified name dummy.dummy1 +EOE + +: ambig +: +$* <'$ambig(abc)' 2>>EOE != 0 +buildfile:1:2: error: ambiguous call to ambig\() + info: candidate: ambig\( [, uint64]), qualified name dummy.ambig + info: candidate: ambig\( [, string]), qualified name dummy.ambig +EOE + +: optional-absent +: +$* <'print $optional()' >'true' + +: optional-present +: +$* <'print $optional(abc)' >'false' + +: null-true +: +$* <'print $null([null])' >'true' + +: null-false +: +$* <'print $null(nonull)' >'false' + +: null-fail +: +$* <'$dummy1([string null])' 2>>EOE != 0 +error: invalid argument: null value +buildfile:1:2: info: while calling dummy1\(string) +EOE + +: print-call-1-untyped +: +$* <'$bogus(abc)' 2>>EOE != 0 +buildfile:1:2: error: unmatched call to bogus\() +EOE + +: print-call-1-typed +: +$* <'$bogus([uint64] 123)' 2>>EOE != 0 +buildfile:1:2: error: unmatched call to bogus\(uint64) +EOE + +#\ +@@ TMP +: print-call-2 +: +$* <'$bogus(abc, [uint64] 123)' 2>>EOE != 0 +buildfile:1:2: error: unmatched call to bogus\(, uint64) +EOE +#\ + +: print-fovl +: +$* <'$ambig([bool] true)' 2>>EOE != 0 +buildfile:1:2: error: unmatched call to ambig\(bool) + info: candidate: ambig\( [, uint64]), qualified name dummy.ambig + info: candidate: ambig\( [, string]), qualified name dummy.ambig +EOE + +: print-fovl-variadic +: +$* <'$variadic(abc)' 2>>EOE != 0 +buildfile:1:2: error: unmatched call to variadic\() + info: candidate: variadic\(bool [, ...]) +EOE + +: member-function +: +$* <'print $dummy.length([string] abc)' >'3' + +: data-member +: +$* <'print $dummy.type([name] cxx{foo})' >'cxx' diff --git a/unit-tests/function/driver.cxx b/unit-tests/function/driver.cxx new file mode 100644 index 0000000..ba51662 --- /dev/null +++ b/unit-tests/function/driver.cxx @@ -0,0 +1,110 @@ +// file : unit-tests/function/driver.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include + +#include +#include +#include +#include +#include + +using namespace std; + +namespace build2 +{ + static const optional arg_bool[1] = + { + &value_traits::value_type + }; + + int + main () + { + init ("false", 1); // No build system driver, default verbosity. + reset (strings ()); // No command line variables. + + function_family f ("dummy"); + + f["fail"] = []() {error << "failed"; throw failed ();}; + f["fail_arg"] = [](names a) {return convert (move (a[0]));}; + + f["null"] = [](names* a) {return a == nullptr;}; + f["optional"] = [](optional a) {return !a;}; + + f["dummy0"] = []() {return "abc";}; + f["dummy1"] = [](string s) {return s;}; + + f["ambig"] = [](names a, optional) {return a;}; + f["ambig"] = [](names a, optional) {return a;}; + + f[".qual"] = []() {return "abc";}; + + f[".length"] = &string::size; // Member function. + f[".type"] = &name::type; // Data member. + + // Variadic function with first required argument of type bool. Returns + // number of arguments passed. + // + functions.insert ( + "variadic", + function_overload ( + nullptr, + 1, + function_overload::arg_variadic, + function_overload::types (arg_bool, 1), + [] (vector_view args, const function_overload&) + { + return value (static_cast (args.size ())); + })); + + // Dump arguments. + // + functions.insert ( + "dump", + function_overload ( + nullptr, + 0, + function_overload::arg_variadic, + function_overload::types (), + [] (vector_view args, const function_overload&) + { + for (value& a: args) + { + if (a.null) + cout << "[null]"; + else if (!a.empty ()) + { + names storage; + cout << reverse (a, storage); + } + cout << endl; + } + return value (nullptr); + })); + + try + { + scope& s (*global_scope); + + parser p; + p.parse_buildfile (cin, path ("buildfile"), s, s); + } + catch (const failed&) + { + return 1; + } + + return 0; + } +} + +int +main () +{ + return build2::main (); +} diff --git a/unit-tests/function/syntax.test b/unit-tests/function/syntax.test new file mode 100644 index 0000000..9e653c8 --- /dev/null +++ b/unit-tests/function/syntax.test @@ -0,0 +1,29 @@ +# file : unit-tests/function/syntax.test +# copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +# license : MIT; see accompanying LICENSE file + +$* <'$dump()' >:'' : none +$* <'$dump( )' >:'' : none-in-spaces +$* <'$dump ()' >:'' : none-out-spaces +$* <'$dump("")' >'{}' : one-empty +$* <'$dump(a)' >'a' : one-single +$* <'$dump(a b c)' >'a b c' : one-list +$* <'$dump(d/t{x y z})' >'d/t{x} d/t{y} d/t{z}' : one-names + +$* <'print a$dummy1 ([string] b)c' >'abc' : concat + +: quoting +: Verify we can inhibit function call with quoting +: +$* <>EOO +foo = FOO +bar = BAR + +print \$foo"\(\$bar)" +print "\$foo"\(\$bar) +print "\$foo""\(\$bar)" +EOI +FOOBAR +FOOBAR +FOOBAR +EOO diff --git a/unit-tests/test/script/parser/buildfile b/unit-tests/test/script/parser/buildfile index 2ed60ec..af3f2d3 100644 --- a/unit-tests/test/script/parser/buildfile +++ b/unit-tests/test/script/parser/buildfile @@ -7,8 +7,8 @@ import libs = libbutl%lib{butl} src = token lexer parser diagnostics utility variable name context target \ scope prerequisite file module operation rule b-options algorithm search \ -filesystem config/{utility init operation} dump types-parsers \ -test/{target script/{token lexer parser script}} +filesystem function functions-path config/{utility init operation} dump \ +types-parsers test/{target script/{token lexer parser script}} exe{driver}: cxx{driver} ../../../../build2/cxx{$src} $libs \ test{cleanup command-if command-re-parse description exit expansion \ -- cgit v1.1