Thread overview
The missing bit in DIP1000
Jun 23, 2022
rikki cattermole
Jun 24, 2022
bauss
June 23, 2022

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.

June 23, 2022
I like it.

It solves non-nullable types and scope as a type constructor.

Although I do hope it wouldn't require more syntax than adding a single new type constructor.
June 23, 2022

On Thursday, 23 June 2022 at 10:17:44 UTC, rikki cattermole wrote:

>

I like it.

It solves non-nullable types and scope as a type constructor.

Although I do hope it wouldn't require more syntax than adding a single new type constructor.

You don't necessarily need to add syntax for it, just "think" about it this way so you end up with something consistent and later can add ful flow typing if that turns out to be a good idea. (Flow typing should work very nicely with meta-programming, but for some reason most languages that provide flow typing does not have meta programming!! :-D Although I guess C++ concepts is indirectly hinting at this combination being fruitful.)

I think you also would need to have «returnscope(int)» which would be a subtype of «scope(int)» ( returnscope <: scope ), but I haven't really gone over it, so maybe I am wrong. Is it safe to put a returnscope type everywhere you would accept a scope type? If yes, then we are good.

June 23, 2022
>

the full type would be:

(scope(int)|heap(int)|null)* p;

It might be better to use something like this instead:

(scope(int)|nonlocal(int)|null)* p;

As you might later want to distinguish between globalstatic, malloc-heap and gc-heap.

So you get:

  • returnscope <: scope
  • globalstatic <: nonlocal
  • mallocheap <: nonlocal
  • gcheap <: nonlocal

Where globalstatic is the same lifetime as the program.

Or something along these lines.

Note that C++ also defines types that are not directly expressible by name in the language to describe move semantics, so the above does not suggest syntax, just desirable semantics.

June 23, 2022

On Thursday, 23 June 2022 at 10:17:44 UTC, rikki cattermole wrote:

>

I like it.

It solves non-nullable types and scope as a type constructor.

On second thoughts, scope has to provide stack-depth information in order to build simple stuff like linked lists where life times matter.

So scope!0 would be "function scope" and scope!1 would be some stack frame further down the stack than scope!0. scope!2 would be some stack frame further down than scope!1 etc.

Then you can connect two nodes in a single linked list:

void connect(scope!1(node)* a, (scope!1(node)|scope!2(node))* b){
  a.next = b;
}

Hmm… But the syntax needs some work.

Also, it has to be compared to standard life time annotations which is more general.

June 23, 2022

On Thursday, 23 June 2022 at 16:43:21 UTC, Ola Fosheim Grøstad wrote:

>
void connect(scope!1(node)* a, (scope!1(node)|scope!2(node))* b){
  a.next = b;
}

Thinking aloud, so I guess this also makes it possible to get rid of the return scope ref thing:

 scope!1(node)* connect(scope!1(node)* a, (scope!1(node)|scope!2(node))* b){
   a.next = b;
   return a;
 }

 auto tmp = x.connect(y).connect(z);

The caller now knows that the returned pointed to object has the same life time as the first parameter it provided.

You could have some simple aliases like:

 scope(x) :=: scope!0(x)
 scope1(x) :=: scope!1(x)
 scope2(x) :=: scope!2(x)
 scope12(x) :=: scope!1(x)|scope!2(x)

or something like that, then the previous example would be:

 scope1(node)* connect(scope1(node)* a, scope12(node)* b){
   a.next = b;
   return a;
 }

 auto tmp = x.connect(y).connect(z);

Still got a feeling that standard life times are about the same level of complexity.

June 24, 2022

On Thursday, 23 June 2022 at 08:56:49 UTC, Ola Fosheim Grøstad wrote:

>

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.

I really like this approach and if that information can be retrievable with something like __traits that would be great.