Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/inline-constant-arguments.polish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Use constant arguments directly when inlining a function in generated JavaScript, instead of binding them to variables first.
88 changes: 73 additions & 15 deletions compiler/core/js_pass_tailcall_inline.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ..
Expand All @@ -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
Expand Down
61 changes: 61 additions & 0 deletions tests/tests/src/inline_constant_args_test.mjs
Original file line number Diff line number Diff line change
@@ -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 */
73 changes: 73 additions & 0 deletions tests/tests/src/inline_constant_args_test.res
Original file line number Diff line number Diff line change
@@ -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())
})
})
3 changes: 1 addition & 2 deletions tests/tests/src/loop_regression_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 1 addition & 2 deletions tests/tests/src/test_demo.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading