Null-checked reference types
Quirin Schroll
qs.il.paperinik at gmail.com
Mon Aug 12 10:02:33 UTC 2024
On Wednesday, 7 August 2024 at 11:30:02 UTC, Richard (Rikki)
Andrew Cattermole wrote:
>
> On 07/08/2024 11:22 PM, Quirin Schroll wrote:
>> On Wednesday, 7 August 2024 at 01:39:29 UTC, Richard (Rikki)
>> Andrew Cattermole wrote:
>>> 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`.
>
> It's going to be valid regardless, due to AssignExpression.
Currently, assignments are not valid for conversion to `bool`.
(``Error: assignment cannot be used as a condition, perhaps `==`
was meant?``)
>>> > 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.)
>
> No, I meant type state.
>
> https://en.wikipedia.org/wiki/Typestate_analysis
>
> unreachable < reachable < initialized < default-initialized <
> non-null < user
I didn’t read the Wikipedia article in detail, but it contains no
“null,” so I’m wondering how it’s related. A variable of
non-nullable type must be initialized. If we’re talking `@system`
code, fine, it need not be, it could even be void initialized.
IIUC, typestate analysis could be used to make void
initialization `@safe` by proving that a void initialized value
has definitely been initialized whenever it’s read (i.e. no
uninitialized read).
IIUC, what you’re suggesting is allowing variables of non-null
type to be initialized by `null`, but that reading one requires
them to be initialized.
>>> 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.
>
> Right, that would have to be disallowed without DFA, since the
> type state must not change throughout a function body.
Why wouldn’t it be able to?
It might make sense to the programmer to initialize a variable
with a definite non-null value, but later, e.g. on some
error-like case, reassign `null`.
If you use inference, it may (depending on implementation) infer
a non-nullable type. The right course of action is to use an
explicit wider type. This is similar to how `auto x = new
Derived` gives you `x` typed as `Derived`, and that bars you from
assigning it some other `Base` type object. The right course of
action is to declare `x` via `Base x = new Derived`.
>>> 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
>> }
>> ```
>
> What matters here is that you do not need to add annotation to
> the type itself. It only needs to exist within the function
> signature. Anywhere else its useless information.
I don’t understand. To me, `Object!` and `Object?` are related
but different types. You can have arrays of them, etc., how else
would the information of nullableness be retained?
Maybe I need some info dump on type state analysis and what you
mean exactly, because as I understand, TSA would only give you an
implicit cast from `T?` to `T!` in some cases, similar to how
uniqueness gives you an implicit cast from `T` to `immutable(T)`
in some cases.
More information about the dip.development
mailing list