Every evaluation step is asked to target an output cell. Before the evaluation, a single bit is set on that cell to say it is "stale". So if the evaluation doesn't write to it--and confirms that wasn't a mistake--then the old result is left around to recover.
The ability to recover the previous result with the flip of a bit is used for invisibility. It's used not just in the evaluator when it goes step by step, but also in things like ANY and ALL.
>> 1 + 2 comment "in the evaluator" == 3 >> all [1 + 2 comment "here too"] == 3
Doing it this way has sacrificed some features. For instance, you can't make an "invisible" enfix function:
>> foo: enfix func [left] [ print ["Left was" left] return void ] >> 304 1020 foo Left was 1020 == 1020
The 1020 from the previous evaluation was used as an argument. But after "consuming all its arguments" the product of FOO could not leave the 304 there. If each evaluation step was to a temporary cell, that temporary cell could be used to fill the enfix slot of FOO... and 304 could be left.
Doing it this way has also required acrobatics to accomplish non-negotiable features. The related problem of making it possible for an enfix function to perceive voidness on the left hand side requires stale bit mechanics that aren't for the faint of heart... e.g. to differentiate these two cases:
>> (else [print "Won't work"]) ** Error >> () else [print "Will work"] Will work
Is All The Bit-Fiddling Worth It Vs. Copying?
Considering the small size of cells (4 platform pointers), the logic to test and clear the "stale" bit may seem to add overhead and complexity that isn't saving that much. Instead, every evaluation could be done into a temporary slot...and then if not invisible, the 4 pointers could be moved.
This is actually a bit misleading--because copying cells is actually a bit more expensive in the general case. Cell format flags have to be checked, bindings may need to be managed, and if a reference count mechanic is implemented this could make it all worse.
Less copying is desirable, and it seems neat to have achieved invisibility thus far without needing an extra eval per-eval-step.
"So if the evaluation doesn't write to it--and confirms that wasn't a mistake..."
This is one of the main reasons I've stuck with the current method. It's useful for debug purposes to know if a native just forgot to write an output cell anyway. So I figured: "so long as the output cell is going to have a flag on it saying it hasn't been written to yet, why not make that flag able to coexist with the previous value...and hence avoid a mechanic of needing to copy every time?"
But Isotopes Mean It's Time For Change
Early on I observed that there was no way to get this to work:
>> 1000 + 20 if true [comment "hi"] == 1020 ; not possible
The IF had to produce something as a proxy for VOID that wasn't void... in order to signal a taken branch (we want THEN to run).
But even if that proxy was able to decay to a void state, it was too late. It had overwritten the output. Today that proxy is a parameter pack with a meta-void in it:
There's more stuff with parameter packs that should work, like this:
>> 1000 + 20 [x @y]: pack [304 void] == 1020
And isotopic objects that represent lazy evaluations should be able to produce void, too. They're a proxy for behavior, and if you pick and choose behaviors that could be accomplished with a normal result that a REIFY method on a lazy object can't, you're saying they're not as powerful.
These features tip the scales. And really, the circuitous nature of void enfix handling was already tipping them.
The concerns over copying are mostly addressed by something I'm calling "cell movement"; this means we can really get closer to the 4 platform pointer copies, because you're destroying the old cell in the process. So if techniques like reference counting came along, you're not adding and removing them--you're just letting the new cell take over the resources of the old.
Plus, detecting whether a cell has been written to or not is a generic debug feature now that has easy coverage.
The stale bit is thus on the chopping block. So expect more robust void-related behavior coming soonish.