Skip to content

Commit 77309cb

Browse files
cristianocclaude
andcommitted
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 aa2078e (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`. #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 Ran: make test on bc2e0e1 + this stack (exit 0: 318 ounit tests OK, tests/tests mocha 606 passing including Int_identity_ops_test, stdlib mocha 744 passing); make checkformat (exit 0); yarn changelog:check (passes); make test-gentype, test-analysis, test-tools (exit 0); make test-rewatch fails locally on a temp-directory symlink (Path is a directory: rescript-gentype-root-*-link), with no generated JS change Depends: #8870 (stacked) PR: #8871 Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 22cdcc4 commit 77309cb

6 files changed

Lines changed: 124 additions & 10 deletions

File tree

‎changes/int-identity-ops.polish.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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

‎compiler/core/js_exp_make.ml‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,10 +1595,18 @@ and float_minus ?comment (e1 : t) (e2 : t) : t =
15951595
| Number (Int {i; _}), Number (Int {i = j; _}) -> int ?comment (Int32.sub i j)
15961596
| _ -> {comment; source_loc = None; expression_desc = Bin (Minus, e1, e2)}
15971597

1598-
let int32_add ?comment e1 e2 = to_int32 (float_add ?comment e1 e2)
1598+
(* An operand of an int operation is an int32 value, so an identity
1599+
operation ([x + 0], [x - 0], [x * 1], [x / 1]) is the operand itself. *)
1600+
let int32_add ?comment (e1 : t) (e2 : t) : t =
1601+
match (e1.expression_desc, e2.expression_desc) with
1602+
| _, Number (Int {i = 0l}) -> e1
1603+
| Number (Int {i = 0l}), _ -> e2
1604+
| _ -> to_int32 (float_add ?comment e1 e2)
15991605

1600-
let int32_minus ?comment e1 e2 : J.expression =
1601-
to_int32 (float_minus ?comment e1 e2)
1606+
let int32_minus ?comment (e1 : t) (e2 : t) : J.expression =
1607+
match e2.expression_desc with
1608+
| Number (Int {i = 0l}) -> e1
1609+
| _ -> to_int32 (float_minus ?comment e1 e2)
16021610

16031611
let float_div ?comment e1 e2 = bin ?comment Div e1 e2
16041612
let float_pow ?comment e1 e2 = bin ?comment Pow e1 e2
@@ -1610,6 +1618,7 @@ let int32_asr ?comment e1 e2 : J.expression =
16101618
let int32_div ~checked ?comment (e1 : t) (e2 : t) : t =
16111619
match (e1.expression_desc, e2.expression_desc) with
16121620
| Length _, Number (Int {i = 2l}) -> int32_asr e1 one_int_literal
1621+
| _, Number (Int {i = 1l}) -> e1
16131622
| e1_desc, Number (Int {i = i1}) when i1 <> 0l -> (
16141623
match e1_desc with
16151624
| 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 =
16541663
| ( {expression_desc = Number (Int {i = i0}); _},
16551664
{expression_desc = Number (Int {i = i1}); _} ) ->
16561665
int (Int32.mul i0 i1)
1666+
| e, {expression_desc = Number (Int {i = 1l}); _}
1667+
| {expression_desc = Number (Int {i = 1l}); _}, e ->
1668+
e
16571669
| e, {expression_desc = Number (Int {i = i0}); _}
16581670
| {expression_desc = Number (Int {i = i0}); _}, e ->
16591671
let i = is_pos_pow i0 in

‎tests/gentype_tests/typescript-react-example/src/Docstrings.res.js‎

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/tests/src/gpr_3609_test.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ function func(state) {
55
if (typeof state !== "object") {
66
return 0;
77
} else {
8-
return 0 + state._0 | 0;
8+
return state._0;
99
}
1010
}
1111

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Generated by ReScript, PLEASE EDIT WITH CARE
2+
3+
import * as Mocha from "mocha";
4+
import * as Test_utils from "./test_utils.mjs";
5+
6+
function addZero(x) {
7+
return x;
8+
}
9+
10+
function zeroAdd(x) {
11+
return x;
12+
}
13+
14+
function subZero(x) {
15+
return x;
16+
}
17+
18+
function mulOne(x) {
19+
return x;
20+
}
21+
22+
function oneMul(x) {
23+
return x;
24+
}
25+
26+
function divOne(x) {
27+
return x;
28+
}
29+
30+
let calls = {
31+
contents: 0
32+
};
33+
34+
function next() {
35+
calls.contents = calls.contents + 1 | 0;
36+
return calls.contents;
37+
}
38+
39+
function effectful() {
40+
return next();
41+
}
42+
43+
Mocha.describe("Int_identity_ops_test", () => {
44+
Mocha.test("identity operations return the operand", () => {
45+
Test_utils.eq("File \"int_identity_ops_test.res\", line 21, characters 7-14", 7, 7);
46+
Test_utils.eq("File \"int_identity_ops_test.res\", line 22, characters 7-14", -7, -7);
47+
Test_utils.eq("File \"int_identity_ops_test.res\", line 23, characters 7-14", 2147483647, 2147483647);
48+
Test_utils.eq("File \"int_identity_ops_test.res\", line 24, characters 7-14", -2147483648, -2147483648);
49+
Test_utils.eq("File \"int_identity_ops_test.res\", line 25, characters 7-14", 42, 42);
50+
Test_utils.eq("File \"int_identity_ops_test.res\", line 26, characters 7-14", -5, -5);
51+
});
52+
Mocha.test("the operand is evaluated once", () => {
53+
Test_utils.eq("File \"int_identity_ops_test.res\", line 29, characters 7-14", effectful(), 1);
54+
Test_utils.eq("File \"int_identity_ops_test.res\", line 30, characters 7-14", calls.contents, 1);
55+
});
56+
});
57+
58+
export {
59+
addZero,
60+
zeroAdd,
61+
subZero,
62+
mulOne,
63+
oneMul,
64+
divOne,
65+
calls,
66+
next,
67+
effectful,
68+
}
69+
/* Not a pure module */
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
open Mocha
2+
open Test_utils
3+
4+
let addZero = (x: int) => x + 0
5+
let zeroAdd = (x: int) => 0 + x
6+
let subZero = (x: int) => x - 0
7+
let mulOne = (x: int) => x * 1
8+
let oneMul = (x: int) => 1 * x
9+
let divOne = (x: int) => x / 1
10+
11+
let calls = ref(0)
12+
let next = () => {
13+
calls := calls.contents + 1
14+
calls.contents
15+
}
16+
17+
let effectful = () => next() * 1 + 0
18+
19+
describe(__MODULE__, () => {
20+
test("identity operations return the operand", () => {
21+
eq(__LOC__, addZero(7), 7)
22+
eq(__LOC__, zeroAdd(-7), -7)
23+
eq(__LOC__, subZero(2147483647), 2147483647)
24+
eq(__LOC__, mulOne(-2147483648), -2147483648)
25+
eq(__LOC__, oneMul(42), 42)
26+
eq(__LOC__, divOne(-5), -5)
27+
})
28+
test("the operand is evaluated once", () => {
29+
eq(__LOC__, effectful(), 1)
30+
eq(__LOC__, calls.contents, 1)
31+
})
32+
})

0 commit comments

Comments
 (0)