Skip to content

Compile integer operations with an identity operand to the operand - #8862

Closed
cristianoc wants to merge 1 commit into
cristianoc/commit-recordsfrom
cristianoc/int-identity-ops
Closed

cristianoc wants to merge 1 commit into
cristianoc/commit-recordsfrom
cristianoc/int-identity-ops

Conversation

@cristianoc

@cristianoc cristianoc commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

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) these operations emitted leaves the value unchanged. int32_add, int32_minus, int32_mul and int32_div in compiler/core/js_exp_make.ml return the other operand when one operand is the literal identity. The dropped operand is a literal, so the kept operand is evaluated exactly as before.

Stacked on #8861. This description is the commit's record converted to markdown. The rendered page is at https://claude.ai/artifact/6nbY5DMccWCk7yYDbuoZrH, or run node scripts/commitRecords.js --out review.html master..cristianoc/int-identity-ops.

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 After
return x + 0 | 0; return x;
return x - 0 | 0; return x;
return (x << 0); return x;
return x / 1 | 0; return x;

Source: bsc Example.res with bsc built from aa2078e (Before) and from this commit (After).

Output: this changes the generated JS for these operations on a non-literal operand. One existing snapshot changes (tests/tests/src/gpr_3609_test.mjs: 0 + state._0 | 0 becomes state._0), and make lib produces no changed stdlib JS. Formats unchanged.

Review

  • Can an int operand hold a value outside the 32-bit range? Every int the compiler computes is already truncated: -x, Int.fromFloat(f) and Int.shiftRightUnsigned(x, y) compile to -x | 0, f | 0 and (x >>> y) | 0. An out-of-range value reaches an int only through an external or %raw that is typed int. For those values the result does change: Number.MAX_SAFE_INTEGER + 0 used to give -1 and now gives 9007199254740991. The compiler already makes the same assumption, since Plsrint by the literal 0 compiles to its operand (compiler/core/lam_compile_primitive.ml), which drops (x >>> 0) | 0.
  • 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.

Test plan

  • New test tests/tests/src/int_identity_ops_test.res (with its .mjs) covers each identity, the int32 boundaries, and checks that a side-effecting operand runs exactly once.
  • make, make lib and make test all exit 0 (269 ounit tests OK, 606 tests/tests mocha passing, 744 stdlib mocha passing). make checkformat exits 0.

🤖 Generated with Claude Code

cristianoc added a commit that referenced this pull request Oct 14, 2026
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`. #8862

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 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
Ran: make (dune exit 0); make lib (exit 0); make test (exit 0: 269 ounit
 tests OK, tests/tests mocha 606 passing including Int_identity_ops_test,
 stdlib mocha 744 passing); make checkformat (exit 0); yarn changelog:check
 (fails only on the XXXX placeholder link, as expected before a PR exists)
Depends: #8861 (stacked)
PR: #8862
Signed-off-by: Cristiano Calcagno <ccrisccris@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristianoc
cristianoc force-pushed the cristianoc/int-identity-ops branch from 3189f1c to faf5ecb Compare October 14, 2026 07:13
@cristianoc
cristianoc added this pull request to stack #8863 October 14, 2026 07:13
cristianoc added a commit that referenced this pull request Oct 14, 2026
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`. #8862

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 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
Ran: make (dune exit 0); make lib (exit 0); make test (exit 0: 269 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)
Depends: #8861 (stacked)
PR: #8862
Signed-off-by: Cristiano Calcagno <ccrisccris@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristianoc
cristianoc force-pushed the cristianoc/int-identity-ops branch from faf5ecb to a253891 Compare October 14, 2026 07:13
@cristianoc
cristianoc marked this pull request as ready for review October 14, 2026 07:14
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`. #8862

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 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
Ran: make (dune exit 0); make lib (exit 0); make test (exit 0: 269 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)
Depends: #8861 (stacked)
PR: #8862
Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cristianoc
cristianoc force-pushed the cristianoc/int-identity-ops branch from a253891 to 92a9528 Compare October 14, 2026 07:14
@cristianoc

Copy link
Copy Markdown
Collaborator Author

Superseded by #8871 (stacked on #8870), which adds a semantic diff link on each PR.

@cristianoc cristianoc closed this Oct 16, 2026
@cristianoc
cristianoc deleted the cristianoc/int-identity-ops branch October 16, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant