Commit Graph

6 Commits

Author SHA1 Message Date
camc314 949d7a9302 chore(deps): update vitest to v5 (#26299)
## Summary

- update workspace test dependencies to Vitest 5.0.0
- pin Vitest and related tooling to Vite 8.2.2
- migrate removed sequential and benchmark APIs
- update the pnpm lockfile
2026-09-03 19:05:05 +00:00
overlookmotel 5e6d537fca test(packages/codegen): include string flattening in benchmarks (#26106)
Over the weekend, I asked Fable to work on optimizing write operations when assembling the output string.

This uncovered a disturbing finding.

### Unrealistic benchmarks

The output string is assembled with repeated `output += segment` calls. This creates "cons strings" - a rope where each append creates a new string which references the previous `output` as its left side, and `segment` as its right side. On each append the rope gets longer - forming a linked list of segments.

That rope-string is what `printSync` returns to the user. It is a string, so it's correct, but there's a problem. In order to actually use that string for anything (write it to a file, index into it, search it, run a regex on it, etc) it has to be flattened. V8 does that transparently, so it works fine, but it's a costly operation.

So the disturbing finding was that we were ignoring that cost - we pushed it onto the user, and didn't measure it in our benchmarks. So, once this hidden cost is included, the codegen isn't as fast as we thought it was.

### Including the flatten cost

This PR alters the benchmarks to include string flattening via the most common path that V8 takes in user code. The effect is pronounced (higher is worse):

| Fixture | Bytes | Change |
|:---| ---:| ---:|
| `tiny.js` | 26 | \+92.4% |
| `RadixUIAdoptionSection.jsx` | 2,424 | \+303.8% |
| `react.development.js` | 50,496 | \+75.2% |
| `binder.ts` | 126,212 | \+85.9% |
| `lodash.js` | 182,262 | \+161.5% |
| `App.tsx` | 298,130 | \+146.8% |
| `kitchen-sink.tsx` | 662,560 | \+116.8% |
| `antd.js` | 5,100,647 | \+72.5% |

`RadixUIAdoptionSection.jsx` suffers in particular (4x slower) because one of the `JSXText` elements contain non-ASCII characters. This means the string has to be flattened to a 2-byte string (UTF-16) instead of the faster 1-byte (Latin1) representation. This encoding is twice as large, and hits V8's slow paths (because its optimized for 1-byte strings).

This doesn't mean that codegen has got slower - we're just now measuring it more realistically.

### Source maps builds

Source map builds are not affected - their perf doesn't change. They already flattened the `output` string by calling `indexOf` on it, so they were already paying the cost, we just didn't realize it.

### What to do?

The good news is that there are some things we can do to mitigate the impact - see following PRs.
2026-08-26 16:37:15 +00:00
overlookmotel ea2880170a test(package/codegen): add benchmark with sourcemaps (#25772)
Add benchmarks for source maps in `oxc-codegen`.

2 benchmarks:

1. Full end-to-end.
2. Without the actual sourcemap generation at the end.

The 2nd is to measure the overhead that pushing to the sourcemap arrays adds while printing in isolation.
2026-08-18 16:17:46 +00:00
camc314 300763f328 feat(packages/codegen): add full source map support (#25585)
## Summary

Align `oxc-codegen` source maps with Rust `oxc_codegen`, using the Rust implementation as the conformance oracle.

This PR:

- changes `printSync` to return `{ code, map? }`;
- returns a standard Source Map v3 object;
- exposes Rust’s generated source map through `codegen_conformance`;
- compares the complete JS and Rust source maps;
- supports Oxc `start`/`end` offsets and TS-ESTree `range` offsets;
- uses `sourceText` to convert UTF-16 offsets and recover original identifier spellings;
- validates both maps and no-maps builds in both `preserveParens` modes;
- aligns mapping placement across JavaScript, TypeScript, JSX, templates, imports, destructuring, delimiters, and postfix chains;
- handles astral characters and all ECMAScript line terminators.
`loc`-only ASTs are not supported for source maps.

## API change

```ts
const { code, map } = printSync(program, {
  sourcemap: true,
  sourceFileName: "input.js",
  sourceText,
});
```

`map` is a Source Map v3 object:

```ts
interface SourceMap {
  version: 3;
  mappings: string;
  names: string[];
  sources: string[];
  sourcesContent?: string[];
}
```

## Implementation

Mapped writes collect only:

- the generated UTF-16 offset;
- the original source offset;
- the optional original identifier name.

After printing, the generator builds line tables for the generated output and `sourceText`, converts the offsets to line/column positions, suppresses duplicate source positions, and encodes the result as base64 VLQ.

The no-maps builds continue to remove mapping-specific writes at build time.

## Validation flow

```mermaid
flowchart TB
    fixture["Fixture<br/>filename + sourceText + parse options"]

    fixture --> rustParse
    fixture --> jsParse

    subgraph rust["Rust oracle"]
        direction TB
        rustParse["Parse with oxc_parser"]
        normalize["Normalize Rust-only AST information"]
        rustPrint["Print with oxc_codegen<br/>source maps enabled"]
        rustResult["Expected result<br/>{ code, map }"]

        rustParse --> normalize
        normalize --> rustPrint
        rustPrint --> rustResult
    end

    subgraph js["JavaScript candidate"]
        direction TB
        jsParse["Parse ESTree with oxc-parser<br/>start/end or range offsets"]

        plainPrint["Print with no-maps build"]
        plainResult["Actual code"]

        mappedPrint["Print with maps-enabled build"]
        deferred["Record generated offsets,<br/>source offsets, and names"]
        encode["Convert UTF-16 offsets to positions<br/>deduplicate and encode VLQ"]
        mappedResult["Actual result<br/>{ code, map }"]

        jsParse --> plainPrint
        plainPrint --> plainResult

        jsParse --> mappedPrint
        mappedPrint --> deferred
        deferred --> encode
        encode --> mappedResult
    end

    rustParse --> parseGate
    jsParse --> parseGate

    rustResult --> codeGate
    plainResult --> codeGate
    mappedResult --> codeGate

    rustResult --> mapGate
    mappedResult --> mapGate

    parseGate["Parse agreement<br/>Rust success requires no JS errors"]
    codeGate["Code equality<br/>both JS builds equal Rust"]
    mapGate["Source Map v3 equality<br/>mappings + names + sources + content"]

    parseGate --> pass["Fixture passes"]
    codeGate --> pass
    mapGate --> pass
```

Each fixture runs with both `preserveParens: false` and `preserveParens: true`.

## AST normalization

Rust’s AST contains information that ESTree cannot represent. The Rust AST is normalized before printing so the comparison measures code generation rather than representation differences.

This includes:

- parenthesized function and arrow-expression flags;
- `import "m"` versus `import {} from "m"`;
- absent versus empty import-attribute clauses;
- `assert` versus `with` attribute keywords;
- the unavailable ESTree span for a complete `with` clause.
2026-08-18 11:41:34 +00:00
camc314 46d237c9a7 chore(codegen,parser,tasks): pin benchmark fixtures by sha (#25557)
Pin the Codegen benchmark fixture to an immutable `benchmark-files` commit. This makes benchmark inputs reproducible and prevents `main` updates from silently changing results.
2026-08-12 15:56:52 +00:00
overlookmotel a4478e960a feat(codegen): add oxc-codegen package (#25488)
Add `oxc-codegen` package - a printer written in TS.

### Features

- Prints JS, JSX, TS, TSX.
- Matches `oxc_codegen` exactly on all Test262, TypeScript, Acorn-JSX fixtures.
- Very fast.

### Deficiencies

- Pretty-printing only (no compact/minified output).
- No support for printing comments.
- Source map support is semi-there, but largely untested, and poor API (design doc has some ideas on how to improve it).

### This PR adds

- The printer code.
- Conformance testing infra.
- Further tests for edge cases.
- README.
- Design doc - how it differs from `oxc_codegen` and why, what produces the perf.

### This PR doesn't add

- Not integrated into CI or release pipeline.

### Notes

This started as a slop port of `oxc_codegen`. I've iterated on that base, cleaned it up a lot, and at least skimmed through all of it. I think it's in fairly decent shape.

The design doc is I think worth a read. It explains the rationale for the choices, in particular the "1 codebase, 4 builds" approach.

I've put it in a new top-level directory called `packages`. It's not an app, so didn't seem to me to fit alongside `apps/oxlint` and `apps/oxfmt`. It's pure JS, so not like e.g. `napi/parser`. And it felt too heavyweight to put into `npm` dir. But maybe there's a better place for it.

Co-authored-by: Cameron <cameron.clark@hey.com>
2026-08-12 13:41:31 +00:00