Up to this point, the foreign function system did not allow using callbacks via Engine::runFunctionObject() until after Engine::execute() or Engine::run() (which calls Engine::execute()) had completed. My recent update changes that and makes recursive execution of opcodes easier.

The trick involved creating a new opcode strand stack. What does this mean? Internally, Copper uses strands (lists) of opcodes per function (and the global scope). Each function has a strand. Obviously, many functions could be queued for running, so to keep them in order, I created a stack of them called the “opcode strand stack”.

When I first created the system, I didn’t concern myself with running callbacks; I figured I should get the basics correct and let the user call their callbacks after the main execution since that’s when you normally use a callback anyways. However, when I wanted to add extensions without invading Engine (that is, making them built-in), I knew I’d have issues trying to use runFunctionObject() (the method designed for running callbacks) in the middle of a call to execute(). The opcode strand stack would become messy.

Fortunately, the revamping proved to be relatively simple, and did result in a bug fix that would’ve otherwise gone unnoticed – forgetting to pop the stack of variables when done.

A New Example Usage: String Map

A number of languages have some kind of map() function (and they all seem to work slightly different depending on the language). The basic idea is applying a function to each item in an iterable (such as a list or string).

For Copper, it could be convenient to iterate through a string and decide by a function whether or not to keep a letter in an output string, and doing the string building on the C++ side would be even better. However, to make such a mechanism requires accepting a callback that has parameters for the index and the character at that index. Furthermore, this callback must be used multiple times and while the engine itself is running (that is, during Engine::execute() or Engine::run()) otherwise, it’s useless. The callback itself has to be owned by the foreign function or it will die. With all that in mind, I implemented a string_map() function whose usage can be seen in the following example:

hello = "hello world"
out = string_map(hello: [i c]{ print("i=" i: ", c=" c: "\n") if (matching(c: "l")) {ret(false) } else { ret(true) } })

The function string_map() accepts a string and a function and returns a new, independent string whose characters are determined by the function passed to string_map(). When the function given to string_map() returns true, the character is kept, but when false, it is dropped.

Bug Watch

While I did test runFunctionObject() and string_map() where the latter is used inside of user-created Copper functions, it remains to be seen whether other bugs will arise. There has been no thorough testing yet.

Foreign Function Return Change

Working with runFunctionObject() got me thinking about the return of foreign functions. Currently, they return a simple boolean. However, this doesn’t encapsulate all of the possible scenarios that could arise or the returns they should make. In the past, I have had to deliberate and arbitrarily decide if some errors were “fatal” and others were not worth considering. The latter would return “true”, resulting in these errors masquerading as “ok” or safe returns. However, in some cases, the user might be dependent on whether a function actually returned true or simply returned empty, and no one might be sitting at the console to see the warning.

In the past, there was a flag for allowing execution of code to continue even if foreign functions had an error. However, what if an error caused an application-wide crash and required a shut-down of the Copper virtual machine? Ignoring errors was a bad idea. On the other hand, trying to divide by zero should not be considered fatal to the entire program.

With these needs in mind, I have to reject the original boolean return system, despite its ease. The new return system will likely utilize an enumeration with the following values: Ok, NonfatalError, FatalError, Exit. The last one – “Exit” – is for when the user wants normal exit of a program (no error messages). In the future, it could also be received from callbacks via runFunctionObject(), which will likely soon return “Done”, not just Engine::Ok.

The sad part about all this is going back and changing lots of code I already wrote. But it’s all for the better.

Other Notes

Keep an eye out for list_map(), which should soon join string_map() in the “exts” folder.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s