Uncallable delegates

Meta jared771 at gmail.com
Fri May 15 06:54:38 UTC 2026


On Friday, 15 May 2026 at 03:13:05 UTC, Timon Gehr wrote:
> On 5/15/26 04:42, Walter Bright wrote:
>> I don't know which of the assertions should hold, and which 
>> ones are salient to your conclusion. I have a very small brain.
>
> (There is enough context in my post to understand what is going 
> on, so you can ask any competent LLM to explain it to you.)

I ran it through Gemini, and the explanation is excellent. LLMs 
are getting very good at understanding code:

This D code demonstrates a subtle **type-system loophole** 
involving delegates, pure functions, and transitive immutability. 
By bypassing D's strict immutability guarantees, it creates a 
mutable alias to immutable memory, resulting in **Undefined 
Behavior (UB)**. This UB allows conflicting compiler 
optimizations to co-exist in the same block of code.

### Component Breakdown

#### 1. Safety and Data Structures
```d
@safe:
struct T{
     int* delegate() dg;
     int* q;
}

```
  * @safe: ensures that the compiler enforces memory safety rules, 
preventing operations like raw pointer casting or uninitialized 
pointer access.
  * struct T aggregates a delegate dg (which returns a mutable 
int*) and a direct mutable pointer field q.

#### 2. Pure Allocation and Implicit Casting
```d
T foo() pure {
     auto x = new int(2);
     auto dg = ()=>x;
     return T(dg, x);
}
```
  * foo is marked pure, meaning it has no side effects and cannot 
access global mutable state.
  * auto x = new int(2); allocates an integer on the heap.
  * auto dg = ()=>x; creates a delegate capturing x. Because this 
delegate escapes the function scope via the return value, D 
automatically moves x into a heap-allocated closure context.
  * Because foo is strongly pure and returns a uniquely allocated, 
isolated object graph, D allows its return type to be implicitly 
converted to immutable.

### Step-by-Step Execution in main

#### Step 1: Type Transitivity and the Delegate Loophole
```d
immutable ps = foo();
auto p = ps.dg(), q = ps.q;
```
When ps is declared immutable, D attempts to apply transitive 
immutability to all its members:
  * ps.q becomes immutable(int*) (a pointer to an immutable 
integer). Thus, q has the type immutable(int*).
  * ps.dg becomes an immutable delegate instance. However, the 
*signature* of the delegate remains int* delegate(). When called, 
it evaluates its return type based on its original signature, 
yielding a **mutable** int*.

This is the loophole: p receives a mutable pointer (int*) to the 
exact same underlying heap data that q views as immutable.

#### Step 2: Verifying the Alias
```d
static assert(is(typeof(p) == int*));
static assert(is(typeof(q) == immutable(int*)));
assert(p is q);
```
  * The compile-time checks (static assert) confirm that p is 
mutable and q is immutable.
  * The runtime check assert(p is q); passes because both pointers 
hold the exact same memory address.

### Why the Final Assertions Pass (The Optimization Paradox)
```d
auto x = *q;    // Reads 2 from memory
*p = 3;         // Mutates the memory location to 3 via the 
mutable pointer

assert(x == 2);        // Passes: x was copied before modification
assert(x == *q);       // Passes: The compiler optimizes based on 
immutability
assert(2 + *q == 5);   // Passes: The compiler emits a fresh load 
from memory
```

The execution of these final lines highlights how the optimizer 
exploits undefined behavior:
  1. **assert(x == *q);**: Because q is explicitly typed as a 
pointer to immutable data, the D compiler's optimizer assumes 
that the value at *q can *never* change during execution. Instead 
of emitting an instruction to reload the value from RAM, it 
reuses the value (2) it already read during auto x = *q;. This 
evaluates to 2 == 2, which is true.
  2. **assert(2 + *q == 5);**: In this mathematical expression, 
the optimizer fails to propagate the constant or decides to emit 
a fresh hardware memory load (often due to register allocation 
constraints). When the processor reads the physical address, it 
fetches the actual modified value (3) written by *p = 3. This 
evaluates to 2 + 3 == 5, which is also true.

### Summary
The code functions as a paradox because it exploits a flaw in D's 
type checking where delegate return types are not deeply 
qualified by the immutability of the parent instance. This allows 
safe code to trigger undefined behavior, causing the compiler to 
make contradictory optimization assumptions about the same memory 
address.



More information about the dip.development mailing list