Carp icon indicating copy to clipboard operation
Carp copied to clipboard

RFC: Implement `delete` for `Ptr` and add `Leak` type

Open scolsen opened this issue 5 years ago • 4 comments

Now that delete is an interface, we can easily implement it for Ptr and allow Carp to also manage this type, all we'd need to add is:

(implements delete Pointer.free)

since Pointer.free already has the right signature. Doing so would limit the purpose of Ptr, making it a type primarily for C interop, that still leverages Carp's memory management in carp code--previously it served a double function, being an unmanaged type also intended for interop.

Implementing delete would remove this "unmanaged memory" use case for Ptr and make it strictly an interop type, after doing so, we could neatly reintroduce this "unmanaged type" functionality with a new type Leak which is precisely an "unmanaged" pointer (core will implement delete on it as a no-op). The type name also communicates that the memory needs to be managed manually. Unsafe.leak could also return this type, indicating at the type level precisely where something was leaked in the program (the sig of this function would change from [a] () to [a] (Leak a)).

Since Leak's delete implementation would be a no-op, destructors on this type must be called manually--in other words, Leak (in core) is the type of things whose destructors must be called manually (unless one defines an implementation of delete for it downstream).

Other names could be:

Memory (this type is just raw, unmanged memory to something) Unmanged (this type is not managed) Eternal (for eternal lifetime, unless manually killed)

Why?

I think the nice thing about having a type like Leak is that it'd grant us a dedicated type in Core that communicates at the type level precisely when something is not going to be managed by Carp's borrow system. One can define interfaces against this and communicate the "unmanaged" nature of values to the user, e.g. as a library user, seeing a function with signature [b] (Leak c) would immediately indicate that I'm getting a long-lived, borrow-checker immune value that I'll have to delete/clean-up manually by calling some function on it in my Carp code--that is, I can be confident delete emissions won't free it. Or, as another example, if the body of some function in a library manually calls a special deleter on an argument, this can be partially communicated (or more so hinted at) [(Leak Foo)] b.

scolsen avatar Dec 23 '20 04:12 scolsen

@carp-lang/maintainers

scolsen avatar Dec 23 '20 04:12 scolsen

I think this is a great proposal!

An alternative would be to call for specific implementations of delete for Pointer where applicable for the user, but the problem here is non-determinism across modules/packages (if a package implements Pointer.delete, that pointer type suddenly becomes managed across all packages). I think making a clear distinction between a managed and umanaged pointer type is more principled, and leads to better code. Maybe we should even disallow delete being implemented for Leak?

As with other generic delete templates, I think this needs to be implemented in the compiler, since it needs access to the deleter of the contained type, and it probably needs to emit a dependency.

hellerve avatar Dec 23 '20 09:12 hellerve

Another thing to note is that the behavior of address would need to change: it would either need to consume the data and return a pointer (since the data is then managed through the pointer) or take a ref and return Leak. We could also make it polymorphic such that it’s equivalent to having both:

(sig address (Fn [(Ref a)] (Leak a)))
(sig address (Fn [a] (Ptr a)))

hellerve avatar Dec 23 '20 09:12 hellerve

Thanks, @hellerve, you raise important points!

Maybe we should even disallow delete being implemented for Leak?

Interesting idea! And actually that would be pretty neat--if we do this, I wonder if we ought to implement it generically so that its a facility available to users as well? We'd have some mechanism for saying "this type cannot implement such and such an interface" --almost like the negative of traits/typeclasses--you'd be making an assertion that this type definitely doesn't have these particular properties.

I don't think it'd be a very common use case, but I can imagine a few cases in which it makes sense. For instance, you might model a really expensive object as a type, like world in a game and want to enforce that it cannot be copy'd since this would be prohibitively expensive, or since you want to enforce passing around a single mutable state. Similarly you might model some type where zero doesn't make sense, etc. It would also clarify library boundaries in the sense of letting authors define "privileged" or "special" types that can't be modified by users of the lib to ensure some constants hold internally in the lib (unless the users writes wrapper types)

As with other generic delete templates, I think this needs to be implemented in the compiler, since it needs access to the deleter of the contained type, and it probably needs to emit a dependency.

Right, that's a good point! We will need this for sure unless we implement no-op deletes for all unmanged types (I think?) (but this would lead to a lot of excess emissions also) If we had delete for all types we could just define an initializer for Leak as (defn leak [x] (do (Leak @&x) (delete x))) where delete Int etc. would just do nothing.

Or we could just rely on the address you proposed for initializing Leaks --then we'd just be casting a Ref to an unmanaged Leak and wouldn't need any deleters (right?)--maybe this is preferable since it's simpler though more limited (since one needs to get a Ref first, but I think that makes sense, perhaps?)

scolsen avatar Dec 23 '20 15:12 scolsen