Extending nuBASIC
How to extend the built-in function set
In this step-by-step guide, we will demonstrate how to add a new built-in function to the nuBASIC native API. As an example, let's consider adding a 1D-convolution math function that calculates the convolution of two vectors. When the vectors represent polynomial coefficients, convolving them is equivalent to multiplying the two polynomials.
To illustrate this, let's assume we have two vectors: u=(1,0,1) and v=(2,7), representing the coefficients of the polynomials x^2+1 and 2x+7, respectively. We aim to create a nuBASIC program that utilizes a new function, Conv, to perform the convolution. Here's an example of how the program would look:
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(0)=2
v(1)=7
w = Conv(u,v)
For i=0 To 3
Print w(i);" ";
Next i
After executing the program with the provided input, the expected output is as follows:
2 7 2 7
This output indicates that the resulting vector, denoted as "w," contains the polynomial coefficients for the polynomial 2x^3 + 7x^2 + 2x + 7.
By convolving the two input vectors, u=(1, 0, 1) and v=(2, 7), the nuBASIC program successfully performs the desired operation and generates the corresponding polynomial coefficients in the output vector w.
nuBASIC Conv prototype
The function Conv can be defined with the following prototype:
function conv( v1(n) as Double, v2(m) as Double) as Double(n+m-1)
Additionally, we can include two optional parameters that represent the size of the vectors. This allows for reusing the same array to represent different vectors. The modified prototype would look like this:
function conv( v1(arraySize1) as Double, v2(arraySize2) as Double, n as Integer, m as Integer) as Double(n+m-1)
Here, the parameters arraySize1 and arraySize2 indicate the size of the arrays representing the vectors, while n and m represent the actual sizes of the vectors. The condition arraySize1 >= n > 0 ensures that the array size is greater than or equal to the size of vector v1, and similarly, arraySize2 >= m > 0 ensures the array size is greater than or equal to the size of vector v2.
Implementation of conv function in C++
Here is a possible implementation of the conv function in C++:
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;
}
This implementation takes two vectors, v1 and v2, as input and returns the convolution result as a new vector. It first determines the sizes of the input vectors, n and m, and calculates the size of the resulting vector as k = n + m - 1. Then, it creates a new vector w with the appropriate size, initialized with zeros.
Next, the function performs the convolution operation using nested loops. It multiplies the corresponding elements of v1 and v2 and accumulates the results at the appropriate index in the w vector.
Finally, the resulting vector w is returned.
Extending the global function set
To add a new function to the existing built-in API set, you would need to make modifications to the lib/nu_global_function_tbl.cc file, specifically within the global_function_tbl_t class and its static function get_instance().
The get_instance() function is responsible for populating a global map that associates each nuBASIC function name with a C++ functor (callback function). The prototype of the functor is as follows:
variant_t functor_name( rt_prog_ctx_t& ctx, const std::string& name, const nu::func_args_t& args );
The functor takes three parameters:
ctx: A runtime context defined in include/nu_rt_prog_ctx.h.
name: The function name (note that multiple names can be mapped to the same functor).
args: A vector of arguments, where each argument is a nuBASIC expression.
To implement the functor, you would typically follow these steps:
Determine the number of input arguments (args size) and validate it.
Convert and validate each input argument into its equivalent C++ object representation.
Call the C++ function that performs the actual computation or task (which can also be implemented inline).
Convert the result of the computation into a nu::variant_t object, which is how nuBASIC interpreter represents its data.
Return the result.
By modifying the global_function_tbl_t class and implementing your specific functor for the new function, you can seamlessly integrate it into the nuBASIC interpreter's built-in API.
Defining new conv functor
To create a conv_functor for the conv function, you would need to make modifications to the lib/nu_global_function_tbl.cc file. The skeleton of the conv_functor could be as follows:
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 arguments
To implement a function that accepts either 2 or 4 arguments in accordance with the two nuBASIC prototypes of Conv, you would need to inspect the size of the args parameter and perform validation. The args parameter is a vector of nuBASIC expressions. Here's an example of how you can achieve this:
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;
}
To validate the number of arguments we have to check if it is 2 or 4, otherwise we have to generate a run-time error.
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.
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
// 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, the throw_if() function generates a runtime error by throwing an exception of type rt_error_code_t::E_INVALID_ARGS when the predicate args_num != 2 and args_num != 4 is true. This ensures that an exception is thrown when the number of arguments is not valid.
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 parameters
Since we have already validated the number of nuBASIC function arguments to be at least 2, we can proceed to retrieve the first two arguments from the args vector. We can accomplish this by accessing the shared pointers to instances of nuBASIC expressions and calling the eval() method using the runtime context associated with executing the nuBASIC function (ctx). Here's the updated code snippet:
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
const auto args_num = args.size();
// 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);
// ...
// Call the C++ function conv for processing the input data and doing the actual job.
// TODO
// Get the result and convert it into nu::variant_t which is how internally the result is represented.
// ...
return result;
}
In the updated code, we retrieve the first two arguments by accessing the elements at index 0 and 1 of the args vector. We then call the eval() method on each argument, passing in the runtime context ctx, and store the evaluated results in arg1 and arg2. These variables will now hold the evaluated values of the first two nuBASIC function arguments.
It is important to respect the original semantics of evaluating expression arguments from left to right, as well as the order in which the arguments are listed in the args vector. To ensure this, we need to evaluate the arguments in the correct order to avoid introducing unexpected behaviors. Each expression may execute functions with side effects on the global context, and the order of these side effects must be preserved.
Once we have evaluated the first two arguments and obtained the resulting variant objects, we can assume that they represent vectors of numbers. It doesn't matter if they are of type Integer, Float, Double, or any other numeric type, as they can be safely converted to Double. To verify if the evaluated objects actually represent vectors, we can use the vector_size() method of a variant object.
Processing and validation of all the input arguments
Now we can move on to processing and validating all the input arguments. We will check the actual size of the vectors. If we have only two parameters, we can assume that the vector size is the same as the array size.
If the number of arguments is four, we need to process the additional two arguments as integers. It is important to generate a runtime error if the first two arguments are not vectors or if any of the additional two arguments are invalid, meaning they are greater than the array size.
Here's an updated code snippet that incorporates these checks:
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 arguments
rt_error_code_t::get_instance().throw_if(
args_num != 4 && args_num != 2, 0, rt_error_code_t::E_INVALID_ARGS, "");
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.
Now we can proceed to calculate the result and convert it into a nu::variant_t, which is the internal representation of the result.
Assuming we have successfully processed the input arguments and obtained the two variant objects (arg1 and arg2) representing the input vectors, as well as their respective sizes (size1 and size2), we can transform them into std::vector<double>. Here's an updated code snippet:
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());
Assuming v1 and v2 are C++ vectors of type double initialized to 0, we can proceed with copying the content of variant_v1 into a vector of double precision floating-point numbers using the copy_vector_content() method. If the conversion is not possible, the method will fail and generate a runtime error. If no errors occur at this point, we can call the C++ function conv to perform the actual computation:
auto vr = conv(v1, v2);
After calling the conv function and obtaining the result in vr, we create a nu::variant_t object named result by moving the result vector vr into it.
nu::variant_t result(std::move(vr));
Finally, we return the result variant as the output of the function.
return result;
Putting it all together, the modified code would look like this:
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;
}
To map the conv function and insert the functor into the fmap defined inside the global_function_tbl_t::get_instance() function, you would modify the nu_builtin_help.cc file as follows:
fmap["conv"] = conv_functor;
This ensures that the conv function is mapped to the conv_functor callback function.
Next, to extend the inline help and include information about the conv function, you would add a specific entry to the _help_content collection in the nu_builtin_help.cc file:
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 ] ))" },
// ...
};
This entry provides a description of the conv function, its usage syntax, and a brief explanation of what it does. This will allow users to access information about the conv function using commands like help and apropos from the nuBASIC console.
Finally, to add conv to the list of reserved words, you would modify the nu_reserved_keywords.cc file, specifically the reserved_keywords_t::list set:
const std::set<std::string> reserved_keywords_t::list = {
// ...
"conv",
//
};
By including "conv" in the list of reserved keywords, you ensure that it is recognized as a valid keyword in the nuBASIC language.
nuBASIC IDE
The nuBASIC IDE (Integrated Development Environment) for Windows and Linux/GTK+ offers a syntax highlighting editor and a powerful debugger, making it an excellent alternative to the inline editor of CLI.
Key Features:
Syntax Highlighting Recognizes keywords and displays them in different colors for easier reading and writing of code.
Debugger: Includes advanced features such as setting breakpoints, adding field watches, stepping through code, running into procedures, taking snapshots, and monitoring execution in real-time.
These features provide comprehensive tools for software development, making the nuBASIC IDE particularly useful for writing and debugging complex programs.