August 08

On Monday, 21 July 2025 at 02:03:54 UTC, Paul Backus wrote:

>

On Monday, 21 July 2025 at 00:31:10 UTC, Richard (Rikki) Andrew Cattermole wrote:

>

Problem is, some people believe it shouldn't. Hence this proposal to give those that want that "reliability" control to make it so.

If there are people who believe that (a) the struct instance should be allocated with the GC, but (b) the GC should not be responsible for its cleanup, then those people are wrong, plain and simple.

On the other hand, if people want to assume control over both the allocation and the cleanup, that's already possible with existing language features.

Not calling destructors at the end of the scope that a variable is declared in is a recipe for disaster.

For example:

{
   auto conn = lockConnection();
   arr.map!(val => conn.send(val)).each; // allocates `conn` into a closure, because reasons.
}

{
   auto conn2 = lockConnection(); // if conn.dtor not run, then this is a deadlock.
}

The better option is to not make arbitrary decisions that affect semantics of RAII.

My suggestion for fixing this problem is to explicitly allocate the struct on the heap, and allow the compiler to merge that into a closure if you want to optimize that allocation (probably not worth it, but if you feel it's important, go for it).

Note that erroring when allocating a struct with a destructor used to be an error, but was made not an error with a "hack" that was supposed to be temporary, but just was left there.

-Steve

August 08

On Friday, 8 August 2025 at 04:28:32 UTC, Steven Schveighoffer wrote:

>

On Monday, 21 July 2025 at 02:03:54 UTC, Paul Backus wrote:

>

On Monday, 21 July 2025 at 00:31:10 UTC, Richard (Rikki) Andrew Cattermole wrote:

>

Problem is, some people believe it shouldn't. Hence this proposal to give those that want that "reliability" control to make it so.

If there are people who believe that (a) the struct instance should be allocated with the GC, but (b) the GC should not be responsible for its cleanup, then those people are wrong, plain and simple.

On the other hand, if people want to assume control over both the allocation and the cleanup, that's already possible with existing language features.

Not calling destructors at the end of the scope that a variable is declared in is a recipe for disaster.

For example:

[...]

Ok, I can see how this could be a footgun, especially given the limitations of the compiler's escape analysis. Ideally, we would only allocate closures for variables that actually escape their scope, and it wouldn't be a surprise when it happened, but as long as we're stuck with these "defensive" closures, it's probably better to make everything explicit.

August 08
On 08/08/2025 4:28 PM, Steven Schveighoffer wrote:
> Not calling destructors at the end of the scope that a variable is declared in is a recipe for disaster.
> 
> For example:
> 
> ```d
> {
>     auto conn = lockConnection();
>     arr.map!(val => conn.send(val)).each; // allocates `conn` into a closure, because reasons.
> }
> 
> {
>     auto conn2 = lockConnection(); // if conn.dtor not run, then this is a deadlock.
> }
> ```
> 
> The better option is to not make arbitrary decisions that affect semantics of RAII.

Doing this is also a disaster.

Inject any copy into heap memory, into any position before an expression/statement. This may be done in another function that accepts it by-ref.

```d
__gshared Connection global;
	...
	global = conn;
	...
```

I.e.

```d
{
    auto conn = lockConnection();
    global = conn;
    arr.map!(val => conn.send(val)).each; // allocates `conn` into
a closure, because reasons.
}

{
    auto conn2 = lockConnection(); // if conn.dtor not run, then
this is a deadlock.
}
```

Note: while I am using a global, any heap memory will exhibit this behavior.

Depending upon how the struct is implemented you will get one of these behaviors:

1. Do nothing. Heap variable is both locked and unlocked depending upon the call stack, and depending upon implementation this may never be able to be used.

2. Ownership transfer (unique). Later operations will fail, but won't look like it should.

3. Disable copying and rely on return value optimization. The assignment will fail, but you won't be able to put it into stack memory that isn't by-ref. Acceptable solution.

4. Detect copy and unlock. Highly error prone at runtime, I've had this issue with my error types.

The only solution as a library author I can recommend is to disable copying. As a user, I would hate this due to past experience anything that enables error at runtime behavior from compiling.

But there is a fifth option, which is what I am proposing.

5. Disable going into heap memory. This gets you all of 3, but also allows you to copy it around within the stack, including into pointers. Ideally we'd also have escape analysis to guarantee going down only and treat it as a borrow to prevent multiple locking.

I do not like playing whack-a-mole on symptoms of a problem. And this issue for closures appears to be exactly that, a symptom of a much bigger problem.

Worth noting that doing nothing is equivalent to what you are proposing in the above example. All you've done is closed one hole in a sieve.
August 08

On Sunday, 20 July 2025 at 17:04:11 UTC, Richard (Rikki) Andrew Cattermole wrote:

>

If a struct destructor is annotated with an attribute @stackonly it may only be called if the this pointer is allocated on the stack. It does not overload.

struct RAII {
    ~this() @stackonly {}
}

void foo() {
    RAII* gc = new RAII; // Error: RAII can only be cleaned up if it is on the stack
    scope RAII* stack1 = new RAII; // ok
    RAII stack2 = RAII(); // ok
}

We used to have scope classes but they were deemed “a quirk in the language without a compelling use case” and deprecated.

1 2
Next ›   Last »