4 days ago

On Friday, 4 July 2025 at 18:49:50 UTC, Timon Gehr wrote:

>

With: It writes a file with the full interaction log that leads to the crash. The user can see the stack trace in a console window that is kept open using system("pause").
(...)
Without: The program randomly closes on the user's machine and I get no further information.

Correct me if I'm wrong, but this is responding to the opening question of throw AssertError vs printBacktrace; exit(-1); right? Rikki's and Jonathan's current proposition is that finally blocks must still always be executed when an Error bubbles up through a nothrow function, for better error logging. Your custom assert handler is nice, but would work just as well when a couple of destructors are skipped as far as I can tell.

4 days ago

On Wednesday, 2 July 2025 at 23:26:36 UTC, Walter Bright wrote:

> >

but it is worth nothing they tend to happen just before a task actually executes the problematic condition. Sure, you weren't supposed to even get to this point, but you can still reason about the likely extent of the mystery

If a variable has an out of bounds value in it, it cannot be determined why it is out of bounds. It may very well be out of bounds because of memory corruption elsewhere due to some other bug or a malware attack.

If you have malware already installed, what would crashing and restarting the process help? You'd need to reinstall your whole OS instead. Of course we can't do that on assert failure, except maybe if we're really talking about no less than a Boeing flight control computer.

Our current response is that we terminate the current process, assuming or hoping that the cause is within it. That's good enough for a default response.

You're right, I think, that a lighter response (like throwing a catchable exception) would be a bad response in a C or C++ program, as there's no mechanism limiting the fault domain within the program. However, in D we do have such a mechanism. If you call a @safe pure function and it triggers an assertion failure (or another error), you do know that the parts of the program the called function doesn't reach are still untouched.

Yes, potentially it could still have corrupted other parts if there is @trusted abuse. But so can buggy program potentially have corrupted the OS if it writes to it's config file, yet we are fine with restarting it. This is no different.

And yes, the bug might not be in the called function but in the parameters or global immutable data. But so can the bug in an asserting program be in the OS C library, yet we are fine with terminating only the program and not the whole OS. This is no different.

4 days ago
On 7/4/25 21:42, Dennis wrote:
> On Friday, 4 July 2025 at 18:49:50 UTC, Timon Gehr wrote:
>> With: It writes a file with the full interaction log that leads to the crash. The user can see the stack trace in a console window that is kept open using `system("pause")`.
>> (...)
>> Without: The program randomly closes on the user's machine and I get no further information.
> 
> Correct me if I'm wrong, but this is responding to the opening question of `throw AssertError` vs `printBacktrace; exit(-1);` right? Rikki's and Jonathan's current proposition is that `finally` blocks must still always be executed when an `Error` bubbles up through a `nothrow` function, for better error logging. Your custom assert handler is nice, but would work just as well when a couple of destructors are skipped as far as I can tell.

Skipping destructors may leave the program in an inconsistent and unpredictable state, including memory corruption due to lack of cleanup of stack pointers. I don't want destructors and finally blocks to have distinct behavior. It may sometimes lead to problems while at the same time not addressing any need that I have.

It is also a key pitfall for data structures with `@trusted` behaviors. Anyone anywhere will have to assume that destructors are not guaranteed to run for stack-allocated variables.

This would be less of a problem if there were at least a way to turn off nothrow inference, but even when there is nothrow inference, eliding destructor calls is just not a need I have and it seems like an unsafe default behavior.

It would be better to just disallow variables with destructors in nothrow functions.

Anyway, this is not only about asserts, and it is not specific to asserts. E.g. a RangeError is the same kind of problem and it is handled the same way.
4 days ago
On 7/4/25 21:31, Walter Bright wrote:
> On 7/4/2025 11:49 AM, Timon Gehr wrote:
>> With: It writes a file with the full interaction log that leads to the crash.
> 
> Cleanup code is what is happening following the crash. If you're logging, it would be the entry code to the function, not the cleanup. 

I am compressing and saving the data on crash. No point in spamming my user's hard drives with uncompressed huge log files that nobody will ever need to look at in almost all cases.

> What you can do is collect and log a stack trace, but that isn't cleanup code.
> ...

A stack trace is a nice hint and better than nothing, but on its own it is very far from reproducing the issue.

>> The user can see the stack trace in a console window that is kept open using `system("pause")`. They can send the data to me and I can immediately reproduce the issue and fix the crash within 24 hours.
> 
> I'm not objecting to a stack trace.
> 

Good, but I think any objection at all to handling an error in a custom way that will allow reproducing it later is not an acceptable position.

You may say there is a custom assert handler, but it's not the only type of error. Also, you cannot safely throw an exception from a custom assert handler if the language insists on not doing cleanup properly.
4 days ago
On 7/4/25 09:24, Walter Bright wrote:
> 
> 2. code that is not executed is not vulnerable to attack

```d
void foo(){
    openDoor();
    performWork();
    scope(exit){
        closeDoor();
        lockDoor();
    }
}
```
4 days ago

On Friday, 4 July 2025 at 20:49:56 UTC, Timon Gehr wrote:

>

Skipping destructors may leave the program in an inconsistent and unpredictable state, including memory corruption due to lack of cleanup of stack pointers.

I'm sorry but I have to ask this question a third time now: What are you doing in your destructors that affects your error handler's output? How does not cleaning up result in memory corruption, instead of just an irrelevant memory leak? From my perspective calling free() after the program entered an invalid state is still more risky than not calling it. I feel like I'm missing something about your program because this:

>

It writes a file with the full interaction log that leads to the crash

Doesn't sound like something that would fail when cleanup is skipped.

4 days ago
On 7/5/25 00:11, Dennis wrote:
> On Friday, 4 July 2025 at 20:49:56 UTC, Timon Gehr wrote:
>> Skipping destructors may leave the program in an inconsistent and unpredictable state, including memory corruption due to lack of cleanup of stack pointers.
> 
> I'm sorry but I have to ask this question a third time now: What are you doing in your destructors that affects your error handler's output?

This specific concern is not something I know I have already experienced, mostly because cleanup in fact happens in practice. Anyway, it is easy to imagine a situation where you e.g. have a central registry of all instances of a certain type that you need to update in the destructor so no dangling pointer is left behind.

> How does *not* cleaning up result in memory corruption, instead of just an irrelevant memory leak? From my perspective calling free() after the program entered an invalid state is still more risky than not calling it.

I'd prefer to be able to make this call myself.

> I feel like I'm missing something about your program because this:
> 
>> It writes a file with the full interaction log that leads to the crash
> 
> Doesn't sound like something that would fail when cleanup is skipped.
> 

In principle it can happen. That's a drawback. There is no upside for me. Whatever marginal gains are achievable by e.g. eliding cleanup in nothrow functions, I don't need them. I am way more concerned about predictable language behavior. If I bracket a piece of code using a constructor and a destructor, I want this to be executed no matter what happens in the middle.

A destructor can do anything, not just call `free`. Not calling them is way more likely to leave behind an unexpected state than even the original error condition. The state can be perfectly fine, it's just that the code that attempted to operate on it may be buggy.
3 days ago
On Friday, July 4, 2025 11:48:15 AM Mountain Daylight Time Walter Bright via Digitalmars-d wrote:
> On 7/4/2025 12:21 AM, Jonathan M Davis wrote:
> > Even if recovering is not acceptable, if the proper clean up is done when the stack is unwound, then it's possible to use destructors, scope statements, and catch blocks to get additional information about the state of the program as the stack unwinds. If the proper clean up is not done as the stack unwinds, then those destructors, scope statements, and catch statements will either not be run (meaning that any debugging information which could have been obtained from them wouldn't be), and/or only some of them will be run. And of course, for each such piece of clean up code that's skipped, the more invalid the state of the program becomes, making it that much riskier for any of the code that does run while the stack unwinds to log any information about the state of the program.
>
> Executing clean up code (i.e. destructors) is not at all about logging errors, because they happen through the normal error-free operation of the program. If you were logging normal usage, you'd want the logs from before the fault happened, not after.

The programmer isn't necessarily looking to log normal usage. In plenty of cases, they may be trying to get additional information specifically because an Error was thrown, and they want that information in order to have some hope of debugging the problem (especially if this isn't a common problem or it's a user who isn't a programmer who encounters it).

Timon uses stuff like scope(failure) and catch(Error e) { ... throw e; } right now for getting information out of his program when an Error is thrown, with the caveat that not all of the clean up code gets run, making the whole endeavor riskier than it would be otherwise. I don't know exactly what information he gets out of it, but if I understand correctly, it's used to produce the information that gets put into an error window in the UI which the user is then supposed to copy the information from in a bug report (and then presumably, when they close that window, it closes the program, since Timon isn't trying to make the program continue to run after that). This isn't stuff that gets logged during normal operation. It's specifically for getting information about what the program was doing that resulted in the Error so that he has some hope of debugging it in spite of the fact that he's dealing with a user who isn't tech-savvy.

And just in general, if scope(failure) is being used, the programmer may want to log addtional information that they wouldn't have wanted to log otherwise. For instance, I do this in unit tests when the assertion failure is inside nested loops, and I need to know which iterations each loop is in when the failure occurs (and certainly wouldn't want to log anything if there weren't a failure).

Destructors probably aren't going to be used to get additional information, since those do run when there are no Errors, but scope(failure) and catch(Error e) { ... throw e; } can certainly be used specifically for when a Throwable of some kind is thrown, and having those be skipped at any point means missing out on whatever information the programmer was trying to get on failure, and having the destructors skipped would mean that the code with scope(failure) or catch(Error) would then be dealing with code that was potentially in a state that wasn't memory safe, because clean up code was skipped, whereas if the clean up code hadn't been skipped, it might have been perfectly memory safe even though an Error was in flight.

Even if you think that it's too risky to have the clean up code run for Errors as the default behavior, there are clearly use cases where getting additional information about the state of the program is worth far more than the risk that doing the clean up code might cause further problems - especially when in many cases, Errors are thrown _before_ anything that isn't memory safe is done (e.g. array bounds checking throws an Error before accessing the memory out-of-bounds, not after). And at least having the option to configure a program such that the full clean up code is run even with Errors would make getting information out of a program as it terminates due to an Error more memory safe and less error-prone - as well as simply making it so that more information can be got out of the program, because the clean up code that's used to get information is actually all run. So, the folks who need that behavior can have it even if it's not the default.

- Jonathan M Davis




3 days ago
On Friday, July 4, 2025 5:09:27 PM Mountain Daylight Time Timon Gehr via Digitalmars-d wrote:
> A destructor can do anything, not just call `free`. Not calling them is way more likely to leave behind an unexpected state than even the original error condition. The state can be perfectly fine, it's just that the code that attempted to operate on it may be buggy.

This is particularly true if RAII is used. For instance, the way that MFC implemented turning the cursor into an hourglass was with RAII, so that you just declared the thing, so when the variable was created, the cursor turned into an hourglass, and when the scope exited, the variable was destroyed, and the cursor went back to normal.

RAII is used less in D than in C++ (if nothing else, because we have scope statements), but it's a design pattern that D supports, and programmers can use it for all kinds of stuff that has absolutely nothing to do with memory allocations.

Another relevant example would be that RAII could be used to log when a scope is entered and exited based on when the object is created and destroyed. If the destructors are skipped, then that logging will be skipped, and it could easily be part of what the programmer wants in order to be able to debug the problem (and if they don't realize that the destructors may be skipped, the logs could be pretty confusing when the destructor is skipped).

So, yeah, there's no reason to assume that destructors have anything to do with allocating or freeing anything. They're just functions that are supposed to be guaranteed to be run when a variable of that type is destroyed. They can be thought of as just being another form of scope(exit) except that they're tied to the type itself and so every object of that type gets that code instead of the programmer having to type it out wherever they want it.

- Jonathan M Davis




3 days ago
On Saturday, July 5, 2025 12:57:21 AM Mountain Daylight Time Jonathan M Davis via Digitalmars-d wrote:
> So, yeah, there's no reason to assume that destructors have anything to do with allocating or freeing anything. They're just functions that are supposed to be guaranteed to be run when a variable of that type is destroyed. They can be thought of as just being another form of scope(exit) except that they're tied to the type itself and so every object of that type gets that code instead of the programmer having to type it out wherever they want it.

Actually, to add to this, one case where skipping cleanup code could be particularly catastrophic would be with mutexes. If a mutex is locked and freed using RAII (or scope statements are used, and any of those are skipped), then you could get into a situation where a lock is not released like it was supposed to be, and then code higher up the stack which does run while the stack is unwinding attempts to get that lock (and locks can be used even if it's only for multi-threaded logging, and not all mutexes are recursive), then the program could deadlock while the stack is unwinding just because some of the cleanup code was skipped. So, skipping cleanup code could actually result in the program failing to shutdown.

- Jonathan M Davis