aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build2/buildfile4
-rw-r--r--build2/function534
-rw-r--r--build2/function.cxx231
-rw-r--r--build2/functions-path.cxx53
-rw-r--r--build2/parser5
-rw-r--r--build2/parser.cxx46
-rw-r--r--build2/utility2
-rw-r--r--build2/variable39
-rw-r--r--build2/variable.cxx107
-rw-r--r--build2/variable.ixx9
-rw-r--r--build2/variable.txx29
-rw-r--r--tests/buildfile2
-rw-r--r--tests/function/buildfile7
-rw-r--r--tests/function/call/buildfile17
-rw-r--r--tests/function/call/test.out9
-rwxr-xr-xtests/function/call/test.sh3
-rw-r--r--tests/function/path/buildfile7
-rw-r--r--tests/function/path/testscript39
-rwxr-xr-xtests/test.sh1
-rw-r--r--unit-tests/buildfile2
-rw-r--r--unit-tests/function/buildfile14
-rw-r--r--unit-tests/function/call.test131
-rw-r--r--unit-tests/function/driver.cxx110
-rw-r--r--unit-tests/function/syntax.test29
-rw-r--r--unit-tests/test/script/parser/buildfile4
25 files changed, 1330 insertions, 104 deletions
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 <utility> // index_sequence
+#include <type_traits> // aligned_storage
+#include <unordered_map>
+
+#include <build2/types>
+#include <build2/utility>
+
+#include <build2/variable>
+#include <build2/diagnostics>
+
+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<T> must be defined)
+ // names - untyped
+ // value - any type
+ // T* - NULL-able argument (here T can be names, value).
+ // optional<T> - 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<value>, 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 optional<const value_type*>>;
+
+ 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<sizeof (void*) * 3>::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 <typename D>
+ 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<D>::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<string, function_overload>;
+ 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<value> 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<value>, 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 <typename T>
+ struct function_arg
+ {
+ static const bool null = false;
+ static const bool opt = false;
+
+ static optional<const value_type*>
+ type () {return &value_traits<T>::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<T> ());
+ }
+ };
+
+ template <>
+ struct function_arg<names> // Untyped.
+ {
+ static const bool null = false;
+ static const bool opt = false;
+
+ static optional<const value_type*>
+ type () {return nullptr;}
+
+ static names&&
+ cast (value* v)
+ {
+ if (v->null)
+ throw invalid_argument ("null value");
+
+ return move (v->as<names> ());
+ }
+ };
+
+ template <>
+ struct function_arg<value> // Anytyped.
+ {
+ static const bool null = false;
+ static const bool opt = false;
+
+ static optional<const value_type*>
+ type () {return nullopt;}
+
+ static value&&
+ cast (value* v)
+ {
+ if (v->null)
+ throw invalid_argument ("null value");
+
+ return move (*v);
+ }
+ };
+
+ template <typename T>
+ struct function_arg<T*>: function_arg<T>
+ {
+ 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<T>::cast (v));
+ return &r;
+ }
+ };
+
+ template <typename T>
+ struct function_arg<optional<T>>: function_arg<T>
+ {
+ static const bool opt = true;
+
+ static optional<T>
+ cast (value* v)
+ {
+ return v != nullptr ? optional<T> (function_arg<T>::cast (v)) : nullopt;
+ }
+ };
+
+ // Number of optional arguments. Note that we currently don't check that
+ // they are all at the end.
+ //
+ template <typename A0, typename... A>
+ struct function_args_opt
+ {
+ static const size_t count = (function_arg<A0>::opt ? 1 : 0) +
+ function_args_opt<A...>::count;
+ };
+
+ template <typename A0>
+ struct function_args_opt<A0>
+ {
+ static const size_t count = (function_arg<A0>::opt ? 1 : 0);
+ };
+
+ // Argument counts/types.
+ //
+ template <typename... A>
+ struct function_args
+ {
+ static const size_t max = sizeof...(A);
+ static const size_t min = max - function_args_opt<A...>::count;
+
+ static const optional<const value_type*> types[max];
+ };
+
+ template <typename... A>
+ const optional<const value_type*>
+ function_args<A...>::types[function_args<A...>::max] = {
+ function_arg<A>::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 value_type*>* const types; // NULL
+ };
+
+ // Cast data/thunk.
+ //
+ template <typename R, typename... A>
+ 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<value>, const void*);
+ R (*const impl) (A...);
+ };
+
+ static value
+ thunk (vector_view<value> args, const void* d)
+ {
+ return thunk (move (args),
+ static_cast<const data*> (d)->impl,
+ std::index_sequence_for<A...> ());
+ }
+
+ template <size_t... i>
+ static value
+ thunk (vector_view<value> args,
+ R (*impl) (A...),
+ std::index_sequence<i...>)
+ {
+ return value (
+ impl (
+ function_arg<A>::cast (
+ i < args.size () ? &args[i] : nullptr)...));
+ }
+ };
+
+ // Specialization for void return type. In this case we return NULL value.
+ //
+ template <typename... A>
+ struct function_cast<void, A...>
+ {
+ struct data
+ {
+ value (*const thunk) (vector_view<value>, const void*);
+ void (*const impl) (A...);
+ };
+
+ static value
+ thunk (vector_view<value> args, const void* d)
+ {
+ thunk (move (args),
+ static_cast<const data*> (d)->impl,
+ std::index_sequence_for<A...> ());
+ return value (nullptr);
+ }
+
+ template <size_t... i>
+ static void
+ thunk (vector_view<value> args,
+ void (*impl) (A...),
+ std::index_sequence<i...>)
+ {
+ impl (function_arg<A>::cast (i < args.size () ? &args[i] : nullptr)...);
+ }
+ };
+
+ // Customization for member functions.
+ //
+ template <typename R, typename T>
+ struct function_cast_memf
+ {
+ struct data
+ {
+ value (*const thunk) (vector_view<value>, const void*);
+ R (T::*const impl) () const;
+ };
+
+ static value
+ thunk (vector_view<value> args, const void* d)
+ {
+ auto mf (static_cast<const data*> (d)->impl);
+ return value ((function_arg<T>::cast (&args[0]).*mf) ());
+ }
+ };
+
+ template <typename T>
+ struct function_cast_memf<void, T>
+ {
+ struct data
+ {
+ value (*const thunk) (vector_view<value>, const void*);
+ void (T::*const impl) () const;
+ };
+
+ static value
+ thunk (vector_view<value> args, const void* d)
+ {
+ auto mf (static_cast<const data*> (d)->impl);
+ (function_arg<T>::cast (args[0]).*mf) ();
+ return value (nullptr);
+ }
+ };
+
+ // Customization for data members.
+ //
+ template <typename R, typename T>
+ struct function_cast_memd
+ {
+ struct data
+ {
+ value (*const thunk) (vector_view<value>, const void*);
+ R T::*const impl;
+ };
+
+ static value
+ thunk (vector_view<value> args, const void* d)
+ {
+ auto dm (static_cast<const data*> (d)->impl);
+ return value (move (function_arg<T>::cast (&args[0]).*dm));
+ }
+ };
+
+ struct function_family::entry
+ {
+ string name;
+ const string& qual;
+ function_impl* thunk;
+
+ template <typename R, typename... A>
+ void
+ operator= (R (*impl) (A...)) &&
+ {
+ using args = function_args<A...>;
+ using cast = function_cast<R, A...>;
+
+ 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 <typename L>
+ void
+ operator= (const L& l) &&
+ {
+ move (*this).operator= (decay_lambda (&L::operator(), l));
+ }
+
+ template <typename L, typename R, typename... A>
+ static auto
+ decay_lambda (R (L::*) (A...) const, const L& l) -> R (*) (A...)
+ {
+ return static_cast<R (*) (A...)> (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 <typename R, typename T>
+ void
+ operator= (R (T::*mf) () const) &&
+ {
+ using args = function_args<T>;
+ using cast = function_cast_memf<R, T>;
+
+ 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 <typename R, typename T>
+ void
+ operator= (R T::*dm) &&
+ {
+ using args = function_args<T>;
+ using cast = function_cast_memd<R, T>;
+
+ 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 <build2/function>
+
+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<value> 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 : "<untyped>");
+ }
+
+ 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<const value_type*> t (
+ i < f.arg_types.size () ? f.arg_types[i] : nullopt);
+
+ os << (t ? (*t != nullptr ? (*t)->name : "<untyped>") : "<anytype>");
+ }
+ }
+
+ 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<value> args, const function_overload& f)
+ try
+ {
+ // Call the cast thunk.
+ //
+ struct cast_data // Prefix of function_cast::data.
+ {
+ value (*const thunk) (vector_view<value>, const void*);
+ };
+
+ auto d (reinterpret_cast<const cast_data*> (&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 value_type*>* 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 <build2/function>
+#include <build2/variable>
+
+using namespace std;
+
+namespace build2
+{
+ static value
+ path_thunk (vector_view<value> 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<path> (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<value, bool>
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 <build2/version>
+#include <build2/file>
#include <build2/scope>
+#include <build2/module>
#include <build2/target>
-#include <build2/prerequisite>
+#include <build2/context>
+#include <build2/function>
#include <build2/variable>
-#include <build2/module>
-#include <build2/file>
#include <build2/diagnostics>
-#include <build2/context>
+#include <build2/prerequisite>
using namespace std;
@@ -1658,12 +1659,14 @@ namespace build2
}
}
- value parser::
+ pair<value, bool> 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<value> (
+ 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 <typename T>
explicit
@@ -372,15 +372,17 @@ namespace build2
//
// - Specialization for vector<names> (if used and becomes critical).
//
- //
+ template <typename T, typename E>
+ struct value_traits_specialization; // enable_if'able specialization support.
+
template <typename T>
- struct value_traits;
+ struct value_traits: value_traits_specialization <T, void> {};
// {
// 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 <typename T> T convert (name&&);
template <typename T> 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 <typename T> 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 <typename T>
+ struct value_traits_specialization<T,
+ typename std::enable_if<
+ std::is_integral<T>::value &&
+ std::is_unsigned<T>::value>::type>:
+ value_traits<uint64_t> {};
+
// string
//
template <>
@@ -534,6 +551,11 @@ namespace build2
static const build2::value_type value_type;
};
+ // Treat const char* as string.
+ //
+ template <>
+ struct value_traits<const char*>: value_traits<string> {};
+
// path
//
template <>
@@ -637,6 +659,7 @@ namespace build2
{
static_assert (sizeof (vector<T>) <= value::size_, "insufficient space");
+ static vector<T> convert (names&&);
static void assign (value&, vector<T>&&);
static void append (value&, vector<T>&&);
static void prepend (value&, vector<T>&&);
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<bool>::
@@ -354,7 +380,7 @@ namespace build2
// Fall through.
}
- throw invalid_argument (string ());
+ throw_invalid_argument (n, r, "bool");
}
const char* const value_traits<bool>::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<uint64_t>::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<path>::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<dir_path>::type_name = "dir_path";
@@ -596,17 +622,26 @@ namespace build2
abs_dir_path value_traits<abs_dir_path>::
convert (name&& n, name* r)
{
- dir_path d (value_traits<dir_path>::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<abs_dir_path>::type_name = "abs_dir_path";
@@ -633,11 +668,13 @@ namespace build2
name value_traits<name>::
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<process_path>::
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<T>::convert (move (l), &r);
}
+ template <typename T>
+ inline T
+ convert (names&& ns)
+ {
+ return value_traits<T>::convert (move (ns));
+ }
+
// bool value
//
inline void value_traits<bool>::
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 <typename T>
+ vector<T> value_traits<vector<T>>::
+ convert (names&& ns)
+ {
+ vector<T> 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<T>::convert (move (n), r));
+ }
+
+ return v;
+ }
+
+ template <typename T>
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 <<EOI >>>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\(<untyped>)
+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\(<untyped>)
+ 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\(<untyped>)
+ info: candidate: ambig\(<untyped> [, uint64]), qualified name dummy.ambig
+ info: candidate: ambig\(<untyped> [, 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\(<untyped>)
+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\(<untyped>, uint64)
+EOE
+#\
+
+: print-fovl
+:
+$* <'$ambig([bool] true)' 2>>EOE != 0
+buildfile:1:2: error: unmatched call to ambig\(bool)
+ info: candidate: ambig\(<untyped> [, uint64]), qualified name dummy.ambig
+ info: candidate: ambig\(<untyped> [, string]), qualified name dummy.ambig
+EOE
+
+: print-fovl-variadic
+:
+$* <'$variadic(abc)' 2>>EOE != 0
+buildfile:1:2: error: unmatched call to variadic\(<untyped>)
+ 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 <iostream>
+
+#include <build2/types>
+#include <build2/utility>
+
+#include <build2/parser>
+#include <build2/context>
+#include <build2/function>
+#include <build2/variable>
+#include <build2/diagnostics>
+
+using namespace std;
+
+namespace build2
+{
+ static const optional<const value_type*> arg_bool[1] =
+ {
+ &value_traits<bool>::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<uint64_t> (move (a[0]));};
+
+ f["null"] = [](names* a) {return a == nullptr;};
+ f["optional"] = [](optional<names> a) {return !a;};
+
+ f["dummy0"] = []() {return "abc";};
+ f["dummy1"] = [](string s) {return s;};
+
+ f["ambig"] = [](names a, optional<string>) {return a;};
+ f["ambig"] = [](names a, optional<uint64_t>) {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<value> args, const function_overload&)
+ {
+ return value (static_cast<uint64_t> (args.size ()));
+ }));
+
+ // Dump arguments.
+ //
+ functions.insert (
+ "dump",
+ function_overload (
+ nullptr,
+ 0,
+ function_overload::arg_variadic,
+ function_overload::types (),
+ [] (vector_view<value> 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
+:
+$* <<EOI >>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 \