3 ms·
> raise AllocationFailure; This is exception semantics which is hidden control flow present in every language with exceptions. Even in checked exceptions it is
by Schroedingersat 4y ago
> raise AllocationFailure;
This is exception semantics which is hidden control flow present in every language with exceptions. Even in checked exceptions it is often obfuscated (ie. a function call that can raise is not distinguishable from one that can't wkthout manually looking at the types, and if they are distinct, then you just have something equivalent to error types). The point is that, upon calling a function that might transitively call a function that raises, you have no idea if the next line will execute or whether you will wind up in some other piece of code altogether.
It's not equivalent to calling a function because it destroys the stack context and violates the invariants and ability to reason about control flow for every function above it in the call stack.
I am starting to grok algebraic effects, and how they can make exceptions-but-not-objectively-incorrect by preserving context during the interrupted control flow and returning everything how it was left, but they come at a price.
You could guarantee that the stack will be reconstructed and execution will continue where it left off. But now 'raise' implicitly does a bunch of stuff including allocating memory on the heap and copying every stack frame between your error and your handler onto it. How does this function if your error is that you are unable to allocate?
Even then, you don't know where control will end up without hunting for the registration of the handler, which may not even be near the call site.
Sure you can build a stack on the heap in userland in C and dump everything there so as to reconstruct it (I skimmed your code and it seems like some implementation of coroutines or similar, I'm notnsure if this is part of the error machinery). There's nothing in zig that prevents you from doing this, or even using that exact library.
> The cool thing is that you can do this in C too, even with cleaning up and preserving invariants. See [2], [3], and [4].
I either can't follow your code with the amount of effort I'm willing to expend (likely) or there isn't anything resembling the descriptions I can find for conditions and restarts there.
Are you saying that the following can happen?
I can have some function foo that is regular C code with the exception of registering that it handles some condition.
Then foo can call, bar, which calls baz which calls qux, all of which are regular C code.
Then qux can call something which raises a condition of the kind foo registered.
Then control flow passes to foo which handles the error using the context in its current stack frame.
Then control passes back into qux with information provided by foo's handling code.
Then qux returns, followed by baz and bar returning without having to have any knowledge of or participation in your system.
And that all of this happens without constructing a closure over foo's context and sneaking it into qux via a global.
- ghoward 4y ago> This is exception semantics which is hidden control flow present in every language with exceptions. This is not exception semantics. A language can make the keyword "raise" do whatever it wants. Should I have used some other keyword? Fine: handle AllocationFailure; In fact, let me expand it a bit: restart { err = allocate(size); if (err != NULL) handle AllocationFailure; } This code calls the AllocationFailure error handler when allocation fails, and has the option of "restarting" (i.e., going back to the restart keyword and trying again). There is no hidden control flow, unless you think `errdefer` is hidden control flow. > It's not equivalent to calling a function because it destroys the stack context and violates the invariants and ability to reason about control flow for every function above it in the call stack. No, it doesn't. Again, I implemented this in C. I don't use longjmp(). It does not destroy stack context. You seem to have no idea what you are talking about. > You could guarantee that the stack will be reconstructed and execution will continue where it left off. But now 'raise' implicitly does a bunch of stuff including allocating memory on the heap and copying every stack frame between your error and your handler onto it. How does this function if your error is that you are unable to allocate? Yes, by doing a function call. It's a function call. Really. > Even then, you don't know where control will end up without hunting for the registration of the handler, which may not even be near the call site. The registration of the handler does not need to use the stack. In my code, it uses a separate heap-allocated stack, which is in the thread-local data. Did you not read the actual C code I sent you? It's all in there. > Sure you can build a stack on the heap in userland in C and dump everything there so as to reconstruct it (I skimmed your code and it seems like some implementation of coroutines or similar, I'm notnsure if this is part of the error machinery). There's nothing in zig that prevents you from doing this, or even using that exact library. It's not coroutines. It's simple function calls. It's using function pointers, yes, but it's just function calls. > I either can't follow your code with the amount of effort I'm willing to expend (likely) or there isn't anything resembling the descriptions I can find for conditions and restarts there. Because your mental model of conditions and restarts is completely wrong. > Are you saying that the following can happen? No, that is not what happens. This is what happens: You have some function foo that is regular C code with the exception that it registers ipsom as a handler for some condition until foo returns. So ipsom is a registered handler until foo is done executing. Then foo can call bar, which calls baz, which calls qux, all of which are regular C code. Then qux can call something which raises a condition of the kind foo registered. Then control flow passes to ipsom, which gets a new stack frame, and which handles the error using the context in its new stack frame. Then control returns back into qux with information provided by foo's handling code. Then qux returns, followed by baz and bar returning without having to have any knowledge of or participation in your system. And that all of this happens without constructing a closure over foo's context and sneaking it into qux via a global because foo's context is never used as the error handler! Again, it's just function calls and a possible jump to the beginning of a marked block. To show this, I have fully implemented restarts now ([1]), which I didn't do because I didn't need them yet. Now, if you had a hypothetical foo that registers ipsom as an error handler, you would do this in foo: y_REGISTER_HANDLER(ipsom); // The rest of the code. y_UNREGISTER_HANDLER; I need to hook error handler registration into my RAII system so that you only have to register it (and it will automatically be unregistered on function exit), but you get the idea. Now, in the function that actually raises the condition: y_RESTART(restart) { err = function_that_returns_err(); if (err != y_ERROR_SUCCESS) y_HANDLE(err, status, restart); } Of course, it's C, so the macros are a little ugly and requiring passing in variable names, but it works. In this code, if `err` is not a success value, then ipsom is called, which will either not handle the error, handle the error but not allow a restart, or handle the error and allow a restart. If the last one is what happens, the code will jump back up to the beginning of the y_RESTART block and start over. Edit: I realized my commit forgot to add y_strucon_handleErrorWithRestart(). It was added in [2]. [1]: https://git.yzena.com/Yzena/Yc/commit/95312b059132dc3e8614b434ec336761befb9150 https://git.yzena.com/Yzena/Yc/commit/95312b059132dc3e8614b4... [2]: https://git.yzena.com/Yzena/Yc/commit/61c4a3ef735446a9478c58c9ee7119125943446d https://git.yzena.com/Yzena/Yc/commit/61c4a3ef735446a9478c58...