diff --git a/Building.md b/Building.md index 45a1f48ed0e..128b4c3c003 100644 --- a/Building.md +++ b/Building.md @@ -53,25 +53,27 @@ For more details on our CI and CI setup, see `CI.md`. ## Making releases -We make frequent releases, at least weekly. The steps to make a release (say, version 0.6.6) are: +We make frequent releases, at least weekly. The steps to make a release (say, version 0.6.13) are: * Make sure that the top section of `Changelog.md` has a title like - == 0.6.6 (2021-08-01) + == 0.6.13 (2021-10-31) with today’s date. - * Look at `git log --first-parent 0.6.5..HEAD` and check + * Define a shell variable `export MOC_MINOR=13` + + * Look at `git log --first-parent 0.6.$(expr $MOC_MINOR - 1)..HEAD` and check that everything relevant is mentioned in the changelog section, and possibly clean it up a bit, curating the information for the target audience. - * `git commit -a -m "Releasing 0.6.6"` + * `git commit -am "Releasing 0.6.$MOC_MINOR"` * Create a PR from this commit, and label it `automerge-squash`. Mergify will merge it into master without additional approval, within 2 or 3 minutes. * `git switch master; git pull`. The release commit should be your `HEAD` - * `git tag 0.6.6 -m "Motoko 0.6.6"` - * `git branch -f release 0.6.6` - * `git push origin release 0.6.6` + * `git tag 0.6.$MOC_MINOR -m "Motoko 0.6.$MOC_MINOR"` + * `git branch -f release 0.6.$MOC_MINOR` + * `git push origin release 0.6.$MOC_MINOR` The `release` branch should thus always reference the latest release commit. @@ -87,10 +89,10 @@ branch to the `next-moc` branch. * Wait ca. 5min after releasing to give the CI/CD pipeline time to upload the release artifacts * Change into `motoko-base` * `git switch next-moc; git pull` -* `git switch -c username/update-moc-0.6.6` +* `git switch -c username/update-moc-0.6.$MOC_MINOR` * Update the `moc_version` env variable in `.github/workflows/ci.yml` and `.github/workflows/package-set.yml` to the new released version -* `git add .github/ && git commit -m "Motoko 0.6.6"` +* `git add .github/ && git commit -m "Motoko 0.6.$MOC_MINOR"` Make a PR off of that branch and merge it using a _normal merge_ (not squash merge) once CI passes diff --git a/Changelog.md b/Changelog.md index 64fa63a481f..083a03b3cc6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,11 @@ # Motoko compiler changelog +== 0.6.12 (2021-10-22) + +* `for` loops over arrays are now converted to more efficient + index-based iteration (#2831). This can result in significant cycle + savings for tight loops, as well as slightly less memory usage. + * Add type union and intersection. The type expression ```motoko diff --git a/src/codegen/compile.ml b/src/codegen/compile.ml index 90d86215ea9..02cd1160e64 100644 --- a/src/codegen/compile.ml +++ b/src/codegen/compile.ml @@ -6052,7 +6052,7 @@ module Var = struct end (* Var *) (* Calling well-known prelude functions *) -(* FIX ME: calling into the prelude will not work if we ever need to compile a program +(* FIXME: calling into the prelude will not work if we ever need to compile a program that requires top-level cps conversion; use new prims instead *) module Internals = struct diff --git a/src/ir_def/construct.ml b/src/ir_def/construct.ml index 2c53b1c4410..0174f304859 100644 --- a/src/ir_def/construct.ml +++ b/src/ir_def/construct.ml @@ -89,8 +89,10 @@ let primE prim es = | ICCallerPrim -> T.caller | ICStableRead t -> t | ICStableWrite _ -> T.unit + | IdxPrim -> T.(as_immut (as_array (List.hd es).note.Note.typ)) | IcUrlOfBlob -> T.text | ActorOfIdBlob t -> t + | BinPrim (t, _) -> t | CastPrim (t1, t2) -> t2 | RelPrim _ -> T.bool | SerializePrim _ -> T.blob @@ -115,7 +117,7 @@ let selfRefE typ = let assertE e = { it = PrimE (AssertPrim, [e]); at = no_region; - note = Note.{ def with typ = T.unit; eff = eff e} + note = Note.{ def with typ = T.unit; eff = eff e } } @@ -219,6 +221,12 @@ let blockE decs exp = note = Note.{ def with typ; eff } } +let natE n = + { it = LitE (NatLit n); + at = no_region; + note = Note.{ def with typ = T.nat } + } + let textE s = { it = LitE (TextLit s); at = no_region; @@ -601,21 +609,21 @@ let loopWhileE exp1 exp2 = ) let forE pat exp1 exp2 = - (* for p in e1 e2 + (* for (p in e1) e2 ~~> let nxt = e1.next ; label l loop { switch nxt () { case null { break l }; - case p { e2 }; + case ?p { e2 }; } } *) let lab = fresh_id "done" () in let ty1 = exp1.note.Note.typ in - let _, tfs = T.as_obj_sub ["next"] ty1 in - let tnxt = T.lookup_val_field "next" tfs in + let _, tfs = T.as_obj_sub [nextN] ty1 in + let tnxt = T.lookup_val_field nextN tfs in let nxt = fresh_var "nxt" tnxt in - letE nxt (dotE exp1 (nameN "next") tnxt) ( + letE nxt (dotE exp1 nextN tnxt) ( labelE lab T.unit ( loopE ( switch_optE (callE (varE nxt) [] (tupE [])) diff --git a/src/ir_def/construct.mli b/src/ir_def/construct.mli index 1c2d085b92b..60d8bdb1000 100644 --- a/src/ir_def/construct.mli +++ b/src/ir_def/construct.mli @@ -60,6 +60,7 @@ val projE : exp -> int -> exp val optE : exp -> exp val tagE : id -> exp -> exp val blockE : dec list -> exp -> exp +val natE : Mo_values.Numerics.Nat.t -> exp val textE : string -> exp val blobE : string -> exp val letE : var -> exp -> exp -> exp diff --git a/src/ir_def/ir_effect.mli b/src/ir_def/ir_effect.mli index ebbce464745..166e4cf27a4 100644 --- a/src/ir_def/ir_effect.mli +++ b/src/ir_def/ir_effect.mli @@ -3,7 +3,7 @@ open Mo_types.Type val max_eff : eff -> eff -> eff -(* (incremental) effect inference on IR *) +(* (incremental) effect inference on IR *) val typ : ('a, Note.t) annotated_phrase -> typ val eff : ('a, Note.t) annotated_phrase -> eff diff --git a/src/lowering/desugar.ml b/src/lowering/desugar.ml index aebcb54a417..bd18fb5c887 100644 --- a/src/lowering/desugar.ml +++ b/src/lowering/desugar.ml @@ -35,7 +35,7 @@ let apply_sign op l = Syntax.(match op, l with let phrase f x = { x with it = f x.it } let typ_note : S.typ_note -> Note.t = - fun {S.note_typ;S.note_eff} -> Note.{def with typ = note_typ; eff = note_eff} + fun S.{ note_typ; note_eff } -> Note.{ def with typ = note_typ; eff = note_eff } let phrase' f x = { x with it = f x.at x.note x.it } @@ -198,6 +198,9 @@ and exp' at note = function | S.WhileE (e1, e2) -> (whileE (exp e1) (exp e2)).it | S.LoopE (e1, None) -> I.LoopE (exp e1) | S.LoopE (e1, Some e2) -> (loopWhileE (exp e1) (exp e2)).it + | S.ForE (p, {it=S.CallE ({it=S.DotE (arr, proj); _}, _, e1); _}, e2) + when T.is_array arr.note.S.note_typ && (proj.it = "vals" || proj.it = "keys") + -> (transform_for_to_while p arr proj e1 e2).it | S.ForE (p, e1, e2) -> (forE (pat p) (exp e1) (exp e2)).it | S.DebugE e -> if !Mo_config.Flags.release_mode then (unitE ()).it else (exp e).it | S.LabelE (l, t, e) -> I.LabelE (l.it, t.Source.note, exp e) @@ -239,6 +242,39 @@ and lexp' = function | S.IdxE (e1, e2) -> I.IdxLE (exp e1, exp e2) | _ -> raise (Invalid_argument ("Unexpected expression as lvalue")) +and transform_for_to_while p arr_exp proj e1 e2 = + (* for (p in (arr_exp : [_]).proj(e1)) e2 when proj in {"keys", "vals"} + ~~> + let arr = arr_exp ; + let size = arr.size(e1) ; + var indx = 0 ; + label l loop { + if indx < size + then { let p = arr[indx]; e2; indx += 1 } + else { break l } + } *) + let arr_typ = arr_exp.note.note_typ in + let arrv = fresh_var "arr" arr_typ in + let size_exp = array_dotE arr_typ "size" (varE arrv) -*- exp e1 in + let indx = fresh_var "indx" T.(Mut nat) in + let indexing_exp = match proj.it with + | "vals" -> primE I.IdxPrim [varE arrv; varE indx] + | "keys" -> varE indx + | _ -> assert false in + let size = fresh_var "size" T.nat in + blockE + [ letD arrv (exp arr_exp) + ; letD size size_exp + ; varD indx (natE Numerics.Nat.zero)] + (whileE (primE (I.RelPrim (T.nat, LtOp)) + [varE indx; varE size]) + (blockE [ letP (pat p) indexing_exp + ; expD (exp e2)] + (assignE indx + (primE (I.BinPrim (T.nat, AddOp)) + [ varE indx + ; natE (Numerics.Nat.of_int 1)])))) + and mut m = match m.it with | S.Const -> Ir.Const | S.Var -> Ir.Var @@ -436,8 +472,8 @@ and array_dotE array_ty proj e = let f = var name (fun_ty [ty_param] [poly_array_ty] [fun_ty [] t1 t2]) in callE (varE f) [element_ty] e in match T.is_mut (T.as_array array_ty), proj with - | true, "size" -> call "@mut_array_size" [] [T.nat] - | false, "size" -> call "@immut_array_size" [] [T.nat] + | true, "size" -> call "@mut_array_size" [] [T.nat] + | false, "size" -> call "@immut_array_size" [] [T.nat] | true, "get" -> call "@mut_array_get" [T.nat] [varA] | false, "get" -> call "@immut_array_get" [T.nat] [varA] | true, "put" -> call "@mut_array_put" [T.nat; varA] [] diff --git a/test/run-drun/ok/optimise-for-array-eff.drun-run.ok b/test/run-drun/ok/optimise-for-array-eff.drun-run.ok new file mode 100644 index 00000000000..7fae6b8fdaa --- /dev/null +++ b/test/run-drun/ok/optimise-for-array-eff.drun-run.ok @@ -0,0 +1,15 @@ +ingress Completed: Reply: 0x4449444c016c01b3c4b1f204680100010a00000000000000000101 +ingress Completed: Reply: 0x4449444c0000 +debug.print: effect +debug.print: hello +debug.print: world +debug.print: hello +debug.print: world +debug.print: effect +debug.print: hello +debug.print: bound +debug.print: world +debug.print: hello +debug.print: bound +debug.print: world +ingress Completed: Reply: 0x4449444c0000 diff --git a/test/run-drun/ok/optimise-for-array-eff.ic-ref-run.ok b/test/run-drun/ok/optimise-for-array-eff.ic-ref-run.ok new file mode 100644 index 00000000000..02b146ac470 --- /dev/null +++ b/test/run-drun/ok/optimise-for-array-eff.ic-ref-run.ok @@ -0,0 +1,18 @@ +→ update create_canister(record {settings = null}) +← replied: (record {hymijyo = principal "cvccv-qqaaq-aaaaa-aaaaa-c"}) +→ update install_code(record {arg = blob ""; kca_xin = blob "\00asm\01\00\00\00\0… +← replied: () +→ update go() +debug.print: effect +debug.print: hello +debug.print: world +debug.print: hello +debug.print: world +debug.print: effect +debug.print: hello +debug.print: bound +debug.print: world +debug.print: hello +debug.print: bound +debug.print: world +← replied: () diff --git a/test/run-drun/ok/optimise-for-array-eff.run-ir.ok b/test/run-drun/ok/optimise-for-array-eff.run-ir.ok new file mode 100644 index 00000000000..8b9b99eb06f --- /dev/null +++ b/test/run-drun/ok/optimise-for-array-eff.run-ir.ok @@ -0,0 +1,12 @@ +effect +hello +world +hello +world +effect +hello +bound +world +hello +bound +world diff --git a/test/run-drun/ok/optimise-for-array-eff.run-low.ok b/test/run-drun/ok/optimise-for-array-eff.run-low.ok new file mode 100644 index 00000000000..8b9b99eb06f --- /dev/null +++ b/test/run-drun/ok/optimise-for-array-eff.run-low.ok @@ -0,0 +1,12 @@ +effect +hello +world +hello +world +effect +hello +bound +world +hello +bound +world diff --git a/test/run-drun/ok/optimise-for-array-eff.run.ok b/test/run-drun/ok/optimise-for-array-eff.run.ok new file mode 100644 index 00000000000..8b9b99eb06f --- /dev/null +++ b/test/run-drun/ok/optimise-for-array-eff.run.ok @@ -0,0 +1,12 @@ +effect +hello +world +hello +world +effect +hello +bound +world +hello +bound +world diff --git a/test/run-drun/optimise-for-array-eff.mo b/test/run-drun/optimise-for-array-eff.mo new file mode 100644 index 00000000000..d3f0e4a272e --- /dev/null +++ b/test/run-drun/optimise-for-array-eff.mo @@ -0,0 +1,18 @@ +import Prim "mo:⛔"; + +actor a { + public func go() : async () { + for (check1 in (await async ["effect", "hello", "world"]).vals()) { Prim.debugPrint check1 }; + + for (check2 in ["hello", "world", "effect"].vals()) { await async { Prim.debugPrint check2 } }; + + let array = ["hello", "bound", "world"]; + + for (check3 in (await async array).vals()) { Prim.debugPrint check3 }; + + for (check4 in array.vals()) { await async { Prim.debugPrint check4 } }; + + for (_ in array.vals(await async ())) { } + } +}; +a.go(); //OR-CALL ingress go "DIDL\x00\x00" diff --git a/test/run/ok/iter-no-alloc.wasm-run.ok b/test/run/ok/iter-no-alloc.wasm-run.ok index 2e47fd4d3ba..73625c7d9a4 100644 --- a/test/run/ok/iter-no-alloc.wasm-run.ok +++ b/test/run/ok/iter-no-alloc.wasm-run.ok @@ -2,7 +2,7 @@ Allocation per iteration (Nat): 0 Allocation per iteration (Nat16): 0 Allocation per iteration (Nat32): 0 Allocation per iteration (?Nat, all null): 0 -Allocation per iteration (FixOpt, all ?null): 8 -Allocation per iteration (FixOpt, all ??null): 8 +Allocation per iteration (FixOpt, all ?null): 0 +Allocation per iteration (FixOpt, all ??null): 0 Allocation per iteration (?Nat, all values): 0 Allocation per iteration (record): 0 diff --git a/test/run/ok/optimise-for-array.run-ir.ok b/test/run/ok/optimise-for-array.run-ir.ok new file mode 100644 index 00000000000..7a4990793f9 --- /dev/null +++ b/test/run/ok/optimise-for-array.run-ir.ok @@ -0,0 +1,12 @@ +hello +world +hello +mutable +world +hello +mutable +world +hello +immutable +world +want to see you diff --git a/test/run/ok/optimise-for-array.run-low.ok b/test/run/ok/optimise-for-array.run-low.ok new file mode 100644 index 00000000000..7a4990793f9 --- /dev/null +++ b/test/run/ok/optimise-for-array.run-low.ok @@ -0,0 +1,12 @@ +hello +world +hello +mutable +world +hello +mutable +world +hello +immutable +world +want to see you diff --git a/test/run/ok/optimise-for-array.run.ok b/test/run/ok/optimise-for-array.run.ok new file mode 100644 index 00000000000..7a4990793f9 --- /dev/null +++ b/test/run/ok/optimise-for-array.run.ok @@ -0,0 +1,12 @@ +hello +world +hello +mutable +world +hello +mutable +world +hello +immutable +world +want to see you diff --git a/test/run/ok/optimise-for-array.wasm-run.ok b/test/run/ok/optimise-for-array.wasm-run.ok new file mode 100644 index 00000000000..7a4990793f9 --- /dev/null +++ b/test/run/ok/optimise-for-array.wasm-run.ok @@ -0,0 +1,12 @@ +hello +world +hello +mutable +world +hello +mutable +world +hello +immutable +world +want to see you diff --git a/test/run/optimise-for-array.mo b/test/run/optimise-for-array.mo new file mode 100644 index 00000000000..9c60ff100ed --- /dev/null +++ b/test/run/optimise-for-array.mo @@ -0,0 +1,94 @@ +import Prim "mo:⛔"; + +// CHECK: (local $check0 i32) + +// CHECK: call $@immut_array_size +// CHECK: call $B_lt +// CHECK: call $Array.idx_bigint +// CHECK: local.set $check0 +// CHECK: local.get $check0 +// CHECK-NEXT: call $debugPrint +// CHECK: i32.const 2 +// CHECK-NEXT: call $B_add +for (check0 in ["hello", "world"].vals()) { Prim.debugPrint check0 }; + +// CHECK: call $@mut_array_size +// CHECK: call $B_lt +// CHECK: call $Array.idx_bigint +// CHECK: local.set $check1 +// CHECK: local.get $check1 +// CHECK-NEXT: call $debugPrint +for (check1 in [var "hello", "mutable", "world"].vals()) { Prim.debugPrint check1 }; + +let array = [var "hello", "remutable", "world"]; +array[1] := "mutable"; +// DON'T-CHECK: call $@mut_array_size +// DON'T-CHECK: call $B_lt +// DON'T-CHECK: local.get $array +// DON'T-CHECK: local.set $check2 +// `arr` being a `VarE` already (but we rebind anyway, otherwise we open can of worms) +// later when we have path compression for variables in the backend, we can bring this back +for (check2 in array.vals()) { Prim.debugPrint check2 }; + +// CHECK: call $@immut_array_size +// CHECK: call $B_lt +// CHECK: call $Array.idx_bigint +// CHECK: local.set $check3 +// interfering parentheses don't disturb us +for (check3 in (((["hello", "immutable", "world"].vals())))) { Prim.debugPrint check3 }; + + +// CHECK: i32.const 84 +// CHECK: call $B_add +// CHECK-NEXT: call $B_eq +// CHECK-NEXT: if +// CHECK-NEXT: loop +// CHECK-NEXT: br 0 +// CHECK-NEXT: end +// CHECK-NEXT: unreachable +// CHECK-NEXT: else +// bottom iteration expression is treated fairly +var c = 42; +if (c == c + 1) { + for (check4 in (loop {}).vals()) { Prim.debugPrint check4 } +}; + +// CHECK: call $B_add +// CHECK-NEXT: call $B_eq +// CHECK-NEXT: if +// CHECK-NEXT: loop +// CHECK-NEXT: br 0 +// CHECK-NEXT: end +// CHECK-NEXT: unreachable +// CHECK-NEXT: else +// typed bottom iteration expression is treated fairly +if (c == c + 1) { + for (check5 in ((loop {}) : [Text]).vals()) { Prim.debugPrint check5 } +}; + +let check6 = [var "hello", "immutable", "world"]; +check6[1] := "mutable"; +// `check6` being a `VarE` already and iteration variable is named identically +// this passes the IR type check, which demonstrates that no name capture happens +for (check6 in check6.vals()) { ignore check6 }; + +// argument to vals can have an effect too, expect it +for (check7 in [].vals(Prim.debugPrint "want to see you")) { }; + +// CHECK: local.set $num8 +// CHECK: call $@immut_array_size +// CHECK: call $B_lt +// CHECK-NOT: call $Array.idx_bigint +// CHECK: local.tee $check8 +// CHECK-NEXT: local.get $num8 +// CHECK-NEXT: call $B_add +var num8 = 42; +num8 := 25; +// `keys` is even easier to rewrite, as the "indexing expression" is just the +// indexing variable itself +for (check8 in ["hello", "keyed", "world"].keys()) { ignore (check8 + num8) }; + + +func f9(array : [A]) { +for (check9 in array.keys()) { } +}