Pure and Refined: Simplifying Refinements to One or Zero Args

I'm doing a bit of maintenance on the bootstrap executable, and I'm quite dizzy looking at the hoops SPECIALIZE tried to jump through to work with the original model.

We still face some questions about how to do reordering of parameters. But this stuff was crazy.

Here's some comments about the convoluted logic:

// A specialization is an ACTION! which has some of its parameters fixed.
// e.g. `ap10: specialize 'append [value: 5 + 5]` makes ap10 have all the same
// refinements available as APPEND, but otherwise just takes one series arg,
// as it will always be appending 10.
//
// The method used is to store a FRAME! in the specialization's Action Body.
// It contains non-null values for any arguments that have been specialized.
// Eval_Core_Throws() heeds these when walking parameters (see `L->special`),
// and processes slots with nulls in them normally.
//
// Code is shared between the SPECIALIZE native and specialization of a
// GET-PATH! via refinements, such as `adp: :append/dup/part`.  However,
// specifying a refinement without all its arguments is made complicated
// because ordering matters:
//
//     foo: func [/ref1 arg1 /ref2 arg2 /ref3 arg3] [...]
//
//     foo23: :foo/ref2/ref3
//     foo32: :foo/ref3/ref2
//
//     foo23 A B ;-- should give A to arg2 and B to arg3
//     foo32 A B ;-- should give B to arg2 and A to arg3
//
// Merely filling in the slots for the refinements specified with TRUE will
// not provide enough information for a call to be able to tell the difference
// between the intents.  Also, a call to `foo23/ref1 A B C` does not want to
// make arg1 A, because it should act like `foo/ref2/ref3/ref1 A B C`.
//
// The current trick for solving this efficiently involves exploiting the
// fact that refinements in exemplar frames are nominally only unspecialized
// (null), in use (LOGIC! true) or disabled (LOGIC! false).  So a REFINEMENT!
// is put in refinement slots that aren't fully specialized, to give a partial
// that should be pushed to the top of the list of refinements in use.
//
// Mechanically it's "simple", but may look a little counterintuitive.  These
// words are appearing in refinement slots that they don't have any real
// correspondence to.  It's just that they want to be able to pre-empt those
// refinements from fulfillment, while pushing to the in-use-refinements stack
// in reverse order given in the specialization.
//
// More concretely, the exemplar frame slots for `foo23: :foo/ref2/ref3` are:
//
// * REF1's slot would contain the REFINEMENT! ref3.  As Eval_Core_Throws()
//   traverses arguments it pushes ref3 as the current first-in-line to take
//   arguments at the callsite.  Yet REF1 has not been "specialized out", so
//   a call like `foo23/ref1` is legal...it's just that pushing ref3 from the
//   ref1 slot means ref1 defers gathering arguments at the callsite.
//
// * REF2's slot would contain the REFINEMENT! ref2.  This will push ref2 to
//   now be first in line in fulfillment.
//
// * REF3's slot would hold a null, having the typical appearance of not
//   being specialized.
//

If You Don't Understand That, Don't Worry About Trying

It's building a data structure inside the refinement slots that jumbles the order and has to be unpacked during traversal... it's not worth it.

Anyway, I was testing the bootstrap executable and hit a bug in this stuff and said "Oh, screw it, I'm ripping this all out."

Refinements just being their arguments... with ~null~ antiforms as the state of being not in use... is so much easier to implement, and ergonomic to use.

The Feature We Don't Have Today Is Refinement Promotion

What the old code could do (sort of) which we can't do today is transform a refinement into an ordinary argument.

apd: append:dup/
[a b c d e d e] = apd [a b c] [d e] 2

It's a valid desire, and things are in a better position to do it correctly and clearly. But the old implementation was no good, even if it showed some decent results.

So here are some tests that aren't going to work any more, ever, in the bootstrap executable:

foo: func [/A aa /B bb /C cc] [  ; Note: was before refinements were null
    return compose [
        (maybe any [A]) (maybe aa)  ; ANY makes blanks into nulls
        (maybe any [B]) (maybe bb) 
        (maybe any [C]) (maybe cc)
    ]
]

fooBC: :foo/B/C
fooCB: :foo/C/B
    
did all [ 
    [/B 10 /C 20] = fooBC 10 20
    [/A 30 /B 10 /C 20] = fooBC/A 10 20 30

    [/B 20 /C 10] = fooCB 10 20
    [/A 30 /B 20 /C 10] = fooCB/A 10 20 30

    error? sys/util/rescue [fooBC/B 1 2 3 4 5 6]
    error? sys/util/rescue [fooBC/C 1 2 3 4 5 6]
    error? sys/util/rescue [fooCB/B 1 2 3 4 5 6]
    error? sys/util/rescue [fooCB/C 1 2 3 4 5 6]
]

Here is another one:

apd: specialize 'append/part [dup: true]
apd3: specialize 'apd [count: 3]
ap2d: specialize 'apd [limit: 2]

xy: [<X> #Y]
abc: [A B C]
r: [<X> #Y A B A B A B]

did all [
    r = apd copy xy abc 2 3
    r = applique 'apd [series: copy xy  value: abc  limit: 2  count: 3]

    r = apd3 copy xy abc 2
    r = applique 'apd3 [series: copy xy  value: abc  limit: 2]

    r = ap2d copy xy abc 3
    r = applique 'ap2d [series: copy xy  value: abc  count: 3]
]

And another:

ap10d: specialize 'append/dup [value: 10]
f: make frame! :ap10d
f/series: copy [a b c]
did all [
    [a b c 10] = eval copy f
    f/count: 2
    [a b c 10 10 10] = eval f
]
1 Like