So there's been a very interesting development in terms of handling things like errors or failures in the API.
It used to be that we hit a bit of a snag when code wanted to raise an error, because your C code would look something like:
if (eof)
rebElide("fail {End of file reached}");
You've called an API with no return value, and you're asking it to abruptly fail... besides terminating the program, what else can it do?
The only way to get control back to the interpreter in C is to use setjmp()/longjmp()
, which is one of those "let's completely break the execution model" things (and if you are using C++ it will corrupt your code completely, by skipping destructors).
If you're using C++, there's exceptions that are safe with respect to destructors, but it's still dodgy territory for you to call an API that throws them. And it puts us in a situation where the client of the API has to care whether the interpreter was built to use exceptions or setjmp()/longjmp()
...which it should not have to care about.
(Note: for the very creative way Ren-C has abstracted exceptions, witness the brilliance of what should be named %sys-rescue.h
)
Now There's rebDelegate() + rebContinue()
If your code is inside a native, you now have another option... to make the return value you give back to the interpreter a signal to run more code.
if (eof)
return rebDelegate("fail {End of file reached}");
This is a much cleaner idea, because you've given control back to the interpreter (even though your native is still technically "on the stack" for error-tracing purposes). You're not crossing any of your code's stack with an exception or a longjmp...just trusting the interpreter to do whatever it does.
(In the case of abrupt failure the mechanics do happen to employ a longjmp or exception. But it's under tight control that isn't your problem, and it's immediately trapped by the trampoline and converted into a cooperative throw, so it crosses very few stack levels..)
Also: It's How To Synthesize Unstable Antiforms
The notion of a handle to a "Value" exported by the API is like the notion of something you can hold in a variable in the system. So you can't hold packs or raised errors in them--only their meta/quasi forms.
That is a very wise decision. But for a time it presented a puzzle: how could a native written using only the libRebol API return a pack or raised error, etc?
Now we know. You pass back the code that produces them as a delegation.
I have a reasonable idea for how to do a polymorphic rebPack()
which could be used as a delegation,:
return rebPack(...); // would act like rebDelegate("pack [", ..., "]")
But also let you synthesize them in a code stream...
rebElide("[a b]: @", rebPack(rebI(1), rebT("hello")));
I think this can be done (due to the magic of @). The two cases just have to handle the instruction that rebPack() produces differently.
The important thing to observe though is that Value* pack = rebPack(...);
would give a compile-time error.
Flexible And Fun
Going back over old code right now in the stdio module, I'm impressed at just how clean everything is getting. This is truly seamless interop.
Looking forward to revisiting what C++ and smart pointers can do to make it even better.