// file : build2/function.hxx -*- C++ -*- // copyright : Copyright (c) 2014-2019 Code Synthesis Ltd // license : MIT; see accompanying LICENSE file #ifndef BUILD2_FUNCTION_HXX #define BUILD2_FUNCTION_HXX #include #include // index_sequence #include // aligned_storage #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* - NULL-able any type (never NULL itself, use value::null) // 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. // // A function can also optionally receive the current scope by having the // first argument of the const scope* type. It may be NULL if the function // is called out of any scope (e.g., command line). // // Note also that we don't pass the location to the function instead // printing the info message pointing to the call site. // // A function can return value or anything that can be converted to value. // In particular, if a function returns optional, then the result will be // either NULL or value of type T. // // 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). // // Note that normally there will be a function overload that has all the // parameters untyped with an implementation that falls back to one of the // overloads that have all the parameters typed, possibly inferring the type // from the argument value "syntax" (e.g., presence of a trailing slash for // a directory path). // struct function_overload; using function_impl = value (const scope*, vector_view, const function_overload&); struct function_overload { const char* name; // Set to point to key by insert() below. const char* alt_name; // Alternative name, NULL if none. This is the // qualified name for unqualified or vice verse. // 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 (const char* an, size_t mi, size_t ma, types ts, function_impl* im) : alt_name (an), arg_min (mi), arg_max (ma), arg_types (move (ts)), impl (im) {} template function_overload (const char* an, size_t mi, size_t ma, types ts, function_impl* im, D d) : function_overload (an, mi, ma, move (ts), im) { // std::is_pod appears to be broken in VC16 and also in GCC up to // 5 (pointers to members). // #if !((defined(_MSC_VER) && _MSC_VER < 2000) || \ (defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 5)) static_assert (std::is_pod::value, "type is not POD"); #endif static_assert (sizeof (D) <= data_size, "insufficient space"); new (&data) D (move (d)); } }; ostream& operator<< (ostream&, const function_overload&); // Print signature. class function_map { public: using map_type = std::multimap; using iterator = map_type::iterator; using const_iterator = map_type::const_iterator; iterator insert (string name, function_overload); void erase (iterator i) {map_.erase (i);} value call (const scope* base, const string& name, vector_view args, const location& l) const { return call (base, name, args, l, true).first; } // As above but do not fail if no match was found (but still do if the // match is ambiguous). Instead return an indication of whether the call // was made. Used to issue custom diagnostics when calling internal // functions. // pair try_call (const scope* base, const string& name, vector_view args, const location& l) const { return call (base, name, args, l, false); } iterator begin () {return map_.begin ();} iterator end () {return map_.end ();} const_iterator begin () const {return map_.begin ();} const_iterator end () const {return map_.end ();} // Return true if the function with this name is already defined. If the // name ends with '.', then instead check if any function with this prefix // (which we call a family) is already defined. // bool defined (const string&) const; private: pair call (const scope*, const string&, vector_view, const location&, bool fail) const; map_type map_; }; extern function_map functions; class function_family { public: // The call() function above catches invalid_argument and issues // diagnostics by assuming it is related to function arguments and // contains useful description. // // In order to catch additional exceptions, you can implement a custom // thunk which would normally call this default implementation. // static value default_thunk (const scope*, 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; static bool defined (string qual) { qual += '.'; return functions.defined (qual); } 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 constexpr 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 constexpr 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 constexpr 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 null = true; static value* cast (value* v) {return v;} // NULL indicator in value::null. }; 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; // VC15 doesn't realize that a pointer to static object (in our case it is // &value_trair::value_type) is constexpr. // #if !defined(_MSC_VER) || _MSC_VER > 1910 static constexpr const optional types[max] = { function_arg::type ()...}; #else static const optional types[max]; #endif }; template #if !defined(_MSC_VER) || _MSC_VER > 1910 constexpr const optional function_args::types[function_args::max]; #else const optional function_args::types[function_args::max] = { function_arg::type ()...}; #endif // Specialization for no arguments. // template <> struct function_args<> { static const size_t max = 0; static const size_t min = 0; #if !defined(_MSC_VER) || _MSC_VER > 1910 static constexpr const optional* types = nullptr; #else static const optional* const types; #endif }; // 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) (const scope*, vector_view, const void*); R (*const impl) (A...); }; static value thunk (const scope*, 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 functions that expect the current scope as a first // argument. // template struct function_cast { struct data { value (*const thunk) (const scope*, vector_view, const void*); R (*const impl) (const scope*, A...); }; static value thunk (const scope* base, vector_view args, const void* d) { return thunk (base, move (args), static_cast (d)->impl, std::index_sequence_for ()); } template static value thunk (const scope* base, vector_view args, R (*impl) (const scope*, A...), std::index_sequence) { return value ( impl (base, 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) (const scope*, vector_view, const void*); void (*const impl) (A...); }; static value thunk (const scope*, 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)...); } }; template struct function_cast { struct data { value (*const thunk) (const scope*, vector_view, const void*); void (*const impl) (const scope*, A...); }; static value thunk (const scope* base, vector_view args, const void* d) { thunk (base, move (args), static_cast (d)->impl, std::index_sequence_for ()); return value (nullptr); } template static void thunk (const scope* base, vector_view args, void (*impl) (const scope*, A...), std::index_sequence) { impl (base, function_arg::cast (i < args.size () ? &args[i] : nullptr)...); } }; // Customization for coerced lambdas (see below). // #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 6 template struct function_cast_lamb { struct data { value (*const thunk) (const scope*, vector_view, const void*); R (L::*const impl) (A...) const; }; static value thunk (const scope*, 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 (L::*impl) (A...) const, std::index_sequence) { const L* l (nullptr); // Undefined behavior. return value ( (l->*impl) ( function_arg::cast ( i < args.size () ? &args[i] : nullptr)...)); } }; template struct function_cast_lamb { struct data { value (*const thunk) (const scope*, vector_view, const void*); R (L::*const impl) (const scope*, A...) const; }; static value thunk (const scope* base, vector_view args, const void* d) { return thunk (base, move (args), static_cast (d)->impl, std::index_sequence_for ()); } template static value thunk (const scope* base, vector_view args, R (L::*impl) (const scope*, A...) const, std::index_sequence) { const L* l (nullptr); // Undefined behavior. return value ( (l->*impl) (base, function_arg::cast ( i < args.size () ? &args[i] : nullptr)...)); } }; template struct function_cast_lamb { struct data { value (*const thunk) (const scope*, vector_view, const void*); void (L::*const impl) (A...) const; }; static value thunk (const scope*, 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 (L::*impl) (A...) const, std::index_sequence) { const L* l (nullptr); (l->*impl) ( function_arg::cast ( i < args.size () ? &args[i] : nullptr)...); } }; template struct function_cast_lamb { struct data { value (*const thunk) (const scope*, vector_view, const void*); void (L::*const impl) (const scope*, A...) const; }; static value thunk (const scope* base, vector_view args, const void* d) { thunk (base, move (args), static_cast (d)->impl, std::index_sequence_for ()); return value (nullptr); } template static void thunk (const scope* base, vector_view args, void (L::*impl) (const scope*, A...) const, std::index_sequence) { const L* l (nullptr); (l->*impl) (base, function_arg::cast ( i < args.size () ? &args[i] : nullptr)...); } }; #endif // Customization for member functions. // template struct function_cast_memf { struct data { value (*const thunk) (const scope*, vector_view, const void*); R (T::*const impl) () const; }; static value thunk (const scope*, 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) (const scope*, vector_view, const void*); void (T::*const impl) () const; }; static value thunk (const scope*, 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) (const scope*, vector_view, const void*); R T::*const impl; }; static value thunk (const scope*, 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})); } template void operator= (R (*impl) (const scope*, 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. // // GCC up until version 6 has a bug (#62052) that is triggered by calling // a lambda that takes a by-value argument via its "decayed" function // pointer. To work around this we are not going to decay it and instead // will call its operator() on NULL pointer; yes, undefined behavior, but // better than a guaranteed crash. // #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 6 template void operator= (const L&) && { move (*this).coerce_lambda (&L::operator()); } template void coerce_lambda (R (L::*op) (A...) const) && { using args = function_args; using cast = function_cast_lamb; insert (move (name), function_overload ( nullptr, args::min, args::max, function_overload::types (args::types, args::max), thunk, typename cast::data {&cast::thunk, op})); } template void coerce_lambda (R (L::*op) (const scope*, A...) const) && { using args = function_args; using cast = function_cast_lamb; insert (move (name), function_overload ( nullptr, args::min, args::max, function_overload::types (args::types, args::max), thunk, typename cast::data {&cast::thunk, op})); } #else 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); } #endif // 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_HXX