diff --git a/changes/inline-constant-arguments.polish.md b/changes/inline-constant-arguments.polish.md new file mode 100644 index 0000000000..d8890b62f4 --- /dev/null +++ b/changes/inline-constant-arguments.polish.md @@ -0,0 +1 @@ +- Use constant arguments directly when inlining a function in generated JavaScript, instead of binding them to variables first. diff --git a/compiler/core/js_pass_tailcall_inline.ml b/compiler/core/js_pass_tailcall_inline.ml index 16dd68fbf0..ca54be32c3 100644 --- a/compiler/core/js_pass_tailcall_inline.ml +++ b/compiler/core/js_pass_tailcall_inline.ml @@ -33,20 +33,73 @@ *) module S = Js_stmt_make -(* module E = Js_exp_make *) +module E = Js_exp_make let super = Js_record_map.super -let substitue_variables (map : Ident.t Map_ident.t) = +(* The truth value of a condition that is a parameter replaced by a boolean or + integer constant, or its negation. Only such conditions are decided while + substituting: the substitution is what made them constant. *) +let rec substituted_truth (consts : J.expression Map_ident.t) (e : J.expression) + = + match e.expression_desc with + | Var (Id id) -> ( + match Map_ident.find_opt id consts with + | Some {expression_desc = Bool b} -> Some b + | Some {expression_desc = Number (Int {i})} -> Some (i <> 0l) + | _ -> None) + | Js_not e -> Option.map not (substituted_truth consts e) + | _ -> None + +(* [vars] maps a parameter to the variable argument that replaces it, [consts] + to the constant argument that replaces it. Both apply only to parameters + that are never assigned, so every occurrence of such a parameter is a read. + A conditional whose condition becomes a constant keeps only the branch it + takes, and the negation of such a condition becomes a boolean. *) +let substitute_arguments (vars : Ident.t Map_ident.t) + (consts : J.expression Map_ident.t) = { super with ident = (fun _ id -> - match Map_ident.find_opt id map with + match Map_ident.find_opt id vars with | Some id -> id | None -> id); + expression = + (fun self e -> + match e.expression_desc with + | Var (Id id) -> ( + match Map_ident.find_opt id consts with + | Some c -> c + | None -> super.expression self e) + | Cond (pred, ifso, ifnot) -> ( + match substituted_truth consts pred with + | Some truth -> self.expression self (if truth then ifso else ifnot) + | None -> super.expression self e) + | Js_not pred -> ( + match substituted_truth consts pred with + | Some truth -> E.bool (not truth) + | None -> super.expression self e) + | _ -> super.expression self e); + statement = + (fun self st -> + match st.statement_desc with + | If (pred, ifso, ifnot) -> ( + match substituted_truth consts pred with + | Some truth -> + S.block (self.block self (if truth then ifso else ifnot)) + | None -> super.statement self st) + | _ -> super.statement self st); } +(* Constants that are as cheap to repeat as the variable they replace: they + allocate nothing, have no side effect, and print short. The Lambda inliner + substitutes the same kinds of constants for let-bound variables. *) +let is_substitutable_constant (e : J.expression) = + match e.expression_desc with + | Number _ | Bool _ | Undefined _ | Null -> true + | _ -> false + (* 1. recursive value ? let rec x = 1 :: x non-terminating 2. duplicative identifiers .. @@ -66,23 +119,28 @@ let substitue_variables (map : Ident.t Map_ident.t) = let inline_call (immutable_list : bool list) params (args : J.expression list) processed_blocks = - let map, block = + let substitute param (arg : J.expression) (vars, consts, acc) = + match arg.expression_desc with + | Var (Id id) -> (Map_ident.add param id vars, consts, acc) + | _ when is_substitutable_constant arg -> + (vars, Map_ident.add param arg consts, acc) + | _ -> (vars, consts, S.define_variable ~kind:Variable param arg :: acc) + in + let vars, consts, block = if immutable_list = [] then - Ext_list.fold_right2 params args (Map_ident.empty, processed_blocks) - (fun param arg (map, acc) -> - match arg.expression_desc with - | Var (Id id) -> (Map_ident.add param id map, acc) - | _ -> (map, S.define_variable ~kind:Variable param arg :: acc)) + Ext_list.fold_right2 params args + (Map_ident.empty, Map_ident.empty, processed_blocks) + substitute else Ext_list.fold_right3 params args immutable_list - (Map_ident.empty, processed_blocks) (fun param arg mask (map, acc) -> - match (mask, arg.expression_desc) with - | true, Var (Id id) -> (Map_ident.add param id map, acc) - | _ -> (map, S.define_variable ~kind:Variable param arg :: acc)) + (Map_ident.empty, Map_ident.empty, processed_blocks) + (fun param arg mask ((vars, consts, acc) as state) -> + if mask then substitute param arg state + else (vars, consts, S.define_variable ~kind:Variable param arg :: acc)) in - if Map_ident.is_empty map then block + if Map_ident.is_empty vars && Map_ident.is_empty consts then block else - let obj = substitue_variables map in + let obj = substitute_arguments vars consts in obj.block obj block (** There is a side effect when traversing dead code, since diff --git a/tests/tests/src/inline_constant_args_test.mjs b/tests/tests/src/inline_constant_args_test.mjs new file mode 100644 index 0000000000..b67263f53d --- /dev/null +++ b/tests/tests/src/inline_constant_args_test.mjs @@ -0,0 +1,61 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +function scaled(extra) { + let label = "value: "; + let a = 1 * 2.5 + -1; + let b = extra * 2.5 - -1; + let c = b - a; + return label + (c + a * b).toString(); +} + +function intsApplied(extra) { + let s = (2 + (-3 * extra | 0) | 0) - 7 | 0; + let t = (s * -3 | 0) + (7 * 2 | 0) | 0; + return 10n + BigInt((s * t | 0) + -3 | 0); +} + +function logged(extra) { + let out = []; + let line = "quiet " + extra.toString(); + out.push(line); + out.push("done"); + out.push("brief"); + return out; +} + +function count() { + let _n = 10; + let _acc = 0; + while (true) { + let acc = _acc; + let n = _n; + if (n <= 0) { + return acc; + } + _acc = acc + n | 0; + _n = n - 1 | 0; + continue; + }; +} + +Mocha.describe("Inline_constant_args_test", () => { + Mocha.test("float, bool and string constants", () => Test_utils.eq("File \"inline_constant_args_test.res\", line 62, characters 7-14", "value: 13.5", scaled(2))); + Mocha.test("int and bigint constants", () => Test_utils.eq("File \"inline_constant_args_test.res\", line 65, characters 7-14", -777n, intsApplied(3))); + Mocha.test("bool constant conditions", () => Test_utils.eq("File \"inline_constant_args_test.res\", line 68, characters 7-14", [ + "quiet 4", + "done", + "brief" + ], logged(4))); + Mocha.test("reassigned parameter", () => Test_utils.eq("File \"inline_constant_args_test.res\", line 71, characters 7-14", 55, count())); +}); + +export { + scaled, + intsApplied, + logged, + count, +} +/* Not a pure module */ diff --git a/tests/tests/src/inline_constant_args_test.res b/tests/tests/src/inline_constant_args_test.res new file mode 100644 index 0000000000..cdb7ede6c8 --- /dev/null +++ b/tests/tests/src/inline_constant_args_test.res @@ -0,0 +1,73 @@ +open Mocha +open Test_utils + +// The partial applications inline the local functions in JS: constant +// arguments replace the parameters, except the string, which stays bound. +let scaled = { + let scale = (k, offset, flip, label, x, y) => { + let a = x *. k +. offset + let b = y *. k -. offset + let c = if flip { + b -. a + } else { + a -. b + } + label ++ Float.toString(c +. a *. b) + } + scale(2.5, -1., true, "value: ", 1., ...) +} + +let intsApplied = { + let ints = (a, b, big, x, y) => { + let s = x + a * y - b + let t = s * a + b * x + big + BigInt.fromInt(s * t + a) + } + ints(-3, 7, 10n, 2, ...) +} + +// A condition on a constant argument keeps only the branch it takes. +let logged = { + let log = (verbose, prefix, x) => { + let out = [] + if verbose { + let line = prefix ++ Int.toString(x) + out->Array.push(line) + } else { + let line = "quiet " ++ Int.toString(x) + out->Array.push(line) + } + if !verbose { + out->Array.push("done") + } + out->Array.push(verbose ? "verbose" : "brief") + out + } + log(false, "x = ", ...) +} + +// [n] is reassigned by the loop, so the constant must stay bound. +let count = () => { + let rec loop = (n, acc) => + if n > 0 { + loop(n - 1, acc + n) + } else { + acc + } + loop(10, 0) +} + +describe(__MODULE__, () => { + test("float, bool and string constants", () => { + eq(__LOC__, "value: 13.5", scaled(2.)) + }) + test("int and bigint constants", () => { + eq(__LOC__, -777n, intsApplied(3)) + }) + test("bool constant conditions", () => { + eq(__LOC__, ["quiet 4", "done", "brief"], logged(4)) + }) + test("reassigned parameter", () => { + eq(__LOC__, 55, count()) + }) +}) diff --git a/tests/tests/src/loop_regression_test.mjs b/tests/tests/src/loop_regression_test.mjs index 1018a52188..f79849f597 100644 --- a/tests/tests/src/loop_regression_test.mjs +++ b/tests/tests/src/loop_regression_test.mjs @@ -6,9 +6,8 @@ import * as Test_utils from "./test_utils.mjs"; function f() { let v = 0; let acc = 0; - let n = 10; while (true) { - if (v > n) { + if (v > 10) { return acc; } acc = acc + v | 0; diff --git a/tests/tests/src/test_demo.mjs b/tests/tests/src/test_demo.mjs index 6d11614500..6b82fc3bd5 100644 --- a/tests/tests/src/test_demo.mjs +++ b/tests/tests/src/test_demo.mjs @@ -55,8 +55,7 @@ function g1(x, y) { let x = 14; function v(extra) { - let xx = 6; - return (xx + extra | 0) + 7 | 0; + return (6 + extra | 0) + 7 | 0; } let nil = "Nil";