Skip to content

Remove cross-crate layering workarounds - #33152

Open
Jarred-Sumner wants to merge 12 commits into
mainfrom
claude/delete-layering-workarounds
Open

Jarred-Sumner wants to merge 12 commits into
mainfrom
claude/delete-layering-workarounds

Conversation

@Jarred-Sumner

@Jarred-Sumner Jarred-Sumner commented Jun 30, 2026 •

Copy link
Copy Markdown
Collaborator

Removes the cross-crate layering workarounds left over from the crate split. Four classes of code are deleted, each replaced by the one canonical definition or a direct call:

  • Legacy re-export shims and facades — path-stability pub use chains and module facades (crate::jsc, the sql-side jsc facade, bun_paths path-buffer re-exports, install's legacy re-exports, the bun_api shell crate, …) are gone; callers name the owning crate directly.
  • Mirrored types + conversion glue — duplicated structs/enums that existed only because a lower tier couldn't name the real type (bake Framework/BuiltInModule, the resolver-visible bundler options, the libuv UV_E* table, parser options mirrors, the JsError mirror variants of ToJSError, BufferedStdin, …) now have a single home; the From/field-copy glue between the copies is deleted.
  • Fn-pointer tables, installed vtables, and erased handles for upward calls — every cross-crate hook with a single concrete implementation is now a set of per-slot typed extern "Rust" functions resolved at link time, with bun_opaque-typed handles instead of *mut c_void / *mut (): the runtime hook table, the JS event-loop and event-loop-ctx vtables, the dev-server, macro, wake-handler, bundle-completion, plugin-resolver, printer-callback, OOM and duplex seams. Single-implementation trait objects (dyn MacroRunner, dyn JsCompletion, dyn PluginResolver, dyn AutoInstaller) are gone.
  • Kept-in-sync tables and shim crates — duplicated constant tables and single-purpose crates (bun_dispatch, bun_output, bun_transpiler, bun_errno, bun_sql_jsc, bun_api, …) are folded into their owners.

Three places intentionally keep a registration or accessor rather than a link call, each for a concrete reason: the pre-exit callback list (lower-tier test binaries can't resolve a symbol defined in bun_runtime), the standalone-graph accessor (a stored &'static cannot soundly produce the required &mut), and the Blob extension traits (static dispatch, not a pointer table).

Net diff: +32.7k / −42.2k lines (−9.5k). Most of the insertions are existing code landing at its canonical home (file moves/splits), not new code.

This is intended to be behavior-preserving. The deliberate, user-visible changes that fall out of deduplicating diverged copies:

  • Bun.inspect/console.log now quote object keys containing non-identifier, non-ASCII codepoints (the printer and lexer share one identifier predicate).
  • Formatted error/inspect output escapes control characters as \u{…} consistently.
  • bun build (bake) reports an out-of-memory failure as OutOfMemory instead of a generic JSError.
  • DNS option parsing reports the real failure (OOM/termination) instead of collapsing everything to a thrown error.
  • npm user-agent/bun pm whoami report the actual CI vendor name (previously hard-coded "ci"), and CI=false is honored.
  • Compile-target cache lookups honor BUN_INSTALL and XDG_CACHE_HOME like the rest of the install cache chain.
  • macOS os_signpost intervals are emitted again (they had become silent no-ops).

Verification on Linux x64 (debug):

  • cargo check --workspace for all supported target triples, cargo clippy --workspace (0 warnings), rustfmt/prettier clean, full bun bd build.
  • Test suites compared one-for-one against a freshly built origin/main binary at the same commit: install (incl. lifecycle scripts), bundler/compile (incl. bytecode), macros, transpiler, shell, spawn/IPC, timers + fake timers, node fs/process/child_process, fetch/Blob, streams, WebSocket (incl. permessage-deflate), Bun.serve, sqlite, sourcemaps, bake dev server, hot reload, bun run. Every remaining local failure reproduces identically with the origin/main binary in the same environment.

@robobun

robobun commented Jun 30, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 12:48 AM PT - Jul 7th, 2026

❌ @autofix-ci[bot], your commit 9f9f9b5 has 8 failures in Build #69531 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33152

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

bun-33152 --bun

@Jarred-Sumner
Jarred-Sumner marked this pull request as draft June 30, 2026 22:37
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/delete-layering-workarounds branch 2 times, most recently from c64e7a0 to e278f95 Compare July 1, 2026 05:30
@Jarred-Sumner
Jarred-Sumner force-pushed the claude/delete-layering-workarounds branch 2 times, most recently from c3813ef to 59b1313 Compare July 5, 2026 23:18
@Jarred-Sumner
Jarred-Sumner marked this pull request as ready for review July 6, 2026 06:11
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator Author

@claude review

@Jarred-Sumner
Jarred-Sumner force-pushed the claude/delete-layering-workarounds branch from ffe1992 to eae70e2 Compare July 6, 2026 06:34
Comment thread src/bun_core/fmt.rs
Comment thread src/ast/transpiler_cache.rs
Comment thread src/bun_core/string/mod.rs
Comment thread src/bun_core/fmt.rs
Comment on lines 1704 to 1710
if js_lexer::is_identifier_start(text[0] as i32) {
let mut i: usize = 1;

while i < text.len() && js_lexer::is_identifier_continue(text[i] as i32) {
while i < text.len() && js_lexer::is_identifier_part(text[i] as i32) {
i += 1;
}

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.

🟡 Rewiring the highlighter from the deleted fmt::js_lexer stub (which returned true for every byte > 0x7F) to crate::string::identifier::{is_identifier_start,is_identifier_part} changes the predicate to expect a Unicode codepoint, but the highlighter still feeds it raw UTF-8 bytes (text[i] as i32 at fmt.rs:1704/1707/2159/2162/2203). Continuation bytes 0x80..0xBF mostly map to non-ID_Continue Latin-1 codepoints, so an identifier like café (bytes …C3 A9) now splits mid-sequence — 0xC3→Ã (true) but 0xA9→© (false) — emitting caf\xC3 + ANSI reset + orphaned 0xA9. Cosmetic only (REPL / markdown fenced-code / bun:internal-for-testing highlightJavaScript, plus the recursive template-literal arm at fmt.rs:1884, all of which pass check_for_unhighlighted_write: false and so bypass the is_all_ascii guard); fix by restoring a local c > 0x7F ⇒ true byte-level fast path here, or decode via CodepointIterator before calling the predicate as FormatValidIdentifier (fmt.rs:1172) already does.

Extended reasoning...

What changed

The PR deletes bun_core::fmt::js_lexer — a local stub whose is_identifier_start/is_identifier_continue had a c > 0x7F fallthrough that returned true for every non-ASCII byte — and rewires QuickAndDirtyJavaScriptSyntaxHighlighter to crate::string::identifier::{is_identifier_start, is_identifier_part} (src/bun_core/string/identifier.rs:1-19). Those functions interpret their i32 argument as a Unicode codepoint: for 0x80..=0x10FFFE they call is_id_{start,continue}_es_next(cp) against the two-stage ID_Start/ID_Continue bitmap tables. But the highlighter still passes individual UTF-8 bytes — text[i] as i32 at fmt.rs:1704, 1707, 2159, 2162, 2203 — not decoded codepoints.

Why this mis-tokenizes multi-byte identifiers

UTF-8 continuation bytes are 0x80..=0xBF. Interpreted as codepoints, 0x80..=0x9F are C1 control characters (never ID_Continue) and 0xA0..=0xBF are mostly Latin-1 punctuation (NBSP, ¡, ¢, §, ©, «…¿); only 0xAA (ª), 0xB5 (µ), and 0xBA (º) are ID_Continue letters. So for essentially every multi-byte UTF-8 character in an identifier, the lead byte may pass (e.g. 0xC3 → U+00C3 Ã, category Lu → true) but the following continuation byte fails, and the identifier scan stops mid-sequence.

Step-by-step proof

Take const café = 1 and trace café = bytes [0x63, 0x61, 0x66, 0xC3, 0xA9] through the identifier arm at fmt.rs:1704-1707:

  1. text[0] = 0x63 (c): is_identifier_start(0x63) → true, enter the loop with i = 1.
  2. i=1,2: 0x61, 0x66 → ASCII letters → true.
  3. i=3: 0xC3 → is_identifier_part(0xC3) → not in 0x00..=0x7F, so is_id_continue_es_next(0xC3) → U+00C3 LATIN CAPITAL LETTER A WITH TILDE, category Lu → true. Loop continues.
  4. i=4: 0xA9 → is_identifier_part(0xA9) → is_id_continue_es_next(0xA9) → U+00A9 COPYRIGHT SIGN, category So → false. Loop exits with i = 4.
  5. The highlighter emits text[..4] = [0x63, 0x61, 0x66, 0xC3] — a torn UTF-8 sequence ending in a lone lead byte — via bstr::BStr::new(...) (lossy → caf�), followed by an ANSI reset (<r>).
  6. text is advanced to [0xA9, ...]. 0xA9 is not an identifier start and matches no punctuation arm, so it falls through to writer.write_char(c as char) (fmt.rs:2228), which widens the byte to U+00A9 and re-encodes it as UTF-8 C2 A9.

Net output: caf� + ANSI escape + © — garbled highlighting with a replacement character and a spurious © where é should be. With the old stub, both 0xC3 and 0xA9 returned true (> 0x7F), the whole 5-byte run was treated as one identifier, and the bytes were emitted verbatim as valid UTF-8.

Why the is_all_ascii guard does not prevent this

The guard at fmt.rs:1690 (if !strings::is_all_ascii(text) { return write_bytes(writer, text); }) is inside if self.opts.check_for_unhighlighted_write { … }. Four callers set check_for_unhighlighted_write: false and therefore reach the byte-indexed loop with non-ASCII input:

  • src/runtime/cli/repl.rs:1242 — REPL syntax highlighting
  • src/md/ansi_renderer.rs:1516 — markdown fenced-code rendering
  • src/jsc/fmt_jsc.rs:37/48 — the bun:internal-for-testing highlightJavaScript binding (exercised by highlighter.test.ts)
  • fmt.rs:1884 — the recursive ${…} template-literal arm (inherits the outer opts)

The default (check_for_unhighlighted_write: true) path — used by the error/log formatters — is unaffected because it bails out to plain write_bytes on any non-ASCII byte.

Impact

Cosmetic only: the REPL, markdown code-fence renderer, and highlightJavaScript test binding emit garbled output (torn UTF-8 + misplaced ANSI resets) for source containing non-ASCII identifiers. No effect on the transpiler, printer, or any code that ships to disk — FormatValidIdentifier (fmt.rs:1172) already decodes via CodepointIterator before calling these predicates and is unaffected. This is a real regression introduced by this PR (the old stub was a deliberate byte-level approximation), but the blast radius is display-only, hence nit.

Fix

Either restore a local byte-level fast path in the highlighter (e.g. wrap the call sites: c > 0x7F || js_lexer::is_identifier_part(c)), which matches the old stub's "quick and dirty" semantics; or decode WTF-8 codepoints via strings::CodepointIterator before calling is_identifier_*, as FormatValidIdentifier at fmt.rs:1172 already does. The former is a two-line change and preserves the highlighter's existing byte-indexed structure.

Jarred-Sumner and others added 12 commits July 7, 2026 01:02
Delete legacy re-export shims, mirrored type definitions, and
hook/vtable indirection left over from the crate split. Types move
to a single canonical home; upward call indirection becomes direct
calls or properly-tiered dependencies. No behavior change.
Declare the bindgen step's Rust output as an input of the cargo step so
rust-only builds generate it, and gate a debug-only import to debug builds.
The shim_standalone configuration compiles without the host feature, so
the read_without_launch payload and the debug_assert-only helpers are
conditionally dead there; gate them explicitly and drop an unused
feature attribute from the bin crate root.
Cross-crate hooks with a single concrete implementation now link directly:
per-slot extern "Rust" functions replace the fn-pointer tables, installed
vtable statics, and single-implementation trait objects, and typed opaque
handles replace the erased pointers they carried. Remaining re-export
facades, mirrored option/framework types, a shell crate, and duplicated
helpers are deleted, and a few types move to the crate that owns them.
Keep the top-level-dir bytes owned by a static instead of Box::leak (leak
checkers see them as reachable), cfg-gate the POSIX-only DNS hint helpers
instead of allowing dead code, and record the new per-file allow counts.
The generated bun:main wrapper moved from the runtime-state box onto the
VirtualMachine, but only the timer's reclaim followed; drop it in destroy
at the same point the old box died so worker VMs stop leaking it.
Point the rebased install code at the canonical type paths, and make the
never-lossy libuv return-code mapping retry an unmapped negative code as a
raw errno magnitude before collapsing to UNKNOWN, matching the previous
translation.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants