I've been bitten a lot over the years when a value that has no side effects just gets dropped on the ground.
For example: say I added a branch to an IF but forgot to add an ELSE (or change it to EITHER):
>> if 1 = 2 [print "then"] [print "else"] print "Boo!"
Boo!
The else didn't get printed. And from experience, I know that mistakes of this form can be surprisingly hard to debug.
I finally got around to adding a simple feature that catches this... sometimes:
>> if 1 = 2 [print "then"] [print "else"] print "Woo!"
Woo!
** PANIC: Non-discardable value discarded: [print "else"]
WHY "SIMPLE"? (AND WHY "SOMETIMES"?)
An interesting aspect is what discardability turned out to not be: it's not some CELL_FLAG_NODISCARD bit that gets piped around on all cells, that everyone in the universe starts to have to triage and forward.
Internals don't get involved. A function result is simply not discardable if its interface says PURE, and discardable otherwise:
>> index of [a b c] print "INDEX-OF is PURE"
INDEX-OF is PURE
** PANIC: Non-discardable value discarded: 1
>> all [1 2 3] print "ALL isn't PURE: not using 3 isn't an error"
ALL isn't PURE: not using 3 isn't an error
So when you evaluate a whole block or a whole function, there's no concept of that being discardable or not. It's an implementation detail of eval stepping, not something that escapes as a flag on full eval products.
The implication is that there are things it just doesn't catch. e.g. if a value drops out as a function result of a non-pure function, there's no alert if you don't use it:
>> if 1 = 1 [if 1 = 2 [print "then"] [print "else"]] print "Boo!"
Boo!
A little unfortunate, but... we can't ask the internals of a function to do bookkeeping on some invisible flag. Especially a flag that--by definition--gets cleared when you assign it!
There are FAILURE! antiforms for this... but they are for exceptional cases, and you only deal with them in error handling.
If "nodiscard" carried the baggage of FAILURE!, it would be like asking all code everywhere to pay the triage tax as if they were dealing with exception handling...just for this discard safety!
(And it would require giving up a precious cell flag, for CELL_FLAG_DISCARDABLE, as well.)
Nope. It's best kept simple like this.
DESPITE SIMPLICITY, THIS DESIGN DOES CATCH BUGS
For instance: all the branches below in some test code were intended to return a string, but the last branch is missing a SPACED:
case [
expected-id [
spaced ["did not error, but expected:" (mold quasi expected-id)]
]
result = '~null~ [
"test returned null"
]
quasi? result [
"test returned antiform:" (mold:limit result 40)
]
]
So it caught that "test returned antiform:" was just dropped on the floor.
It Does Disallow "Ignoring Inert Values As A Feature"
I've had some weird ideas in the past. Think of for example a modification of EITHER that lets you label the branches, for instance to say which is taken more often (along the lines of [[likely]] and [[unlikely]] in C++20)
my-either condition [<unlikely> ...code...] [<likely> ...code...]
Skipping the tag silently might be considered a feature. ![]()
But when all is considered, I believe that forcing constructs to skip the tags before executing is better practice.