dfinity / motoko

Simple high-level language for writing Internet Computer canisters
Apache License 2.0
503 stars 98 forks source link

IR: First class muts #3351

Open nomeata opened 2 years ago

nomeata commented 2 years ago

The PR at #3084 shows that the handling of mutable variables and mutable fields on the IR level is lacking. Instead of increasing the complexity, I think we can actually simplify the existing code and trivially unblock the features needed for #3084 (i.e. without any IR changes there).

The plan would be roughly this:

  1. Treat mut t as its own first-class type. This applies to mutable variables (var x) and object fields ({var x : t}), but not the elements of mutable arrays. No more as_immut in `Check_IR!
  2. The API for this type includes NewE : exp -> exp with type t -> mut t; ReadE : exp -> exp with type mut t -> t and WriteLE : exp -> lexp for the left of an assignments. It is backed via a MutBox, just as it is now.
  3. VarD x e can go. It now simply becomes LetD x (NewE e).
  4. LodLE e n can go. We can use WriteLE (DotE e n)
  5. Records are now simply records of pure values. No special handling of mut anywhere! Much simplification here, and unblocking #3084.
  6. The desugarer, when compiling to VarE, has to check the type and if it is mutable, insert a ReadE.

Two things are not clear to me:

nomeata commented 2 years ago

Did a quick experiment to see to what extend DefineE x e and x := e do the same thing, at least when the type is mutable:

-let define_idE x mut exp1 =
-  { it = DefineE (x, mut, exp1);
-    at = no_region;
-    note = Note.{ def with typ = T.unit }
-  }
+let define_idE (x,t) mut exp1 = match mut with
+  | Const ->
+   { it = DefineE (x, mut, exp1);
+     at = no_region;
+     note = Note.{ def with typ = T.unit }
+   }
+  | Var -> assignE (x,T.Mut t) exp1

but that didn’t quite work. This may need more thought.

crusso commented 2 years ago

I think I'm for it, even at the source level to be honest. I've always missed the ML ref type.

I'll have to think about/revist declareE and defineE but I think it's essentially allocating an uninitialised ref and then initialising it, without a definedness check on read. We needed it to be able to await within recursive blocks, IIRC.

nomeata commented 2 years ago

I think the problem with declare and define here is that they don't just allocate refs, but also non-refs?

But maybe it's fine to turn them all into proper refs (including changing the type) in the lowering pass?