digitalmars.D - Escaping objects and what is escape analysis modelling anyway?
- Richard (Rikki) Andrew Cattermole (205/205) Jul 25 Another year has passed where people have felt the need to use
- Dukc (24/77) Jul 27 Probably the compiler can't rely on this, other than for
- Richard (Rikki) Andrew Cattermole (12/79) Jul 27 I have implemented a null check to disallow it, in addition to the DFA's...
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) => ¶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.
Jul 25
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) => ¶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 compilationI'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
On 28/07/2026 4:27 AM, Dukc wrote:On Saturday, 25 July 2026 at 14:52:30 UTC, Richard (Rikki) Andrew Cattermole wrote: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.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) => ¶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 compilationI'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.
Jul 27








"Richard (Rikki) Andrew Cattermole" <richard cattermole.co.nz>