| |
 | Posted by Timon Gehr in reply to Walter Bright | Permalink Reply |
|
Timon Gehr 
Posted in reply to Walter Bright
| On 12/31/22 07:34, Walter Bright wrote:
> On 12/30/2022 1:07 PM, Timon Gehr wrote:
>>> In your description of pattern matching checks in this thread, the check was at runtime.
>>> ...
>>
>> No, the check was at compile time.
>
> The pattern matching is done at run time.
>
>> The check I care about is the check for _failure_. The check for _null_ may or may not be _necessary_ depending on the type of the reference.
> NonNull pointers:
>
> int* p = ...;
> nonnull int* np = isPtrNull(p) ? fatalError("it's null!") : p;
> *np = 3; // guaranteed not to fail!
>
> Null pointers:
>
> int* p = ...;
> *p = 3; // seg fault!
>
> Which is better? Both cause the program to quit on a null pointer.
> ...
You have deliberately chosen an example where it does not matter because your aim was specifically to dereference a possibly null pointer.
I care about this case:
nonnull int* p = ...; // possibly a compile time error
*p = 3; // no runtime check. no seg fault!
Note that the declaration and dereference can be a few function calls apart. The further away the two are, the more useful tracking it in the type system becomes.
Manual checks can be used to turn possibly null pointers into non-null pointers anywhere in the program where there is a sensible way to handle the null case separately. This is just a special case of sum types, where the compiler checks that you dealt with all cases exhaustively.
The especially efficient tag encoding provided by `null` is just an additional small detail.
>
>> This technology has a proven track record.
>
> A proven track record of not seg faulting, sure.
Of making people think about, and handle the null case if it is necessary at all. I have already told you that my main gripe here is not specifically the segfault (though that does not help), it's the fatal and implicit nature of the crash.
> A proven trackrecord of no fatal errors at converting a nullable pointer to nonnull, I'm not so sure.
> ...
Converting a nullable pointer to nonnull without handling the null case is inherently an unsafe operation. D currently does it implicitly. Explicit is better than implicit for fatal runtime errors that will shut down your program completely.
Typically you'd mostly use nonnull pointers and not get any fatal errors. It is true that if you have nontrivial logic determining whether some pointer should be null or not you may have to check that invariant at runtime with the techniques present in popular languages, but at least it's explicit.
My experience has been that null pointer segfaults usually happen in places where either a null pointer is never expected (and a nonnull pointer should have been used, making the type system ensure that the caller provides one) or there should have been a check, with different logic for the null case. I.e., they happen because people failed to think about the null case at all. The language encourages this lack of thinking by treating all references as non-null references during type checking and then crashing at runtime implicitly once the type checker's assumptions are inevitably violated.
Nonnull pointers allow expressing such assumptions in the type system. They are actually more useful than runtime segfaults and assertion failures, because they document expectations and the error will be at the place where the bad null pointer originates instead of at the place where it was not expected to occur.
Runtime segfaults/assertion failures are actually much more susceptible to being papered over by subtly changing a function's interface and making it more complex by doing some checking internally and ignoring null instead of addressing the underlying issue. This is because it's harder to find the root cause, especially in a large undocumented code base. Nonnull is compiler-checked documentation and it will direct your attention to the function that is actually wrong by default.
>
> > Relying on hardware memory protection to catch the null
> > reference is never necessary,
>
> If you manually code in a runtime check, sure, you won't need a builtin check at runtime.
> ...
No, you don't need any runtime check at all to dereference a nonnull pointer.
nonnull x = new A;
x.y = 3; // runtime checks completely redundant here
> > because _valid programs should not even compile if
> > that's the kind of runtime check they would require to ensure type safety_.
>
> Then we don't need sumtypes with pattern matching?
> ...
That's not what I said. I am specifically talking about _implicit_ runtime checks causing a _program panic/segfault_. It's just a bad combination for null handling. Bad UX and hardly defensible with technical limitations.
> > The hardware memory protection can still catch compiler bugs I guess.
>
> Having a hardware check is perfectly valid for checking things.
> ...
Sure, in principle it can still be leveraged for some sort of explicit runtime-checked null pointer dereference syntax. Personally, the convenience of having the assertion failure tell me where it happened (even if I don't happen to be running in a debugger) is probably _by far_ worth the additional runtime check in the couple places where it would even remain necessary.
Also as Sebastiaan points out, there are actually relevant targets that don't give you the check.
> BTW, back in the bad old DOS days, I used to write a lot of:
>
> assert(p != NULL);
>
> It was very effective. But with modern CPUs, this check adds no value, and I removed them.
I have not much to add to this off-topic point. As I told you many times by now, I mostly agree here, but I want to be able to move most of this checking to compile time instead.
BTW: I really dislike the terminology "nonnull pointer/reference". It's a weird inversion of defaults. nonnull is a much better default.
|