From e0b61cac8910d4cea9fd35b8830681d858726c1c Mon Sep 17 00:00:00 2001 From: luc-blaeser Date: Thu, 9 Jan 2025 11:44:58 +0100 Subject: [PATCH] Generic closure: Add test case, design rationale --- design/GenericsInStableClosure.md | 132 ++++++++++++++++++ test/run-drun/generic-stable-function.drun | 4 + .../generic-stable-function/version0.mo | 25 ++++ .../generic-stable-function/version1.mo | 28 ++++ .../ok/generic-stable-function.drun.ok | 11 ++ 5 files changed, 200 insertions(+) create mode 100644 design/GenericsInStableClosure.md create mode 100644 test/run-drun/generic-stable-function.drun create mode 100644 test/run-drun/generic-stable-function/version0.mo create mode 100644 test/run-drun/generic-stable-function/version1.mo create mode 100644 test/run-drun/ok/generic-stable-function.drun.ok diff --git a/design/GenericsInStableClosure.md b/design/GenericsInStableClosure.md new file mode 100644 index 00000000000..870b307fe00 --- /dev/null +++ b/design/GenericsInStableClosure.md @@ -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(initial: X) { + var content = initial; + + public func get(): X { + content; + }; + + public func set(value: X) { + content := value; + }; +}; + + +stable let stableFunction1 = Test(1).get; // stable () -> Nat +stable let stableFunction2 = Test(1).set; // stable Nat -> () +``` + +cannot be changed to +``` +stable let stableFunction1 = Test("...").get; // stable () -> Nat +stable let stableFunction2 = Test("...).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(value: X, op: X -> ()) { + public func run() { + op(value); + } +}; + + +func printNat(x: Nat) { + Prim.debugPrint(debug_show(x)); +}; + +stable let stableFunction = Test(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("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() { + var first: X = ...; + var second: Y = ...; + + public func method() { + ... Use X and Y + }; + }; + + stable let stableFunction = Test.method; + ``` + + Now, in the new version, I cannot e.g. swap `X` and `Y`. + + ``` + class Test() { + 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() { + var content: X = ...; + + public func method() { ... }; + }; + + stable let stableFunction = Test.method; + ``` + + cannot be changed to: + ``` + class Test() { + var content: X = ...; + + public func method() { debugPrint(content) }; + }; + ``` + + + +Now, assume we have a closure with generic captured variables. + +``` +class Test(initial: X) { + var content = initial; + + public func get(): X { + content; + }; + + public func set(value: X) { + content := value; + }; + }; + + + stable let stableFunction = Test(1).get; diff --git a/test/run-drun/generic-stable-function.drun b/test/run-drun/generic-stable-function.drun new file mode 100644 index 00000000000..ea54663017d --- /dev/null +++ b/test/run-drun/generic-stable-function.drun @@ -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 "" diff --git a/test/run-drun/generic-stable-function/version0.mo b/test/run-drun/generic-stable-function/version0.mo new file mode 100644 index 00000000000..0c2b19d70d5 --- /dev/null +++ b/test/run-drun/generic-stable-function/version0.mo @@ -0,0 +1,25 @@ +import Prim "mo:prim"; + +actor { + Prim.debugPrint("Version 0"); + + func outer(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(true, setBool); + + Prim.debugPrint("Before: " # debug_show (global)); + stableFunction(); + Prim.debugPrint("After: " # debug_show (global)); +}; diff --git a/test/run-drun/generic-stable-function/version1.mo b/test/run-drun/generic-stable-function/version1.mo new file mode 100644 index 00000000000..15175c9d66b --- /dev/null +++ b/test/run-drun/generic-stable-function/version1.mo @@ -0,0 +1,28 @@ +import Prim "mo:prim"; + +actor { + Prim.debugPrint("Version 0"); + + func outer(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("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)); +}; diff --git a/test/run-drun/ok/generic-stable-function.drun.ok b/test/run-drun/ok/generic-stable-function.drun.ok new file mode 100644 index 00000000000..9108a0a08c8 --- /dev/null +++ b/test/run-drun/ok/generic-stable-function.drun.ok @@ -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