There are some reflections to be had with how DIP1000 changes the type system. I think this change is coming from not making the true type system explicit.
Imagine that all types come in two varieties: scope and heap. When we write
void f(int x){…}
the full type would be:
void f(scope(int) x){…}
If we now introduce «|» to mean union, so that the union type A and type B becomes «A|B» then we can think that when we write
int* p;
the full type would be:
(scope(int)|heap(int)|null)* p;
So in the context of DIP1000, when we write:
void f(int x){
int* p = null;
if (condition) p = &x;
g(p);
if (condition) p = null;
else p = new int(5);
g(p)
}
the full typing ought to be:
void f(scope(int) x){
(scope(int)|heap(int)|null)* p=null; // *p narrowed to: null
if (condition) p = &x; // *p widened to: scope(int)|null
g(p); // called as g((scope(int)|null)*)
if (condition) p = null;
else p = new int(5); // *p becomes: heap(int)|null
g(p); // called as g((heap(int)|null)*)
}
This is basic flow typing and not too difficult to understand, it is also more versatile than DIP1000 as it can be used for tracking other things (like nullable).
The current scheme of only narrowing, but never widening is overly pessimistic, most likely confusing and difficult to explain/justify to newcomers.