`deinit` proposal

Discussion thread for the deinit proposal.

cc @clattner as proposal author

1 Like

I like everything up to Step 3: Restrict usage of the deinit convention. However, I think that making it so that you can’t have one type deinit another is going to restrict some of the use-cases for linear types, such as compile-time refcounting were you want to deinit your ā€œsub-handleā€ to the type using some sort of main handle which is actually responsible for the destruction. Other places where I could see something like this are for allocators with pointer types designed to force you to ā€œfreeā€ the pointer into the correct arena, where this makes it impossible to batch-deallocate objects without some annoying workarounds such as doing a typestate transformation on all of them, which may require a buffer to move them into as part of the transformation.

One other consideration is whether this unnecessarily restricts various forms of the typestate pattern, for instance if you wished to prove that two independent things had happened, the methods which cause them to happen could return linear-type ā€œproof tokensā€ and require you to pass in some number of them to prove that prior events happened. As an ML-flavored example, you might want to use them to prove that 4 GPUs exist in order to construct a 4-GPU model pipeline, requiring 4 differently-typed tokens.

I prefer keyword del for argument convention. Looks straightforward and self-explanatory:

struct Foo:
  fn __del__(del self): ...
3 Likes

+1 for del as the name of the argument convention. It is concise and denotes the argument that will be deleted eventually.

fn __moveinit__(out self, del existing: Self):
1 Like

I think the proposed design for deinit would be a mistake. Whether a function directly or indirectly destroys a value is an implementation detail, and should not be part of the function’s signature. The user of the function doesn’t need to know the difference between var and deinit, and putting this information in the public API of Mojo libraries is probably going to cause unnecessary confusion. Mojo learners will see ā€œdeinitā€ in function signatures and be confused about what it means, when ultimately, from the user’s perspective, it has identical semantics to ā€œvarā€.

I think the alternative Chris listed at the end of his proposal is the right one: give __disable_del a better name, and require __del__ and __moveinit__ to invoke it explicitly. This makes the ā€œfinal deinitialization stepā€ for a value an implementation detail, and therefore frees people from needing to think about it!

Final note: The proposal mentions that var should be used instead of deinit in function types. This is further evidence that deinit should not appear in a function’s signature. The type checker doesn’t care about deinit vs var, and the programmers that are using the function shouldn’t need to care either.

3 Likes

The proposal mentions that var should be used instead of deinit in function types.

I do have a small concern about this. This means that I can pass the function fn foo(deinit x): #...to a parameter that expects fn foo(var x). Even if this works since both functions have the same signatures, this is still a little unintuitive as I would (and did) assume initially that their signatures would be distinct.

I do also see __disable_del as a little bit of a foot gun too. I’m not necessarily for or against this proposal; I’m just pointing out something that should be taken into consideration.

Final note: The proposal mentions that var should be used instead of deinit in function types. This is further evidence that deinit should not appear in a function’s signature.

I agree. This might make explaining argument conventions more difficult than before.

In this case I would opt to stick with __disable_del but require it to be invoked explicitly: just remove the magic. How about renaming it to del?

struct String:
    ...
    fn __del__(var self):
        """Destroy the string data."""
        self._drop_ref()
        del self

    fn __moveinit__(out self, var existing: Self):
        ...
        del existing
5 Likes

@christoph_schlumpf I think the solution you’ve proposed would be fantastic. It’s simple, and it keeps the function signature as var, which is what we want.

I’m not sure whether it makes sense to use the keyword del, because we’re discussing an unsafe/restricted feature (marking a struct instance as deleted), whereas in Python del is a safe feature that anybody can invoke at any time.

Also, I think Mojo is currently using del to mean ā€œinvoke the destructor on this particular lineā€. del x is sugar for x.__del__()

In summary: Your code snippet is very compelling; but we might need to use a different keyword.

I did a scan on the modular repo and did not find any usage of del in .mojo files. But if the name should better indicate that it is a restricted unsafe feature a different name might be better. __del?

struct String:
    ...
    fn __del__(var self):
        """Destroy the string data."""
        self._drop_ref()
        __del self

    fn __moveinit__(out self, var existing: Self):
        ...
        __del existing

For linear types this feature will be used quite often and I thought del is the closest to Phyton del for Mojo structs. But I see that del has additional/different semantics in Phyton that Mojo maybe wants to support in the future like deleting items from a list (__delitem__).

Python:

numbers = [1, 2, 3, 4, 5]
del numbers[2]
numbers # [1, 2, 4]

So del might not be a good choice if this is the case. __mark_del or just __del then?

I think if we require the devs to explicitly call __disable_del on some of the methods, it is no longer an implementation detail of free choice. Having it in function signature also documents what happens to the passed variable which i would see as a positive..

BTW another term we could use instead of `del is`destroy`. But then it is inconsistent with many places we use del..

Should this be taken into account? I think no, as in both Python and Mojo del will denote deleting object

Iā€˜m not a Python expert. But if itā€˜s semantically and compiler side possible del would be ideal in Mojo IMO.

1 Like

Yes, using arg convention is making __disable_del more like an interface,
so that isolation of change is pretty useful!
Also, this makes it structured and a part of something,
which can be used for logic.

I see the point that specifying the disable del in the signature seems to be irrelevant for librabry user sand may cause inconsistency as `var` and `deinint`may be used interchangeably at some places.

However, I think in some cases deinit does convey important information to the user, as it communicates ā€œthere is a custom desctruction implemented hereā€, which is important, since the custom behaviour may diverge from the otherwise implemented __del__. That means, I cannot rely on the things put in __del__ to be executed even though the type may be properly destroyed.

I would like to thorugh in some alternatives, not being convinved of them myself, but maybe they help in the further discussion. Often they would require some extra work to actually make proper sense. The main idea is to add another modifier to var, defined to be ignored at the places where var and deinint could be used interchangeably:

  1. Version with two key words:
fn fun(var del argument):
    ...
  1. Version with joint key word (communicates that this is a version of var even though this is distinct from var:
fn fun(var_del argument):
    ...
  1. Version with destructor specified in square brackets (may clash with origins, but there are also some similarities, e.g. ā€œwho is responsible for destroying thisā€. Maybe one could work out a consistent interpreation of var[…] and ref[...]):
fn fun(var[] argument): # no destructor specified, so no del
    ...
fn fun_alternative(var[None] argument): # no destructor specified, so no del
    ...
fn fun_default(var[argument.__del__] argument): # default, same as no square brackets
    ...
  1. b) The object responsible for destruction (calling the destructor if it goes out of scope) in curly braces (we may disallow anything except the two given options and raise an incompatible origin error…):
fn fun(var[] argument): # no instance marking that the destructor is called when it is out of scope is specified, so no del
    ...
fn fun_alternative(var[None] argument): # same as above
    ...
fn fun_default(var[argument] argument): # default, same as no square brackets
    ...
  1. Version with decorator (not sure if this matches the proper decorator syntax; I think in Python it would not…):
@no_destroy(argument)
fn fun(var argument): # no destructor specified, so no del
    ...

Maybe we could find a solution along those lines… I think I like the idea of designing something in the direction of origins, i.e., option 3b.

Btw. if we stick to using some kind of __disable_del, I would prefer something like mark_as_destroyed(arg), as this is seems more like a proper function rather than a modifier to compiler behaviour (though it might in fact be the latter). I simply think it reduces the load of special syntax.

Re: __del: Shouldn’t it be __no_del? It disables the destructor…

Unfortunately this is not the case as deinit would only be allowed for functions that directly destroy the argument. Therefore it is an internal implementation artefact that would change if the function calls another function that does the destruction. It would not show up in the function type declaration too. So the user will still not know from the function signatur if the argument will be destroyed eventually. A decorator would have the same issue. The same holds for functions that conditionally destroy the argument. Those functions too would have to use the var argument convention.

1 Like

Re: __del: Shouldn’t it be __no_del? It disables the destructor

True. But from the viewpoint of the implementer and caller of the function __del value indicates that the value will be deleted eventually (by allowing the function to destroy the value internals and postponing the immediate deletion of the whole value).

Would an unsafe opt-out of the restriction work here?