Null-checked reference types

Quirin Schroll qs.il.paperinik at gmail.com
Wed Aug 7 11:22:20 UTC 2024


On Wednesday, 7 August 2024 at 01:39:29 UTC, Richard (Rikki) 
Andrew Cattermole wrote:
> > Add operators for null-respecting access: ?., ?(…) (call if
> not null), ?[…] (index if not null), ?= (assign if null).
>
> This should be split out into a separate DIP. They are things I 
> already want for similar reasons.

Right. They’re useful anyways.

> However, they must decompose to loads and stores inside if 
> statements. This makes it temporally safe.
>
> ``var1.var2?.field = 2;``
>
> ```d
> if (auto var2 = var1.var2) {
> 	var2.field = 2;
> }
> ```

That seems like a little too much magic. `var?.x = 0` can be two 
things, a field assignment or a property setter call. For the 
latter, the property setter call wouldn’t be executed if `var` is 
null.

It may sound harsh, but probably, it’s best not to allow 
assignments like that and require the programmer write it out:
```d
if (Field* field = &var1.var2?.field) *field = 2;
if (void delegate(Field) setter = &var1.var2?.field) setter(2);
```

> This allows you to do both loads and stores and do something if 
> it failed transitively.
>
> ```d
> if (var1.var2?.var3?.field = 3) {
> 	// success
> } else {
> 	// failure
> }
> ```

I somehow don’t like `if (… = …)` when it’s not a declaration. At 
first sight, I thought you intended `… == 3`.

> > No data flow analysis is proposed. Null checking is local and
> done by tracking ? and ! by the type system.
>
> DFA is only required if you want the type state to change as 
> the function is interpreted. So that's fine. That is a me thing 
> to figure out.

If I understand correctly, by “type state” you means something 
like value range propagation. It basically *is* value range 
propagation, however the ranges in question are `null` and all 
non-null values. You don’t suggest `typeof` type of a variable or 
expression changes, correct? (I think that would be very weird.)

> However, you do not need to annotate function body variables 
> with this approach.
>
> Look at the initializer of a function variable declaration, 
> it'll tell you if it has the non-null type state.
>
> ```d
> int* ptr1;
> int* ptr2 = ptr1;
> ```

The only issue is, just because e.g. a pointer is initialized 
with something non-null (e.g. the address of a variable), that 
doesn’t mean some logic later won’t assign `null` to it.

> Function parameters, (including return and this) need to be 
> annotateable with their type state.
>
> ```d
> int* func(?nonnull return, ?nonnull this, ?nonnull int* ptr) {
> 	return ptr;
> }
> ```

So, using my syntax, that would be:
```d
int*! func(int*! ptr) => ptr;
```

If we want null-annotations for member functions’ `this`, my 
suggestion would be to use `null` as a function attribute.

```d
int*! func(int*! ptr) null => ptr;
```

It’s similar how `scope` and `return` as member function 
attributes work.

> They can also be inferred by first usage.
>
> ```d
> void func(int* ptr) {
> 	int v = *ptr;
> }
> ```
>
> Clearly ``ptr`` has the type state non-null.

We can only do this when stuff is to be inferred. Otherwise, it 
would be weird changing a precisely given signature because of 
what’s going on inside the function.

In a context where `int*` is nullable, the usage of `*ptr` could 
suggest declaring it non-nullable.

> However the problem which caused me some problems in the past 
> is on tracking variables outside of a function. You cannot do 
> it.
>
> Variables outside a function change type state during their 
> lifespan. They have the full life cycle, starting at reachable, 
> into non-null and then back to reachable. If you tried to force 
> it to be non-null, the language would force you to have an 
> .init value that is non-null. This is an known issue with 
> classes already. It WILL produce logic errors that are 
> undetectable.

I don’t care much about tracking. Probably, with `if (auto) ...`, 
you can just rename the variable, but typed non-nullable:

```d
void f(int*? p)
{
     if (int* q = p) ... else return;
     int v = *q; // no error, q isn’t nullable, not by analysis, 
just by type
}
```


More information about the dip.development mailing list