Should END-able constructs all use ^META parameters?

UPDATE 2026: This is settled by <hole>.

"Endable" parameters are actually potentially BEDROCK_0 parameters ("holes") which manifest as NULL to people who don't care about the details. But you can test for them with hole? or extract the PARAMETER! underlying it with GET:DUAL.

This merges partial specialization techniques with "endability"...permitting you to have any parameter type at all act as endable, and know if it hit the end with certainty... while still having the convenience to test it for it with null (if null is not in-band of the values you expect!)


If you had a function that took a literal argument in Rebol2, it could be "endable"... if it accepted UNSET! as an argument.

This feature was added primarily for HELP, so that you could say either:

>> help
...GENERIC HELP usage information...

>> help topic
... HELP for specific TOPIC...

It was a very limited form of variadic-ness...generally used only in console-oriented commands (HELP, LS).

You couldn't use it with a function that was evaluative. Hence this wasn't possible:

redbol>> printer 1 + 2
3

redbol>> printer
You called the PRINTER function with no arguments
    ; ^-- not possible to accomplish with an otherwise evaluative argument!

So handling missing arguments was tied to taking the argument literally.

It was also ambiguous. The signal for literal parameters that were endable-and-missing was to make the parameter accept UNSET!. Which meant they couldn't tell the difference between help #[unset!] and just-plain-help:

red>> help #[unset!]
To use HELP, supply a word or value as its
argument:

    help insert
    help system
    help system/script
    ...

Ren-C Can Resolve the Ambiguity for Literal Arguments

Because antiforms can't occur in "source" code, a function taking a literal argument could receive an antiform as an unambiguous signal that the end was reached.

Any antiform could serve as that signal (and different options have been tried). But for the moment let's say it was the antiform of the word ~end~:

>> help ~end~
~end~ is a quasiform word!  ; received the quasiform of END

>> help
To use HELP, supply a word or value...  ; received the antiform of END

Normal Evaluative Arguments are Still Ambiguous

When a quasiform evaluates, it produces an antiform. So we still would have ambiguity if we tried to use the ~end~ antiform:

>> printer 1 + 2
Received 3

>> printer
Received the antiform of ~end~  

>> printer ~end~
Received the antiform of ~end~  

But ^META Arguments Can Be Unambiguous

Meta-arguments are evaluative, but if the evaluated value is not an antiform, it will just have a quote level added. Antiforms will be passed as the quasiform. This means you can never "legitimately" receive an antiform as an argument to a ^META function.

>> meta-printer 1 + 2
Received '3

>> meta-printer first ['3]
Received ''3

>> meta-printer ~end~
Received the quasiform of ~end~

>> meta-printer first [~end~]
Received '~end~

So a special rule could be invoked that endable ^META arguments give an antiform to signal the missing parameter:

>> meta-printer
Received the antiform of ~end~  

Should Only Literal / ^META Arguments Permit Endability?

The ambiguity for "normal" evaluative arguments only arises if your type checking would allow the antiform. If your function only accepts INTEGER! (for instance) then the antiform of ~end~ isn't "in-band", and it may be more convenient for you to not worry about the ^META convention.

 int-printer: func [arg [<end> integer!]] [
    either arg = ~end~ [
        print "Received end antiform"
    ][
        print ["Received integer:" arg]
    ]
]

That seems okay to me.

The system could try to stop you from shooting yourself in the foot... so that if you marked a normal parameter as <end> it would try to type check an ~end~ antiform against your parameters:

 >> anti-printer: func [arg [<end> antiform?]] [...]
 ** Error: typespec [<end> antiform?] ambiguous for ~end~ antiform

So is that annoying, or helpful?

3 Likes

So I want to talk briefly about this choice for the "end signaling antiform", and how that ties into "default values and make frame!"


Right now, when you do make frame! you get a copy of a frame with all the unspecialized slots (e.g. PARAMETER!) initialized to trash (~ antiforms):

>> frame: make frame! either/
== #[frame! [
    condition: ~
    okay-branch: ~
    null-branch: ~
]]

(Quick reminder that although what's shown there are quasiform blanks, the convention is that you interpret foo: xxx as meaning the FOO field actually holds what xxx evaluates to. So condition: ~ means that the CONDITION field holds the evaluative product of the ~ quasiform, e.g. the ~ antiform... an "unset!" if you prefer to think of it that way.)

The question on the table relates to whether normal evaluative parameters are able to receive "trash" or not. So let's imagine I did this:

>> frame.okay-branch: [print "truthy"]
>> frame.null-branch: [print "falsey"]

>> eval frame
; ...is this an error?  prints truthy?  prints falsey?

In the current paradigm, it's an error:

>> eval frame
** Script Error: either has condition unspecified (~ antiform)

So the rule is that "trash" is the one stable antiform which you can't accept as a normal argument.

If this is truly the case, it would seem like the obvious candidate for signaling an <end>, as the state is already reserved in all cases...

...or is <END> a different intent from Unspecified?

More accurately, the question is: "Even if there's a difference in intent, is it worth it for overall system complexity to have a separate antiform and handling?"

Because clearly when an end of evaluator input is reached, all subsequent argument gathering is also at the end. Whereas unspecified parameters in a frame can occur in any order.

(e.g. I didn't assign the condition field of the EITHER frame above, but then filled in both branches. That's impossible in left-to-right fulfillment to mean "the end was reached at the condition")

Hmmm. :thinking:

Case Study: MAKE FRAME! on Variadic Input

There's an experimental variation of SWITCH that allows you to use partial expressions:

>> switch2 1020 [match integer! => [#i], match tag! => [#t]]
== #i

So here you see an arity-2 function (MATCH) receiving only one argument. And the SWITCH2 fulfills the missing argument:

How it works is that the evaluator is called on the incomplete expression, and then writes an ~end~ antiform into any slots that are at the end:

>> frame: make frame! [match integer!]
== #[frame! [
    test: &[integer!]
    value: ~end~
]]

Today that's conflated: you'd get the same thing if you wrote:

>> frame: make frame! [match integer! ~end~]
== #[frame! [
    test: &[integer!]
    value: ~end~
]]

So it seems like an improvement if the situation was:

>> frame: make frame! [match integer!]
== #[frame! [
    test: &[integer!]
    value: ~
]]

>> frame: make frame! [match integer! ~]
** Script Error: match can't take (~ antiform) at callsite

The deal would just be that you can't take ~ antiforms at the callsite unless the parameter is ^META (which means you'd receive the parameter as a ~ quasiform, still leaving the ~ antiform free for unspecified-ness).

Could Typechecking "Trash" Replace The <end> Flag?

Imagine if a parameter typechecks against the trash antiform, then we assume it is "unspecifyable". So not assigning it in a frame... or reaching the end of an input evaluation... however you do it, it's all right to receive as a trash antiform.

But if it's driven by typechecking alone, then it's actually the ^META parameters that get mucked up. Because if they say they typecheck the trash antiform, they (historically) mean they will receive evaluative trash arguments as meta-trash.

So one of these things would have to be true in such a world:

  1. ^META parameters can't be endable/unspecifyable

  2. ^META parameters which accept evaluative trash conflate it with unspecifyability

  3. ^META parameters can't accept evaluative trash, and typechecking it means unspecifyable

[1] is right out. ^META is an expansive parameter convention when you've hit the limits of what a normal parameter can do. It needs to be strictly more powerful.

[2] and [3] seem to involve unpleasant tradeoffs.

Consider (set var ~) I think that needs to work, because (var): ~ works And SET has to take its value argument as ^META (for other reasons, like skipping the assignment and propagating definitonal errors). But this shouldn't imply that (set var) works.

In conclusion, unspecifyability needs to be a separate flag from typechecking.

What To Call Unspecifyability? <end> Feels Wrong Now

I've been calling refinements "optional" parameters. e.g. ":DUP is an optional argument to APPEND"

>> append.dup.optional
== ~okay~  ; anti

But maybe I should stick with the name "refinement". And this idea of being willing to be left unspecified is <opt>... true optionality. Like, you can literally omit it from the callsite.

It creates a little bit of a problem, because not specifying a refinement in a MAKE FRAME! situation--leaving it as trash--has meant that refinement argument will be set to NULL when the function runs.

But an alternative perspective on MAKE FRAME! would remove the unused refinements:

>> make frame! append/
== #[frame! [
    series: ~
    value: ~
]]

I proposed what has traditionally been thought of as MAKE FRAME! be some other operator. But maybe that operator doesn't leave things as antiform trash, yet rather sets them to null?

>> XXX append/
== #[frame! [
    series: ~null~
    value: ~null~
    part: ~null~
    dup: ~null~
    line: ~null~
]]

This could get rid of the concept that typechecking ever morphs ~ antiforms into anything else. And FWIW, could mean that there could be such a thing as an optional refinement--not that I can think of any actual applications of such a thing.

Devil's in the Details, but...

Regardless of what happens, I do think I like moving away from "endability" to "unspecifyability".

Seems fairly elegant. It's also in line with Carl's "UNSET! is not first class" proclamation:

It's important to understand the unset! datatype; otherwise, we run the risk of assuming that it is first class (assignable, passable, returnable) when it's really not intended for that kind of usage!

...although assignment is one of those "meta" circumstances where it's better to allow than disallow. While comparison and such don't count, though you can "meta-compare" an unset.

>> if ^(print "hi") = '~ [print "meta-comparison works!"]
hi
meta-comparison works!

:face_with_head_bandage:

Due to LIFT the UNIVERSE, ^META arguments no longer leave room in the "unlifted" band for special states to indicate <end> or "unspecialized". Because all variables are stored "lifted", and "unlifted" when fetched. The point of distinction is merely whether a FRAME! gets them automatically decayed before the lift.

So the only way to go any "lower" in representation than what arguments can be is the "dual band"...special states that can't be reached with SET or GET. This is the home of getters, setters, type checkers, aliases... AAAAND "true unset"... the unspecialized state.

So the questions here are:

  1. Do we want more than one true unset state in the dual band, to account for END as distinct from regular "left unspecialized"?

  2. For parameters that can't be antiforms (literal args from the callsite) is it worth it to try and exploit that to use what would normally in-band as NULL to make it easier to test as if not var vs. using unset? $var

(1. No) (2. No.)

It seems to me that just letting the true unset state cover this is the best plan.

If you mark a parameter as <end>-able (maybe not the best name for the attribute... <unset> may actually be the right name for it, now that there's a state that's "not even SET") then it should be tested with (unset? $x)

I've gotten to this to work, and it's the strongest solution yet!

It's a "bedrock" PARAMETER! state...representing an unspecialized parameter, but that manifests as NULL if you're not asking a more detailed question.

So when you MAKE FRAME! you get something with all of its fields in this state... but it acts like the fields are NULL. So they conveniently all test as falsey, and they'll correctly react to DEFAULT.

If you don't need to know the difference when checking an "endable" parameter you can just go by the fact that it's a NULL to know it was an "end". But if nulls are in-band for what your parameter can hold, you can use a deeper query that "sees beneath" the value level.

This is basically the best of both worlds.


In a system with "lots of different kinds of nothing"... unset is a loaded term, and I don't think it's very good for this.

I Think "Hole" Is The Best Name For This State

This name choice has come up before, to say that unspecialized slots were "holes" in the frame.

So that's what PARAMETER! antiforms were called. But it was recognized that making parameters antiforms was really just pushing the problem off a bit, because they still represented potentially "real things" in the frame. It was inconvenient to work with antiform parameters in enumerations, so they were turned into plain parameters... with the consequence that you could no longer specialize functions with parameters.

Now that holes are "bedrock states", you can specialize with PARAMETER! again.

This is good! Because you want to be able to do things like specialize TYPECHECKER with a PARAMETER!.


I suspect one state is enough.

It's a fairly specialized need (no pun intended) to have to distinguish a cell's representation from anything that an expression evaluation can produce. The tool you are generally supposed to reach for is usually lifted representation. The problem was just that in the function call machinery itself, it was too much of a tax to ask people to always speak in lifts.

In reality storing a "bedrock parameter!" is just an efficiency trick. What these "holes" are doing is nothing that couldn't be done by storing a FRAME! to act as a GETTER and SETTER. That frame could be queried for the parameter type, the frame could return a NULL on access. But function calls are so important to the system that doing this efficiently matters.


I've been increasingly thinking that NOTHING? might be a good question that returns truthy for any state that DEFAULT would overwrite, and then a special parameter mode would make (nothing? $var) test a variable for nothingness...sidestepping the need to have an UNSET? question in the system at all.