Intrinsics: Functions without Frames

Redbol's historical type system really had only one design point: be fast. There were 64 fundamental datatypes, and parameters of a function could either accept each datatype or not. So a simple bitset of 64 bits was stored alongside each parameter, and checked when the function was called. That was it.

Ren-C's richer design explodes the number of "types" in the system. Not only are there more fundamental types, but antiform isotopes like ~null~ are variations on WORD!, but you don't want every function that takes a WORD! to take nulls...and you don't want to have the type checking be so broad as to take [antiform!] just because you want to be able to take nulls (because that would include splices, packs, etc.)

It's not just this reason that Redbol's type checking was too simple, but it forced my hand in coming up with some sort of answer. I couldn't think of any better idea than Lisp, which does type checking via functions ("predicates"). So I rigged it up where if you want to say a function can take an integer or null, you can write [null? integer!] You can freely mix LOGIC-returning functions with fundamental types, and we're no longer stuck with the 64 fundamental type limit.

Isn't It Slow To Call A List of Functions For Typechecking?

It can be. And in particular, it can be if you have to go through calling those functions twice.

Why twice? Because of "coercion". For example, if you pass a pack to a function that expects packs, you'll get the meta-pack:

>> foo: func [^x [pack?]] [probe x]

>> foo pack [1 "hi"]
~['1 '"hi"]~

But if your function didn't want packs, but wanted the type the pack decays to, it has to work for that as well:

>> bar: func [^x [integer?]] [probe x]

>> bar pack [1 "hi"]

Did the function want the meta form or the meta-decayed form? There's no way of knowing for sure in advance. The method chosen is to offer the meta form first, and if that doesn't match then the decayed form is offered.

It didn't know before walking through the block of functions to typecheck that a pack wouldn't have been accepted. So it had to go through offering the pack, and then offering the integer.

But I Noticed Something About These Functions...

Typically these functions are very simple:

  • They take one argument.

  • They can't fail.

  • They don't require recursive invocations of the evaluator.

This led me to wonder how hard it would be to define a class of actions whose implementations were a simple C function with an input value and output value. If you weren't in a scenario where you needed a full FRAME!, you could reach into the ACTION's definition and grab the simple C function out of it. All these functions would use the same dispatcher--that would be a simple matter of proxying the first argument of a built frame to pass it to this C function.

I decided to call these "intrinsics", which is named after a trick compilers use when they see certain function calls that they implement those functions via direct code inlining. It's not a perfect analogy, but it's similar in spirit.

It Wasn't All That Hard To Implement (relatively speaking :roll_eyes: )

All of the native function implementations were assumed to have the same type signature, taking a frame as an argument. I took away that assumption and added an /INTRINSIC refinement to the NATIVE function generator. If it was an intrinsic, then the C function in the native table would take a single value argument and an output slot to write to.

So it's still one C function per native. But if it's an intrinsic, then the function is not a dispatcher... the Intrinsic_Dispatcher() is used, and the C function is poked into the properties of the function.

Callsites that want to optimize for intrinsics just look to see if an action has the Intrinsic_Dispatcher(), and if so they have to take responsibility for procuring an argument and type checking it. But if they do, they can just call the C function directly with no frame overhead.

This helps make the switchover to functions in type spec blocks much more palatable. It's never going to be as fast as the bitset checking, but it's fast enough to allow things to make progress.