Worth noting that Golang’s defer is different from the Zig/C version: in Golang defer is scoped to a function, whereas in Zig and C it is scoped to a block. If you defer in a loop, Golang will build up an unbounded amount of work to do later (allocating on the heap), but Zig and C will run each defer statement between each iteration of the loop (using constant space).
For what it's worth, defer ends up (IMO) not cleaning up the code that much. It got standardized partly because everyone was already inventing their own versions of it with compiler extensions, so the standards committee had to chose between the language having it in a standard form or a nonstandard form.
I'm not going to address goto vs defer and just talk about the control flow graph structure:
Note that goto for error handling (and similar uses) maps directly to structured programming with multi-level exits. Translating the article's example to Rust using this recipe:
'err_mem: {
// open the fd
if fd == -1 { break 'err_mem; }
// return successfully
}
// free mem
// return unsuccessfully
It maps to Wasm's structured control flow in exactly the same way: br or br_if to an enclosing block.
Anyway, this should convince you that the difference is not that the control flow is unstructured (irreducible) with goto error handling and structured (reducible) with multi-level exits. The difference is that with blocks and multi-level exits the syntactic nesting (and indentation) grows as you add multiple exit points. But you can change that by making different syntax design choices in your language.
And if you continue to use goto for C error handling (like many of us do), then you can use this to help win the argument the next time someone objects. :)
I would be interested to see a language experiment with a version of goto that is even more limited than in C, where in addition to being prohibited from jumping out of the function it would also be prohibited from jumping backwards or from jumping into blocks. I believe with these rules this version of goto would guarantee reducible control flow and become truly equivalent to the labeled-break approach demonstrated above. I believe this, not defer, would be the true ultimate successor to C-style error handling.
Yes, I've tried this in homebrew languages, including the conventional label/goto syntax. That's what I was alluding to with "you can change that by making different syntax design choices in your language". One downside with the conventional syntax is that the forward-looking scoping discipline for labels is awkward, and you don't know how many implicit blocks there are and where they start while performing a single forward pass. You can avoid that with different choices of syntax, but the alternatives are unlikely to be as syntactically lightweight.
fanf | 4 hours ago
C is getting a Zig-like defer feature https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3928.pdf
Worth noting that Golang’s defer is different from the Zig/C version: in Golang defer is scoped to a function, whereas in Zig and C it is scoped to a block. If you defer in a loop, Golang will build up an unbounded amount of work to do later (allocating on the heap), but Zig and C will run each defer statement between each iteration of the loop (using constant space).
orib | 2 hours ago
For what it's worth, defer ends up (IMO) not cleaning up the code that much. It got standardized partly because everyone was already inventing their own versions of it with compiler extensions, so the standards committee had to chose between the language having it in a standard form or a nonstandard form.
(I attended that standards meeting)
pervognsen | 3 hours ago
I'm not going to address goto vs defer and just talk about the control flow graph structure:
Note that goto for error handling (and similar uses) maps directly to structured programming with multi-level exits. Translating the article's example to Rust using this recipe:
It maps to Wasm's structured control flow in exactly the same way: br or br_if to an enclosing block.
Anyway, this should convince you that the difference is not that the control flow is unstructured (irreducible) with goto error handling and structured (reducible) with multi-level exits. The difference is that with blocks and multi-level exits the syntactic nesting (and indentation) grows as you add multiple exit points. But you can change that by making different syntax design choices in your language.
And if you continue to use goto for C error handling (like many of us do), then you can use this to help win the argument the next time someone objects. :)
ghoti | 31 minutes ago
I would be interested to see a language experiment with a version of goto that is even more limited than in C, where in addition to being prohibited from jumping out of the function it would also be prohibited from jumping backwards or from jumping into blocks. I believe with these rules this version of goto would guarantee reducible control flow and become truly equivalent to the labeled-break approach demonstrated above. I believe this, not defer, would be the true ultimate successor to C-style error handling.
pervognsen | 13 minutes ago
Yes, I've tried this in homebrew languages, including the conventional label/goto syntax. That's what I was alluding to with "you can change that by making different syntax design choices in your language". One downside with the conventional syntax is that the forward-looking scoping discipline for labels is awkward, and you don't know how many implicit blocks there are and where they start while performing a single forward pass. You can avoid that with different choices of syntax, but the alternatives are unlikely to be as syntactically lightweight.
dualvariable | 4 hours ago
Kids these days think they invented psychedelics, bisexuality, and using gotos in C...