From b4d7e8c3967acf2710dc83646c9bf77727882ce3 Mon Sep 17 00:00:00 2001 From: Cristiano Calcagno Date: Wed, 14 Oct 2026 06:52:03 +0200 Subject: [PATCH] Compile integer operations with an identity operand to the operand Integer `x + 0`, `0 + x`, `x - 0`, `x * 1`, `1 * x` and `x / 1` compile to `x`. A ReScript `int` value is a 32-bit integer in JavaScript, so the `| 0` truncation (or `<< 0` for `* 1`) that these operations emitted leaves the value unchanged. The JavaScript expression builders in `Js_exp_make` (`int32_add`, `int32_minus`, `int32_mul`, `int32_div`) return the other operand when one operand is the literal identity. The dropped operand is a literal, so evaluation of the kept operand is unchanged. Example (rescript): let addZero = (x: int) => x + 0 let subZero = (x: int) => x - 0 let mulOne = (x: int) => x * 1 let divOne = (x: int) => x / 1 Before (js): function addZero(x) { return x + 0 | 0; } function subZero(x) { return x - 0 | 0; } function mulOne(x) { return (x << 0); } function divOne(x) { return x / 1 | 0; } After (js): function addZero(x) { return x; } function subZero(x) { return x; } function mulOne(x) { return x; } function divOne(x) { return x; } Source: `bsc Example.res` (CommonJS, to stdout) with bsc built from aa2078e3d (Before) and from this commit (After); function bodies shown, the module header and exports are identical. Changelog entry: - Compile integer `x + 0`, `0 + x`, `x - 0`, `x * 1`, `1 * x` and `x / 1` to `x`. https://github.com/rescript-lang/rescript/pull/8871 Review: Can an `int` operand hold a value outside the 32-bit range? Every int the compiler computes is already truncated: compiling `-x`, `Int.fromFloat(f)` and `Int.shiftRightUnsigned(x, y)` with this commit gives `-x | 0`, `f | 0` and `(x >>> y) | 0`. A value outside the range reaches an `int` only through an external or `%raw` typed `int`; for those the result changes, for example `Number.MAX_SAFE_INTEGER + 0` gave -1 and gives 9007199254740991. The compiler already relies on the same assumption: `Plsrint` by the literal 0 compiles to the operand (compiler/core/lam_compile_primitive.ml, `[e1; 0] -> e1`), dropping `(x >>> 0) | 0`. Review: Does `x / 1` still check for division by zero? The divisor is the literal 1, so no check applies; the existing rule for a nonzero literal divisor already skipped the runtime `div` call. Area: codegen tests Changelog: polish Output: changed integer `+ 0`, `0 +`, `- 0`, `* 1`, `1 *`, `/ 1` on a non-literal operand, now the bare operand; in tests/tests/src one existing snapshot changes (gpr_3609_test.mjs, `0 + state._0 | 0` -> `state._0`) and in the gentype example Docstrings.res.js six `+ 0` functions drop the addition (`a + 0 | 0` -> `a`), and the stdlib build (make lib) emits no changed JS Formats: none Tests: +tests/tests/src/int_identity_ops_test.res +tests/tests/src/int_identity_ops_test.mjs ~tests/tests/src/gpr_3609_test.mjs ~tests/gentype_tests/typescript-react-example/src/Docstrings.res.js PR: #8871 Signed-off-by: Cristiano Calcagno Co-Authored-By: Claude Opus 5.5 --- changes/int-identity-ops.polish.md | 1 + compiler/core/js_exp_make.ml | 18 ++++- .../src/Docstrings.res.js | 12 ++-- tests/tests/src/gpr_3609_test.mjs | 2 +- tests/tests/src/int_identity_ops_test.mjs | 69 +++++++++++++++++++ tests/tests/src/int_identity_ops_test.res | 32 +++++++++ 6 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 changes/int-identity-ops.polish.md create mode 100644 tests/tests/src/int_identity_ops_test.mjs create mode 100644 tests/tests/src/int_identity_ops_test.res diff --git a/changes/int-identity-ops.polish.md b/changes/int-identity-ops.polish.md new file mode 100644 index 00000000000..04d7378a556 --- /dev/null +++ b/changes/int-identity-ops.polish.md @@ -0,0 +1 @@ +- Compile integer `x + 0`, `0 + x`, `x - 0`, `x * 1`, `1 * x` and `x / 1` to `x`. https://github.com/rescript-lang/rescript/pull/8871 diff --git a/compiler/core/js_exp_make.ml b/compiler/core/js_exp_make.ml index 0f42b366495..d9279a9744a 100644 --- a/compiler/core/js_exp_make.ml +++ b/compiler/core/js_exp_make.ml @@ -1595,10 +1595,18 @@ and float_minus ?comment (e1 : t) (e2 : t) : t = | Number (Int {i; _}), Number (Int {i = j; _}) -> int ?comment (Int32.sub i j) | _ -> {comment; source_loc = None; expression_desc = Bin (Minus, e1, e2)} -let int32_add ?comment e1 e2 = to_int32 (float_add ?comment e1 e2) +(* An operand of an int operation is an int32 value, so an identity + operation ([x + 0], [x - 0], [x * 1], [x / 1]) is the operand itself. *) +let int32_add ?comment (e1 : t) (e2 : t) : t = + match (e1.expression_desc, e2.expression_desc) with + | _, Number (Int {i = 0l}) -> e1 + | Number (Int {i = 0l}), _ -> e2 + | _ -> to_int32 (float_add ?comment e1 e2) -let int32_minus ?comment e1 e2 : J.expression = - to_int32 (float_minus ?comment e1 e2) +let int32_minus ?comment (e1 : t) (e2 : t) : J.expression = + match e2.expression_desc with + | Number (Int {i = 0l}) -> e1 + | _ -> to_int32 (float_minus ?comment e1 e2) let float_div ?comment e1 e2 = bin ?comment Div e1 e2 let float_pow ?comment e1 e2 = bin ?comment Pow e1 e2 @@ -1610,6 +1618,7 @@ let int32_asr ?comment e1 e2 : J.expression = let int32_div ~checked ?comment (e1 : t) (e2 : t) : t = match (e1.expression_desc, e2.expression_desc) with | Length _, Number (Int {i = 2l}) -> int32_asr e1 one_int_literal + | _, Number (Int {i = 1l}) -> e1 | e1_desc, Number (Int {i = i1}) when i1 <> 0l -> ( match e1_desc with | Number (Int {i = i0}) -> int (Int32.div i0 i1) @@ -1654,6 +1663,9 @@ let int32_mul ?comment (e1 : J.expression) (e2 : J.expression) : J.expression = | ( {expression_desc = Number (Int {i = i0}); _}, {expression_desc = Number (Int {i = i1}); _} ) -> int (Int32.mul i0 i1) + | e, {expression_desc = Number (Int {i = 1l}); _} + | {expression_desc = Number (Int {i = 1l}); _}, e -> + e | e, {expression_desc = Number (Int {i = i0}); _} | {expression_desc = Number (Int {i = i0}); _}, e -> let i = is_pos_pow i0 in diff --git a/tests/gentype_tests/typescript-react-example/src/Docstrings.res.js b/tests/gentype_tests/typescript-react-example/src/Docstrings.res.js index 0aa8d157e7e..f5995822b84 100644 --- a/tests/gentype_tests/typescript-react-example/src/Docstrings.res.js +++ b/tests/gentype_tests/typescript-react-example/src/Docstrings.res.js @@ -6,27 +6,27 @@ function signMessage(message, key) { } function one(a) { - return a + 0 | 0; + return a; } function two(a, b) { - return (a + b | 0) + 0 | 0; + return a + b | 0; } function tree(a, b, c) { - return ((a + b | 0) + c | 0) + 0 | 0; + return (a + b | 0) + c | 0; } function oneU(a) { - return a + 0 | 0; + return a; } function twoU(a, b) { - return (a + b | 0) + 0 | 0; + return a + b | 0; } function treeU(a, b, c) { - return ((a + b | 0) + c | 0) + 0 | 0; + return (a + b | 0) + c | 0; } function useParam(param) { diff --git a/tests/tests/src/gpr_3609_test.mjs b/tests/tests/src/gpr_3609_test.mjs index 3aea07bc90e..52fc0efa8dd 100644 --- a/tests/tests/src/gpr_3609_test.mjs +++ b/tests/tests/src/gpr_3609_test.mjs @@ -5,7 +5,7 @@ function func(state) { if (typeof state !== "object") { return 0; } else { - return 0 + state._0 | 0; + return state._0; } } diff --git a/tests/tests/src/int_identity_ops_test.mjs b/tests/tests/src/int_identity_ops_test.mjs new file mode 100644 index 00000000000..3829d3e9fa7 --- /dev/null +++ b/tests/tests/src/int_identity_ops_test.mjs @@ -0,0 +1,69 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +function addZero(x) { + return x; +} + +function zeroAdd(x) { + return x; +} + +function subZero(x) { + return x; +} + +function mulOne(x) { + return x; +} + +function oneMul(x) { + return x; +} + +function divOne(x) { + return x; +} + +let calls = { + contents: 0 +}; + +function next() { + calls.contents = calls.contents + 1 | 0; + return calls.contents; +} + +function effectful() { + return next(); +} + +Mocha.describe("Int_identity_ops_test", () => { + Mocha.test("identity operations return the operand", () => { + Test_utils.eq("File \"int_identity_ops_test.res\", line 21, characters 7-14", 7, 7); + Test_utils.eq("File \"int_identity_ops_test.res\", line 22, characters 7-14", -7, -7); + Test_utils.eq("File \"int_identity_ops_test.res\", line 23, characters 7-14", 2147483647, 2147483647); + Test_utils.eq("File \"int_identity_ops_test.res\", line 24, characters 7-14", -2147483648, -2147483648); + Test_utils.eq("File \"int_identity_ops_test.res\", line 25, characters 7-14", 42, 42); + Test_utils.eq("File \"int_identity_ops_test.res\", line 26, characters 7-14", -5, -5); + }); + Mocha.test("the operand is evaluated once", () => { + Test_utils.eq("File \"int_identity_ops_test.res\", line 29, characters 7-14", effectful(), 1); + Test_utils.eq("File \"int_identity_ops_test.res\", line 30, characters 7-14", calls.contents, 1); + }); +}); + +export { + addZero, + zeroAdd, + subZero, + mulOne, + oneMul, + divOne, + calls, + next, + effectful, +} +/* Not a pure module */ diff --git a/tests/tests/src/int_identity_ops_test.res b/tests/tests/src/int_identity_ops_test.res new file mode 100644 index 00000000000..673cea7cc56 --- /dev/null +++ b/tests/tests/src/int_identity_ops_test.res @@ -0,0 +1,32 @@ +open Mocha +open Test_utils + +let addZero = (x: int) => x + 0 +let zeroAdd = (x: int) => 0 + x +let subZero = (x: int) => x - 0 +let mulOne = (x: int) => x * 1 +let oneMul = (x: int) => 1 * x +let divOne = (x: int) => x / 1 + +let calls = ref(0) +let next = () => { + calls := calls.contents + 1 + calls.contents +} + +let effectful = () => next() * 1 + 0 + +describe(__MODULE__, () => { + test("identity operations return the operand", () => { + eq(__LOC__, addZero(7), 7) + eq(__LOC__, zeroAdd(-7), -7) + eq(__LOC__, subZero(2147483647), 2147483647) + eq(__LOC__, mulOne(-2147483648), -2147483648) + eq(__LOC__, oneMul(42), 42) + eq(__LOC__, divOne(-5), -5) + }) + test("the operand is evaluated once", () => { + eq(__LOC__, effectful(), 1) + eq(__LOC__, calls.contents, 1) + }) +})