On Sunday, 6 July 2025 at 15:34:37 UTC, Timon Gehr wrote:
> Also it seems you are just ignoring arguments about rollback that resets state that is external to your process.
I deliberately am, but not in bad faith. I'm just looking for an answer to a simple question to anyone with a custom error handler: how does the compiler skipping 'cleanup' in nothrow functions concretely affect your error log?
But the responses are mostly about:
- It's unexpected behavior
- I don't need performance
- Some programs want to restore global system state
- Contract inheritance catches AssertError
- The stack trace generation function isn't configurable
- Not getting a trace on segfaults/null dereference is bad
- Removing Error is a breaking change
- Differences between destructors/scope(exit)/finally are bad
- Separate crash handler programs are over-engineered
Which are all interesting points! But if I address all of that the discussion becomes completely unmanageable. However, at this point I give up on this question and might as well take the plunge. 😝
> It's a breaking change. When I propose these, they are rejected on the grounds of being breaking changes.
I might have been too enthusiastic in my wording :) I'm not actually proposing breaking everyone's code by removing Error tomorrow, I was just explaining to Jonathan that it's not how I'd design it from the ground up. If we get rid of it long term, there needs to be something at least as good in place.
> A nice thing about stack unwinding is that you can collect data in places where it is in scope. In some assert handler that is devoid of context you can only collect things you have specifically and manually deposited in some global variable prior to the crash.
That's a good point. Personally I don't mind using global variables for a crash handler too much, but that is a nice way to access stack variables indeed.
> It's an inadmissible conflation of different abstraction levels and it is really tiring fallacious reasoning that basically goes: Once one thing went wrong, we are allowed to make everything else go wrong too.
Let's make 2+2=3 within a catch(Throwable){ ... }
handler too, because why not, nobody whose program has thrown an error is allowed to expect any sort of remaining sanity.
Yes, I wouldn't want the compiler to deliberately make things worse than they need to be, but the compiler is allowed to do 'wrong' things if you break its assumptions. Consider this function:
__gshared int x;
void f()
{
assert(x == 2);
return x + 2;
}
LDC optimizes that to return 4;
, but what if through some thread/debugger magic I change x
to 1 right after the assert check, making 2+2=3. Is LDC insane to constant fold it instead of just computing x+2, because how many CPU cycles is that addition anyway?
Similarly, when I explicitly tell 'assume nothing will be thrown from this function' by adding nothrow
, is it insane that the code is structured in such a way that finally blocks will be skipped when the function in fact, does throw?
I grant you that nothrow
is inferred in templates/auto functions, and there's no formal definition of D's semantics that explicitly justifies this, but skipping cleanup doesn't have to be insane behavior if you consider nothrow to have that meaning.
> Adding a subtle semantic difference between destructors and other scope guards I think is just self-evidently bad design, on top of breaking people's code.
Agreed, but that's covered: they are both lowered to finally blocks, so they're treated the same, and no-one is suggesting to change that. Just look at the -vcg-ast
output of this:
void start() nothrow;
void finish() nothrow;
void normal() {
start();
finish();
}
struct Finisher { ~this() {finish();} }
void destructor() {
Finisher f;
start();
}
void scopeguard() {
scope(exit) finish();
start();
}
void finallyblock() {
try {
start();
} finally { finish(); }
}
When removing nothrow
from start
, you'll see finally blocks in all function except (normal), but with nothrow
, they are all essentially the same as normal()
: two consecutive function calls.
> I am talking about actual pain I have experienced, because there are some cases where unwinding will not happen, e.g. null dereferences.
That's really painful, I agree! Stack overflows are my own pet peeve, which is why I worked on improving the situation by adding a linux segfault handler: https://github.com/dlang/dmd/pull/15331
I also have a WIP handler for Windows, but couldn't get it to work with stack overflows yet. Either way, this has nothing to with how the compiler treats nothrow
or throw Error()
, but with code generation of pointer dereferencing operations, so I consider that a separate discussion.
> You are talking about pie-in-the-sky overengineered alternative approaches that I do not have any time to implement at the moment.
Because there seems to be little data from within the D community, I'm trying to learn how real-world UI applications handle this problem. I'm not asking you to implement them, ideally druntime provides all the necessary tools to easily add appropriate crash handling to your application. My question is whether always executing destructors even in the presence of nothrow
attributes is a necessary component for this, because this whole discussion seems weirdly specific to D.
> We can, make unsafe cleanup elision in nothrow
a build-time opt-in setting. This is a niche use case.
The frontend makes assumptions based on nothrow. For example, when a constructor calls a nothrow function, it assumes the destructor doesn't need to be called, which affects the AST as well as attribute inference (for example, the constructor can't be @safe if it might call a @system field destructor because of an Exception).
But also, I thought the whole point of nothrow was better code generation. If it doesn't do that, it can be removed as far as I'm concerned.
> it is somehow in the name of efficiency.
It's an interesting question of course to see how much it actually matters for performance. I tried removing nothrow
from dmd itself, and the (-O3) optimized binary increased 54 KiB in size, but I accidentally also removed a "nothrow" string somewhere causing some errors so I haven't benchmarked a time difference yet. It would be interesting to get some real world numbers here.
I hope that clarifies some things, tell me if I missed something important.