October 24, 2022

On Monday, 24 October 2022 at 20:29:55 UTC, Walter Bright wrote:

>

On 10/24/2022 12:12 PM, Paul Backus wrote:

>

The problem with using @live and @safe together is that (a) you cannot safely call a @live function from a non-@live function, or vice-versa (because non-@live functions will not honor the above rules), and (b) as a consequence, changing a function from @safe to @safe @live is a breaking API change.

This means that existing @safe D projects will not be able to adopt @live, and the community will have to build an entirely new @safe @live ecosystem from the ground up in order to see any benefit in practice. The more likely outcome is that D users will stick with their existing @safe codebases (which they've invested time and money in) and ignore @live altogether.

@live does indeed allow for incremental, function by function use of @live. This is inevitable as using an ownership/borrowing system requires restructuring the algorithms and data structures.

I demonstrate what I think Paul means. Suppose we have a manually managed custom pointer type, designed for safe usage in @live functions:

struct MMptr (Pointee)
{ @system Pointee* data; // Assuming @system variables DIP is implemented
  // implementation...
}

// The only way to create MMptrs except for nulls
MMptr!T mallocate(T)()
{ import core.lifetime;
  import core.stdc : malloc;
  auto mem = cast(T*)malloc(sizeof(T));
  return MMptr(mem.emplace);
}

// The only way to get rid of owned MMptrs
void free(T)(MMptr!T expired)
{ import core.stdc : free;
  destroy!false(*expired.data);
  free(expired.data);
}

This is great when all client code is @live. Perfectly @safe as far as I see. Yet you cannot mark free @trusted. This is because then there would be nothing to prevent a @safe but non-@live function using it and corrupting the heap with dangling pointers.

October 24, 2022
On Monday, 24 October 2022 at 20:38:59 UTC, Walter Bright wrote:
>
> It seems you're suggesting attaching this behavior to the pointer, rather than the function. That means multiple pointer types.
>

I think what he means are movable types. Movable types are moved by default and if a copy is needed there is an explicit method to do so. In the case of D that could be newPointer = oldPointer.dup. dup implementation is dependent on type. In the case of raw pointers like D has today, dup wouldn't do much other than assigning the pointer and perhaps some compiler intrinsic for the life time system.
October 25, 2022
On 25/10/2022 9:38 AM, Walter Bright wrote:
> It seems you're suggesting attaching this behavior to the pointer, rather than the function. That means multiple pointer types.
> 
> Multiple pointer types have been tried many times. They are attractive in theory, but work out poorly in practice. For example, take:

That's not what I'm suggesting, and I'm familiar with that issue (sadly far too well). I have something like 700LOC just for replace in my Unicode string builder. For all the different string types. Can't use templates at that level due to shared libraries and wanting to guarantee everything is initialized + tested in it.

I have suggested this before, but scope as a type qualifier.

A type qualifier (especially if its scope!) adds the extra checks for a specific object, without invading the entire function call graph.

Consider:

```d
struct Nullable(T) {
	T value;

	ref borrow(T) get() {
		return value;
	}

	alias get this;
}

void func1() {
	Nullable!U n;
	func2(n);
}

void func2(scope U input) {

}
```

We have this sort of behavior in the language already i.e. shared. Why is lifetime tracking any different?
October 24, 2022
On 10/24/2022 2:13 PM, rikki cattermole wrote:
> We have this sort of behavior in the language already i.e. shared. Why is lifetime tracking any different?

I deliberately avoided making scope a type qualifier.

My concern with a complex type structure is (besides the combinatorial explosion) people will just find it too complicated to use.

What a relief it was to be rid of near and far pointers.
October 24, 2022
On 10/24/2022 1:50 PM, Paul Backus wrote:
> It is impossible for both of the following statements to be true simultaneously:
> 
> 1. Existing @safe code can incrementally adopt @live without breaking API changes.
> 2. @live allows code to be made @safe or @trusted that could previously only be @system.
> 
> If (1) is true, then allowing @live functions to do anything a @safe function could not already do would make it possible for memory corruption to occur in @safe code. Therefore, (2) must be false.
> 
> If (2) is true, then the argument in my previous message applies, which means that (1) must be false.

(1) is false. @live functions come with the assumption that:

    @live void foo(int* p, int* q) { ... }

will not be passed pointers to the same object.

October 24, 2022
On 10/24/2022 1:58 PM, Dukc wrote:
> struct MMptr (Pointee)
> { @system Pointee* data; // Assuming @system variables DIP is implemented
>    // implementation...
> }
> 
> // The only way to create MMptrs except for nulls
> MMptr!T mallocate(T)()
> { import core.lifetime;
>    import core.stdc : malloc;
>    auto mem = cast(T*)malloc(sizeof(T));
>    return MMptr(mem.emplace);
> }
> 
> // The only way to get rid of owned MMptrs
> void free(T)(MMptr!T expired)
> { import core.stdc : free;
>    destroy!false(*expired.data);

.data wouldn't be accessible from @safe code.

>    free(expired.data);
> }


October 25, 2022
On 25/10/2022 11:04 AM, Walter Bright wrote:
> On 10/24/2022 2:13 PM, rikki cattermole wrote:
>> We have this sort of behavior in the language already i.e. shared. Why is lifetime tracking any different?
> 
> I deliberately avoided making scope a type qualifier.
> 
> My concern with a complex type structure is (besides the combinatorial explosion) people will just find it too complicated to use.

My concern is that memory owners simply cannot expose their owned memory without the possibility of logic errors & segfaults.

A good recent example:

https://dlang.org/changelog/2.101.0.html#borrow_for_refcounted

Ultimately the owner must come first, regardless of what the caller wants if we want this to be safe.

> What a relief it was to be rid of near and far pointers.

Oh I bet.

I'm glad I have never needed to use them.
October 24, 2022
On 10/24/2022 6:14 AM, Timon Gehr wrote:
> - have more than one lifetime that exceeds the current function's lifetime. I.e., multiple distinct return annotations, ideally an arbitrary number, by allowing the return annotation to be parameterized by a lifetime variable.

This has been proposed before (I think by deadalnix), and much much earlier (before Rust) by Bartosz Milewski.

My objection to it was based on its complexity. People have a hard enough time with `scope`. I am frankly amazed that Rust has been able to sell the idea of such complicated annotations, though Rust does seem to attract programmers who revel in such :-) I often hear stories about Rust developers just trying random annotations to get it to pass the compiler, having little concept of how it actually works.

The design of dip1000 does not preclude this, however, and it's good to see how far we can push things and avoid needing such complexity.

> - have such parameterized scope return annotations on fields of aggregates.

Same problem. Let's see how far we can get without them.

P.S. another annotation that nobody is able to use effectively (or correctly) is C's `restrict` keyword. C++ never bothered to adopt it. Though it was a bad design anyway, as the compiler cannot check if it is used correctly, it just generates bad code. ImportC just ignores `restrict`.


October 25, 2022
On 10/25/22 00:26, Walter Bright wrote:
> On 10/24/2022 6:14 AM, Timon Gehr wrote:
>> - have more than one lifetime that exceeds the current function's lifetime. I.e., multiple distinct return annotations, ideally an arbitrary number, by allowing the return annotation to be parameterized by a lifetime variable.
> 
> This has been proposed before (I think by deadalnix), and much much earlier (before Rust) by Bartosz Milewski.
> ...

Pretty sure this has already been proposed in some academic paper in the 1990's or earlier.

> My objection to it was based on its complexity.

It's barely more complicated than return annotations. `inout` grew into a complex type unsound mess exactly because D was avoiding such parameters.

> People have a hard enough time with `scope`. I am frankly amazed that Rust has been able to sell the idea of such complicated annotations, though Rust does seem to attract programmers who revel in such :-) I often hear stories about Rust developers just trying random annotations to get it to pass the compiler, having little concept of how it actually works.
> ...

Well, one goal of @safe is to reduce the amount of damage that can be done by incompetent developers. Let them randomly try stuff. If that allows them to write memory safe code, more power to them I guess.

Anyway, for the cases where DIP1000 is adequate, no other annotations are needed. Library developers do need them though.

> The design of dip1000 does not preclude this, however, and it's good to see how far we can push things and avoid needing such complexity.
> 
>> - have such parameterized scope return annotations on fields of aggregates.
> 
> Same problem. Let's see how far we can get without them.
> ...

It does not allow me to do most of the things I'd like to do with lifetime tracking. At least `scope` is checked by the compiler more reliably now, which is good.

> P.S. another annotation that nobody is able to use effectively (or correctly) is C's `restrict` keyword. C++ never bothered to adopt it. Though it was a bad design anyway, as the compiler cannot check if it is used correctly, it just generates bad code. ImportC just ignores `restrict`.
> 

(That's a completely different case.)

October 25, 2022
On 10/24/22 20:18, Walter Bright wrote:
> There's a misunderstanding here.

Not really.

> @live *does* work in @safe code,

Yes.

> and confers benefits.
> 

Barely.

> I oversimplified things by talking about `free`. Of course, `free()` is @system and is not callable from @safe code. But what I am *really* talking about is when a pointer is passed as an argument to a function, is it being "moved" or "copied"?
> 
> If it is "copied", that means the caller retains ownership of the pointer. If it is "moved", the caller transfers ownership to the callee. What the callee does with the pointer that was transferred to it is not relevant to the caller.
> 
> So,
> 
> 1. void foo(int* p) => argument is moved to foo(), caller can no longer use it
> 
> 2. void foo(scope int* p) => argument is copied to foo(), caller retains ownership
> 
> 
> If we change the annotations to:
> 
> 1. void foo(owner int* p) => argument is moved to foo()
> 
> 2. void foo(borrow int* p) => argument is copied to foo()
> 
> it means the same thing.

The difference is that now you have annotated an adequate thing.

> This is why dip1000 is foundational to implementing an ownership/borrowing system.
> 
> move == owner == notscope
> 
> copy == borrow == scope
> 
> If I was smarter, scope would have been spelled "borrow" :-/

I am aware. I also know a thing or two about move semantics and borrowing. @live falls short of my expectations in at least two fundamental ways:

- it is a function annotation (this makes no sense)
- it works on built-in pointers instead of library-defined types

It does not guarantee any of the invariants one usually tries to establish with an ownership/borrowing system. Hence it is pretty much useless in @safe code.