How R3-Alpha Natives Returned Their Result
The protocol for return result for natives in R3-Alpha was that an enumerated type said where the output result could be found:
enum {
R_RET = 0,
R_TOS,
R_TOS1,
R_NONE,
R_UNSET,
R_TRUE,
R_FALSE,
R_ARG1,
R_ARG2,
R_ARG3
};
Each invocation of a native pushed some space for a cell where you could write a return result, and DS_RETURN
was that arbitrary cell. If that's where the result was, the native would return R_RET
.
The other return values were shorthands to save you from having to copy or initialize a result into DS_RETURN from somewhere else. e.g. return R_TOS
meant to look for the result at the "Top Of Stack", so your native wouldn't have to copy the cell from that location and then drop an element off the stack. So it was a shorthand for:
*DS_RETURN = *DS_TOP;
DS_DROP;
return R_RET;
return R_TRUE
kept you from having to initialize the DS_RETURN
slot with a logic, hence a shorthand for:
SET_TRUE(DS_RETURN);
return R_RET;
Ren-C Writes Directly To A Target Cell
Early changes for Ren-C brought more rigor to the data stack and checks on how it was used. (I have explained many of these changes.)
It also expanded the return value of natives to be a fairly arbitrary pointer...a role that I call a Bounce
. This which can be detected as being Cells, or UTF-8 strings, or other indicators (some indicators ask for the trampoline to cycle back and run a pushed stack level, without creating a nested C stack). You can even return a C nullptr to indicate a ~null~
antiform.
But rather than having a slot on the data stack where results are expected to be written, each interpreter stack level has an OUT
pointer. When you instantiate a stack level, this pointer is specified in the instantiation...and it's supposed to be somewhere that already exists.
Notably, this pointer cannot be in the data stack...because the data stack can be resized at arbitrary moments (e.g. on a stack expansion). However, it would be possible to do something similar to R3-Alpha and have a Bounce signal that said the result lived there...which would just mean the code executing natives would copy whatever was on the top of the stack into the OUT
location at the moment of return as a convenience.
Direct Write Was Conceived As An Optimization
It would have been possible for Ren-C to have a Cell's worth of space in the "Level" representing an intepreter stack level, instead of being given a pre-existing pointer. But the concept was that saying where to write the output would save on needing to move the result after evaluation was finished.
But there's a few catches that have come up...
-
Indirect Writes Are Slower - The OUT cell is used for intermediate calculations. Locality-wise, performance has shown that writing to
L->out
is noticeably more expensive than if it were a plain cell and you were writing to&L->out
. If you do a lot of these intermediate calculations the extra dereferences wind up outweighing having to do a single move of the output cell at the end. -
Stack Suspension Gets Complex - In things like generators, you want to suspend a Level stack. When you do so, the place that was requested as "where to write to" will change... so anywhere in the suspended stack where the output-to pointer is mentioned has to be turned into a placeholder value, so that when you restore the stack with a new idea of where to write the output cell of the top of stack it has to go through and fix up those placeholders to point to the new location.
-
Handling Failures May Have Invalid Stack Locations - In the model that has been established regarding things like abrupt failures, it's possible for a stack Level to run some cleanup code if it needs to. So the throw or longjmp happens and the Trampoline catches it with the last pushed Level still intact. But if the Level's
L->out
pointer was to a cell on the stack, then it may be invalid during this handling code.
[2] was annoying to work through, but it's really [3] that I am struggling with. Things would be simpler if there was a cell as part of the Level itself, whose lifetime was equal to the Level's, where results were written.
Changing It Feels Like A Step Backward
But...
It would still be possible to avoid copying if all you were interested in was the result. (Think of something like rebUnboxInteger()
which could push a Level, do an evaluation keeping the Level on the stack, extract the integer, drop the Level, return the integer.)
I'm not thrilled, but I'll try the change and see how much damage/help it does.
ChatGPT thoughts: