Trim ~2 MB from the release binary without touching hot paths - #39770
Conversation
- GlobalAlloc: diverge on failed small allocations so liballoc's per-site handle_alloc_error branch folds away - semver::String: branchless inline length instead of an unrolled scan - CallFrame/sys::Error/css ParserState: drop checked casts JSC/libc guarantee - opaque_deref: one shared cold null panic instead of per-site expect - host_fn: outline the Err arm of every host-call trampoline - comptime_string_map: only force-inline small maps; case-insensitive lookup reuses the exact-match tree - node_fallbacks: build the table from a const spec array - css: split parse_nested_block into non-generic enter/exit; avoid re-wrapping the result in parse_entirely - DefineEnumProperty: length-dispatched lowercase lookup instead of an eq_ignore_ascii_case chain per variant - run_tasks: type-erase the callbacks so the 2k-line body compiles once - Output::err / Log::add_error: keep the generic surface thin - C++: table-driven BunBuiltinNames, process.binding constants, node:constants, process.versions; generated classes visit/analyze cached fields from an offset table
…wer monomorphs
- comptime_string_map: maps over 64 keys look up via binary search over
sorted key data instead of an inlined compare tree
- css compat: Feature::is_compatible reads a per-feature min-version table
(generator updated to emit it)
- css colors: convert_via! conversions are no longer force-inlined
- MimeType: replace the lazily built 2310-entry runtime hash map with a
static set of common types; drop the ALL table
- spawn/spawnSync, coverage reporters, install_package_with_name_and_resolution
take runtime bools instead of const generics
- JestPrettyFormat routes every writer through one adapter type
- C++: createInternalModuleById reads a module table; shared invalid-this
throw for generated host functions; table-driven HTTP method lists and
process.binding('uv')
…y table - Generated lazy class structures live in one array with a shared initLater callback that dispatches through a creator table - Zig::GlobalObject visits its LazyProperty/LazyClassStructure/WriteBarrier members from an offset table - bun_md: pack the HTML entity table (names blob + offsets + codepoints) instead of 2125 pointer-carrying structs; bitset boundary checks - css Token is Copy; console typed-array printing shares one loop - clippy/fmt cleanups for earlier changes
… table, js2native, runtime source dedup - Shell completions and browser-only bundles (dev-server client runtime, error overlay/page, react-refresh, framework client.tsx) are embedded zstd-compressed in release and inflated on first use; nothing Bun itself executes or parses is compressed - jsFunctionMakeErrorWithCode: fixed-text-around-arguments messages come from a table instead of a switch case each - js2native wrappers share one out-of-line JSFunction::create - bundler runtime source: assemble the per-target variant once instead of embedding four concatenated copies of runtime.js
…er HTML passes lol_html's rewriter/tokenizer was instantiated once per sink closure type (three copies). Every Bun HtmlRewriter is now built with the same boxed sink type.
…handler de-duplication - subspaceForImpl: one out-of-line slow path keyed by subspace member offsets - Zig::GlobalObject lazy properties/class structures initialised from offset tables - specialObjectsDequal: per-type comparisons compiled once with a runtime mode - out-of-line putDirectNamed / createClassStructure / reifyStaticPropertyTable / allocatePlainObjectCell / setSubclassStructure / structureForNewTarget helpers - IsoSubspace tables hold owned pointers with a single looping destructor - JSStreamsRuntime internal structures as an array - CSS: SizeHandler::flush helper, margin/padding handler over a static spec, PropertyId::from_name_and_prefix via comptime_string_map, Property::parse value helper, single at-rule parser type for parse/parse_bundler - XML parser repr(C) so sink-independent methods fold
…ng parity, cold-path helpers - MimeType::by_name_static interns exactly the previous MIME set from a generated sorted table (mime_type_list.generate.ts + sync test) - comptime_string_map: #[inline] opt-in for the compare tree (Method::which), compare-tree maps stay inlined; only >64-key maps use the sorted table - keep visitChildren of generated classes inline (GC-hot); analyzeHeap table only - keep Map/Set deep-equals in the specialised template - revert the GlobalAlloc OOM fold (it pushed RawVec growth out of line) - lazy GlobalObject member tables tolerate a foreign owner global - HTMLRewriter/HTMLScanner share one lol_html sink enum with a statically dispatched buffer arm; JsCell moves to bun_ptr (re-exported from bun_jsc) - one inflated copy of bake.client.js (bundler accessor), no extra NUL copy - process.binding constants tables end with a sentinel (Windows dlopen table) - minsize on a few cold, large diagnostic/setup functions; table-driven ExposeNodeModuleGlobals / addBuiltinGlobals private functions / getConstructor - drop dead installer downcast trait methods, EmbedKind variants, stray file - tests: table-driven ERR_* messages, Blob type interning
No-Verification-Needed: lint inventory only
… baseline the SizeSpec table finding No-Verification-Needed: lint-only changes (clippy not_unsafe_ptr_arg_deref shape, mordant baseline, named locals at one call site)
|
Updated 4:50 PM PT - Aug 20th, 2026
✅ @Jarred-Sumner, your commit ddb8d72fab23534c8fdbc9f8a6cee18689a15d7c passed in 🧪 To try this PR locally: bunx bun-pr 39770That installs a local version of the PR into your bun-39770 --bun |
There was a problem hiding this comment.
I reviewed this PR and the automated bug-hunting pass found no issues. Given the scope — 300 files spanning GC iso-subspace ownership, offset-driven subspaceForImpl/lazy-class-structure initialization in ZigGlobalObject, generated-class codegen, and type-erased run_tasks with raw-pointer context casts — a human review is still warranted.
Checked: the new subspaceForImplSlow offset-based slot access preserves the destructible/custom heap-cell-type selection and output-constraint registration; the DOMIsoSubspaces raw-pointer table's looping destructor covers every slot; BundlerAtRuleParser with track_layers_and_imports=false matches the removed DefaultAtRuleParser no-op behaviour; ErasedCallbacks pointer casts are gated on the same IS_*_INSTALLER flags that previously selected the identity-cast trait methods.
Extended reasoning...
Overview
This PR reduces the release binary by ~2 MB via source-only de-monomorphization and table-ification across ~300 files: C++ JSC bindings (subspaceForImpl, ZigGlobalObject::finishCreation lazy initializers, generated-class codegen, Bun__deepEquals, BunBuiltinNames, ProcessBindingConstants, IsoSubspace ownership), Rust (comptime_string_map! sorted-blob lookup for large maps, CSS SizeHandler/is_compatible/PropertyId table-ification, run_tasks type erasure, PackageInstaller const-generic → runtime bools, single lol_html sink type, MIME/entity blob tables), and zstd-compressed embedding of browser-only assets.
Security risks
No new attack surface. Changes are internal codegen/compilation-shape refactors. The offset-based member access in BUN_SUBSPACE_SLOTS and GeneratedCachedField is bounded by compile-time OBJECT_OFFSETOF on known struct members. The ErasedCallbacks raw-pointer casts in run_tasks recover the same &mut C::Ctx that was erased at the single call site and are gated on the same const flags as before.
Level of scrutiny
High. This touches GC-critical paths: IsoSubspace allocation/ownership (now raw pointers freed by a loop over the struct reinterpreted as a pointer array — relies on DOMIsoSubspaces containing nothing but pointer members), the shared initLater callback that recovers its class index by pointer arithmetic against m_generatedLazyClasses, and subspaceForImpl's fast-path load. Each of these is a place where a subtle mismatch (a non-pointer field added to the subspace tables, a lazy-class accessor called with an owner from the wrong realm) would be a memory-safety bug. The PR description is thorough and the perf/behaviour verification is extensive, but the design choices (offset tables vs. per-class lambdas, raw-pointer subspace slots, __attribute__((minsize)) placement) deserve maintainer sign-off.
Other factors
No prior reviews on the timeline. New tests cover * messages and Blob MIME interning; the large-map comptime_string_map path has a unit test. The author documents two approaches that were tried and reverted for measurable regressions, which suggests careful validation. Nonetheless, 300 files across bindings/codegen/GC is well beyond the auto-approval threshold.
No-Verification-Needed: storage type only; cfg(bun_codegen_embed) arm checked
There was a problem hiding this comment.
I reviewed this PR and the automated bug-hunting pass didn't surface any issues. Given the scope — 300 files spanning JSC GC subspace ownership, generated-class initialization, ZigGlobalObject::finishCreation, type-erased run_tasks with new *mut () casts, IsoSubspace lifetime changes, and the JsCell crate move — a maintainer should still look this over before it lands.
Checked: the shared LazyClassStructure initializer's index recovery via pointer arithmetic and its defaultGlobalObject fallback; subspaceForImplSlow preserves the destructible/cell/custom heap-cell-type selection and output-constraint registration; the DOMIsoSubspaces looping destructor covers every slot; ErasedCallbacks cast targets match the two IS_*_INSTALLER flags; BundlerAtRuleParser with track_layers_and_imports=false never dereferences the null import_records pointer.
Extended reasoning...
Overview
This PR is a broad binary-size reduction touching ~300 files across nearly every subsystem: JSC C++ bindings (subspace allocation, ZigGlobalObject::finishCreation, generated classes, IsoSubspace ownership), Rust code generation (comptime_string_map!, CSS property handlers, run_tasks type erasure), embedded assets (zstd-compressed browser bundles), and dozens of mechanical putDirect → putDirectNamed / Structure::create → createClassStructure rewrites. The stated goal is to compile shared logic once instead of N times and replace code with data tables, without changing hot-path behaviour.
Security risks
No new attack surface is introduced — this is internal refactoring. The main risk category is memory safety rather than security per se: the IsoSubspace tables now hold raw owned pointers freed by a size-based loop in a hand-written destructor; run_tasks erases &mut C::Ctx to *mut () and casts back via is_package_installer/is_store_installer flags; the shared LazyClassStructure initializer recovers its array index by subtracting a member pointer from the global's array base. None of these looked wrong on inspection, but they are exactly the kind of change where a subtle mismatch (a non-pointer field added to a subspace table, a callbacks flag set on the wrong impl) would be a UAF or wild write.
Level of scrutiny
High. This is neither small nor mechanical: it changes GC subspace lifetime management, rewrites how ~160 wrapper classes get their structures/prototypes/constructors initialized, moves JsCell between crates, introduces new unsafe blocks in the installer, and adds __attribute__((minsize)) to functions across crypto, WebSocket, worker construction, and error formatting. The PR description is thorough and the perf/behaviour verification is extensive, but the blast radius covers essentially every JS-visible class in the runtime.
Other factors
The bug-hunting system found nothing, and the diff is internally consistent where I spot-checked (the BundlerAtRuleParser unification guards every layer/import hook on track_layers_and_imports so the null import_records pointer in the plain parse path is never dereferenced; subspaceForImplSlow preserves the outputConstraintSpaces append; the deleteSubspaceTable static_assert enforces the all-pointers layout it depends on). But per the approval guidelines this is squarely in "large, touches critical code paths, contains design decisions a maintainer should weigh in on" territory — the choice to hold IsoSubspaces as raw pointers with a reinterpret-cast destructor loop, and the pointer-arithmetic index recovery in the shared lazy-class initializer, are patterns a human reviewer should explicitly sign off on.
…e construction (#39833) Follow-up to #39770, which moved the IsoSubspace creation path out of `WebCore::subspaceForImpl` into one out-of-line `subspaceForImplSlow`. #39503 did the same thing and is closed as superseded. These are the two pieces of it that still apply. ### Problem - `.claude/skills/implementing-jsc-classes-cpp/SKILL.md:30-35` still shows the four-lambda `subspaceForImpl` call. That signature no longer exists, so the snippet does not compile when copied. - Nothing stops a new class, or a codegen template, from constructing its own `IsoSubspace` again. The compiler does not enforce that invariant, and a template that does it pays for it once per generated class. ### Fix - The skill snippet now shows the `BUN_SUBSPACE_SLOTS` form that every caller on main uses (for example `src/jsc/bindings/AsyncContextFrame.h:39`), and says where the subspaces are created. - `test/internal/source-lints/iso-subspace-creation.test.ts` scans `src/**/*.{h,cpp}` and `src/codegen/**/*.ts` for `ISO_SUBSPACE_INIT`, `makeUnique<IsoSubspace>` or `new IsoSubspace` outside `src/jsc/bindings/BunClientData.cpp`. It also asserts that file still constructs one, so the lint cannot pass on an empty scan. - `source-lints.yml` now also triggers on `src/**/*.h`, `src/**/*.cpp` and `src/codegen/**`, the files this lint reads (the README in that directory asks for this). `src/codegen/**` replaces the narrower `src/codegen/class-definitions.ts` entry. - Verified: the lint passes on main (`bun bd test` and the released bun). It fails on the tree before #39770, listing the four constructions in `BunClientData.h`, and it fails when a construction is added to a header or to `generate-jssink.ts`, naming the file and line. ### Background - Every JS class with C++ fields has a `subspaceFor<T>` hook that JSC calls on each allocation of `T`. The creation of the subspace behind it (lock, construct, register, fill the two slots) is about 640 bytes of code and runs once per type. Inlined per class it cost about 160 KB of the linux-x64 binary, which is what #39770 removed. - `test/internal/source-lints/` holds grep-style lints over `src/`. They run on GitHub Actions against a released bun and are excluded from the Buildkite shards, so they only run on PRs that touch the paths listed in the workflow. <!-- robobun:evidence:begin --> --- **[stamp-90s]** gate passed · iteration 1 · 3 files touched <details><summary>passes on PR (with fix)</summary> ```console Test-only change. Debug/ASAN (expected pass): $ bun bd test 'test/internal/source-lints/iso-subspace-creation.test.ts' $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/internal/source-lints/iso-subspace-creation.test.ts bun test v1.4.0 (6e906e4) test/internal/source-lints/iso-subspace-creation.test.ts: (pass) IsoSubspaces are only constructed in BunClientData.cpp [1035.72ms] 1 pass 0 fail 3 expect() calls Ran 1 test across 1 file. [4.46s] Exit: 0 ``` </details> <details><summary>diff hotspot</summary> ``` .../skills/implementing-jsc-classes-cpp/SKILL.md | 8 +-- .github/workflows/source-lints.yml | 8 ++- .../source-lints/iso-subspace-creation.test.ts | 67 ++++++++++++++++++++++ 3 files changed, 76 insertions(+), 7 deletions(-) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 1 <details><summary>evidence per changed file</summary> ``` file reads edits tests .claude/skills/implementing-jsc-classes-cpp/SKILL.md 2 2 0 .github/workflows/source-lints.yml 1 1 0 test/internal/source-lints/iso-subspace-creation.test.ts 4 8 0 ``` </details> <!-- robobun:evidence:end -->
#39795 removed four Default impls this branch had converted (Query, ClassStaticBlock, NamedImport and the MetadataResolve one), so they are gone. #39770 split add_error, add_range_error_with_notes and range_data into an inline generic wrapper and one shared body; here the wrapper also does the Option<Loc> / Option<Range> conversion and the body takes the Option. Four diagnostics main added without a location (no bundlable entry point, external entry point, and the two builtin / browser-field entry point errors) pass None. main itself does not build at this point (PackageInstaller.rs:1359, #39816 against #39770); that is tracked separately and not touched here.
…uilt-in JS, bindgen, uSockets, and 66 Cargo manifests (#39732) ### Problem - The tree carries code that nothing references: FFI shims with no caller on either side, enum variants that are never constructed, write-only fields, exports that no module imports, and Cargo dependency edges that no source file uses. - 16 dead-code PRs that removed much of this were closed as stale after merge conflicts. Their deletions were never re-applied (list in the notes). ### Fix - Re-apply the deletions that still apply to current main and add new ones in the same areas: `simdutf_sys`, `ncrypto` and the WebCore bindings, `js_parser`, `bundler`, `js_printer`, `bun_install`, built-in JS, bindgen, uSockets, and 66 Cargo manifests. 176 files, +95 / -3129 (`Cargo.lock` is -436 of that). - Every deletion was checked again with `rg` over `src/`, `packages/`, `src/codegen/` and the generated code. Items that became live again since the old PRs stay (examples in the notes). - No deletion duplicates an open dead-code PR. Deletions that need #39618 or #39697 to land first are listed as follow-ups in the notes. - Verified: `bun bd`, `bun run rust:check-all` (12 targets pass), byte-identical bindgen output, `test/internal/source-lints`, and the test files of the touched areas (list in the notes). New tests in `transpiler.test.js` and `bundler_edgecase.test.ts` pin the two diagnostics that `mark_strict_mode_feature` still emits and the class body printing that `visit_class` still does. ### Background - A Rust `extern "C"` declaration with no call site creates no link reference, so a dead FFI chain is removed on both sides at once. Three C++ definitions whose Rust side #39618 removes stay until that PR lands. - `cargo check` covers the host target only. `rust:check-all` repeats it for every shipped target, which proves the `#[cfg]`-gated deletions and the Windows-only dependency removal (`bun_sys` -> `bun_output`). - The dependency removals are manifest-only. `Cargo.lock` loses the matching entries and nothing else: no version changes. <details><summary>Notes</summary> **Closed PRs re-applied**: #35437, #35559, #35775, #35880, #36115, #36237, #37012, #37062, #37089, #37208, #37272, #37454, #37788, #38005, #38439, #38703. **Kept because they are live again on main**: `GenericIndexOptional::{get, is_some, is_none}`, the WebSocket deflate `OutOfMemory` variants, `V8Local::reinterpret`, `SystemErrno::MAX`, `MarkPopErrorOnReturn::peekError`, `ResourceTiming::populateServerTiming`, `UvHandle` in `test/parallel/Channel.rs`, the `h2::FrameType` entries that `hawk.toml` marks as a code table. **Tests run**: node-http, node-http2, fetch headers, streams, readable-stream-blob-consumed, filesink, transpiler, buffer, url, FormData, TextEncoder, MessageChannel, serve-direct-readable-stream, serve-body-leak, crypto key objects, crypto-rsa, scrypt, pbkdf2, sqlite-sql, local-sql, postgres-simple-query-pipeline, sql-helpers-validation, bun-outdated, websocket-server, test/internal (bindgen, codegen outputs, source lints). The `localhost` proxy test in node-http and the concurrent WebSocket send tests fail the same way on unmodified main in this environment (the first is a `localhost` resolution issue, the second is the 300k-message benchmark starving its concurrent neighbours in a debug build). **By kind**: C/C++ -1327, Rust -702 (+75, mostly signature updates at call sites), built-in JS/TS -130, codegen TS -75 (+13), Cargo manifests -459, `Cargo.lock` -436. **simdutf_sys** (`simdutf.rs`, `bun-simdutf.cpp`, `parsers/benches/support/simdutf_shim.cpp`): 16 shim chains with no caller, each removed as Rust wrapper + `extern` declaration + C++ definition: `simdutf__convert_utf8_to_utf16le`, `_utf16be`, `_utf16be_with_errors`, `convert_utf8_to_utf32_with_errors`, `convert_valid_utf8_to_utf32`, `convert_utf16be_to_utf8_with_errors`, `convert_valid_utf16be_to_utf8`, `convert_utf32_to_utf8_with_errors`, `convert_valid_utf32_to_utf8`, `convert_utf32_to_utf16be_with_errors`, `convert_valid_utf32_to_utf16be`, `convert_utf16be_to_utf32_with_errors`, `convert_valid_utf16be_to_utf32`, `utf8_length_from_utf16be`, `utf32_length_from_utf16be`, `utf32_length_from_utf8`, plus the now empty `utf32` modules and the `be` wrappers. **ncrypto** (`ncrypto.h/.cpp`): `BignumPointer::isOne`, `X509View::ifRsa`, `X509View::ifEc`, `BIOPointer::NewFp`, `checkScryptParams`, `scrypt`, `pbkdf2`, `EVPKeyCtxPointer::setRsaMgf1Md`, `Rsa::encrypt`, `Rsa::decrypt` and the `RSA_Cipher` template, `Cipher::ForEach` with `CipherCallbackContext`/`array_push_back`, `NCRYPTO_REQUIRE`, `NCRYPTO_VERSION` and the version enum. **JSC / WebCore bindings**: `ZigGlobalObject`: `functionFulfillModuleSync` (and the `fulfillModuleSync` builtin name plus `$fulfillModuleSync` stub), `JSDOMFileConstructor_getter/_setter`, `navigatorObject`, `functionLazyNavigatorGetter`, `GlobalObject_getPerformanceObject`, `hasNapiFinalizers`, `jsFunctionNotImplemented`, `jsFunctionCreateFunctionThatMasqueradesAsUndefined`, `Zig__GlobalObject__getModuleRegistryMap`/`resetModuleRegistryMap`, `NodeVM*ModulePrototype()` accessors, `ZIG_GLOBAL_OBJECT_DEFINED`. `BunString.cpp`: `Bun__WTFStringImpl__ref`/`deref` definitions (Rust inlines these; #39618 removes the declarations). `JSBuffer.cpp`: the `JSValue`-name `validateOffset` overload and the three unused `jsBufferConstructorAlloc*WithoutTypeChecks` JIT operations. `NodeValidator`: `validateString(JSValue name)` and `validateOneOf(span<ASCIILiteral>)`. `ScriptExecutionContext`: `ensureOnMainThread`, `executionContext`. `napi.h`: `hasFinalizers`, `currentFinalizer`, `isVMTerminating`. `IDLTypes.h`: `IDLDate`, the `NullableTypeWithLessPadding` helpers and two includes. `BunProcess.cpp`: three unused `*CodeGenerator` aliases. `c-bindings.cpp`: `HNS_PER_SEC`, `NS_PER_HNS`, `HNS_PER_US`. `BunCommonStrings.h`: `ConnectionWasClosed`, `ec`, `ed25519`, `rsa`, `rsaPss`, `jwkDsa`, `jwkG`, `systemError`, `x25519`. `BakeAdditionsToGlobalObject.h`: the never-read `m_bakeGetAsyncLocalStorage` lazy property (the function is still installed directly) and the `LazyPropertyOfGlobalObject` alias. `JSBundlerPlugin.cpp`: the `JSBundlerPlugin__onVirtualModulePlugin` declaration, which has no definition. WebCore: `DeferredPromise::whenSettled` and the `PromiseFunction`/`BindingPromiseFunction` adapters, `JSEventListener::sourceURL/sourcePosition`, `Event::receivedTarget`, `toJS(PerformanceObserverCallback)` and `callbackData()`, `jsFetchHeaders_getRawKeys` (its only caller in `internal/http.ts` is removed too), stale forward declarations in `Performance.h`/`ResourceTiming.h`, and the commented-out `BINDING_INTEGRITY` blocks in 8 generated-style files. `node/crypto`: `JSPrivateKeyObjectConstructor` and `JSPublicKeyObjectConstructor` (4 files, superseded by `JSKeyObjectConstructor`). Bake: `BakeRegisterProductionChunk`, `BakeProdSourceMap`, `BakeProduction.h`, the `IncrementalGraph` log scope. **uSockets**: `us_poll_ext`, `us_loop_iteration_number`, `us_socket_is_tls`, `us_connecting_socket_get_loop`, `us_udp_packet_buffer_local_ip` / `bsd_udp_packet_buffer_local_ip`. **Rust**: `js_parser`: the six `StrictModeFeature` variants that are never constructed (and the `can_be_transformed` branch), `FnOnlyDataVisit::{class_name_ref, should_replace_this_with_class_name_ref, is_inside_async_arrow_fn}` with the `this` substitution path that was gated on the always-false flag (the `shadow_ref` arena cell becomes a plain `Ref`). `bundler`: `Linker::{resolver, hashed_filenames}`, `IS_CACHE_ENABLED`, `InputFileFlags::IS_PLUGIN_FILE`, `parse_task::Step::ReadFile`. `js_printer`: the write-only `Options::transform_only`. `bun_install`: `CacheBehavior`/`ManifestLoad` (every caller passed `LoadFromMemoryFallbackToDisk`, so the parameter and the memory-only branch are gone from `by_name`, `by_name_hash` and `by_name_hash_allow_expired`), `pub use patch_install as patch`. `webcore`: `ReadableStream::detach_if_possible` (empty) and the `global` parameter of `done()`, `BlobExt::{on_structured_clone_transfer, get_mime_type}`, six `StartTag` variants that no sink uses. `server`: `AnyRoute::ref_`, the write-only `OPENED_BIT`. `bun_core`: `concat`, `ExternalShared::as_ptr`, `QuoteEscapeFormatFlags::ascii_only`. `bun_io`: stale `Waker`/`Closer` re-exports. `bun_sys`: `UTIME_OMIT`. `cli`: the never-read `IS_MAIN_THREAD` thread local. `css`: `DeclarationContext::Keyframes`, the empty `generated_color_conversions` module. `html_rewriter`: the `EndTag.replace` host function that `html_rewriter.classes.ts` does not expose. **Built-in JS/TS**: `internal/http.ts`: 29 unused symbol constants, `filterEnvForProxies`, `getRawKeys`, `emitCloseNTAndComplete`, `ClientRequestEmitState`. `node/http2.ts`: `kSettingNames`, three unused primordials. `internal/sql/query.ts` and `internal/repl/node-shims.js`: export entries nothing imports, and the `BuiltinModule` shim methods nothing calls. `builtins.d.ts`: 8 stubs for builtin names that no longer exist. **bindgen** (`src/codegen/bindgen*.ts`): `allFunctions`, `ArgStrategyChildItem`, `Variant.argStruct`, `Struct.namespace`/`toString`, `FuncMetadata`/`exposedOn`/`ExposedOn`, `FuncWithoutOverloads`, the dead `debug` binding, two shadowed duplicate `case` labels and an unreachable `return`. Generated output is byte-identical. **Cargo**: 459 dependency lines across 66 manifests (mostly the `strum`/`bstr`/`scopeguard`/`const_format`/`enum-map`/`enumset`/`libc`/`bitflags` boilerplate block, plus 94 `bun_*` edges such as `bun_jsc -> bun_simdutf_sys` and `bun_bundler_jsc -> 8 crates`). One dev-dependency (`bun_router -> bun_js_parser`, checked with `cargo check -p bun_router --tests`). The manifests that #39618 and #39697 already edit (`collections`, `io`, `paths`, `css`, `shell_parser`, `sql`, `sql_jsc`) were left alone. **Rebase note**: main restructured the private builtin function registration in `ZigGlobalObject::addBuiltinGlobals` into a table (#39770). The conflict was resolved by dropping the `k_fulfillModuleSync` row from the new table, which is the same registration the first version of this PR removed. #39770 also touched the two `JS*KeyObjectConstructor.h` files before this PR deletes them; they are still unreferenced on main, so the deletion stands. Re-verified after the rebase: `bun bd`, `rust:check-all` 12/12, and the test files listed above. **Follow-ups once open PRs land** (not done here to avoid duplicating them): after #39618: the C++ definitions of `URL__fromJS` (BunString.cpp) and `Bun__allocUint8ArrayForCopy` (ZigGlobalObject.cpp), the seven `<Sink>()` constructor accessors in `ZigGlobalObject.h` that only the generated `__getter` functions use, the Rust `extern` declarations of `Bun__WTFStringImpl__ref/deref`, and `BufferWriter::append_null_byte` (no writer ever sets it to true). After #39697: the root `[workspace.dependencies] typed-arena` entry. Independently of those: the `DeferredPromise::{promise, resolve(), reject(...)}` overloads and `DOMPromise::whenPromiseIsSettled` have no callers but sit next to code #39618 edits. </details> <!-- robobun:evidence:begin --> --- **no test proof** · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bundler_edgecase.test.ts <!-- robobun:evidence:end -->
…-physical Conflict in src/css/properties/margin_padding.rs: #39770 moved the SizeHandler body into the non-generic SizeHandlerImpl that reads a SizeSpec table. The changes from this branch are ported onto it (S::X becomes spec.x, spec is passed to compiles_logical, flush_before_unparsed and flush_compiled_inline). No logic change.
What does this PR do?
Source-only changes (no compiler / linker / profile flags) that make the release
bunbinary ~2 MB smaller with no measurable change to throughput, startup, or memory.bun run build:release, strippedbun)--profile=btg, i.e. the codegen CI ships)How
Nothing here changes an algorithm on a hot path; it is almost entirely "compile this once instead of N times" and "make this a table instead of code":
C++ bindings
subspaceForImpl<T>: the create-on-first-use slow path is one out-of-line function keyed by the two subspace member offsets (BUN_SUBSPACE_SLOTS), instead of a full instantiation per wrapper class (~160 classes). The inline fast path is unchanged.Zig::GlobalObject::finishCreation: the 121LazyProperty/LazyClassStructureinitLaterlambdas become four offset tables with one shared initializer each (onecallFuncinstantiation per table instead of per member). Generated lazy class structures likewise share one initializer via an array. The shared initializer tolerates being reached through a different realm's global (defaultGlobalObject).generate-classes.ts): sharedstructureForNewTargetfor thenew.targetsubclass path, sharedthrowInvalidThisCallError,analyzeHeapdriven by an offset/name table (visitChildren stays inline — it is GC-hot),X__getConstructorvia one helper, prototypes allocated through oneallocatePlainObjectCell.Bun::createClassStructure(Structure::create),Bun::reifyStaticPropertyTable,Bun::putToStringTagWithoutTransition,JSDOMConstructorBase::initializeBaseProperties,WebCore::setSubclassStructure,Bun::putDirectNamed(only used in cold object builders: process.report/config/versions, X509 legacy object, perf timing toJSON, …).Bun__deepEquals: the per-type special-object comparisons (typed arrays, Date, RegExp, Error, DOM wrappers, boxed primitives, …) are compiled once and take the mode at runtime; Map/Set and the plain-object tail stay in the specialised template soexpect().toEqualon collections is unchanged.process.binding('constants')/node:constants(~850 numeric constants),process.versions, uv errname map, HTTP parser method lists,BunBuiltinNames(array + loop instead of a member per identifier), the 38 fixed-template$ERR_*messages,createInternalModuleById,ExposeNodeModuleGlobals,addBuiltinGlobalsprivate functions.unique_ptrdestructors ×2.JSStreamsRuntimeinternal structures as an array with one initializer.__attribute__((minsize)): CPU/heap profile report generation,process.report.getReport(),process.config,process.dlopen/execveargument handling,$ERR_*construction, X509 legacy object,WebSocket::connect,MIMEParamsparsing, jwk key import, RSA keygen option parsing,new Worker()option parsing,Bun.WebViewconstruction,mock.module, uncaught-error formatting.Rust
comptime_string_map!: maps over 64 keys (file extensions, HTML entities, CSS property names, encoding labels, named colours, …) store sorted key data and binary-search per length bucket instead of emitting a compare tree;#[inline]on a map opts back into the tree (used forMethod::which).SizeHandler::flushhelper instead of an 18-way macro expansion (40 KB → 1 KB); margin/padding/inset/scroll-margin handlers share one body over a static spec table;PropertyId::from_name_and_prefixwas a 248-wayeql_case_insensitivechain and is now a length-bucketed table (smaller and faster);Property::parsearms go through oneparse_value::<T>per value type;parse_nested_blockprologue/epilogue out of line; the parse/parse_bundler at-rule parsers are one type;is_compatibleis a[[u32; 9]; 215]table generated bybuild-prefixes.js;TokenisCopy.bun installrun_tasksis compiled once behind a small callbacks table instead of once per installer type;PackageInstallerconst-generic bools that only gate cold branches are runtime bools. Same forBun.spawn/spawnSyncglue andbun test --coveragereporters.JestPrettyFormat(snapshot serializer) is instantiated for one writer type.HTMLRewriterand the bundler's HTML scanner (theHTMLRewriterarm is statically dispatched).repr(C)field order so the sink-independent DTD methods are byte-identical across sink types and fold under ICF.Entitystructs); MIMEby_nameuses a sorted name blob (generated bymime_type_list.generate.tsfrommime_type_list.txt) instead of building a 2300-entry hash map at runtime.CallFrame::arguments_count,opaque_deref,sys::Error::from_code_int), cold error arms out of line inhost_fn,Log::add_error*,output::err.bake.client.js,bake.error.js,bun-error,client.tsx,react-refresh.js) — are embedded zstd-compressed and inflated once on first use (bun_zstd::embed_compressed!). Everything Bun runs or parses at startup (builtin modules,bake.server.js, runtime.js, …) is untouched.How did you verify your code works?
css.test.ts1181 tests, workers, streams, expect/jest-extended, node assert, url/urlpattern, x509, vm, serve, node-http, sqlite, napi, plugin, process, shell, install lifecycle scripts, html rewriter, bake, bundler css/minify); outputs compared byte-for-byte againstmainforbun buildof typescript.js and several large stylesheets,bun completions,$ERR_*messages, MIME/extension lookups, markdown entities,process.report, constructorname/length/toStringTagof the Web globals. New tests:test/js/node/errors/error-code-messages.test.ts, Blob type-interning cases intest/js/web/fetch/blob.test.ts.perf stat -e instructions:u, concurrent GC/JIT off for determinism) vsmainon the same machine are within ±0.5% for: startup (-e 1,--version, a small script),new Blob/Event/Request/Headers,Bun.deepEquals(plain/strict/Map),Bun.color,URLPattern.exec,requireof 40 builtins, fs stat/read, bun:sqlite, JSON, Buffer, TextDecoder,$shell, Transpiler, resolve, URL,Bun.inspect, HTMLRewriter (get/set/removeAttribute handlers), full-GC with 100k live wrappers, Bun.serve+fetch loop, spawnSync, test runner (toEqual + snapshots),bun build typescript.js --minify; CSS minification of bootstrap/tailwind is 1–2% fewer instructions.visitChildrenfor generated classes (+~50 instructions per wrapper per GC) and diverging on OOM insideGlobalAlloc::alloc(pushedRawVecgrowth out of line at every allocation site).Notes for reviewers
mime_type_list.txtdrops 9 entries that were never in the interned table (by_nameparity withmainis exact; the new blob test pins a few in/out-of-set cases).JsCellmoved frombun_jsctobun_ptr(re-exported, no call-site changes) so the bundler-level HTML sink can name it; the twoJsCell<JsRef>forwarding accessors became theJsCellRefExttrait.bake.client.js(~100 KB) once on first use instead of pointing at.rodata; RSS-neutral (one heap copy replaces the old NUL-terminated copy).