www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Escaping objects and what is escape analysis modelling anyway?

reply Richard (Rikki) Andrew Cattermole <richard cattermole.co.nz> writes:
Another year has passed where people have felt the need to use 
DIP1000 in projects.
So I want to go over some important details of what escape 
analysis is trying to model, and what DIP1000 is trying to model.

But first we need to go over some basic terminology.
First a memory allocation is called an object.
This could be the stack or heap; but ultimately it doesn't matter 
what the memory allocator was used to do the allocation.

Second, we need to identify locations in memory that can hold 
values (whatever that may be), this could be a stack variable, a 
field in a class, or even an element in an array.
This is called a cell; it's all about storage locations for 
values.
All objects are cells, but not all cells are objects.
Not all objects have cells as children either.

The following description is based upon my study of escape 
analysis, design work and current implementation work of 
introducing escape analysis into the fast DFA engine.
It is currently waiting on being 
[merged](https://github.com/dlang/dmd/pull/23314) and has been 
completed for a little while now.
However, I haven't really put together a good description in the 
changelog entry, and a good reason for this is that it isn't 
supposed to require you to understand it.
If it requires attributes for normal code, I've failed and need 
to fix the engine.

Escape analysis is formed from the basis of points-to and 
dependency analysis.
Points-to analysis is used for modelling the stack variables 
within a function body, it allows finding what out what a pointer 
stored in one points at.
For example, another stack variable.
It is used for null pointer dereference detection.

For example, the following code can error with points-to analysis:

```d
void checkViaObjNullDeref(bool cond, int** ptrArg)  system
{
     int* var;
     int** ptr = cond ? &var : ptrArg;
     **ptr = 2; // error
}
```

Dependency analysis goes a step further than points-to analysis.
It can track that one object stored in a cell relates to another.

For example, we can see that the return value is dependent upon 
the parameter's object:

```d
int** grab(int* param)
{
     int** obj = new int*;
     *obj = param;
     return obj;
}
```

The information gained from inference gets serialized down to a 
compact form that can be stored at a function level.
This information may not be so simple that it may be represented 
with attributes like D has.
For this static analyzers may use a dedicate file format to 
communicate this information between compiler calls.

An example of this is you can utilize conditionals to model 
relationships:

```d
int* returnCond(bool cond, int* obj)
{
     if (cond)
         return null;
     else
         return obj;
}
```

So this could have a graph that looks like:

```
[
     ( if (params[0] == true)
         return = null
     ),
     ( if (params[0] == false)
         return = params[1]
     )
]
```

This isn't some unlikely-to-exist feature for D either; it's a 
very real possibility that D may need to gain this to model 
inline declarations of variables within function calls:

```d
if (tryGet(int i))
```


[``[NotNullWhen(true)] out string? 
message``](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis#conditional-postconditions-notnullwhen-maybenullwhen-and-notnullifnotnull)
Naturally, I'm not keen on adding this when there is an 
alternative design that doesn't require it.

So far the examples are quite simple, but how do we differentiate 
the relationship between all the objects involved in a function 
call?
Do the following functions have the same relationship between 
their function parameter and return value?

```d
int** rel1(int** param) => param;
int** rel2(ref int* param) => &param;
```

Yes and no.
A big part of the problem here is `rel2`, you can look at it and 
think oh I know it's got two cells, just like `rel1`!
And that is true as far as what you should have access to.
But its not; it's actually the same parameter as the first 
`rel1`, but you've specified that the first indirection is 
guaranteed to be non-null and should be ignored.
This gives you three cells, with the first being hidden to the 
user, and the second being automatically dereferenced.
That is storage of the parameter, pointer to `int*` that was 
passed in, and then the `int*` itself.

Knowing this allows us to model function calls of functions like 
these and know that the assert will hold true during compilation:

```d
int* var;
int** ptr = rel2(var);

assert(&var is ptr);
```

This is a particularly useful bit of information, but sometimes 
we don't want an identity function, but want to return what was 
contained within that object.

```d
int* rel3(ref int* param) => param;

int* var = new int;
int* ptr = rel3(var);

assert(var is ptr);
```

That is not the same relationship as say `rel1` or `rel2`.
Remember the big three cells that I just mentioned?
"That is storage of the parameter, pointer to `int*` that was 
passed in, and then the `int*` itself."
Well, that is a tad important; see before we were dealing with 
the second cell, now we are onto the third.

So what these three cells actually represent when deconstructed:

1. Stack variable that holds a value.
2. Pointer that was passed in and stored in the stack variable.
3. Pointer that can be accessed by performing a dereference on 
the stack variable.

And you know what? That third one doesn't have to be a pointer, 
it could be a struct for all we care about.

Pragmatically, this tells us what we need to model in an escape 
analysis solution.
We need to model the following scenarios:

1. We are escaping stack memory to an output location.
2. We are escaping an object that was passed in to an output 
location.
3. We are escaping an object acquired from an object that was 
passed in but is not the input, to an output location either 
directly or indirectly.

```
[ CELL 1 ]                       [ CELL 2 ]                       
[ CELL 3 ]
+------------------------+       +------------------------+       
+------------------------+
| Storage Location       | ----> | Pointer / Reference    | ----> 
| Pointed-to Payload     |
| (Stack Frame / Slot)   |       | Value                  |       
| (Primitive / Struct)   |
+------------------------+       +------------------------+       
+------------------------+
   Parameter storage                First indirection              
   Second indirection
```

By knowing if it is two or three, that tells us whether to 
activate the first error up the call stack.

```
[ caller frame ] ──(passes &localPtr)──► [ rel2(ref int* param) ]
        │                                         │
        │                                         │ Returns Cell 2 
(&param)
        ▼                                         ▼
    [ ERROR: Cell 2 points to stack slot of localPtr in caller ]

[ caller frame ] ──(passes &localPtr)──► [ rel3(ref int* param) ]
        │                                        │
        │                                        │ Returns Cell 3 
(param)
        ▼                                        ▼
    [ PASS: Cell 3 payload is Heap / Unrestricted Lifetime ]
```

Now DIP1000 tried to model exactly this, but it got it pretty 
badly wrong.
The `rel1` and `rel2` have two separate attributes `return` and 
`return ref`.
And `rel3` has another `return scope ref` on top of that.

When really the differentiation is just: "am I returning the 
outermost object?"

This is not the only issue with DIP1000, but it's a pretty big 
one.
That has led to the D community not understanding how or why 
DIP1000 works the way that it does.
It's not how the attributes are presented; it's what the 
attributes are representing.

I have and continue to call the differentiation between two and 
three "relationship strengths".
There is another that we need, and it's to activate a borrow 
checker.

Hopefully this explanation helps people to understand how and 
what escape analysis is supposed to be representing.
It's not a particularly easy subject to get into, but yet 
everyone learns this stuff implicitly when using D.

P.S.
We can thank Gemini for the help with the ASCII diagrams, I'm 
terrible at it.
Jul 25
parent reply Dukc <ajieskola gmail.com> writes:
On Saturday, 25 July 2026 at 14:52:30 UTC, Richard (Rikki) Andrew 
Cattermole wrote:
 An example of this is you can utilize conditionals to model 
 relationships:

 ```d
 int* returnCond(bool cond, int* obj)
 {
     if (cond)
         return null;
     else
         return obj;
 }
 ```

 So this could have a graph that looks like:

 ```
 [
     ( if (params[0] == true)
         return = null
     ),
     ( if (params[0] == false)
         return = params[1]
     )
 ]
 ```
Probably the compiler can't rely on this, other than for optimisations, unless we'll also have attributes to specify this manually. Otherwise `returnCond` couldn't be put to a `.di` file, nor could you type a function pointer so that you could call it like this.
 Do the following functions have the same relationship between 
 their function parameter and return value?

 ```d
 int** rel1(int** param) => param;
 int** rel2(ref int* param) => &param;
 ```

 Yes and no.
 A big part of the problem here is `rel2`, you can look at it 
 and think oh I know it's got two cells, just like `rel1`!
 And that is true as far as what you should have access to.
 But its not; it's actually the same parameter as the first 
 `rel1`, but you've specified that the first indirection is 
 guaranteed to be non-null and should be ignored.
 This gives you three cells, with the first being hidden to the 
 user, and the second being automatically dereferenced.
 That is storage of the parameter, pointer to `int*` that was 
 passed in, and then the `int*` itself.

 Knowing this allows us to model function calls of functions 
 like these and know that the assert will hold true during 
 compilation
I'm afraid you have misanalysed this and might be basing your escape analyser on sand. If this was C++ you would be right, since calling a C++ function with a `&` reference to null is undefined behaviour. However, the D spec doesn't say so (nor could it, since it's allowed in ` safe`. Could in principle be implementation-defined, though), and in fact a `ref` variable residing at null will [not even crash](https://forum.dlang.org/post/flutpcacmrxoqmnshrrw forum.dlang.org) if you don't use it anyhow. It seems likely this is the intended behaviour, though I don't have a confirmation.
 The `rel1` and `rel2` have two separate attributes `return` and 
 `return ref`.
 And `rel3` has another `return scope ref` on top of that.

 When really the differentiation is just: "am I returning the 
 outermost object?"

 This is not the only issue with DIP1000, but it's a pretty big 
 one.
 That has led to the D community not understanding how or why 
 DIP1000 works the way that it does.
 It's not how the attributes are presented; it's what the 
 attributes are representing.
This is certainly one way to view it. However, the differentation DIP1000 is making isn't any more complex: either you're (potentially) returning the `ref` address, or the address inside the variable. Two distinct cases, no more, just like in your formulation. So I still think the DIP1000 confusion about the return attribute is mostly in it's representation (when does `return` mean `return ref`, when `return scope`), not in the very idea. Yours would be about as hard to understand.
Jul 27
parent "Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz> writes:
On 28/07/2026 4:27 AM, Dukc wrote:
 On Saturday, 25 July 2026 at 14:52:30 UTC, Richard (Rikki) Andrew 
 Cattermole wrote:
 An example of this is you can utilize conditionals to model 
 relationships:

 ```d
 int* returnCond(bool cond, int* obj)
 {
     if (cond)
         return null;
     else
         return obj;
 }
 ```

 So this could have a graph that looks like:

 ```
 [
     ( if (params[0] == true)
         return = null
     ),
     ( if (params[0] == false)
         return = params[1]
     )
 ]
 ```
Probably the compiler can't rely on this, other than for optimisations, unless we'll also have attributes to specify this manually. Otherwise `returnCond` couldn't be put to a `.di` file, nor could you type a function pointer so that you could call it like this.
 Do the following functions have the same relationship between their 
 function parameter and return value?

 ```d
 int** rel1(int** param) => param;
 int** rel2(ref int* param) => &param;
 ```

 Yes and no.
 A big part of the problem here is `rel2`, you can look at it and think 
 oh I know it's got two cells, just like `rel1`!
 And that is true as far as what you should have access to.
 But its not; it's actually the same parameter as the first `rel1`, but 
 you've specified that the first indirection is guaranteed to be non- 
 null and should be ignored.
 This gives you three cells, with the first being hidden to the user, 
 and the second being automatically dereferenced.
 That is storage of the parameter, pointer to `int*` that was passed 
 in, and then the `int*` itself.

 Knowing this allows us to model function calls of functions like these 
 and know that the assert will hold true during compilation
I'm afraid you have misanalysed this and might be basing your escape analyser on sand. If this was C++ you would be right, since calling a C++ function with a `&` reference to null is undefined behaviour. However, the D spec doesn't say so (nor could it, since it's allowed in ` safe`. Could in principle be implementation-defined, though), and in fact a `ref` variable residing at null will [not even crash](https://forum.dlang.org/ post/flutpcacmrxoqmnshrrw forum.dlang.org) if you don't use it anyhow. It seems likely this is the intended behaviour, though I don't have a confirmation.
I have implemented a null check to disallow it, in addition to the DFA's ability to catch such cases. Currently needs maturing. But it is in master. Nullability is something Walter hasn't placed much weight on solving due to MMU's, however this makes debugging harder and does limit D's use cases when it comes to application development for things like web development. There are requirements on recoverability that Walter doesn't agree with, but needs to be offered regardless. Just because it has been allowed, doesn't mean that it is good code that should be accepted moving forward. In the case of derefencing null, it is never good code that should be accepted.
Jul 27