Packs Decaying To First Item When Others are ERROR!

The concept of an "undecayable PACK!" came up when I was trying to reason about what should happen when you had things like PACK!s of PACK!s...

>> x: pack [1 2]
== \~('1 '2)~\  ; antiform (pack!)

>> x 
== 1

>> x: pack [pack [1 2] 3]
== ???

My belief was that this should panic, rather than do the decay. If you truly wanted the first element of an unpacked pack to decay, you could write:

>> [x]: pack [pack [1 2] 3]
== \~(~[1 2]~ '3)~\  ; antiform (pack!)

>> x
== 1

This created the notion of an undecayable pack. The first cut of the undecayable rule was that that any pack with an unstable antiform in the first spot (VOID!, PACK!, ERROR!) would panic if you tried to decay it (vs unpack it).

Safety Motivation: Don't Let ERROR! Disappear

Putting ERROR!s in PACK!s isn't the usual way of doing things. If a function runs and wants to make you aware of an ERROR!, it should typically return that as the main result of a function...not stow it away inside a pack. Only if it's the main result can it be reacted to by things like EXCEPT.

But some cases don't allow the error to be the sole return result. For instance, EVALUATE:STEP

[pos ^result]: evaluate:step [fail "abc" ...]

We need the updated position of the code, even if the code FAIL'd. Hence, EVALUATE can't make its main result an ERROR! in the case of an error result, it has to give back a PACK! which encodes both the error and the new position.

("Has to" is a strong statement... it's possible that the next position could be a field of the error, I've discussed some of these odd ideas before. But it's better to allow errors in packs.)

If you don't receive result as ^META, then the ERROR! wouldn't be able to store in the ^result and would panic:

[code result]: evaluate:step [fail "abc" ...]  ; not ^result, so panic

But what if you didn't store the result at all?

code: evaluate:step [fail "abc" ...]  ; ???

I feel like that should panic. But why should you have to do an assignment to get the panic?

evaluate:step [fail "abc" ...]  ; same behavior as when assigned to `code:` ?

That would suggest that if a PACK has an ERROR! in any slot, then should the pack decay and not extract that error into a ^META variable, that error becomes a panic.

But this thought leads to another invasive thought...

...What About ERROR!s In PACK!s... in PACK!s?

I started by talking about undecayability, e.g. you can't put a PACK! in the first position of a PACK! and have it "double decay". Decaying happens once.

And then I discussed decaying when there's an ERROR! in the pack... suggesting that an error at any position (not just the first) which is not unpacked into a ^META-variable should panic.

But what if you have a PACK! that's not in the first position of a PACK!... ? Should that be willing to decay silently?

>> x: pack [1 pack [2 3]]
== 1  ; silently discarded PACK! in second position...

That may seem harmless, BUT, what if that PACK! contained an ERROR! ?

This led me to theorize that packs which contained unstable antiforms at any position would be "undecayable"

However, that started to feel too strict, when we look at:

[code result]: evaluate:step [pack [1 2] ...]  ; works, result decays to 1

code: evaluate:step [pack [1 2] ...]  ; innocuous, why shouldn't it work?

And even if I'm assigning the result, PACK!s propagate in assignments now by default

while [[code result]: evaluate:step code] [
    ...
]

It would be a shame if this decayed a PACK! for result, and then refused to decay it for the WHILE, making you write:

while [[{code} result]: evaluate:step code] [
    ...
]

So pre-emptively refusing to toss PACK!s just because they might contain an ERROR! doesn't seem very ergonomic.

Non-First Item Recursive PACK! Decay Search For ERROR!

So... what if any PACK!s which aren't in the first position, that you want to discard, will recursively unpack themselves looking for ERROR!, and if they find any then panic?

(And ERROR!s which aren't in the first position, and aren't in PACK!, are also sources of panic?)

This would make the system more robust to dropping errors on the floor. This might make it more reasonable to say that PACK is willing to pack up errors, generally... today you have to use a special operation:

 pack [1020 fail "won't work"]  ; panics 

 pack* [1020 fail "will work"]  ; allows the error

It's a little bit disconcerting, to imagine that instead of erroring at the moment of the PACK you trust that wherever the pack is going will handle the error. But really, that only becomes a problem when you're dealing with using PACKs in non-multi-return situations, e.g. you make a PACK and then put it in suspended animation somewhere.

If you're not putting packs in suspended animation, but "packing with the intent of unpacking", then you're leaving it up to the recipient as to whether they want the error or not.

All things being equal, having just one PACK primitive is preferable.

Things do seem to be falling into place in other ways, and I'm sensing that maybe the right thing to do is to say that if you're a client with "pack with intent to store", then you bear the burden of the indefinite lifetime you may give errors in that pack...vs. making it harder to put errors in packs.

:thinking:

There's Some Semantic Trouble Here... :frowning:

When you DECAY something, you have to produce a stable result in the process.

This means that a DECAY of a PACK! containing an ERROR! can't make an ERROR!... that would be creating one unstable antiform from another.

So if you write:

let [foo ^result]: something-that-might-make-error-in-pack ...

You can't solve this problem the way I was thinking of with:

try let [foo ^result]: something-that-might-make-error-in-pack ...

Because the PACK! can't decay to make an ERROR!. It has to PANIC.

So you can't let that PACK! with an error in it ever leak out. If a PACK! contains any ERROR!s, you'd have to unpack it one way or another:

let [{foo} ^result]: something-that-might-make-error-in-pack ...

try let [foo {^result}]: something-that-might-make-error-in-pack ...

Although... another option would be making TRY "smart" enough to unpack packs and look for errors in them, and defuse them.

That smarter TRY would allow you to do:

try let [foo ^result]: something-that-might-make-error-in-pack ...

It's a bit misleading, as it might suggest you could write:

(something-that-might-make-error-in-pack ...) except e -> [...]

Or, it would suggest that EXCEPT has to do unpacking to get you the error... but this seems sketchy... because you don't want to conflate some strange downstream error that's nestled into packs with an actual informative ERROR! that you are reacting to.

:frowning:

Contemplation of Circling

With "circling" you can say which result you want, and I pointed out you can write:

while [[{code} ^result]: evaluate:step code] [
    ...
]

This would be saying that in the case that you were writing code that was designed to handle raised errors as evaluation products (vs. just panic), you would store the result in a ^META-variable (hence it would not fail in the assignment), and then indicate that you wanted the next step of the code to be the synthesized result of the assignment... not the initial undecayable pack.

You don't have to name variables, but you do have to use a ^ instead of a _ in the unnamed slot if you want it to gloss over error assignments:

while [[{code} _]: evaluate:step code] [  ; panic if ERROR! in second slot
    ...
]

while [[{code} ^]: evaluate:step code] [  ; tolerates ERROR!
    ...
]

Then we have the question of what to do if you didn't provide any result storage at all:

while [[{code}]: evaluate:step code] [  ; panic if ERROR! in second slot
    ...
]

In the philosophy I've been putting forward here, that should not let your ERROR! fall on the floor and should panic. It seems to me too risky to do otherwise...

If not TRY, how about IGNORE?

Today's ELIDE won't let you sweep errors under the rug.

I mentioned my discomfort with TRY being willing to equate an undecayable pack with an ERROR! signal, because that equivalence doesn't hold up.

But what if there was an ELIDE-like construct called IGNORE that synthesized a VOID and consumed any input?

ignore let [foo ^result]: something-that-might-make-error-in-pack ...

You'd have the choice there of that, or not to make the synthesis result the ERROR!

let [{foo} ^result]: something-that-might-make-error-in-pack ...

I'm Sad This Isn't Simpler, But Not Everything Is Simple

Error-handling is conventionally a tough thing.

I like the idea that you "get what you pay for", in that if you're doing some casual evaluations and not expecting to have to handle errors then the code can be simple. But there's a certain amount of "essential complexity" you can't get away from in error handling.

The idea of that basic case:

probe ^some-error

There's a new level of complexity here, where:

try probe ^some-error  ; works

try probe ^some-pack-with-errors-in-it  ; won't work

Hence you have to go to this new level of:

ignore probe ^some-pack-with-errors-in-it

But in that case, PROBE isn't the right tool to reach for...rather DUMP because it does not pass-through the result:

dump ^some-error