Skip to content

feat!: use SWC Next for stripping and remove transform mode - #507

Draft
kdy1 wants to merge 8 commits into
nodejs:mainfrom
kdy1:kdy1/try-swc-next
Draft

kdy1 wants to merge 8 commits into
nodejs:mainfrom
kdy1:kdy1/try-swc-next

Conversation

@kdy1

@kdy1 kdy1 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Switch TypeScript type stripping from swc_ts_fast_strip to the SWC Next crates published on crates.io, pinned to 0.5.0. Preserve fixed-width stripping, the transformSync API, and the existing Node.js REPL helpers.

Remove mode: "transform", the amaro/transform loader, transformation-only options, and their dependencies and tests. Requests for the removed mode now fail instead of falling back to stripping. This is a breaking change, consistent with the removal of transformation support in nodejs/node#61803.

The adapters preserve Amaro's diagnostic codes and location fields, while messages and highlighted ranges come from SWC Next. Existing SWC crates remain for REPL helpers and diagnostic formatting.

Validation

Local validation at 9981c731d with SWC Next 0.5.0:

  • cargo test --manifest-path crates/amaro/Cargo.toml --locked: 18 passed.
  • wasm-pack build crates/amaro --out-name wasm --release --target nodejs -- --locked: passed.
  • npm run build, npm run typecheck, and npm run ci: passed.
  • npm test: 99 passed, including removed-mode rejection, loader behavior, source positions, and REPL helpers.

Draft status

Additional bug analysis of the related swc-next components is currently in progress to look for potential upstream bugs. This PR remains a draft while that analysis continues; the passing tests above do not establish complete compatibility or prove that swc-next is free of bugs.

AI assistance and human review

AI was used to prepare these changes. Human-reviewed by @kdy1.

  • Harness: T3 Code (Codex harness)
  • Model: gpt-6.1-sol
  • Reasoning Effort: medium

@marco-ippolito

Copy link
Copy Markdown
Member

Amazingg work, does this boost the performance?

@kdy1

kdy1 commented Oct 31, 2026

Copy link
Copy Markdown
Member Author
image

It's currently mixed, so I'll optimize the swc-next first

@kdy1

kdy1 commented Nov 1, 2026

Copy link
Copy Markdown
Member Author
image

It's now faster for all cases! The swc-next commit I used is swc-project/swc-next@95e2394, and I'll try to find and fix any remaining bugs before merging.

kdy1 and others added 2 commits November 2, 2026 18:59
`deprecatedTsModuleAsError` is enabled by default, and the check parsed the
whole input a second time when the text contained "module". Most real files
contain that word in `module.exports`, `node:module`, or comments, so most
inputs were parsed twice.

Run the second parse only when a `module` word is followed by a possible
declaration name and that word is erased in the stripped output.
`strip_types` erases every `module` declaration as a whole statement, so a
keyword that survives unchanged is in a comment, a string, or an
expression. Inputs with strip diagnostics keep the previous behavior.

Results for 442 corpus variants, including injected declarations, are
identical to the previous build. This includes error codes, messages, and
locations. Median `transformSync` time in the release WASM build:

- hono-types.ts (76 KB): 1.11 ms -> 0.68 ms
- checker.ts (3 MB): 34.7 ms -> 21.0 ms
- typescript.js (8 MB): 100.7 ms -> 62.9 ms
- astro-config.ts (125 KB): 0.97 ms -> 1.05 ms (the parse is still needed
  because a comment in an erased interface contains "module ...")

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kdy1 added a commit to swc-project/swc-next that referenced this pull request Nov 2, 2026
## Motivation

On targets without a native SIMD backend, including
`wasm32-unknown-unknown` without `simd128`, `swc_next_simd` processed
each 16-byte vector one lane at a time. Loops written with `u8x16` were
then much slower than byte loops. Block comment scanning in the lexer
and blanking in `strip_types` were affected the most. For example, on a
1 MiB single-line block comment, swc-next parse took 5.4 ms, and the old
`swc_ecma_parser` took 0.35 ms. Node.js amaro and the swc-next npm
WebAssembly bindings build for this target without `simd128`.

## Changes

- The scalar backend now processes each vector as two `u64` words
(SWAR). The public API, the per-lane results, and the x86_64, aarch64,
and wasm32 `simd128` backends are unchanged. Call sites are unchanged.
- The scalar module is now compiled for tests on every host. New tests
place every byte pair in every lane position and compare each operation
with a per-lane reference. They also cover lane boundaries, `first_set`,
and `any`.
- On wasm32 without `simd128` (`opt-level = "z"`, fat LTO), compared
with `main`:
  - Parse of a 1 MiB single-line block comment: 5.39 ms → 0.79 ms.
  - Parse of 1 MiB of JSDoc comments: 2.16 ms → 0.45 ms.
- `strip_types` on `tasks/benchmark/files/astro-config.ts`: 0.57 ms →
0.11 ms.
  - No input is slower.
- The `strip_types` output and diagnostics are identical to `main` for
22 inputs: the benchmark files, the inputs from #1418, and edge cases
for line terminators, `0xE2` sequences, unterminated comments, and
16-byte boundaries.

## Related Links

- Close #1418
- nodejs/amaro#507

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
`strip_types` erases every `module` declaration, so compare the input
with the stripped output and check only `module` words in erased text.
This replaces the full-text `match_indices` search and the UTF-16 offset
mapping. Also skip a `module` word that follows a non-keyword word on
the same line, because it cannot start a statement. This avoids the
second parse for prose such as `its module dependency` in comments
inside erased interfaces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kdy1 added a commit to swc-project/swc-next that referenced this pull request Nov 3, 2026
## Motivation

Size-optimized builds keep the `#[inline]` wrappers in `swc_simd` as
function calls. Amaro builds `swc_next_type_strip` for
`wasm32-unknown-unknown` with `opt-level = "z"` and without `simd128`,
so the scalar backend runs. In a profile of that build, `simd_eq` and
`first_set` stay separate functions inside the block comment scanner,
and each call passes a 16-byte vector through memory. A 1 MB block
comment on one line takes 2.44 ms with `main`, compared with 1.72 ms on
Amaro `main`, which uses `swc_ts_fast_strip`.

## Changes

- Mark the vector and mask operations in `crates/swc_simd/src/lib.rs`
`#[inline(always)]`, like the backend functions that they call. Behavior
does not change.
- Amaro `transformSync`, wasm32 without `simd128`, this branch compared
with `main` (2fe537f). Medians of 6 rounds, measured locally:

  | Input | `main` | This PR | Speedup |
  | --- | ---: | ---: | ---: |
  | 1 MB block comment, one line | 2.44 ms | 2.19 ms | 1.11× |
  | 1 MB block comment, many lines | 2.00 ms | 1.91 ms | 1.05× |
  | 1 MB of JSDoc blocks | 2.14 ms | 2.04 ms | 1.05× |
  | 1 MB of plain block comments | 2.07 ms | 1.97 ms | 1.05× |
  | `typescript.js` (8 MB) | 56.61 ms | 54.88 ms | 1.03× |

The other measured inputs (jQuery, three.js, `checker.ts`, cal.com,
Hono, Effect, and 1 MB string, type, line comment, and whitespace
inputs) changed by less than 1.5%.
- The output matched the `main` build for 400 inputs: the Amaro
benchmark corpus and synthetic inputs, each also with comments, line
separators, and whitespace inserted.
- `cargo fmt --check`, `cargo clippy -- -D warnings` (host and
`wasm32-unknown-unknown`), and the tests of `swc_next_simd`,
`swc_next_ecma_parser`, `swc_next_type_strip`, and
`swc_next_ecma_codegen` pass. Native wall time was not measured.

## Related Links

- #1419
- nodejs/amaro#507

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@kdy1

kdy1 commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

@marco-ippolito Is removing mode: "transform" fine? I'm not sure if it's acceptable at this stage.

@kdy1

kdy1 commented Nov 3, 2026 •

Copy link
Copy Markdown
Member Author

Performance profiling result

  • idle linux machine
Input main PR + 0.5.0 PR + swc-next main vs main vs previous run
typescript.js (8 MB) 135.98 ms 55.94 ms 50.53 ms 2.69× 1.09×
checker-noenum.ts 44.61 ms 18.92 ms 17.80 ms 2.51× 1.09×
three-0.138.3.js 22.16 ms 10.05 ms 8.80 ms 2.52× 1.09×
cal.com-noenum.tsx 19.68 ms 7.92 ms 6.96 ms 2.83× 1.08×
jquery.mobile-1.4.2.js 7.95 ms 3.50 ms 3.23 ms 2.46× 1.07×
angular-1.2.5.js 6.82 ms 3.92 ms 3.07 ms 2.22× 1.15×
jquery-1.9.1.js 5.21 ms 2.32 ms 2.20 ms 2.37× 1.08×
mootools-1.4.5.js 4.02 ms 1.71 ms 1.59 ms 2.53× 1.07×
yui-3.12.0.js 3.97 ms 2.14 ms 1.72 ms 2.31× 1.14×
effect-schema-ast.ts 2.43 ms 1.13 ms 989.9 µs 2.46× 1.07×
err-large-enum.ts 1.70 ms 814.2 µs 651.7 µs 2.61× 1.64×
hono-types.ts 1.60 ms 671.6 µs 504.2 µs 3.17× 1.01×
backbone-1.1.0.js 907.1 µs 358.9 µs 337.8 µs 2.69× 1.14×
underscore-1.5.2.js 764.4 µs 278.9 µs 264.6 µs 2.89× 1.10×
astro-config.ts 388.5 µs 724.2 µs 239.0 µs 1.63× 1.53×
err-small.ts 7.6 µs 7.4 µs 7.4 µs 1.03× 1.00×
hello.ts 2.3 µs 1.6 µs 1.5 µs 1.55× 1.03×
hono-types.ts (check off) 1.61 ms 647.3 µs 481.2 µs 3.34× 1.02×
astro-config.ts (check off) 389.0 µs 676.4 µs 195.4 µs 1.99× 1.08×
checker-noenum.ts (check off) 45.24 ms 18.46 ms 17.37 ms 2.60× 1.01×
Synthetic: block-comments 1 MB 1.92 ms 3.60 ms 1.77 ms 1.09× 1.21×
Synthetic: plain-block-comments 1 MB 1.92 ms 3.39 ms 1.74 ms 1.10× 1.19×
Synthetic: line-comments 1 MB 2.05 ms 1.77 ms 1.77 ms 1.16× 1.11×
Synthetic: one-big-comment-lines 1 MB 1.71 ms 3.42 ms 1.68 ms 1.02× 1.19×
Synthetic: one-big-comment 1 MB 1.71 ms 6.76 ms 1.87 ms 0.91× 1.29×
Synthetic: strings 1 MB 7.27 ms 7.29 ms 3.73 ms 1.95× 1.07×
Synthetic: types-only 1 MB 26.60 ms 11.33 ms 9.33 ms 2.85× 0.99×
Synthetic: whitespace 1 MB 1.73 ms 1.96 ms 1.98 ms 0.87× 1.04×

@marco-ippolito

Copy link
Copy Markdown
Member

@marco-ippolito Is removing mode: "transform" fine? I'm not sure if it's acceptable at this stage.

I think its acceptable as semver major change

kdy1 added a commit to swc-project/swc-next that referenced this pull request Nov 3, 2026
…ckend (#1443)

## Motivation

#1422 made block comments slower in Amaro's `wasm32-unknown-unknown`
build (`opt-level = "z"`, no `simd128`), which uses the scalar backend.
In Amaro `transformSync`, a 1 MB block comment on one line went from
1.87 ms to 5.09 ms. In a profile of that build,
`Lexer::scan_block_comment` is no longer inlined into `next_token`.
Inside it, `<[u8; 16] as IndexMut<RangeTo<usize>>>::index_mut` stays a
separate call and takes 10% of the time. This call comes from
`lanes[..8].copy_from_slice(..)` in the scalar backend. `Option::expect`
in `Simd::from_slice` also stays a call. The 1 MB comment inputs do not
call `TokenReader::read`, so the slowdown comes from code generation,
not from extra work.

## Changes

- The scalar backend's `lanes()` builds the array by destructuring the
two words, and `Simd::from_slice` matches on `first_chunk` instead of
calling `expect`. Behavior does not change. Only the scalar backend uses
`lanes()`. The `from_slice` change applies to all backends.
- Amaro `transformSync`, wasm32 without `simd128`. I measured the same
diff locally on `69b31108`. Since then, `main` changed only
`crates/swc_type_strip` (#1442). Medians of 4 rounds:

| Input | Before #1422 (87adca5) | `69b31108` | `69b31108` + this PR |
  | --- | ---: | ---: | ---: |
  | 1 MB block comment, one line | 1.87 ms | 5.09 ms | 1.85 ms |
  | 1 MB block comment, many lines | 1.69 ms | 2.39 ms | 1.68 ms |
  | 1 MB of JSDoc blocks | 1.77 ms | 2.56 ms | 1.76 ms |
  | 1 MB of plain block comments | 1.73 ms | 2.42 ms | 1.71 ms |
  | Astro config types (125 KB) | 238.1 µs | 320.0 µs | 236.6 µs |
  | AngularJS 1.2.5 | 3.06 ms | 3.39 ms | 3.02 ms |
  | YUI 3.12.0 | 1.72 ms | 1.77 ms | 1.72 ms |
  | `typescript.js` (8 MB) | 49.95 ms | 51.03 ms | 48.89 ms |

- For 650 inputs, the output matched the `69b31108` build. The inputs
are the Amaro benchmark corpus and synthetic inputs, each also with
TypeScript `module` declarations and comments inserted at several
positions.
- The `swc_next_simd` tests and Amaro's 99 tests pass with this change.
`cargo fmt`, `cargo clippy`, and the tests of the other crates were not
run locally. Native wall time was not measured.

## Related Links

- #1422
- #1426
- nodejs/amaro#507

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
kdy1 and others added 2 commits November 4, 2026 11:10
`SourceMap::new_source_file` removes a leading BOM, but SWC Next spans
include it. With a BOM, every diagnostic was shifted by three bytes, and
a shifted span inside a multi-byte character made snippet rendering
panic, which traps in WebAssembly. A BOM before a hashbang also caused
an `InvalidSyntax` error. Remove the BOM before stripping, as
`swc_ts_fast_strip` did, and clamp diagnostic spans to character
boundaries.

Sort all diagnostics so that the first reported error is the first one
in the source, also for the errors that `validate` adds. Keep an
explicit error code with each diagnostic instead of matching messages,
and remove the unused `Unsupported transform option` match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kdy1

kdy1 commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

Verification results

I pushed 558cbb2 to this branch. It fixes a leading-BOM problem and the order of reported errors. This comment covers what I checked and what still needs a decision.

What I checked

  • Differential test against swc_ts_fast_strip 59.0.2 (the current implementation). I ran both implementations on the same inputs and compared the output code and the errors.
    • Real-world code: 40,356 units from vscode, Deno std, vite, hono, zod, trpc, and type-fest. There were 0 output differences and 0 inputs that the old implementation accepts and the new one rejects.
    • TypeScript v5.9.3 tests/cases: I ran each unit in the Unambiguous, Module, and Script modes.
      • The 63 inputs that the old implementation accepts and the new one rejects are, with one exception, invalid TypeScript. The exception is import type X = require("...") (see below).
      • The new implementation accepts 285 inputs that the old one rejects. For 128 of them, V8 accepts the output. All 128 outputs have the same semantics as the tsc emit (overloads, abstract, ambient declarations, modifiers). V8 rejects the other outputs.
      • The old implementation erases namespace\nn\n{}, which is runtime code. The new implementation keeps it, which is correct.
      • The two old panics (JSX namespaced names) do not occur with the new implementation.
  • Fast path for the deprecated module check: I fuzzed about 275k inputs, including 39,560 real module declarations and variants with LS/PS, NBSP, BOM, escapes, and comments. There were 0 inputs where the parser finds a module X {} declaration but the fast path skips the check.
  • cargo test, clippy, cargo fmt --check, the wasm build, npm test (99/99), typecheck, and biome all pass.

Open items

  1. import type X = require("...") is now rejected. This matches erasableSyntaxOnly in tsc 5.9. But the current amaro erases this statement, so this is a breaking change for Node users. We must decide if this is acceptable and, if yes, document it.
  2. The error messages are less specific. All unsupported syntax now gives SyntaxError: This syntax is not allowed when \erasableSyntaxOnly` is enabled. Previously, the message identified the construct, for example TypeScript enum is not supported in strip-only mode. With the error name, the result is UnsupportedSyntaxError: SyntaxError: ...`, which has a duplicated prefix. Node core tests that match the old messages will need updates.
  3. Upstream: SourceType::Script accepts import and export without a diagnostic. This PR adds its own check in validate as a workaround. When SWC Next fixes this, we can remove the workaround.
  4. Upstream: the parser does not report strict-mode early errors (for example, with statements and legacy octal literals in Module mode). V8 rejects this output when it loads it, so the runtime behavior does not change. Only the error source and message change.
  5. Escaped keyword: module Foo {} now gives an InvalidSyntax error instead of the deprecated module keyword error. This is a minor difference, but it is a behavior change.
  6. Parser options: only tsx from the parser option has an effect. Other fields, for example dts, are ignored. If callers can set these fields, we should document this or reject them.

kdy1 added a commit to swc-project/swc-next that referenced this pull request Nov 4, 2026
## Motivation

#1443 made the scalar backend's `lanes()` build its `[u8; 16]` result by
destructuring two words into 16 bytes. This removed a call from the
block comment scanner, but it made string scanning slower in Amaro's
`wasm32-unknown-unknown` build (`opt-level = "z"`, no `simd128`). In
Amaro `transformSync`, 1 MB of string declarations went from 3.57 ms to
4.45 ms. When I revert only the `lanes()` change, this time returns to
3.57 ms, and the block comment slowdown comes back. When I revert only
the `from_slice` change, nothing changes. A possible cause is that the
inlined string scanner now does byte-wise work where it had two 8-byte
stores before. I did not confirm this in the generated code.

## Changes

- `lanes()` converts both words with one `u128::to_le_bytes`. It has no
range indexing and no per-byte array. Behavior does not change. Only the
scalar backend uses `lanes()`.
- Amaro `transformSync`, wasm32 without `simd128`, measured locally.
Medians of 4 rounds:

  | Input | Before #1443 (`69b31108`) | `main` (`0334f9d7`) | This PR |
  | --- | ---: | ---: | ---: |
  | 1 MB of string declarations | 3.57 ms | 4.45 ms | 3.58 ms |
  | 1 MB block comment, one line | 5.09 ms | 1.86 ms | 1.85 ms |
  | 1 MB block comment, many lines | 2.40 ms | 1.66 ms | 1.67 ms |
  | 1 MB of JSDoc blocks | 2.55 ms | 1.77 ms | 1.77 ms |
  | Astro config types (125 KB) | 321.1 µs | 241.4 µs | 236.9 µs |
  | AngularJS 1.2.5 | 3.40 ms | 3.05 ms | 3.01 ms |
  | `typescript.js` (8 MB) | 51.50 ms | 49.80 ms | 49.05 ms |
  | cal.com TSX (1 MB) | 7.10 ms | 7.21 ms | 6.94 ms |

I also tried `u128` in `words()`. With that change, the string input
took 3.90 ms, so this PR does not include it.
- For 650 inputs, the output matched the `main` build. The inputs are
the Amaro benchmark corpus and synthetic inputs, each also with
TypeScript `module` declarations and comments inserted at several
positions.
- The `swc_next_simd` tests pass. `cargo fmt`, `cargo clippy`, the tests
of the other crates, and the other Amaro benchmark inputs were not run
for this change. Native wall time was not measured.

## Related Links

- #1443
- #1422
- nodejs/amaro#507

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants