diff --git a/changes/bound-inline-depth.polish.md b/changes/bound-inline-depth.polish.md new file mode 100644 index 0000000000..9fdc024a7f --- /dev/null +++ b/changes/bound-inline-depth.polish.md @@ -0,0 +1 @@ +- Stop inlining nested calls more than four levels deep, so a chain of small functions that call each other no longer makes compile time and output size grow quadratically. https://github.com/rescript-lang/rescript/pull/8801 diff --git a/compiler/core/lam_pass_remove_alias.ml b/compiler/core/lam_pass_remove_alias.ml index 83cbc15f06..44b879ab35 100644 --- a/compiler/core/lam_pass_remove_alias.ml +++ b/compiler/core/lam_pass_remove_alias.ml @@ -46,7 +46,15 @@ let is_const_some (cst : Lambda.structured_constant) : bool = | Const_some _ -> true | _ -> false +(* How deep inlining may nest: simplifying an inlined body can inline the + functions it calls, and so on. Without a bound, a chain of small functions + that each call the previous one gets the whole chain inlined into every + function, which is quadratic in the length of the chain. Functions marked + [@inline] are always inlined. *) +let max_inline_depth = 4 + let simplify_alias (meta : Lam_stats.t) (lam : Lambda.t) : Lambda.t = + let inline_depth = ref 0 in let rec simpl (lam : Lambda.t) : Lambda.t = match lam with (* Keep a JSX tag like [M.make] as it is: flattening it may give a variable @@ -204,10 +212,19 @@ let simplify_alias (meta : Lam_stats.t) (lam : Lambda.t) : Lambda.t = Lam_hit.hit_variable v lam) (*avoid nontermination, e.g, `g(g)`*) then normal () - else - simpl - (Lam_beta_reduce.propagate_beta_reduce_with_map meta - param_map params body ap_args)) + else if + !inline_depth >= max_inline_depth + && m.attr.inline <> Always_inline + then normal () + else ( + incr inline_depth; + let inlined = + simpl + (Lam_beta_reduce.propagate_beta_reduce_with_map meta + param_map params body ap_args) + in + decr inline_depth; + inlined)) | _ -> normal () else normal () else normal () diff --git a/tests/tests/src/inline_chain_test.mjs b/tests/tests/src/inline_chain_test.mjs new file mode 100644 index 0000000000..4aea3e5f20 --- /dev/null +++ b/tests/tests/src/inline_chain_test.mjs @@ -0,0 +1,81 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +function f0(x) { + return x + 1 | 0; +} + +function f1(x) { + return (x + 1 | 0) + 1 | 0; +} + +function f2(x) { + return ((x + 1 | 0) + 1 | 0) + 2 | 0; +} + +function f3(x) { + return (((x + 1 | 0) + 1 | 0) + 2 | 0) + 3 | 0; +} + +function f4(x) { + return ((((x + 1 | 0) + 1 | 0) + 2 | 0) + 3 | 0) + 4 | 0; +} + +function f5(x) { + return (((((x + 1 | 0) + 1 | 0) + 2 | 0) + 3 | 0) + 4 | 0) + 5 | 0; +} + +function f6(x) { + return ((((f1(x) + 2 | 0) + 3 | 0) + 4 | 0) + 5 | 0) + 6 | 0; +} + +function g0(x) { + return (x << 1); +} + +function g1(x) { + return ((x << 1) << 1); +} + +function g2(x) { + return (((x << 1) << 1) << 1); +} + +function g3(x) { + return ((((x << 1) << 1) << 1) << 1); +} + +function g4(x) { + return ((((x << 1) << 1) << 1) << 1) + 1 | 0; +} + +let zero = { + contents: 0 +}; + +Mocha.describe("Inline_chain_test", () => { + Mocha.test("chain of small functions", () => { + Test_utils.eq("File \"inline_chain_test.res\", line 28, characters 7-14", (((f2(zero.contents) + 3 | 0) + 4 | 0) + 5 | 0) + 6 | 0, 22); + Test_utils.eq("File \"inline_chain_test.res\", line 29, characters 7-14", ((((zero.contents + 1 | 0) + 1 | 0) + 1 | 0) + 2 | 0) + 3 | 0, 8); + }); + Mocha.test("chain of @inline functions", () => Test_utils.eq("File \"inline_chain_test.res\", line 32, characters 7-14", (((((zero.contents + 1 | 0) << 1) << 1) << 1) << 1) + 1 | 0, 17)); +}); + +export { + f0, + f1, + f2, + f3, + f4, + f5, + f6, + g0, + g1, + g2, + g3, + g4, + zero, +} +/* Not a pure module */ diff --git a/tests/tests/src/inline_chain_test.res b/tests/tests/src/inline_chain_test.res new file mode 100644 index 0000000000..96a088426f --- /dev/null +++ b/tests/tests/src/inline_chain_test.res @@ -0,0 +1,34 @@ +// Inlining a call can inline the functions that the inlined body calls, but +// only a few levels deep: in a chain of small functions that each call the +// previous one, a later function calls an earlier one instead of containing +// the whole chain. Functions marked @inline are still inlined all the way. + +let f0 = x => x + 1 +let f1 = x => f0(x) + 1 +let f2 = x => f1(x) + 2 +let f3 = x => f2(x) + 3 +let f4 = x => f3(x) + 4 +let f5 = x => f4(x) + 5 +let f6 = x => f5(x) + 6 + +@inline let g0 = x => x * 2 +@inline let g1 = x => g0(x) * 2 +@inline let g2 = x => g1(x) * 2 +@inline let g3 = x => g2(x) * 2 +let g4 = x => g3(x) + 1 + +// Read through a ref so the calls below are not evaluated at compile time +let zero = ref(0) + +open Mocha +open Test_utils + +describe(__MODULE__, () => { + test("chain of small functions", () => { + eq(__LOC__, f6(zero.contents), 22) + eq(__LOC__, f3(zero.contents + 1), 8) + }) + test("chain of @inline functions", () => { + eq(__LOC__, g4(zero.contents + 1), 17) + }) +})