Skip to content

test(transpiler): record which early errors are erased in code that nothing runs - #48011

Open
robobun wants to merge 4 commits into
mainfrom
robobun/69e462fa/erased-early-errors-ledger
Open

robobun wants to merge 4 commits into
mainfrom
robobun/69e462fa/erased-early-errors-ledger

Conversation

@robobun

@robobun robobun commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Behaviour change: none

Problem

  • Bun's parser leaves some early errors to JavaScriptCore. Where a pass removes the code, nothing reports the error: if (0) { x = /(/; } runs.
  • bun test --coverage turns dead code elimination off and refuses the same file.
  • No test records this class.

Fix

  • A table in test/bundler/transpiler/runtime-transpiler.test.ts holds 95 programs. A child bun loads each program in two files: under if (0), and under a condition that Bun can't decide.
  • Today 11 early errors are reported in both files. 64 are erased under if (0), and 16 where the code stays. The other 4 programs are valid controls.
  • The table is a floor. The test fails when Bun reports an error less than its line says. A fix passes, and its author can move the lines up.
  • Verified: runtime-transpiler.test.ts on release and debug builds of b1305dd. Self-reviewed: 9 concerns raised, 9 addressed, and the 5 review comments too.

Background

  • An early error is a syntax error that the specification requires before any code runs. It applies to code that never runs too.
  • "Erased where the code stays" means that both files load although the engine refuses the first as written.
  • The table records what Bun does, not pending fixes. bun run --debug-aot refuses 67 of the 80 erased programs. 11 of the other 13 have an invalid RegExp literal.
  • Considered a scan of the lint suites' tables or the AOT suite's lists. A change to those lists then breaks this test.
Notes

The table on main (95 programs)

List Reported Erased under if (0) Erased where it stays No early error
accessors.js 2 1
assignment-targets.js 2 4
await.js 6
class-members.js 1 2 2
declarations.js 2 4 2
delete.js 2
delete.ts 1
labels-and-jumps.js 1 6
modules.js 1
new.js 4 1
regexp-literals.js 1 4 7
reserved-names.js 2 18
reserved-names.ts 2
sloppy-code.cjs 3
strict-code.cjs 2 5 2
using.js 4 1
Total 11 64 16 4

/\P{assigned}/u; in regexp-literals.js is the program of #15219.

How an outcome is decided

  • Each program is in two files that nothing runs: if (globalThis.never) { <program> } and if (0) { <program> }. A "use strict"; at the start of a program stays above the if.
  • A child bun --no-install loads each file, a .cjs file with require and any other file with import(). The package has "type": "module".
  • If both files load, the engine reads the first file as written: a .js file as a module, a .cjs file as the body of a CommonJS function. If it refuses the text, the error is erased where the code stays. The engine can't read TypeScript, so a .ts program that loads in both files is an error of the table.
  • The table holds no error message, so a WebKit upgrade that rewords an error changes nothing.

Other checks, outside the test

  • Node.js 26.3.0: node --check refuses all 88 .js and .cjs programs that have an early error. It accepts the 4 controls. The other 3 programs are TypeScript.
  • bun run --debug-aot was asked about each of the 80 erased programs in the file where it is erased. It stops on 67 with a TypeScript diagnostic, such as TS2392 for two constructors. It runs 13: the 11 with an invalid RegExp literal, "use strict"; x = 010; and function f() { "use strict"; with ({}) {} }. So bun run and bun run --aot already disagree on 67 of these files.

Positions that are in the tree

  • An invalid RegExp literal in removed code: test/aot/parallel/language/invalid-regexp-in-dropped-code.js has to run under --aot as it does under bun. src/aot/lower/expression/array.rs says that bun run never parses a literal that its transpiler leaves out.
  • new.target outside a function: src/js_parser/visit/visit_expr.rs says that the parser's error "is intentionally not emitted". The AOT front end refuses it, also under if (0).
  • Question for a maintainer: for if (0) { new.target; } in a module, is bun run (it runs) or bun run --debug-aot (TS17013) right?

Open PRs that change an outcome

A floor, not an exact record

  • The outcomes have an order: reported, erased under if (0), erased where the code stays. The test fails when a program is below its line, or when a valid program is refused.
  • A strict comparison would fail on every fix. A fix and this PR would each pass alone, and main would be red after the second merge.
  • Each line is exact on main today. Commit 0dfa8b3 compared the whole table with toEqual and passed.
  • The test passes on the base build, by design. The PR has no fix to prove: it records what the base build does.

Earlier shapes of this PR

  • The first version scanned the tables of lint.test.ts and oxc-errors.test.ts. To share them, 13,400 lines move, and git merge-tree shows that bun check: TypeScript 7.1 #46383 and Reject ? and ! after an object literal property name like oxc #47762 then conflict.
  • The second version scanned the AOT suite's early-error lists with Bun.Transpiler. It kept the result in a list file. A program added to those lists would fail this test with no conflict for git to show. With the lists of AOT: front end, test runs and tests for the rewritten compiler #47942 it asked for six more lines. Programs with an export were never examined, because a block can't hold one.
  • Turning dead code elimination off at run time is not a fix. It also changes which files are CommonJS: if (0) { module.exports = 1; } new.target; is a module under bun test and CommonJS under bun test --coverage.

What the test doesn't find

  • An early error in a program that is not in the table.
  • An error in syntax that only the top level holds. let a; export { a as "\uD83D" }; runs, and Node.js refuses it.
  • An error that JavaScriptCore accepts too.
  • An erased error in TypeScript where the code stays. The engine can't read that source.

Cost

  • The test takes 40 ms on a release build and 1 s on a debug build with ASAN.

[auto-merge] gate passed · iteration 1 · 1 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/bundler/transpiler/runtime-transpiler.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/bundler/transpiler/runtime-transpiler.test.ts
bun test v1.4.4 (367d939d9)

test/bundler/transpiler/runtime-transpiler.test.ts:
(pass) use strict causes CommonJS [372.76ms]
(pass) non-ascii regexp literals [4.05ms]
(pass) ascii regex with escapes [1.82ms]
(pass) // @bun > async transpiler [26.11ms]
(pass) // @bun > require() [10.95ms]
(pass) // @bun > synchronous [212.46ms]
(pass) json imports > require(*.json) [13.51ms]
(pass) json imports > import(*.json) [6.93ms]
(pass) json imports > should support comments in tsconfig.json [7.54ms]
(pass) json imports > should handle non-boecjts in tsconfig.json [5.36ms]
(pass) json imports > should handle duplicate keys [5.02ms]
(pass) with statement > works [203.38ms]
(pass) math.pow [3.97ms]
(pass) unterminated string literals in large files > reports an unterminated string literal at the end of a large JavaScript file [368.31ms]
(pass) unterminated string literals in large files > reports an unterminated string literal at the end of a large JSON file [274.12ms]
(skip) printing more than 2 GiB of modules in one process keeps the space after a keyword
(pass) an early error in code that nothing runs > is reported no less than the table says [966.84ms]

 16 pass
 1 skip
 0 fail
 37 expect() calls
Ran 17 tests across 1 file. [4.81s]
Exit: 0
diff hotspot
test/bundler/transpiler/runtime-transpiler.test.ts | 212 +++++++++++++++++++++
 1 file changed, 212 insertions(+)

gate history · 4 passed · 0 rejected · iteration 1

evidence per changed file
file                                                reads  edits  tests
test/bundler/transpiler/runtime-transpiler.test.ts      2      0     20

…ed code

Bun's parser leaves some early errors to the engine: it prints the code
and JavaScriptCore refuses the printed text. Where a pass removes the
code or prints it differently, nothing reports the error.

The test transpiles each program under `if (1) { }` and under
`if (0) { }` and asks the engine about the printed text.
erased-early-errors.txt lists the programs that are refused in one place
and accepted in the other. A second test loads each listed program as a
file in a child process.

The programs are the early-error lists of the AOT suite, read in place,
and a table in the test for the rules that those lists lack.
@github-actions github-actions Bot added the claude label Nov 6, 2026
@robobun

robobun commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:46 AM PT - Nov 6th, 2026

❌ @robobun, your commit e3a5ae9 has 38 failures in Build #137962 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 48011

That installs a local version of the PR into your bun-48011 executable, so you can run:

bun-48011 --bun

@robobun

robobun commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: the PR is ready for a maintainer. The test passes on a release build and a debug build of b1305dd.

How to see what the table records, with a build of main:

echo 'if (globalThis.never) { x = /(/; }' > kept.js && bun kept.js
# SyntaxError: Invalid regular expression: missing )
echo 'if (0) { x = /(/; } console.log("ran");' > removed.js && bun removed.js
# ran

How to run the test: bun bd test test/bundler/transpiler/runtime-transpiler.test.ts -t "an early error"

CI: build 137962 is finished. test/bundler/transpiler/runtime-transpiler.test.ts passed at the first attempt on each of the 11 lanes that run it. The build has 28 red tests. All 28 are tests of the AOT suite, and all 28 are red on main (build 137822). One ASAN job reached its time limit in test/aot/three-layers-agree.test.ts. This PR changes none of these files.

@coderabbitai

coderabbitai Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The change adds a regression test for early-error behavior in JavaScript, CommonJS, and TypeScript snippets. It compares expected classifications with engine rejection checks and Bun’s results for retained and removed files.

Changes

Early-error comparison

Layer / File(s) Summary
Case table and engine check
test/bundler/transpiler/runtime-transpiler.test.ts
The test adds source snippets grouped by expected error behavior. Helpers preserve a leading strict-mode directive and check whether node:vm rejects .js and .cjs source.
Paired file-loading validation
test/bundler/transpiler/runtime-transpiler.test.ts
The test generates retained and removed files for each case, loads .cjs files with require and other files with import, then compares results with the expected classifications. It also checks for empty stderr and a zero exit code.

Suggested reviewers: alii

Merge Risk: Minimal · up to e3a5a

The test provides the intended early-error regression coverage, with no identified issue requiring a fix before merge.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title describes the test for early errors erased in code that does not run.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description explains the test-only change and how the author verified it. The headings differ from the template, but both required topics are covered.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 0dfa8b3 and e3a5ae9.

Files selected for processing: 1

  • test/bundler/transpiler/runtime-transpiler.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

…st.ts

The table has its own programs, each under the outcome that it has
today: reported, erased under `if (0)`, erased where the code stays, or
no early error. One child process loads each program in two files.

The test reads no list of another suite, and it has no list file and no
switch to write one.
@robobun robobun changed the title test(transpiler): list the early errors that nothing reports in removed code test(transpiler): record which early errors are erased in code that nothing runs Nov 6, 2026
@robobun

robobun commented Nov 6, 2026

Copy link
Copy Markdown
Collaborator Author

The three review comments are addressed in de8915c, and each thread has the details.

  • The test is now one describe in test/bundler/transpiler/runtime-transpiler.test.ts. The two new files are gone.
  • The table has its own programs, with the outcome of each beside it. The test reads no list of the AOT suite, so a change to those lists can't fail it.
  • There is no list file and no switch to write one.
  • A "use strict"; at the start of a program stays above the if, so the strict CommonJS rows are examined too.

The title and the description are updated to match.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline nit, I also checked that vm.SourceTextModule parses eagerly in its constructor, so the isRefusedAsWritten oracle for .js rows is live; that the classifier throws on any unexpected error name and yields an outcome absent from the table (so a surprise fails toEqual rather than passing silently); and that the "use strict" hoisting in fileOf matches the three directive rows. The rewrite into runtime-transpiler.test.ts with an inline table resolves the earlier points about a separate file and the AOT-list coupling.

Extended reasoning...

Test-only change: 197 added lines in test/bundler/transpiler/runtime-transpiler.test.ts adding one test that spawns a single Bun process over ~200 generated files and compares a hard-coded four-way outcome table of ~100 early-error programs against observed behavior. It touches no production code and no security-sensitive surface. The table is an empirical snapshot of current Bun+JSC behavior that cannot be verified here without running the test, and a maintainer may want to weigh its maintenance cost on WebKit or parser changes, so a human look is still worthwhile.

…r table can't judge

The table takes .js, .cjs and .ts lists. Any other extension is an
error. A .ts program that loads in both files is an error too: the
engine can't read it, so nothing says whether it has an early error.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

The extension fall-through raised last round is addressed: unknown list extensions now throw in the row builder, and isRefusedAsWritten throws for .ts instead of filing the row under "has no early error". Beyond the inline nit, I also checked hermeticity (--no-install, harness tempDir, using registered before any assertion), that stdout/stderr/exited are drained together, and that vm.SourceTextModule exists in Bun's node:vm (src/js/node/vm.ts:479) — none of these are a problem. Since the table deliberately records current behaviour rather than desired behaviour, and the description itself poses a question for a maintainer about new.target under if (0), a maintainer's look is still worthwhile.

Extended reasoning...

The change adds one characterization test (+205 lines) to test/bundler/transpiler/runtime-transpiler.test.ts with no source changes and touches no security-sensitive surface. The prior-round classifier gap for .mjs/.cts rows is fixed in this push, and the remaining inline finding is a nit about the test passing under USE_SYSTEM_BUN=1. Deferring rather than approving because the table encodes a product position (which early errors Bun is allowed to erase) that the author explicitly asks a maintainer to decide.

The test fails when Bun reports an error less than its line says, or
refuses a valid program. A change that reports more of these errors
passes, so a fix does not fail the test before its lines are moved.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code review found no issues

No high-confidence issues detected in this change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant