Labeled TRASH! in The Wild

If you access a void variable, you don't get a lot of information back:

>> state: ~
== \~\  ; antiform (void!)

>> state
** Script Error: state is VOID!, use ^STATE to fetch

But you're not just limited to the VOID! to make "unset" variables... because you can use TRASH!...the antiform TAG!.

>> state: ~<INITIALIZE-STATE not called yet>~

; ^-- trash doesn't show up in console with an "==" result

>> state
** Script Error: state is trash: ~<INITIALIZE-STATE not called yet>~

(This concept was originally called a "tripwire", but TRASH! was favored as a shorter and more common name.)

DEFAULT reacts to TRASH! as it does to voids and nulls, considering the variable to be "defaultable"

>> defaultable? $state
== ~okay~  ; anti

>> state: default [10 + 20]
== 30

>> state
== 30

Can Be Better Than A Meandering Comment!

A comment won't direct people to the relevant issue at runtime. But a well-chosen labeled trash can.

Compare:

; !!! UPARSE is not available in SYS because it is higher level.  We hack it
; up so that when %uparse.r runs it pokes itself into sys.util/parse
;
parse: ~

With:

parse: ~<higher-level %uparse.r hasn't set SYS.UTIL/PARSE yet>~

Much better! Gives you the information you need, when you need it!

(Note: I was inspired to this concept by a style of usage @rgchris had in his scripts, using TAG!s to provide guidance for what was supposed to be filled into certain slots. I just wanted to give that some teeth, to know that the variable was conceptually unset.)

3 Likes

Labeled Trash Catches Bugs Failing Functions Won't

Prior to the existence of labeled TRASH!, there were some functions in LIB for things like RETURN*, CONTINUE*, THROW*, etc. to give you messages when you tried to use these definitional constructs in places that didn't provide them:

return*: func [] [
    fail:blame "RETURN* called when no generator is providing it" $return*
]

continue: func [] [
    fail:blame "CONTINUE called when no loop is providing it" $return*
]

throw: func [] [
    fail:blame "THROW called when no catch is providing it" $return*
]

...

Passing the $return* bound word as the :BLAME argument is a bit awkward to write. But it means the error will blame the callsite, showing the error there--instead of implicating the FAIL itself inside the stub function. That makes the error much more useful.

But this still makes it look like the functions are available. :frowning:

You won't get an error if you say return*/ to try and get a RETURN* function. You'll get a function--and it will pass the rule that things ending in slash must look up to functions--but it's not the kind of RETURN* function you actually wanted.

This led to confusing downstream bugs when some code wasn't binding correctly, but proceeded merrily along as if it had gotten a RETURN* function.

Labeled trash makes it nice and tidy!

return*: ~<RETURN used when no function generator is providing it>~

continue*: ~<CONTINUE used when no loop is providing it>~

throw*: ~<THROW used when no catch is providing it>~

...

You get the "blame" of the callsite for free, because trashes aren't function instantiations.

And if you say return*/ then that's an error, because it's not a function.

This is quite an improvement!

2 Likes

I've already shown that trashes are better for error locality than stub failure functions, because you get an error when you try to fetch a function e.g. via return*/ ... vs. getting a function that's just going to fail later.

But there's another benefit over the stub function: it gives you the right error even if you don't have a refinement, vs. saying "no refinement available"

I noticed this when I had a function for adding things to the built-in SymId table during bootstrap:

add-sym: func [
    "Add SYM_XXX to enumeration"
    return: "position of an already existing symbol if found"
        [null? block!]
    text [word! text!]
    :relax "tolerate an already-added symbol"
][
    ...
]

After the table is finalized and written to disk, you don't want more stray calls to ADD-SYM. It's bootstrap so there's no labeled trash, so I just did the old crappy stub function trick:

add-sym: does [
   fail "Symbol table finalized, can't ADD-SYM at this point"
]

But what if you called ADD-SYM:RELAX ? If you use a refinement, you don't get the informative message, you get a message about it not having the refinement:

** Script Error: incompatible or duplicate refinement: :relax

So even though your stub function doesn't require any arguments, it has to copy all the refinements as well, or you don't get a good error!

With labeled trash, it would be all good:

add-sym: ~<Symbol table finalized, can't ADD-SYM at this point>~

Just Another Big W For TRASH! :trophy:

They're kind of the sleeper hit of the isotope world...

1 Like

For this particular application... in the world of "Lift the Universe"... we have an alternative that I should point out...

...that's just to store FAILURE!s.

You have to assign them with ^META, and you have to ignore them

ignore ^return*: fail "RETURN* used when no function generator is providing it"

ignore ^continue*: fail "CONTINUE* used when no loop is providing it"

ignore ^throw*: fail "[THROW* used when no catch is providing it]"

But if you try to access them without ^META, you'll get an panic. And if you do access them but don't somehow disarm them, you'll get a panic.

Despite that, it briefly crossed my mind as: "Wait, does this mean we don't need TRASH!?" But despite this little overlap in capability, the meaningless-value has a distinct purpose... including suppressing console output, which errors aren't suited to.