Escaping objects and what is escape analysis modelling anyway?
Richard Andrew Cattermole (Rikki)
richard at cattermole.co.nz
Sat Jul 25 14:52:30 UTC 2026
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))
```
C# has implemented this back in 2019 to model the TryGet pattern.
[``[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) => ¶m;
```
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
(¶m)
▼ ▼
[ 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.
More information about the Digitalmars-d
mailing list