Skip to content

Commit

Permalink
Generic closure: Add test case, design rationale
Browse files Browse the repository at this point in the history
  • Loading branch information
luc-blaeser committed Jan 9, 2025
1 parent dadde1f commit e0b61ca
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 0 deletions.
132 changes: 132 additions & 0 deletions design/GenericsInStableClosure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Generics in Stable Closures

Currently, type generics are not reified in Motoko runtime system but erased by the compiler.

Therefore, when checking upgrade compatibility of stable closures, we need to make sure that it is safe to ignore the concrete types for captured generic variables.

## Safe to Ignore Concrete Types of Generics

Intuitively, the reasoning is as follows:
- Either, the generic variables in a closure are checked separately for compatibility at stable declarations which use the variables.


```
class Test<X>(initial: X) {
var content = initial;
public func get(): X {
content;
};
public func set(value: X) {
content := value;
};
};
stable let stableFunction1 = Test<Nat>(1).get; // stable () -> Nat
stable let stableFunction2 = Test<Nat>(1).set; // stable Nat -> ()
```

cannot be changed to
```
stable let stableFunction1 = Test<Text>("...").get; // stable () -> Nat
stable let stableFunction2 = Test<Text>("...).set; // stable Nat -> ()
```

- Or, if this is not the case, the generic variable is isolated in the closure and cannot be operated on a new concrete type.

```
class Test<X>(value: X, op: X -> ()) {
public func run() {
op(value);
}
};
func printNat(x: Nat) {
Prim.debugPrint(debug_show(x));
};
stable let stableFunction = Test<Nat>(1, printNat).run; // stable () -> ()
```

If migrated to an different generic instantiation, the captured variables continues to operate on the old concrete type.

```
stable let stableFunction = Test<Text>("Hello", printText).run; // stable () -> ()
run();
```

`run` still accesses the old `X = Nat` declararations, incl. `printNat`. And the signature of `printNat` cannot be changed to `Text`.


## Rules Being Checked

The only two aspects of compatibility for generics need to be checked:
1. The generics are not swapped, i.e. captured variables retain the same type generic (e.g. according to the declaration order of the generics).

```
class Test<X, Y>() {
var first: X = ...;
var second: Y = ...;
public func method() {
... Use X and Y
};
};
stable let stableFunction = Test<Nat, Text>.method;
```
Now, in the new version, I cannot e.g. swap `X` and `Y`.
```
class Test<Y, X>() {
var first: X = ...; // incompatible: Must be Y (first declared generic in scope)
var second: Y = ...; // incompatible: Must be X (second declareed generic in scope)
public func method() { ... };
};
```
2. The type bounds of the generics are compatible.
```
class Test<X>() {
var content: X = ...;
public func method() { ... };
};
stable let stableFunction = Test<Nat>.method;
```
cannot be changed to:
```
class Test<X : Text>() {
var content: X = ...;
public func method() { debugPrint(content) };
};
```
Now, assume we have a closure with generic captured variables.
```
class Test<X>(initial: X) {
var content = initial;

public func get(): X {
content;
};

public func set(value: X) {
content := value;
};
};


stable let stableFunction = Test<Nat>(1).get;
4 changes: 4 additions & 0 deletions test/run-drun/generic-stable-function.drun
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# ENHANCED-ORTHOGONAL-PERSISTENCE-ONLY
# SKIP ic-ref-run
install $ID generic-stable-function/version0.mo ""
upgrade $ID generic-stable-function/version1.mo ""
25 changes: 25 additions & 0 deletions test/run-drun/generic-stable-function/version0.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Prim "mo:prim";

actor {
Prim.debugPrint("Version 0");

func outer<T>(x : T, op : stable T -> ()) : stable () -> () {
func inner() {
op(x);
};
return inner;
};

var global = false;

func setBool(x : Bool) {
Prim.debugPrint("Writing bool");
global := x;
};

stable let stableFunction = outer<Bool>(true, setBool);

Prim.debugPrint("Before: " # debug_show (global));
stableFunction();
Prim.debugPrint("After: " # debug_show (global));
};
28 changes: 28 additions & 0 deletions test/run-drun/generic-stable-function/version1.mo
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Prim "mo:prim";

actor {
Prim.debugPrint("Version 0");

func outer<T>(x : T, op : stable T -> ()) : stable () -> () {
func inner() {
op(x);
};
return inner;
};

var global = "";

stable func setBool(x : Bool) {
Prim.debugPrint("Calling setBool");
};
func setText(x : Text) {
Prim.debugPrint("Writing text");
global := x;
};

stable let stableFunction = outer<Text>("Hello", setText);

Prim.debugPrint("Before: " # debug_show (global));
stableFunction(); // stays with old `X = Bool` and calls the old `setBool`.
Prim.debugPrint("After: " # debug_show (global));
};
11 changes: 11 additions & 0 deletions test/run-drun/ok/generic-stable-function.drun.ok
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ingress Completed: Reply: 0x4449444c016c01b3c4b1f204680100010a00000000000000000101
debug.print: Version 0
debug.print: Before: false
debug.print: Writing bool
debug.print: After: true
ingress Completed: Reply: 0x4449444c0000
debug.print: Version 0
debug.print: Before: ""
debug.print: Calling setBool
debug.print: After: ""
ingress Completed: Reply: 0x4449444c0000

0 comments on commit e0b61ca

Please sign in to comment.