WRT having error unions just be a userland tagged union type, here's a reason why we don't do that: https://ziglang.org/documentation/master/#Error-Return-Trace...
WRT having error unions just be a userland tagged union type, here's a reason why we don't do that: https://ziglang.org/documentation/master/#Error-Return-Trace...
The way zig seems to think about errors is that early returns via `try` shouldn't be leveraged as a control flow mechanism to bail and bubble up values across the call stack. I think rather than saying why the error handling is done the way it is, it'd be more valuable if there were documentation on how one is supposed to go about coding to be able to use this bail-to-bubble-up-values pattern. Currently, the workaround is indeed somewhat verbose as it requires mimicking the `try` mechanism (and/or using some sort of global error store or whatever).
The workaround would be to store that info in some sort of global struct (which has all the usual drawbacks of global variables); add a context/diagnostics parameter to your function, or manually manage your own error union in userspace, which means you lose the nice `try` syntax.
throw new Error("Expected foo, found bar at line " + line)
// ...then elsewhere
catch (e) {
log("In file " + file + ":\n" + e.message);
}
In zig, you can't attach arbitrary strings to errors because errors are effectively enums (in the sense of that constant names map exclusively to ints). You can only do this `try callMe() catch return error.Something` (The `return error.Something` part is zig's "equivalent" to `throw error`, but notice how I can't tack on the error message string to the error)`try` only shortcircuits into the `catch return` expression for errors, but for cases like parsers you want to shortcircuit and bubble up error messages simultaneously. You can kinda do weird workarounds by having a pointer to an error field in your input struct and mutating that prior to returning an error value, and/or doing weird nullable-to-sentinel-value-promotion shenanigans with idioms like `callMeMaybe() orelse return someSentinel("Expected foo")` to simulate the early-return-from-expression semantics from `try callMe() catch return error.Something`.
Specifically for recursive descent parsers, I pass around a reference to an regular struct that acts as an ErrorInfo object, and use that to pass diagnostic information up the stack. Control flow is still managed by the error returns, any additional info goes in the parameter.
There is (was?) a proposal to add that capability to the language but Im not sure it had much traction.
And a parse error should (I will argue) not be what Zig thinks of as an error.
There are languages where try/catch became in effect the poor man's coroutine, and it's doubly confusing with parser "errors" because this is not something we need to handle as equivalent to an actual error in our program. A parser error is normal control flow.
I would suggest that in Zig one might use suspend/resume to pass a parser error to something which prints an error, possibly returning with a 'fix' which lets parsing continue and maybe find more errors. https://ziglearn.org/chapter-5/
The other workarounds at least look somewhat transparent about their costs. Even continuation passing style might be something I could grok. But what is the cost of a suspension frame? I legitimately have no idea.
Another issue is that suspend/resume are conceptually highly procedural and I personally find it difficult to reason about in recursive contexts (e.g. can I/should I pass suspension frames around? What idiomatic idioms from other languages does doing such a thing map to? Do I need to manually allocate heap for recursion like I need to for async recursion? Is it reasonable to expect a e.g. golang person to be ok with that sort of stuff?)
Stackful asymmetric coroutines are a good choice, these are stackless but an error is one stack frame so it's unclear that's a problem in practice.
I shrug a little too much when it comes to Zig, although documentation and tutorials improve every time I come back to take a look.
My main point is that a parse error isn't a program error, even if it's implemented with exceptions unwinding the stack for one reason or another (and this should be avoided if possible).