FINAL Helping Optimization For Infix

This feature can help with the performance/semantics of INFIX dispatch.

Background

There has historically been an optimization which, where if you say something like:

 foo: does [print "foo" 1000]
 bar: does [print "bar" 20]

And then run:

 foo bar ...
  • A lookahead fetches BAR to a holding Cell, and checks to see if BAR's value is a leftward-literal-looking infix function (like how -> preempts x in (x -> [...])

  • Since BAR's value is not a leftward-literal infix function in this case, FOO is evaluated.

  • Because FOO gathers no arguments, the holding Cell for the next value doesn't (necessarily) get wiped out. So BAR's already-cached value (might) be consulted again to see if it's an infix function that isn't left-literal. It's not an infix function at all, so the step ends.

  • As this isn't being run with EVAL:STEP, the next step can use the cached value for BAR.

Sounds Good In Theory, But...

...in practice the optimization couldn't historically be used unless FOO wasn't a function.

The problem being that if FOO did arbitrary side-effects, you couldn't trust that it didn't change BAR's value.

This meant calling a function would clear the holding cell. You could only reuse your infix knowledge in the after step when FOO was not a function.

With PURE functions, we get a leg up here. Because calling a pure function doesn't invalidate the cache.

Note that just because you're in a pure function doesn't mean that things can't change out from under you. Reusing the infix lookahead value as the next step is still dangerous due to the inner world of the pure function being mutable:

foo: pure does [
    let x: 10
    let bar: does [x: 20]
    bar x
]

There's a lookahead step in which X is fetched to see if it's a leftward-leaning infix function, and it is not.

But after you run BAR, X has mutated. So you can't reuse X for the next evaluator step (unless it was FINAL).

But I Think We Can (Should) Reuse The Cache For Infix

There's been this question of "what happens in this case":

my-add: ~

foo: does [
   my-add: infix lambda [x y] [add x y]
   1000
]

foo my-add 20

We looked up MY-ADD to see if it took its left side literally. It was VOID! for starters, so no. Then we ran FOO. At the end of FOO there's MY-ADD on the right and now it's a function that looks leftward literally.

We know that since FOO is not PURE, the value of MY-ADD can be changed so we can't take it for granted as the value for the next step.

But I feel like having to re-fetch MY-ADD to know if it's infix or not--within the same step--is a tax we don't want to pay in every such execution.

I don't want to pay that tax on the off chance that within a single evaluator step you changed an in-step WORD! from an infix function to not one (or from not one to one).

Should FINAL be a requirement for infix-dispatching WORD!s?

Meh.

It makes a slight difference in the sense that if you visit it and it's infix when you check for left-literalism, but it isn't actually left-literal infix, you can feel good about holding it in the cache and reusing it if the left doesn't that the right literally.

But the issue is if it wasn't infix when we checked for left-literalism, we aren't going to consult it again. So we wouldn't know if it wasn't and then popped into existence as an infix function.

You'd only find out if you started a new step, and something is an infix function--in which case there's a question of whether that's treated as a panic or as an infix function which claims there's nothing to its left.

I'm a little bit wary to have an infix function acting like there's nothing on its left because it popped into existence between steps. Maybe at minimum it should have to be on its own line?

In any case, the asymmetry of "you get a FINAL guarantee if it was infix, but no guarantee if it wasn't" creates a situation where you're just putting restricts on one side and not the other. People could even consider it a feature: you can change something away from an infix function, but if it was consulted before the change it will still do the infix dispatch even if the variable has changed since. If what happens is arbitrary, might as well pick the behavior that has the fastest runtime behavior.

1 Like

When you consider some of the wild combinatorics, there might be a stronger case for this than one would think, e.g.

(...): type of ...

I've described some of the nuance here where to support pure functional style, you don't want to offer the (...): to TYPE as your first idea. Because TYPE could just be a random word that's not related to the pure scope at all.

Instead you have to offer TYPE to OF first. And if it takes you up on it, then the efficient thing to do would be to remember that and build a FRAME! for OF, load the TYPE word into it, and use it.

But you still have code to run, and levels to push. That GROUP! on the left hand side still needs evaluation, and in the Ren-C world left evaluates before right.

Anyway, you can imagine something in that GROUP! disrupting OF if it isn't final. Right now the problem is the bookkeeping is difficult and you have to fetch it again, to where it might give a different answer.

I'm still kind of in the "meh" territory of "this isn't the most concerning thing in the universe to me". But looking at this does seem to lend a little more weight to the idea that maybe requiring infix functions to be FINAL has some non-trivial benefit.