Skip to content

fix(es/ast): support hashing non-finite numbers - #12043

Merged
Donny/강동윤 (kdy1) merged 1 commit into
mainfrom
fix/ecma-number-hash
Jul 20, 2026
Merged

fix(es/ast): support hashing non-finite numbers#12043
Donny/강동윤 (kdy1) merged 1 commit into
mainfrom
fix/ecma-number-hash

Conversation

@magic-akari

@magic-akari magic-akari commented Jul 20, 2026

Copy link
Copy Markdown
Member

Description:

Canonicalize non-finite numeric values in Lit::Num while preserving the existing ECMA AST Hash API.

SWC currently represents Infinity and NaN in several forms. eval_global_vars rewrites Infinity to 1 / 0, while eval_numbers can rewrite division by zero back to an Infinity identifier. Other constant-evaluation paths produce Lit::Num(INFINITY), which codegen prints as Infinity. This churn makes optimization passes repeatedly recognize equivalent values in different AST forms.

Follow esbuild’s approach of using semantic constants during optimization. Allow Lit::Num to store valid JavaScript non-finite values as the canonical internal representation, and leave syntax selection to codegen. Codegen can emit readable Infinity and NaN when safe or shorter 1 / 0 and 0 / 0 when minifying.

BREAKING CHANGE:

Number now treats all NaN values as equal and distinguishes -0.0 from +0.0.

Related issue (if exists):

@changeset-bot

changeset-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d9854d2

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codspeed-hq

codspeed-hq Bot commented Jul 20, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 189 untouched benchmarks
⏩ 61 skipped benchmarks1


Comparing fix/ecma-number-hash (d9854d2) with main (54467fe)2

Open in CodSpeed

Footnotes

  1. 61 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (062c608) during the generation of this report, so 54467fe was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@magic-akari
magic-akari marked this pull request as ready for review July 20, 2026 10:09
@magic-akari
magic-akari requested review from a team as code owners July 20, 2026 10:09
@kdy1

Copy link
Copy Markdown
Member

Why do we need this?

@magic-akari

Copy link
Copy Markdown
Member Author

Why do we need this?

My motivation comes from a technique used by esbuild. In internal/config/globals.go, its define phase replaces the global undefined, NaN, and Infinity identifiers with semantic constants. Later transform and minification passes operate on these internal values instead of preserving their identifier-based AST representation.

I traced SWC’s current compressor and found that we already mix several representations. eval_global_vars converts Infinity to 1 / 0, while eval_numbers can convert another x / 0 back to an Infinity identifier and even has explicit branches saying “Prefer 1/0 over Infinity” and “Prefer 0/0 over NaN.” Other constant-evaluation paths produce Lit::Num(INFINITY), which codegen prints as Infinity. This is not an infinite loop within one compression pass, but it creates real representation churn across optimization, codegen, and reparsing. The same value must also be recognized repeatedly in different AST forms.

Using Lit::Num as the internal working representation gives these transformations a canonical endpoint. Codegen can then choose the appropriate syntax at the output boundary—for example, readable Infinity/NaN in normal output when safe, or shorter 1/0/0/0 when minifying.

This PR removes the artificial restriction that prevents Lit::Num from representing these valid JavaScript numeric values, while preserving the existing Hash API.

@kdy1 Donny/강동윤 (kdy1) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the explanation! I'll merge this PR after adding the context to the PR body (and the commit message)

@kdy1
Donny/강동윤 (kdy1) merged commit f4b85a6 into main Jul 20, 2026
163 checks passed
@kdy1
Donny/강동윤 (kdy1) deleted the fix/ecma-number-hash branch July 20, 2026 12:17
@github-actions github-actions Bot added this to the Planned milestone Jul 20, 2026
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 21, 2026
**Description:**

Emit non-finite `Lit::Num` values safely in every expression context.
Readable output preserves an existing `NaN` or `Infinity` raw value,
while generated and minified values use capture-free numeric
expressions. Hygiene prevents preserved identifier forms from being
shadowed.

**Related issue (if exists):**

- #12043
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 22, 2026
**Description:**

ECMAScript bitwise NOT first applies `ToInt32`, including wraparound for
out-of-range numbers. Folding with direct Rust casts does not reproduce
those conversions at the boundaries.

This PR delegates numeric bitwise NOT folding to the existing `JsNumber`
implementation and adds coverage for boundary, wraparound, and very
large values. The simplifier now uses the same number semantics as the
rest of the optimizer.

**Related issue (if exists):**

- Close: #12062 

---

- #12043
  - #12047
    - #12050
      - #12048
        - #12051
      - #12052
        - #12053
      - #12054
      - #12055
      - #12056
      - #12057
    - **#12058** (current)
    - #12049
      - #12059
        - #12060
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 22, 2026
**Description:**

Several property-key optimizations rely on Rust number formatting or
eagerly parse strings as numbers. This can merge distinct ECMAScript
keys, including `Infinity` and `inf`, `-0` and `0`, or `"01"` and `"1"`.

This PR introduces shared canonical-index parsing and ECMAScript
number-to-string conversion, then uses them across property-key
transforms, literal folding, and TypeScript name extraction. Each path
now preserves property-key identity before optimizing it.

**Related issue (if exists):**

- Close: #12061
---
- #12043
  - #12047
    - **#12050** (current)
      - #12048
        - #12051
      - #12052
        - #12053
      - #12054
      - #12055
      - #12056
      - #12057
    - #12058
    - #12049
      - #12059
        - #12060
Donny/강동윤 (kdy1) added a commit that referenced this pull request Jul 22, 2026
…12049)

**Description:**

Enum evaluation currently rebuilds computed `NaN` and infinity values as
identifiers. That turns numeric constants into apparent references and
loses number-literal semantics as values move through the TypeScript
transform and fast-DTS evaluators.

This PR keeps computed enum numbers as `Number` literals in both paths,
applies JavaScript number stringification when names are required, and
corrects modulo evaluation for non-finite operands.

**Related issue (if exists):**

- #12043
  - #12047
    - #12050
      - #12048
        - #12051
      - #12052
        - #12053
      - #12054
      - #12055
      - #12056
      - #12057
    - #12058
    - **#12049** (current)
      - #12059
        - #12060

Co-authored-by: Donny/강동윤 <kdy.1997.dev@gmail.com>
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 23, 2026
**Description:**

`Number#toString(radix)` folding currently converts integer values
through `u128`. Unsafe integers can already have rounded
representations, so this conversion can produce digits that do not match
JavaScript for non-power-of-two radices.

This PR restricts radix folding to safe integers, with a separate exact
path for supported power-of-two radices below the `u128` bound. Values
outside those limits remain for runtime evaluation.

**Related issue (if exists):**

- #12043
  - #12047
    - #12050
      - #12048
        - #12051
      - #12052
        - #12053
      - #12054
      - #12055
      - #12056
      - **#12057** (current)
    - #12058
    - #12049
      - #12059
        - #12060
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 23, 2026
**Description:**

`Array(length)` throws a `RangeError` when the length is fractional or
otherwise invalid. Folding every small numeric length into an array
literal suppresses that observable exception.

This PR requires candidate lengths to be integers before applying the
literal-folding optimization and adds runtime coverage for invalid
lengths. Valid small arrays are still folded as before.

**Related issue (if exists):**

- #12043
  - #12047
    - #12050
      - #12048
        - #12051
      - #12052
        - #12053
      - #12054
      - #12055
      - **#12056** (current)
      - #12057
    - #12058
    - #12049
      - #12059
        - #12060
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 23, 2026
**Description:**

Computed `arguments` properties are currently parsed or cast directly as
numeric indices. Non-canonical names and invalid indices can therefore
be mistaken for parameter positions and replaced incorrectly.

This PR resolves `arguments` slots through the shared canonical-index
parser and ECMAScript number-to-string conversion before substituting
parameters. An access is optimized only when its property denotes the
same canonical argument index.

**Related issue (if exists):**

- Closes: #12066 

---

- #12043
  - #12047
    - #12050
      - #12048
        - #12051
      - **#12052** (current)
        - #12053
      - #12054
      - #12055
      - #12056
      - #12057
    - #12058
    - #12049
      - #12059
        - #12060
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 23, 2026
**Description:**

`String.fromCharCode` applies ECMAScript `ToUint16` to every argument.
The current floor-and-cast folding path does not match that behavior for
negative, wrapping, or non-finite inputs.

This PR adds `JsNumber::to_uint16` and uses it before folding ASCII
`String.fromCharCode` results. Boundary coverage keeps the optimization
limited to values whose output can be produced safely.

**Related issue (if exists):**

- #12043
  - #12047
    - #12050
      - #12048
        - #12051
      - #12052
        - #12053
      - #12054
      - **#12055** (current)
      - #12056
      - #12057
    - #12058
    - #12049
      - #12059
        - #12060
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 23, 2026
**Description:**

Enum template evaluation currently reads raw template source and
converts atoms lossily. Escapes, interpolation, and lone surrogates can
therefore diverge from the cooked JavaScript string used at runtime.

This PR evaluates template quasis and static template member keys from
cooked WTF-8 values in both the transform and fast-DTS paths. Lookup and
concatenation retain lone surrogates without changing the surrounding
enum evaluation model.

**Related issue (if exists):**

- #12043
  - #12047
    - #12050
      - #12048
        - #12051
      - #12052
        - #12053
      - #12054
      - #12055
      - #12056
      - #12057
    - #12058
    - #12049
      - **#12059** (current)
        - #12060
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 23, 2026
**Description:**

JavaScript string property indexing uses UTF-16 code units. Existing
constant folding checked string-literal indices against WTF-8 byte
length, which can treat out-of-bounds indices as valid for non-ASCII BMP
characters, astral characters, and lone surrogates. In the expression
simplifier, that mismatch could also reach an `unreachable!` branch.

This PR validates and reads string-literal indices using UTF-16 code
units in both the minifier and expression simplifier. It preserves the
WTF-8 byte-length check as a fast upper-bound rejection, folds valid
indices to the correct code unit, and handles UTF-16 out-of-bounds
indices according to each pass's existing semantics.

Coverage includes BMP non-ASCII characters, astral surrogate pairs, and
lone surrogates. The change was verified with:

- `cargo test -p swc_ecma_minifier -p swc_ecma_transforms_optimization`
- `(cd crates/swc_ecma_minifier && ./scripts/exec.sh)`
- `cargo fmt --all`
- `cargo clippy --all --all-targets -- -D warnings`

**Related issue (if exists):**

- #12043
  - #12047
    - #12050
      - #12048
        - #12051
      - #12052
        - #12053
      - **#12054** (current)
      - #12055
      - #12056
      - #12057
    - #12058
    - #12049
      - #12059
        - #12060
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 24, 2026
**Description:**

The optimizer currently represents `NaN` and infinities in several
forms, including identifiers, divisions, and numeric literals. As these
forms move through multiple optimization passes, they can be folded
inconsistently and acquire incorrect truthiness or stringification.

This PR canonicalizes non-finite optimization results as raw-free
`Lit::Num` values. Minifier and simplifier helpers then preserve
ECMAScript behavior for these numbers, while codegen remains responsible
for choosing valid output syntax.

**Related issue (if exists):**

- #12043
  - #12047
    - #12050
      - **#12048** (current)
        - #12051
      - #12052
        - #12053
      - #12054
      - #12055
      - #12056
      - #12057
    - #12058
    - #12049
      - #12059
        - #12060
Donny/강동윤 (kdy1) pushed a commit that referenced this pull request Jul 28, 2026
**Description:**

The JSON parse optimization had a number fast path that cast through
`i64`. That path could be unsafe for out-of-range values and for `-0`,
changing observable results.

This PR keeps the optimization but only uses the `i64` path when safe:
when the number is an integer within `i64` range and does not alter the
zero sign.
Other finite numbers are serialized via `serde_json::Number::from_f64`,
and non-finite handling is updated so `NaN` is rejected while
`±Infinity` is emitted as `±2e308`.

**Related issue (if exists):**

- #12043
  - #12047
    - #12050
      - #12048
        - **#12051** (current)
      - #12052
        - #12053
      - #12054
      - #12055
      - #12056
      - #12057
    - #12058
    - #12049
      - #12059
        - #12060
@github-actions github-actions Bot modified the milestones: Planned, v1.15.47 Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants