EVAL + EVALUATE and REEVAL + REEVALUATE

Internally to the Ren-C code, the term "Eval_Xxx()" has come to imply one step of evaluation, while "Do_Xxx" will take in something like a block and always execute it to the end. Hence a DO is a sequence of N "steps" of EVAL.

I mentioned in "Re-imagining DO/NEXT" why changing the interface was needed, so that DO/NEXT became a new routine called EVALUATE:

 >> var: null

 >> block: [1 + 2 10 + 20]

 >> block: evaluate/set block 'var
 == [10 + 20]

 >> var
 == 3

 >> block: evaluate/set block 'var
 == []  ; Notice it did not go directly to `null` when there was a result

 >> var
 == 30

 >> block: evaluate/set block 'var
 ; null  ; null is only signaled when the evaluation had no result

 >> var
 == 30  ; ... in which case the variable you passed in is undisturbed

It has a subtle behavior of leaving a lingering "empty" step before returning null and leaving the variable as-is. This turns out to be foundational. It allows invisibles to remain pending and not run "too soon" (imagine if the last step was actually [comment "hi"] and not [], e.g. not being at the end of the block isn't the only way you can wind up in a situation where there's not going to be a value coming from the next evaluation...so the steps need to be consistent).

For this to work, it really does require you to pass a variable in. Even if there was a clean implementation of multiple return values, you don't want to overwrite a previous result on a last potentially-vaporizing step. (The mechanics of how this is done inside the evaluator are actually rather slick, but beyond the scope of this post. Userspace you have to do it by passing in a variable that is potentially left as-is.)

However, I think we can improve this interface now with @words as skippable parameters:

 >> block: evaluate @var block
 == [10 + 20]

If you provide an @(...expression...) or @var it will be quoted and used. If not, it will just take the block to evaluate. That looks nice to me!

Naming Collision: EVAL

When EVALUATE came on the scene, there was already a native called EVAL which did something useful...also, exactly one step:

 >> x: null

 >> eval (lit x:) 1 + 2
 == 3

 >> x
 == 3

Hence this EVAL is a very tricky variadic that takes a first normal (hence evaluated) argument, and then re-evaluates it as if the expression had been there all along. You'd have quite a hard time writing such a thing yourself (!) but what it does is use some slippery code to re-feed the evaluator with a previous output cell.

However, I'm not very comfortable with having eval be anything other than a shorter name for evaluate. So what if we made those two synonyms, and called this one reevaluate with the shorthand reeval?

I think that cleans up some confusion, and would bring the userspace terminology more in sync with the C implementation.

I implemented this and it feels nice and solid.

While doing it, I went ahead and made it possible to use PATH!s. You could not do that with DO/NEXT (e.g. pos: do/next pos 'obj/field), but now you can:

 pos: evaluate @obj/field pos

It feels good for EVAL and EVALUATE to be the same function, so worth it to rename REEVALUATE / REEVAL. It doesn't actually get used as often as I thought it might, anyway. Whereas EVAL is the new and improved DO/NEXT, so having a short name available is good.

The skippable parameter which holds the @-word, @-path, or @-group is called VAR. See my remarks about why that's a pretty good name for this argument, and why Rebol as a language probably shouldn't take the word var to mean declaring a local...due to the reflective nature of wanting to talk about variables as variables themselves.

One kind of unfortunate side-effect of the type system is there's no way for an APPLY which is going to specify VAR directly to use a plain WORD! or PATH! here. Because using a plain word would mean that if you weren't applying, you would wind up quoting it at the callsite...and we want evaluate block to mean do one step of evaluation with no variable writing the result. So you have to say:

 block: [1 + 2]
 my-word: 'word-to-write-result-to
 f: make frame! :eval
 f/array: block
 f/var: as sym-word! my-word

Though you can actually write that also as:

 f/var: @(my-word)

Not to be confused with f/var: @my-word, which would mean actually to write the result into the "my-word" variable. :-/

It's not so bad, although saying @(my-word) would mean the group evaluation doesn't happen until you actually run the frame. In today's world, this could mean that the frame where my-word lives could expire, meaning you'd get an error if that call was too late.

...but I really think that shouldn't be an issue, because the "closure" semantics pretty much need to be the default. It would really kind of suck to be beaten by the likes of JavaScript on a feature like this. They can closure-capture variables wherever they want, so even if it means worse performance in the near term I think we should do the same and then figure out how to make it more efficient later.

1 Like