Rebol And Scopes: Well, Why Not?

Hopefully it makes sense now in retrospect.

Your example of making INTERPOLATE an argument does show another form of "contention". But in that contention, people would be biased to thinking that the preservation of the original binding is the obvious answer.

My point was that based on the contract between the caller and callee it could be either intent:

  • Maybe wrapper is supposed to supply the meaning for X... e.g. X is something you take for granted that it will know what that is, not mix it up with some local you incidentally called X

  • Maybe wrapper is not supposed to disrupt X

Anyhow, I think the proposals have evolved to where the answers are clear enough to make a prototype to explore further.

I believe I managed to pinpoint where "fully virtualized binding" went wrong, while writing thoughts on the thread "What Dialects Need From Binding".

  • It started the same way as what we are now discussing...with everything being unbound, and binding spreading down from the "tip"

  • But specifiers were trying to automatically propagate binding information in primitive operations (such as PICK and FOR-EACH), by making that propagation part of the underlying mechanics of manipulating array structure.

  • This was so that the "world is unbound" model could keep supporting a coding style of dialects that wanted to make assumptions about bindings being available and "magically working" when using structural operations, despite the lack of deep pre-pass walk to add those bindings:

    double-assigner: func [block] [
        for-each [sw i] block [
            assert [(set-word? sw) (integer? i)]
            set sw 2 * i  ; historically assumes X: and Y: are bound
        ]
    ]
    
    double-assigner [x: 10 y: 20]
    
  • That's a broken idea! Unbound material (which merely acts bound under evaluation) must be structurally extracted as unbound. "Automatic" specifier propagation should have been limited to the evaluator, and the stepwise propagation that is needed by dialects must be the responsibility of the dialect author (or the abstractions they use)...and tuned where applicable

    double-assigner: func [block] [
        for-each [sw i] block [
            assert [(set-word? sw) (integer? i)]  ; both SW and I are *unbound*
            set (in block sw) 2 * i  ; <-- the IN BLOCK makes all the difference
       ]
    ]
    
    double-assigner [x: 10 y: 20]
    

The Good News Is: The Core Mechanics Are Fine

All the cell structures--with specifiers in blocks, and linked chains and everything--were basically done right (or "well enough for now").

There's a little twist that we want special instructions in the specifier chain related to "overbinding" to be swapped out with instructions related to "hole punching". This will affect some things here and there.

But this doesn't actually need to be changed on day one. Overbinding works with the code we have today. Switching to hole punching can be a separate step.

What will be different is just that you won't get the influences of overbinding from things like FOR-EACH or PICK of a structure carrying it, unless you merge it to what you extract explicitly using IN.

The Bad News Is: Nearly Every Dialect Needs Rewriting

Where before people could FOR-EACH or PICK and get bound things back, they'll now be getting unbound things back nearly all of the time.

Dialect authors will have to get used to a programming style of running an IN operation each time they extract structure that they expect to look things up in later. (Hence choosing such a short name.)

So if you pick a block out of a block, you (usually) won't be able to do lookups inside that block unless you do a binding operation (possibly IN the block you just extracted from... or maybe somewhere else). And this process continues recursively down structure as you go.

 nested-double-assigner: func [block] [
     for-each group block [
         group: in block group
         sw: in group group.1
         set sw 2 * group.2
     ]
 ]
 
 nested-double-assigner [(x: 10) (y: 20)]

You might ask why if FOR-EACH is just going to look everything up IN the block, why not do that automatically? But (a) there's a cost to running IN, and (b) it's not going to always be what you want.
Obvious example: if you were implementing your own version of the evaluator, you'd need QUOTED! items as-is, so you didn't conflate an unbound item in the block with something bound into it's environment.

Some People :red_square: Will Say "That can't be the answer!"

But... I actually think it might be.

This is what the system has been doing internally. But it simply can't promise that it's doing the binding wiring correctly. Structural operations extracting unbound values have to give back unbound values, and trying to do otherwise will inevitably do the wrong thing.

Hopefully this can lead to areas of innovation in ways of making this simpler, when it can be. I've mused a bit about parameterizing the evaluator so it's easier to use parts of it a la carte. It may be that getting what you want out of a dialect using a set of "evaluators" (instead of tailoring parse with a particular set of "combinators") could save you the trouble of doing the binding propagation that comes from manual descent into structure.

4 Likes