-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Generic closure: Add test case, design rationale
- Loading branch information
1 parent
dadde1f
commit e0b61ca
Showing
5 changed files
with
200 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 "" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |