If this optimization is going to work, then it also needs a complement to the "unbounds optimized to find their membership in the frame, if you ask"... which is "unbounds optimized to NOT find their membership in the frame, if you ask".
Consider this:
y: <something>
foo: func [x <local> a b c d e f g h i j ...] [ ; imagine costly to lookup
code: copy [add x]
append code to word! "x"
do code
]
The [add x] block gets bound to the environment before it is passed to COPY. That environment can find X and Y and ADD and whatever else.
What we're talking about in the optimization is that when the function body was copied, the X of [add x] got a binding pasted on it to say "if you try and look X up in a frame instance for the foo function, here's the index of X...you don't have to look". The index and the pointer to the function definition can fit in the cell.
But that X is conceptually unbound. There's no reason the other appended X that has no binding whatsoever shouldn't be found just as legitimately as it. (I used TO WORD! "X" here instead of just 'X because quoted things would need to be labeled with the lookup optimization too...or at least not labeled with the negative optimization. Again, it's conceptually still unbound. But if you convert a string to a word at runtime, there's no optimization there.)
By extension, all unbound material needs to be considered in the search when the frame is in the specifier. You can't rule out looking to see if ADD is in the frame just because it doesn't have the optimization applied, for the same reason you can't rule out looking for the no-optimized-binding X.
So that's why I mention the negative hint. All unbound material that's not in the frame during the copy would be tagged with the action definition to say "don't bother looking me up in the environment for a frame instance of this action".
Dynamic material run with the frame context applied that's unbound won't have this information. But if it's unbound, there's no harm in having the lookup cache the information about whether the lookup succeeded or not. Only one frame lookup can win, so it would either need to be sticky with the first lookup done or re-cache (based on last lookup, or maybe if a frame comes along that's bigger than the one cached...)
Note that what this is replacing was a model by which bindings would be fully "hardened" inside the function body during the copy. So everything in the body would point directly (or relatively) at what it needed to look up to, giving speed in future runs (modulo the various "overbindings" that would override bindings for things like loop variables during a FOR-EACH, etc.)
So the example I cite would not work (it would work if you used 'x instead of to word! "x"):
foo: func [x] [
code: copy [add x]
append code to word! "x"
do code
]
>> foo 10
** Script Error: x word is not bound to a context
Should be able to make it work in the new model, without needing to explicitly bind the word to the frame instance.
Keeping the body mostly unbound across every run has nice properties, but it'll be slow without some tricks like this optimization in play.
...Another Argument For The .member
Selection Notation
With binding being purely virtual, there's competition not just from multiple frames (e.g. nested functions) over the same cell's cache, but also lookups for object members.
This seems to make an interesting case for being able to tell from the value itself if it needs to be looked up in an object.
Without that:
o: make object! [
field1: <one>
field2: <two>
... ; imagine a non-trivial number of fields
foo: method [arg1 arg2] [
return reduce [arg1 field1 arg2 field2]
]
]
When that method runs, it gets a specifier for the object instance and for the frame of FOO. So if we label everything in the body that gets copied as either "unbound, but findable in foo frames" or "unbound, and NOT findable in foo frames"... that's used up all the cache space in the cells.
But if we know that .xxx will never look up to a function argument, but only in the members, the caching can be saved relative to the object prototype.
o: new object [ ; something where FIELD1 and FIELD2 don't bind deeply
field1: <one>
field2: <two>
... ; imagine a non-trivial number of fields
foo: method [arg1 arg2] [
return reduce [arg1 .field1 arg2 .field2]
]
]
This is on top of the benefit I've already mentioned of "giving you some chance of figuring out what is going on".
Note: Module Lookup Is Already (Sort Of) Optimized
The way module lookup works is that words hold a symbol, and that symbol points to a linked list of variable stubs--one for each module that defines that variable.
This wouldn't scale well for objects (think 10000 objects which all had a field "X", searching that list would be slow). But for modules it's fairly fast, as few modules define the same symbol. Also, once a word becomes bound it points directly to the stub for the variable.
(Just mentioning in case you were wondering why I'm fretting over lookup in frames and objects but not the cost of lookup in modules.)