What broke is that as null's role has solidified as a kind of vacant soft failure, it no longer acts implicitly as a way to opt out of things. You need void.
But void is an unstable antiform. As a simplifying rule I deemed the API should not expose handles for unstable antiforms. So while null pointers are the API representation for the stable null antiform, there is no corresponding answer for void, to be passed to a lift.
One of the original justifications of forbidding RebolValue API handles to hold unstable antiforms was that "variables can't hold unstable antiforms, so neither should API handles... you store them in lifted form as you would for a variable."
But with the advent of June's "Lift the Universe", you can store unstable antiforms in variables. So I realized that not allowing unstable antiforms in the API was an anachronism.
However, the trick is that API handles don't have a var vs. ^var distinction when splicing in intent, to know whether to decay or not. So I deemed this was a good application of the ^ operator.
We could conceivably have a rebVoid() which was a special signal (not a RebolValue*) that could be used inline and act like "^void"... or a rebGhost() which would act like "()". Then this would be legal:
RebolValue* var = rebVoid(); // wouldn't compile with "weird" rebVoid()
return rebValue("ensure date! (make-date-ymdsnz",
...,
var, // timezone (void)
")"); }
So it seems to me that rebVoid() is only useful if it returns a RebolValue at which you face a substitutability principle... how should var behave?
In the general case it simply can't act like a WORD! that shields you from the value--because APIs are like a COMPOSE where the slots have put their material in. So I think it has to be an error.
You need to lift it:
RebolValue* var = rebVoid(); // wouldn't compile with "weird" rebVoid()
return rebValue("ensure date! (make-date-ymdsnz",
...,
rebLIFT(var), // or rebL()
")"); }
Or use ^:
RebolValue* var = rebVoid(); // wouldn't compile with "weird" rebVoid()
return rebValue("ensure date! (make-date-ymdsnz",
...,
"^", var,
")"); }
For this particular case: maybe you should just use "()"... and I think we can make that pattern just be detected in the API and speed it up by synthesizing the void without invoking the scanner:
We face another design question when it comes to the impedance match of the host language and Rebol, because assignments are missing the designation of whether the target is ^META or not:
foo: (...expression...)
^foo: (...expression...)
vs.
RebolValue foo = rebValue("...expression...");
So should there be two versions... rebValue() and rebDecayedValue() ?
I think decaying needs to be the default. Hence if anything it would be rebValue() and rebUndecayedValue().
But an alternative might be to say that the evaluator tracks whether you use a "^" or not on the last evaluation, and if you do then it doesn't decay it:
RebolValue one = rebValue("pack [1 2]"); // one will be 1
RebolValue pack = rebValue("^ pack [1 2]"); // pack as ~['1 '2]~ antiform
This seems like the clearly better direction. Putting the signal inside also means that if there are variations on rebValue() you want to make, you don't have to have decaying and non-decaying entry points for them.
Quite The Revolution To Have RebolValue be Anything...
It's hard at times to absorb just how far the design has come.