How R3-Alpha Console Binding Worked

Load up an R3-Alpha session and try this:

>> print mold words-of bind? 'foobar
[system print mold words-of bind? foobar]

We've called out a "non-built-in" LIT-WORD! ('foobar), and are asking it to show us the keys of the object to which it is bound. It gives us back the so-called "user context".

It's a small-looking context (for now), which has the word system along with "magically" all of the words we just typed in. This may seem strange, because foobar aside...aren't print/mold/bind?/words-of known to live in the lib context? Shouldn't those words be bound into lib?

But consider this: if PRINT living in the lib context would make WORD! bindings go directly there, what should SET-WORD!s do? For instance, print: func [x] [write-stdout reverse copy x]. If that overwrote the FUNCTION! value of PRINT in lib, it would be gone...and a lot of lower-level and mezzanine functions would be broken because of this whimsical reassignment.

So it would seem the rules are:

  • If the input uses an ANY-WORD! that's in the user context already, bind it there and continue
  • If not already in the user context, but found in the lib context, create a new key/value in the user context which copies the existing value from lib, and bind the word to that new variable
  • If the above two fail, bind to a new unset value in the user context

Let's look for something like that.

The Code

The R3-Alpha console code loop just took that string you typed in and passed it to RL_Do_String():

while (TRUE) {
     Put_Str(PROMPT_STR);
     if ((line = Get_Str())) {
         RL_Do_String(line, 0, 0);
         RL_Print_TOS(0, RESULT_STR);
         OS_Free(line);
    }
    else break; // EOS
}

RL_Do_String merely calls over to Do_String(). This code would set up a kind of "exception handler" to trap errors. Then it would turn the textual source into structural code (via Scan_Source()), bind it, and execute it.

The code that handled the console's binding looked like this:

rc = VAL_OBJ_FRAME(Get_System(SYS_CONTEXTS, CTX_USER));
len = rc->tail;
Bind_Block(rc, BLK_HEAD(code), BIND_ALL | BIND_DEEP);
SET_INTEGER(&vali, len);
Resolve_Context(rc, Lib_Context, &vali, FALSE, 0);

So the "context to be resolved" (rc) is the user context. Considering only the user context, Bind_Block walks the passed in code from the console deeply (BIND_DEEP), looking for ANY-WORD! (BIND_ALL). It creates new bindings for anything not found already.

However--before any of that binding happened--it took a note of how many variables were already in the context. This way it can tell the difference between those that already had an entry in the user context but were unset, vs. those that weren't in the user context at all before this binding.

That information is taken advantage of by the Resolve_Context operation, which is passed the Lib_Context to use to fill any of the new unset things that happen to have entries in the Lib_Context. The code for resolving looks a bit complicated. But really it's just trying to efficiently deal with the indexes of words in the lib context:

>> index? find (words-of lib) 'print
== 262

Code being loaded will make mention of a lot of declarations from lib. Yet the list of new words is being tacked onto the end of the user context in no particular order...all of which want to be searched by word in the lib context. So all of lib is walked to put into a hash table mapping words to indices in lib, and then the new declarations are walked and checked against that table. If there's a significant number of words, then this is faster than linear searching to find each word.

So the actual mechanism is:

  • Remember what words are already in the user context...even the unset ones
  • If the input uses an ANY-WORD! that's in the user context already, bind it there and continue, else add a new unset value
  • For the lib context, build a table mapping word names to their variable index in lib
  • For all new unset values, see if they're in the mapping table, and if so bind them to lib at that index
  • Any of the new unset values that don't look up in lib are left unset
  • Uninitialize the mapping table

Clutter?

This might strike one as being a lot of clutter. You're ending up with every ANY-WORD!--that is seen in scanned source--getting a binding in the user context. This happens even if that binding will remain unset indefinitely, because its intent was always to be bound again in an object or function.

But you don't see that clutter in the lib context. Let's just randomly pick a local from R3-Alpha's minimum-of mezzanine... the /local spot:

>> find lib 'minimum-of
== true

>> find lib 'spot
== none

But try the following in a fresh R3-Alpha session, and you'll see local and spot:

>> my-function: func [/local spot] []

>> words-of bind? 'my-function
== [system my-function func local spot words-of bind?]

The code that produces lib doesn't get this extra junk, because %base-xxx.r, %sys-xxx.r, and %mezz-xxx.r files are processed using a method that's a bit more like how objects are defined. They are scanned for top-level SET-WORD!s first.