Performance Implications of Antiform-FRAME!-is-Action

Another Performance Wrinkle: Looking In Two Places

During the argument gathering process, when the evaluator is visiting a slot it needs to have one of two things on hand:

  • the parameter specification (so it can know if it's a refinement or quoted or whatever while it gathers it)
    -or-
  • the specialized value

With the idea that FRAME!s are just automatically transmuted into actions by virtue of being antiform, we are saying that the specialization comes from the frame's cell for that slot if it's not "trash" (~ antiform). But if it is trash, we have to look somewhere else for the parameter information (namely the "parent" of the frame--whatever frame this was copied or instantiated from).

Having to look in two different places adds a not-insignificant cost. It means traversing two lists instead of one (incrementing two unrelated pointers), and is also a locality issue from not being able to just work on one location.

That caused me to wonder if the moment I describe of a Force_Frame_Runnable() that locks the frame and caches optimizations could sneakily write in the parameter information over any unset cells. This would be done only once, and execution could be faster.

But this would weave a tangled web. It would mean that an actual specialized frame would wind up looking like this after an execution:

>> f
== make frame! [
    series: ~#[parameter! [types: [any-series?] refinement: ~false~ ...]]~
    value: 5
    part: ~#[parameter! [types: [integer! any-series?] refinement: ~true~ ...]]~
    dup: ~#[parameter! [types: [integer!] refinement: ~true~ ...]]~
    line: ~#[parameter! [types: ~null~  refinement: ~true~ ...]]~
]

(If you're wondering what a PARAMETER! is, it's an internal type which I've been inching toward exposing as a user-visible type for manipulating parameters. Basically a compressed object--something like R3-Alpha's EVENT!--which only has very specific fields pertinent to parameter fulfillment.)

Hence I've been wracking my brain trying to figure out how to preserve the illusion of these fields being unset, when they've actually got a parameter specification in them.

It's a difficult illusion to implement, and trying to do so starts bending the codebase pretty far from the goal of simplicity, just to accomplish some more performance.

WHAT IF... We Embrace PARAMETER!-as-Unspecialized?

This way the "optimization" of using an unspecialized parameter slot to hold a parameter definition becomes the actual way in which parameters can be specified and reflected.

You could build a function from scratch by making a FRAME! with these antiform parameters in it, and then ADAPT'ing some code into it.

>> f: make frame! [x: anti make parameter! [integer!]]
== make frame! [
    x: ~#[parameter! [integer!]]~  ; anti
]

>> test: adapt f [print ["x is" x]]
== ~#[frame! ...]~  ; anti

>> test 10
x is 10

This is starting to look very much like my prophetic post: "Seeing all ACTION!s as Variadic FRAME!-makers". The idea gives the reflection we'd want of being able to extract the parameter information, as well as to be able to modify it. The help string could actually go inside the parameter as well, which would solve some bloat that we experience due to putting this in a copied state elsewhere.

This would also mean that you could specialize parameters to be trash, as an isotopic parameter would be the only kind of value you couldn't specialize to. The term for an isotopic parameter! could be "unspecialized?", so above unspecialized? f.series would return true.

What About Downsides?

Obviously the rendering is hideous, and I lament losing the elegance of the ~ that the unset state had. The web console has much more leeway than a pure text one (for this and other rendering difficulties), like being able to show a collapsed form you can expand for more details.

We could try workarounds, like simply abbreviating when things are inside the frame, and then blow them up when they were extracted:

>> f: make frame! [x: isotopic make parameter! [integer!]
== make frame! [
    x: ~#[parameter! ...]~
]

>> f.x
== ~#[parameter! [integer!]]  ; anti

You wouldn't be able to use DEFAULT to act on unspecialized fields, like f.x: default [...], unless DEFAULT was rethought to consider unspecialized states as one of the things it considers "non-valued".

Once you overwrote an unspecialized slot, there wouldn't be an easy way to undo it. Not necessarily important... just mentioning that it's different from today, where you can put a value in a slot and then change your mind by just overwriting it with trash again.

Using an unspecialized frame state wouldn't cause an error on access (though we'd presume there'd be relatively few places you could pass an antiform parameter as an argument, especially since they're not legal in parameter fulfillments since they represent the unspecialized state).

Still Only Partial Exposure of FRAME! Magic

The code that runs from a frame still lurks behind the scenes. When you say adapt frame [...] to put code into it, you get back a FRAME! that looks identical to the one you put in--at least as far as the parameters and specializations are concerned.

This is kind of par for the course. In JavaScript, digging into what it gives back for a function doesn't really give a body when you drill down...just the prototype:

And of course, native code won't show a body (though we conceivably could expose the C code as a string, at the cost of having to ship that C code in the executable and take up space).

It's good to look at something like JavaScript just to get a reminder that nothing is perfect, and at least Ren-C's model is packing some serious functionality.

1 Like