API Breakthrough: Scope Detection In JavaScript and C !

Background

When writing the implementation of a Rebol function using C or JavaScript code, getting at the arguments and locals of that function was something of an annoyance.

You had to use a separate rebArg() API that looked on the Rebol stack for the last Rebol function call, and retrieved arguments in that frame. This broke the flow, generating an extra API handle that had to be released:

quadrupler: js-native [num [integer!]] {
    let rebol_num = reb.Arg("num")
    let num = reb.UnboxInteger(rebol_num, "* 2")  // Rebol multiplying
    reb.Release(rebol_num)
    return reb.Integer(num * 2)  // JavaScript multiplying
}

Of course, you could have used reb.R() to make a "releasing" splice, so the variadic would release it as it went along:

quadrupler: js-native [num [integer!]] {
    let num = reb.UnboxInteger(reb.R(reb.Arg("num")), "* 2")
    return reb.Integer(num * 2)
}

This was made "prettier" cough with a fused instruction called reb.ArgR()

quadrupler: js-native [num [integer!]] {
    let num = reb.UnboxInteger(reb.ArgR("num"), "* 2")
    return reb.Integer(num * 2)
}

Kind of a headache. But even worse... what if it's a WORD! or something evaluative?

reverse-spell: js-native [
    {If REVERSE-SPELL 'FOO you get "OOF"}
    word [word!]
]{
    return reb.Spell("reverse to text!", reb.ArgR("word"));  // !!! WRONG
}

The TO TEXT! won't convert the word, because the word will be spliced into the instruction stream and looked up as a variable.. You need to quote it (or meta it, if it's an antiform):

reverse-spell: js-native [word [word!]] {
    return reb.Spell("reverse to text!", reb.Q(reb.ArgR("word")));
}

reverse-spell: js-native [word [word!]] {
    return reb.Spell("reverse to text! @", reb.ArgR("word"));  // as good as it got
}

This problem comes from what I have described as saying that all API calls are effectively doing an EVAL COMPOSE, because the C variable name isn't something Rebol can see...hence it cannot "protect" the value it holds. You'd have the same problem with:

 eval compose [reverse to text! (word)]

Wouldn't it be nice if the API knew it was inside a native?

Then it could just do the lookup by name, with normal code...:

quadrupler: js-native [num [integer!]] {
    let num = reb.UnboxInteger("num * 2")
    return reb.Integer(num * 2)
}

reverse-spell: js-native [word [word!]] {
    return reb.Spell("reverse to text! word");
}

But how would it know when you called out from the code for the native body to some service routine that also used the API... where the parameters should not be visible?

Well, I've Solved It! :star: In C/C++ and JavaScript

It's the most significant API change in a while, and it has a big impact:

C Improvements

JavaScript Improvements

How's It Done? For Starters, Pure Virtual Binding...

"Pure Virtual Binding" gives us the possibility finding function arguments and locals dynamically, once the function is already running...even if we just have text in our hand:

demo: func [arg] [
    name: "arg"
    word: inside [] to word! name  ; BLOCK! evaluation captures specifier
    print ["The value of arg is" get word]
]

>> demo 1020
The value of arg is 1020

So that's how we're managing to look up the text.

Capturing Shadowed Variables As A Proxy For Stack

The next trick is: how do functions like reb.UnboxInteger() or reb.Spell() (or their C equivalents rebUnboxInteger() or rebSpell()) know what Rebol function is currently executing... or if you're in the body of the implementation or not?

The variadic C API functions are actually macros that look like this:

#define rebSpell(...) \
    rebSpell_helper( \
        LIBREBOL_SPECIFIER,  /* captured from callsite! */ \
        __VA_ARGS__, rebEND \
    )

LIBREBOL_SPECIFIER is something that is defined before you #include "rebol.h", that gives the expression to evaluate which will give you the stack. So when you see a native definition like:

DECLARE_NATIVE(native_name_here) {
    INCLUDE_PARAMS_OF_NATIVE_NAME_HERE;
    ...
}

That expands to:

RebolBounce N_native_name_here(RebolLevel* level_) {
    RebolSpecifier librebol_specifier;
    librebol_specifier = rebSpecifierFromLevel_internal(level_)
    ...
}

This overrides a global static. If you're inside the function, the API macros will receive the specifier that's in the function, otherwise the static. It drives its decision from there.

The JavaScript uses a similar technique, but in that case it can override the reb used in the reb.Xxx() functions. There's a global reb, and then a reb that's tweaked which comes in as a parameter to the JavaScript function implementing the native.

2 Likes