We are going to show, step by step, how to add a new built-in function to nuBASIC native API. Just for example let us assume we want to add a 1d-convolution math function which returns a convolution of two vectors. If they are vectors of polynomial coefficients, convolving them is equivalent to multiplying the two polynomials. For example, let's image to have two vectors u=(1,0,1) and v=(2,7) representing the coefficients of the polynomials x2+1 and 2x+7, in order to convolve them we want to use a nuBASIC program which employees a new function Conv as shown in the following example: Dim u(3) as Double Dim v(2) as Double Dim w(4) as Double u(0)=1 u(1)=0 u(2)=1
v(1)=7
w = Conv(u,v) For i=0 To 3 Print w(i);" "; Next i 2 7 2 7 which means the vector w contains the polynomial coefficients for the polynomial 2x3+7x2+2x+7. nuBASIC Conv prototypeSuch function Conv will have a prototype defined as follows: function conv( v1(n) as Double, v2(m) as Double) as Double(n+m-1) We could also have an additional two (optional) parameters representing the size of vectors in order to allow to reuse the same array for representing different vectors. In other words supporting a prototype like the following: function conv( v1(arraySize1) as Double, v2(arraySize2) as Double, n as Integer, m as Integer) as Double(n+m-1) Implementation of conv function in C++A possible implementation in C++ of conv function could be the following:template<typename T> std::vector<T> conv(const std::vector<T> &v1, const std::vector<T> &v2) {
const int n = int(v1.size()); const int m = int(v2.size()); const int k = n + m - 1; std::vector<T> w(k, T()); for (auto i=0; i < k; ++i) { const int jmn = (i >= m - 1) ? i - (m - 1) : 0; const int jmx = (i < n - 1) ? i : n - 1; for (auto j=jmn; j <= jmx; ++j) { w[i] += (v1[j] * v2[i - j]); } } return w; } We can just assume it works fine for our purpose. Extending the global function setIn general, to add a function to existing built-in API set we need to modify lib/nu_global_function_tbl.cc, and more in detail the static function of class global_function_tbl_t.global_function_tbl_t& global_function_tbl_t::get_instance() First time the static class function get_instance() is executed, populates a global map, which associates to each nuBASIC function name to a C++ functor (in other words a callback function) having the follow prototype: variant_t functor_name( rt_prog_ctx_t& ctx, const std::string& name, const nu::func_args_t& args ); Such functor takes as parameters:
A generic implementation of such functor requires to:
Defining new functor convTo proceed creating a conv_functor we need to modify the file lib/nu_global_function_tbl.cc. A skeleton of conv_functor could be the following: variant_t conv_functor( rt_prog_ctx_t& ctx, const std::string& name, const nu::func_args_t& args) { // Get number of arguments // TODO
// Validate the number of arguments // TODO // Process and validate the arguments // TODO // Convert the input parameters into C++ parameters // TODO
// Compute the result // TODO // Convert the result into nu::variant_t object // TODO
return result; } Get and validate the number of input argumentsAccording the two nuBASIC prototypes of Conv, we want to implement a function which accepts either 2 or 4 arguments. So, first implementation step is to get the number of the arguments, which means we need to inspect the args parameter size. We also need to check if such size is valid. Knowing args is a vector of nuBASIC expressions (see also include/nu_expr_any.h), its size() method will return the number of arguments of the nuBASIC function. variant_t conv_functor( rt_prog_ctx_t& ctx, const std::string& name, const nu::func_args_t& args) { // Get number of arguments const auto args_num = args.size();
// Validate the number of arguments // TODO
// Process and validate the arguments // TODO // Convert the input parameters into C++ parameters // TODO
// Compute the result // TODO // Convert the result into nu::variant_t object // TODO return result; } For generating an error we can call the method throw_if() of the rt_error_code_t singleton object which generates an error depending on a specific error code.
rt_prog_ctx_t& ctx, const std::string& name, const nu::func_args_t& args) { // Get number of arguments const auto args_num = args.size(); // Validate the number of arguments rt_error_code_t::get_instance().throw_if( args_num != 4 && args_num != 2, 0, rt_error_code_t::E_INVALID_ARGS, ""); // Process and validate the arguments // TODO // Convert the input parameters into C++ parameters // TODO
// Compute the result // TODO // Convert the result into nu::variant_t object // TODO return result; } In the previous source code throw_if() generates a runtime error by throwing an exception rt_error_code_t::E_INVALID_ARGS whenever the predicate args_num != 2 and args_num != 4 is true. See the rt_error_code_t class implementation (lib/nu_error_codes.cc) for more information on run-time error codes and related messages. Converting the input args into C++ equivalent parametersBecause we have validated the number of nuBASIC function arguments which is at least 2, we can now pick the first two of them up.To do that we have to get the the first two element of vector args (which are shared pointers to instance of nuBASIC expression) and call the method eval() against the runtime context related to executing nuBASIC function ctx, as shown in the following source code: variant_t conv_functor( rt_prog_ctx_t& ctx, const std::string& name, const nu::func_args_t& args) { // Get the number of input arguments // Validate the input arguments (args). rt_error_code_t::get_instance().throw_if( args_num != 4 && args_num != 2, 0, rt_error_code_t::E_INVALID_ARGS, ""); // Process and validate the arguments auto variant_v1 = args[0]->eval(ctx); auto variant_v2 = args[1]->eval(ctx); // TODO // Call the C++ function conv for processing the input data and doing the actual job. // Get the result and convert it into nu::variant_t which is how internally the result is represented. // TODO return result; }
Note that the execution order matters for respecting the original semantics which evaluates expression arguments from left to right, and the arguments are listed from left to right using the lowest index 0 to the highest index which is given by the number of arguments - 1. We have to evaluate the arguments respecting such order, otherwise we could violate the semantics introducing unexpected behaviours. Each expression, in fact, could execute functions with side effect on the global context, and the order of such side effects must be preserved. As result of the previous two highlighted statements we get two variant objects that we can assume to be vector of numbers (no matters if Integer, Float, Double, ... etc, because all of them have a safe conversion into Double). Checking the method vector_size() of a variant object we can verify if they actually represent vectors. Processing and validation of all the input argumentsNow we can proceed to check the actual vectors size. If we have just two parameters we can assume the vector size is coincident with size of array. If the number of arguments is 4, we have to process the additional two arguments assuming them integer numbers. We have to generate a runtime error whenever the first two arguments are not vectors, or in case any additional two arguments are invalid because greater than the array size. In other words, we can implement something like the following statements: variant_t conv_functor( rt_prog_ctx_t& ctx, const std::string& name, const nu::func_args_t& args) { // Get number of arguments const auto args_num = args.size(); // Validate the number of arguments rt_error_code_t::get_instance( ).throw_if( args_num != 4 && args_num != 2, 0, rt_error_code_t::E_INVALID_ ARGS, ""); // Process and validate the arguments auto variant_v1 = args[0]->eval(ctx); auto variant_v2 = args[1]->eval(ctx); const auto actual_v1_size = variant_v1.vector_size(); const auto actual_v2_size = variant_v2.vector_size(); const size_t size_v1 = args_num == 4 ? size_t(args[2]->eval(ctx).to_long64()) : actual_v1_size; const size_t size_v2 = args_num == 4 ? size_t(args[3]->eval(ctx).to_long64()) : actual_v2_size; rt_error_code_t::get_instance().throw_if( size_v1 > actual_v1_size || size_v1<1, 0, rt_error_code_t::E_INV_VECT_SIZE, args[0]->name()); rt_error_code_t::get_instance().throw_if( size_v2 > actual_v2_size || size_v2<1, 0, rt_error_code_t::E_INV_VECT_SIZE, args[1]->name()); // Convert the input parameters into C++ parameters // TODO // Compute the result // TODO // Convert the result into nu::variant_t object // TODO return result; }
Calculate the result and convert it into nu::variant_t which is how internally the result is represented.At this point, in absence of exceptions, we will have two variant objects, the input vectors, and two respective sizes: size_v1 and size_v2.We need to transform them into std::vector<double>, and we can proceed as follows: std::vector<double> v1(size_v1); bool ok = variant_v1.copy_vector_content(v1); rt_error_code_t::get_instance().throw_if( !ok, 0, rt_error_code_t::E_INV_VECT_SIZE, args[0]->name()); std::vector<double> v2(size_v2); ok = variant_v2.copy_vector_content(v2); rt_error_code_t::get_instance().throw_if( !ok, 0, rt_error_code_t::E_INV_VECT_SIZE, args[1]->name()); v1 and v2 represent C++ vectors of double (initialised to 0). The method copy_vector_content() will copy the variant_v1 content into a vector of double precision floating point numbers. If the conversion is impossible the method would fail and a runtime error will be generated as result. If we don't have any error at this point, we can call the C++ function conv which does the actual work: auto vr = conv(v1, v2); Then we can convert the result into a variant (internally typed as a vector of Double) nu::variant_t result(std::move(vr)); Finally, we can return the result: return result; And putting all together: variant_t conv_functor( rt_prog_ctx_t& ctx, const std::string& name, const nu::func_args_t& args) { // Get number of arguments const auto args_num = args.size(); // Validate the number of arguments rt_error_code_t::get_instance().throw_if( args_num != 4 && args_num != 2, 0, rt_error_code_t::E_INVALID_ARGS, ""); // Process and validate the arguments auto variant_v1 = args[0]->eval(ctx); auto variant_v2 = args[1]->eval(ctx); const auto actual_v1_size = variant_v1.vector_size(); const auto actual_v2_size = variant_v2.vector_size(); const size_t size_v1 = args_num == 4 ? size_t(args[2]->eval(ctx).to_long64()) : actual_v1_size; const size_t size_v2 = args_num == 4 ? size_t(args[3]->eval(ctx).to_long64()) : actual_v2_size; rt_error_code_t::get_instance().throw_if( size_v1 > actual_v1_size || size_v1<1, 0, rt_error_code_t::E_INV_VECT_SIZE, args[0]->name()); rt_error_code_t::get_instance().throw_if( size_v2 > actual_v2_size || size_v2<1, 0, rt_error_code_t::E_INV_VECT_SIZE, args[1]->name()); // Convert the input parameters into C++ parameters std::vector<double> v1(size_v1); std::vector<double> v2(size_v2); bool ok = variant_v1.copy_vector_content(v1); rt_error_code_t::get_instance().throw_if( !ok, 0, rt_error_code_t::E_INV_VECT_SIZE, args[0]->name()); ok = variant_v2.copy_vector_content(v2); rt_error_code_t::get_instance().throw_if( !ok, 0, rt_error_code_t::E_INV_VECT_SIZE, args[1]->name()); // Compute the result auto vr = conv(v1, v2); // Convert the result into nu::variant_t object nu::variant_t result(std::move(vr)); return result; } fmap["conv"] = conv_functor; To extend the inline help we need to add a specific entry to the _help_content collection, as follows: static help_content_t _help_content[] = {
// ...
{ lang_item_t::FUNCTION, "conv", "Returns a vector of Double as result of convolution 2 given vectors of numbers", "Conv( v1, v2 [, count1, count2 ] ))" },
// ... }; const std::set<std::string> reserved_keywords_t::list = { // ... "conv", // }; |