Skip to content

Remove dead code from react_compiler, the WebCore/IDL bindings, install_jsc, node-fallbacks, and misc crates - #39420

Merged
Jarred-Sumner merged 2 commits into
mainfrom
farm/086d5366/dead-code-react-webcore-install-jsc
Aug 17, 2026
Merged

Jarred-Sumner merged 2 commits into
mainfrom
farm/086d5366/dead-code-react-webcore-install-jsc

Conversation

@robobun

@robobun robobun commented Aug 17, 2026 •

Copy link
Copy Markdown
Collaborator

Net -4,402 lines (172 files, +509 / -4,911). Everything removed has zero references across src/, scripts/, test/, packages/ and the regenerated build/debug/codegen/ output, and the removal builds: removing a Rust or C++ definition that still had a caller fails to compile or link, so a green build is the reference check for those; JS, codegen and manifest removals were additionally grepped by name (including bun:internal-for-testing consumers under test/).

Most of these were first identified by the sweeps that were closed yesterday for merge conflicts (#37272, #37062, #38439, #37089, #38703). This PR re-applies the subset that still applies on current main, minus anything an open PR already deletes and minus small hunks in files that change daily (see "Left out" below), plus a few new finds.

react_compiler (-1,166)

  • validate_no_derived_computations_in_effects_exp and its ~22 exclusive helpers/types (~1.1k lines in validation/validate_no_derived_computations_in_effects.rs). The pipeline only ever calls the non-_exp validation; the _exp env-config flag was parsed from fixture pragmas and read by nothing. react-compiler-fixtures.test.ts now lists the two pragmas as ignored instead of handled; the fixture suite still passes.
  • SymbolHost (back-compat alias of Host; DESIGN.md updated), HirBox, is_use_state_type, default_true.

C++ bindings (-1,350 across 64 files)

  • JSDOMConvertBufferSource.h: the IDL typed-array specializations and toPossiblyShared*Array helpers for every element type no binding converts (only the Uint8Array/ArrayBuffer views are used).
  • JSDOMPromiseDeferred.h/.cpp: resolveWithJSValue, resolveWithNewlyCreated, resolveCallbackValueWithNewlyCreated and friends.
  • IDLTypes.h: the IDLUnsupportedType family, the IDB / WebGL / ScheduledAction wrappers and stale forward declarations; JSDOMConvertDate.h/.cpp (the only IDLDate converter) deleted along with its two includes. The 8-line IDLDate struct itself stays for now (see "Left out"). JSDOMConvertScheduledAction.h, orphaned in the same way, is already deleted by Remove dead code from C++ webcore headers, src/js, and pretty_format #35775, so it is not touched here.
  • NetworkLoadMetrics.h: WebKit networking-stack fields/accessors bun never reads.
  • Deleted files: JSWorkerOptions.h/.cpp (JSWorker.cpp builds WorkerOptions by hand), JSMIMEBindings.h/.cpp (createMIMEBinding had no callers; include dropped from ZigGlobalObject.cpp), JSDOMIterator.cpp (addValueIterableMethods).
  • ncrypto.h/.cpp: peekError, BignumPointer::isZero, EVPKeyCtxPointer::sign, unused copy/move operator= overloads and an AsymmetricKeyEncodingConfig constructor.
  • Smaller: JSDOMOperationReturningPromise.h (call*ReturningOwnPromise), JSDOMAttribute.h (setPassingPropertyName, setStatic), JSDOMGuardedObject (DoNotRegisterWithGlobalObjectTag), Event/EventTarget (resetBeforeDispatch, default-handled flags, isNode()), EventEmitter::{eventTypes,eventListeners}, HTTPHeaderIdentifiers::identifierFor, JSURLPatternResult convertDictionary stubs, PerformanceTiming::monotonicTimeToIntegerMilliseconds, ContextDestructionObserver::protectedScriptExecutionContext, expectedEnumerationValues<CryptoKeyUsage>, BufferSource::mutableData/toBufferSource, BunString__toInt32, BunString::utf8ByteLength, the Ref<StringImpl> overload of toCrossThreadShareable, normalWorld(), TextEncoding(const String&), JSBuffer createBuffer/constructFromEncoding overloads, the JSX509Certificate m_infoAccess lazy property and the non-legacy branch of computeInfoAccess (the prototype getter reads the view directly; x509 tests pass), BakeAdditionsToGlobalObject::wrapComponent, jsFunction_lsanDoLeakCheck, and the dead BunObject+exports.h macro entries (together with the Rust BunObject_callback_nanoseconds export the nanoseconds entry declared; Bun.nanoseconds is functionBunNanoseconds in BunObject.cpp).

install_jsc / install_types / bun:internal-for-testing (-344)

  • install_jsc/dependency_jsc.rs and update_request_jsc.rs deleted: their only consumers were the npa / npmTag exports of bun:internal-for-testing, which no test imports. The dispatch_js2native.rs re-exports, the generate-js2native.ts file-map entry and the lsanDoLeakCheck export (tests use isASANEnabled) go with them.
  • install_types/lib.rs: the ExternalString / SlicedString / SemverString re-export modules; nothing names those paths (everything imports the types from bun_semver). New in this PR.

node-fallbacks (-400) and codegen (-392)

  • util.js: a ~270 line commented-out util.types block.
  • package.json / bun.lock / tsconfig.json: dependencies and path mappings nothing imports (esbuild, buffer, events, util, url, process, path-browserify, os-browserify, timers-browserify, tty-browserify, vm-browserify, ...). build-fallbacks.ts marks every builtin name external, and the remaining sources only import the packages still listed; bun install --frozen-lockfile in the directory is a no-op and bundler_browser.test.ts passes.
  • src/codegen/generate-unified-source-bundles.rb: WebKit's Ruby generator, superseded by scripts/build/unified.ts; nothing invokes it.

Rust crates (-1,200 across bun_jsc, bun_runtime, css, uws_sys and leaf crates)

  • bun_jsc: BuiltinName::get + BUILTIN_NAME_MAP, MarkedArrayBuffer::to_js (the ArrayBuffer::alloc Uint8Array arm and its Bun__allocUint8ArrayForCopy binding are kept, so alloc keeps mirroring create), the __dangerouslySetPtr wrapper js_class_module! emitted into every class, ErrorBuilder::new, TopExceptionScope::new, Task::new, JSCell::to_js, JSGlobalObject::{to_js,ref_,ctx}, job::{on_js_thread,off_thread}, AbortReason impl, JSPromise settle helpers, UUID::ZERO, TagPayload::get.
  • bun_runtime: the target_os = "wasi" directory-iterator backend in dir_iterator.rs (no shipped target is wasi and the bun_sys::wasi module it imports does not exist), the Display impls in assert/myers_diff.rs, the non-unix stub and not-macos escapes in fs_events.rs (the file is only compiled on macOS), ArrayBufferSink::to_js, unused re-exports in node.rs / api/bun/spawn.rs / ffi/mod.rs (with abi_type formatters narrowed to pub(crate)), Error::UnableToDecode, MyersDiff::Error::OutOfMemory. The first three are new in this PR.
  • css: the inherent eql / to_css / parse forwarders whose callers all go through the CssEql / ToCss / Parse trait impls (values/calc.rs and friends), generics::{implement_eql,parse}, TokenList::parse_with_options, CssString::parse.
  • uws_sys: the uws_loop_defer, uws_res_clear_corked_socket, uws_ws_iterate_topics and uws_h3_req_get_parameter C shims plus their Rust declarations and wrappers (Loop::{uncork,wake,next_tick,run}), AnyResponse::init, SocketGroup::is_empty, socket.rs group() accessors and the SocketTcp/SocketTls aliases, Opcode::Close, WindowsLoop.
  • leaf crates: windows_sys constants and their bun_sys::windows re-exports, zlib/zlib_sys declarations (deflateInit_, inflateInit_, the gz* file API, legacy type aliases), sha_hmac deprecated-API hashers (SHA512 raw, RIPEMD160, MD5_SHA1, Blake2 evp), wyhash HashInt impls for u16/u64, libarchive commented-out Zig-era callbacks, bun_alloc (AllocError::name, usable_size, BSSList::init), clap::Error::WriteFailed, csrf error variants, pe::Error::{InputIsSigned,InsufficientSpace}, md Setextheader, opaque_mut_nn, cares_sys AddrInfo_hints::is_empty, boringssl_sys constants, errno Mode re-exports, bounded_array::get, string::write::Result, SplitIterator::rest, OutOfRangeValue impls, symbol::Map::init, sql_jsc re-exports.

src/js (-32)

  • Unused REPL primordials entries in internal/repl/node-primordials.js; with that gone SafeWeakSet had no importer, so internal/primordials.js stops exporting it (new in this PR). Unused export-object entries in internal/fs/watch.ts and internal/readline/interface.js.

scripts (-9)

  • glob-sources.ts src/*.c pattern (matched nothing since asan-config.c was deleted), the write-only BUN_DEP_* defines in depVersionsHeader.ts, the unread kqueue config field.

Verification

  • bun bd (full debug build) passes.
  • bun run rust:check-all: all 11 target triples ok (covers the windows/darwin-only removals in windows_sys, sys/windows, zlib_sys/win32.rs, fs_events.rs, windows-shim).
  • cargo fmt --check, cargo clippy --workspace, clang-format on every touched C++ file, prettier, and bun run lint are clean.
  • All of test/internal/source-lints/ (including the new dead-symbols-react-compiler-webcore-idl-misc.test.ts that guards these symbols, and dead-code-escapes against the updated dead-code-escape-limits.json), react-compiler-fixtures.test.ts, bundler_browser.test.ts, css, cryptohasher/hash, node:assert, zlib, url, events, websocket, inspect and x509 tests pass. serve.test.ts has the same 4 failures as the unmodified release build in this container (IPv6 / root port range), nothing else.

Left out on purpose (follow-up candidates)


[review] gate passed · iteration 1 · 172 files touched

fails on main (without fix)
ASAN without fix: 10 failed, 320 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/react-compiler-fixtures.test.ts test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts
bun test v1.4.0 (8326d1bd3)

test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts:
88 |     // Arena box alias with zero uses (HirVec is the one HIR actually uses).
89 |     ["src/react_compiler/hir/mod.rs", /\bHirBox\b/],
90 |     // Type predicate whose only callers were in the removed _exp validation.
91 |     ["src/react_compiler/hir/mod.rs", /\bis_use_state_type\b/],
92 |   ];
93 |   expect(resurrected(checks)).toEqual([]);
                                   ^
error: expect(received).toEqual(expected)

- []
+ [
+   "src/react_compiler/validation/validate_no_derived_computations_in_effects.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/hir/environment_config.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/program.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/program.rs: \bSymbolHo
... (truncated)

release without fix: 10 failed, 1146 skipped
bun test v1.4.0-canary.1 (21a4206cb)

test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts:
88 |     // Arena box alias with zero uses (HirVec is the one HIR actually uses).
89 |     ["src/react_compiler/hir/mod.rs", /\bHirBox\b/],
90 |     // Type predicate whose only callers were in the removed _exp validation.
91 |     ["src/react_compiler/hir/mod.rs", /\bis_use_state_type\b/],
92 |   ];
93 |   expect(resurrected(checks)).toEqual([]);
                                   ^
error: expect(received).toEqual(expected)

- []
+ [
+   "src/react_compiler/validation/validate_no_derived_computations_in_effects.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/hir/environment_config.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/program.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/program.rs: \bSymbolHost\b",
+   "src/react_compiler/lib.rs: \bSymbolHost\b",
+   "src/react_compiler/hir/mod.rs: \bHirBox\b",
+   "src/react_compiler/hir/mod.rs: \bis_use_state_type\b",
+ ]

- Expected  - 1
+ Received  + 9

      at <anonymous> (/workspace/bun/test/internal/source
... (truncated)
passes on PR (with fix)
ASAN with fix: 320 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/react-compiler-fixtures.test.ts test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts
bun test v1.4.0 (8326d1bd3)

test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts:
(pass) dead react_compiler symbols do not reappear [21.23ms]
(pass) dead exe_format symbols do not reappear [2.63ms]
(pass) dead bun_core / bun_alloc / bun_ast / bun_ptr items do not reappear [18.66ms]
(pass) dead bun_css items do not reappear [21.91ms]
(pass) dead bun_jsc items do not reappear [20.67ms]
(pass) dead FFI-crate items do not reappear [30.81ms]
(pass) dead Rust symbols (install, webcore, jsc, leaf crates) do not reappear [18.03ms]
(pass) unused re-export names do not reappear [12.35ms]
(pass) stale build-script entries do not reappear [6.80ms]
(pass) dead Rust FFI wrappers and trait methods do not reappear [22.18ms]
(pass) dead C++ binding helpers do not reappear [99.82ms]
(pass) dead WebCore / IDL binding code does not reappear [114.33ms]
(pass) dead code in install_jsc, install_t
... (truncated)

release with fix: 1146 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 641ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/130] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (21a4206cb)

Checked 111 installs across 104 packages (no changes) [3.00ms]
[2/130] gen node-fallbacks/react-refresh.js
Bundled 1 module in 5ms

  react-refresh.js  4.81 KB  (entry point)

[3/130] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[4/130] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 241 extern-C blocks audited
[5/130] gen node-fallbacks/*.js
[6/130] gen cpp.rs (cppbind)
[7/130] gen JS modules (bundle-modules)
Preprocess modules (7887ms)
Bundle modules (42ms)
Postprocesss modules (259ms)
Bundle Functions (695ms)
Generate Code (32ms)

[8.94s] Bundled "src/js" for production
  2630 kb
  197 internal modules
  13 native modules
  91 internal functions across 17 files
[7/129] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknow
... (truncated)
diff hotspot
scripts/build/config.ts                            |    4 -
 scripts/build/depVersionsHeader.ts                 |    3 -
 scripts/build/source.ts                            |    2 +-
 scripts/glob-sources.ts                            |    1 -
 src/ast/symbol.rs                                  |    8 -
 src/boringssl_sys/boringssl.rs                     |    8 -
 src/bun_alloc/lib.rs                               |   29 +-
 src/bun_core/bounded_array.rs                      |    8 -
 src/bun_core/fmt.rs                                |   17 -
 src/bun_core/string/immutable.rs                   |    7 -
 src/bun_core/string/mod.rs                         |   11 -
 src/bun_core/string/write.rs                       |    3 -
 src/bun_core/windows_sys.rs                        |    2 +-
 src/cares_sys/c_ares.rs                            |    6 -
 src/clap/error.rs                                  |    9 -
 src/clap/lib.rs                                    |    5 -
 src/codegen/generate-js2native.ts                  |    1 -
 src/codegen/generate-unified-source-bundles.rb     |  392 -------
 src/csrf/lib.rs                                    |    3 -
 src/css/css_parser.rs                              |   19 +-
 src/css/generics.rs                                |   10 -
 src/css/lib.rs                                     |    2 +-
 src/css/properties/custom.rs                       |    8 +-
 src/css/rules/mod.rs                               |    7 +-
 src/css/values/angle.rs                            |    4 -
 src/css/values/calc.rs                             |  132 +--
 src/css/values/css_string.rs                       |    6 -
 src/css/values/time.rs                             |    7 -
 src/css_derive/lib.rs                              |    4 +-
 src/css_jsc/css_internals.rs                       |   11 -
 src/errno/darwin_errno.rs                          |    1 -
 src/errno/freebsd_errno.rs                         |    1 -
 src/errno/linux_
... (truncated)

gate history · 4 passed · 1 rejected · iteration 1

evidence per changed file
file                                reads  edits  tests
scripts/build/config.ts                 0      0      0
scripts/build/depVersionsHeader.ts      0      0      0
scripts/build/source.ts                 0      0      0
scripts/glob-sources.ts                 0      0      0
src/ast/symbol.rs                       0      0      0
src/boringssl_sys/boringssl.rs          0      0      0
src/bun_alloc/lib.rs                    0      0      0
src/bun_core/bounded_array.rs           0      0      0
src/bun_core/fmt.rs                     0      0      0
src/bun_core/string/immutable.rs        0      0      0
src/bun_core/string/mod.rs              0      0      0
src/bun_core/string/write.rs            0      0      0
src/bun_core/windows_sys.rs             0      0      0
src/cares_sys/c_ares.rs                 0      0      0
src/clap/error.rs                       0      0      0
src/clap/lib.rs                         0      0      0
(+ 156 more files)

@robobun

robobun commented Aug 17, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:47 PM PT - Aug 17th, 2026

@Jarred-Sumner, your commit b55ec04 is building: #100151

@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.

Beyond the two inline nits, I spot-checked the non-pure-deletion hunks: the JSX509Certificate m_infoAccess/computeInfoAccess(legacy) removal is safe — jsX509CertificateGetter_infoAccess in JSX509CertificatePrototype.cpp reads view().getInfoAccess() directly and never touched the lazy property; kRestorePreviousState and FSWatcher remain defined and used internally after their export-object entries were dropped; and SafeMap (unlike SafeWeakSet) still has importers and stays in internal/primordials.js.

Extended reasoning...

This is a 171-file, -4.4k-line dead-code sweep spanning react_compiler, the WebCore/IDL C++ bindings, ncrypto, install_jsc, node-fallbacks, uws_sys, and dozens of leaf Rust crates. The bug-hunting pass surfaced two nits (an orphaned JSDOMConvertScheduledAction.h and a stale test comment), neither with runtime impact. I additionally verified the one hunk that isn't a pure deletion — the JSX509Certificate::computeInfoAccess legacy-parameter collapse — against its remaining caller in JSX509CertificatePrototype.cpp, and confirmed the src/js export-object removals (FSWatcher, kRestorePreviousState, SafeWeakSet) leave the underlying definitions intact for their internal users. Given the breadth (Event bitfield layout, ncrypto move-assignment operators, node-fallbacks lockfile) a human pass is still appropriate; this note just records what was already checked so it isn't re-derived.

Comment thread src/jsc/bindings/IDLTypes.h
Comment thread test/bundler/transpiler/react-compiler-fixtures.test.ts Outdated
@robobun
robobun force-pushed the farm/086d5366/dead-code-react-webcore-install-jsc branch from 9d31ae2 to 09f7bc4 Compare August 17, 2026 08:47
Comment thread src/css/rules/mod.rs
Comment thread src/css/values/calc.rs
Comment thread src/css_derive/lib.rs
Comment thread src/install_jsc/lib.rs
Comment thread src/jsc/job.rs
Comment thread src/runtime/webcore/ArrayBufferSink.rs Outdated
Comment thread src/runtime/webcore/Response.rs
Comment thread src/uws_sys/Loop.rs
Comment thread src/zlib_sys/shared.rs Outdated
@robobun
robobun force-pushed the farm/086d5366/dead-code-react-webcore-install-jsc branch from 09f7bc4 to 4870a79 Compare August 17, 2026 08:55
Comment thread src/runtime/webcore/ArrayBufferSink.rs
Comment thread src/runtime/webcore/ArrayBufferSink.rs
@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review in 4870a79:

  • react-compiler-fixtures.test.ts: reworded the IGNORED_PRAGMAS comment; the keys are no longer recognized by parse_fixture_pragmas at all, as pointed out.
  • JSDOMConvertScheduledAction.h: intentionally not touched here because the open Remove dead code from C++ webcore headers, src/js, and pretty_format #35775 already deletes that file. Removing IDLScheduledAction from IDLTypes.h and deleting the header compose in either order (nothing includes it). Noted in the description.
  • zlib_sys: the comment flagged on shared.rs was defending aliases with no users, so the aliases went instead: z_alloc_func, z_free_func, struct_z_stream_s, z_stream_s and internal_state, plus the posix.rs / win32.rs re-export entries that were their only mentions. cargo check --target x86_64-pc-windows-msvc passes.
  • ArrayBufferSink.rs: trimmed the ownership comment; the reason the call site uses to_js_unchecked (previously documented on the removed to_js) now lives as a two-line note at that call site.
  • The remaining flagged comment hunks only shorten existing comments so they stop describing items this PR deletes; replied on each thread.

Also kept the 8-line IDLDate struct itself (only its converter files are deleted) so the header edits and the file deletions are independent of each other; it is listed as a follow-up in the description.

Comment thread src/jsc/bindings/IDLTypes.h
@robobun
robobun force-pushed the farm/086d5366/dead-code-react-webcore-install-jsc branch from 4870a79 to 2bb6919 Compare August 17, 2026 09:10
@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (2bb6919, no conflicts, same diff). The binary-size annotation on the previous build compared this branch against canary #99935, which is #39326 (rust std built without the backtrace symbolizer); the branch predated that commit, so the ~0.5-0.8 MB per-target delta was the missing baseline change rather than anything in this diff. The one test failure (bun-lock.test.ts) passed on retry.

IDLDate staying in IDLTypes.h for now is intentional, see the "Left out on purpose" list in the description and the reply on the thread.

@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.

I reviewed this PR and didn't find any bugs; the two earlier nits (pragma comment wording, JSDOMConvertScheduledAction.h) and the comment-cop flags have all been addressed, and the IDLDate deferral is called out in the description. Because it's a ~5k-line deletion across 171 files spanning the C++ JSC bindings, ncrypto, uws_sys shims, node-fallbacks lockfile, and a large react_compiler validation removal, a human look would still be worthwhile.

What was reviewed:

  • JSX509Certificate m_infoAccess/computeInfoAccess(legacy) removal — the prototype getter reads view().getInfoAccess() directly and both remaining callers passed legacy=true, so the non-legacy branch was dead.
  • internal/fs/watch and readline/interface export-object trims — importers only destructure watch/createIgnoreMatcher; kRestorePreviousState is still used internally.
  • ncrypto.h move-operator= removals and Event.h bitfield removals — build-verified; no implicit users.
  • ArrayBuffer::alloc_typed_array_uninit Uint8Array arm — no callers instantiate that const-generic value.
Extended reasoning...

Overview

This PR removes ~4,900 lines of dead code across 171 files: the ~1.1k-line experimental validate_no_derived_computations_in_effects_exp pass in react_compiler, unused WebCore/IDL binding helpers (typed-array converter specializations, JSDOMPromiseDeferred resolve variants, NetworkLoadMetrics fields, JSWorkerOptions/JSMIMEBindings/JSDOMConvertDate files), install_jsc host functions whose only consumers were unused bun:internal-for-testing exports, ~400 lines of node-fallbacks package.json/lockfile entries and commented-out code, WebKit's Ruby unified-source generator, and a long tail of unused Rust items across bun_jsc, bun_css, uws_sys, zlib_sys, sha_hmac, windows_sys, and leaf crates. A new source-lint test guards against reintroduction.

Security risks

None identified. The removals in ncrypto, boringssl_sys, sha_hmac, and csrf are unused helpers (BignumPointer::isZero, EVPKeyCtxPointer::sign, deprecated raw hasher instantiations, never-constructed error variants) — not changes to any live security path. The JSX509Certificate::computeInfoAccess change collapses a two-branch function to its only-called branch; the prototype getter's behavior is unchanged.

Level of scrutiny

High, due to breadth. Each individual removal is mechanical and compiler-verified (Rust/C++ removals with remaining callers would fail to build; rust:check-all passed all 11 target triples for the platform-gated windows_sys/fs_events/zlib_sys hunks). JS/codegen removals were grepped and I spot-checked the internal/fs/watch, readline/interface, and repl/node-primordials export trims against their importers. The node-fallbacks lockfile prune is the least compiler-verifiable piece, but the description states build-fallbacks.ts externalizes builtin names and bundler_browser.test.ts passes.

Other factors

All prior review feedback on this PR has been addressed or explicitly deferred with rationale (IDLDate and JSDOMConvertScheduledAction.h are noted in "Left out on purpose"). The description is unusually thorough and the verification story (bun bd, rust:check-all, targeted test suites) is strong. However, at 171 files including C++ JSC bindings, ncrypto, uWS C++ shims, and a package lockfile, this exceeds the "simple, mechanical, or obvious" threshold for auto-approval — a maintainer should sign off on a sweep of this scale.

@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

CI is green on the rebased head (2bb6919, build 99949: 179/179 jobs, the only red-then-green entries are retry-passed flakes in install/hot/napi tests this diff does not touch). Against the same canary baseline the binary is now 0 to 32 KB smaller on every target, which confirms the earlier +0.5 MB annotation was the missing #39326 baseline and not this diff.

Ready for a maintainer. For a focused look, the only hunks that are not plain deletions are: the computeInfoAccess legacy-branch collapse in JSX509Certificate.cpp, the abi_type formatters narrowed to pub(crate), and the node-fallbacks package.json / bun.lock / tsconfig trim (verified with bun install --frozen-lockfile in that directory plus bundler_browser.test.ts). Everything else is removal of definitions that the build, rust:check-all on all 11 targets, and the new source lint confirm have no remaining references.

@robobun
robobun force-pushed the farm/086d5366/dead-code-react-webcore-install-jsc branch from 2bb6919 to 4ddb4e2 Compare August 17, 2026 19:19
@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main again (4ddb4e2). One conflict, in src/jsc/VM.rs: #38660 added is_entered() and termination_exception() next to the has_termination_request() wrapper this PR removes; resolved by keeping the new methods and dropping the wrapper, which still has no callers on main. Diff is otherwise unchanged (171 files, -4,911 / +505); full debug build plus the source lints, react-compiler fixtures and the x509 / events / zlib / css / browser-bundler suites pass on the rebased head.

Comment thread src/jsc/array_buffer.rs
@robobun
robobun force-pushed the farm/086d5366/dead-code-react-webcore-install-jsc branch 2 times, most recently from b8719a2 to 21a4206 Compare August 17, 2026 19:36
@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 21a4206 for the array_buffer.rs review note: the ArrayBuffer::alloc Uint8Array arm and its Bun__allocUint8ArrayForCopy declaration are kept (so alloc keeps mirroring create and the C++ definition keeps its caller); only MarkedArrayBuffer::to_js is removed from that file now. The intermediate push b8719a2 briefly re-added the ten deleted files by mistake and was replaced within a few minutes; 21a4206 is the previous head plus exactly that 8-line restore and one dropped guard entry (171 files, -4,903 / +504).

Comment thread test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts Outdated
Comment thread src/jsc/bindings/BunObject+exports.h
Jarred-Sumner pushed a commit that referenced this pull request Aug 17, 2026
…ifiers cache (#39469)

Follow-up to #39448, which deleted the dead `Http2CommonStrings` cache.
This wires the live inbound path up to the cache that is actually in
use.

### Problem
- `Bun__h2__materializeHeaders`
(`src/jsc/bindings/H2HeadersMaterializer.cpp`) runs once per inbound
HEADERS / PUSH_PROMISE / trailer block on both the client and the
server. For every field it allocated a new `JSString` cell for the name
(`jsString(vm, String(httpHeaderNameStringImpl(...)))`), then read it
back and called `Identifier::fromString` on it for the headers-object
put, which is an atom-table lookup per field.
- Pseudo-headers (`:method`, `:path`, `:scheme`, `:authority`,
`:status`) are not in `HTTPHeaderNames.in`, so they took the
unknown-name path: a new `StringImpl` copy plus a new `JSString` plus an
atomization, on every request and every response.
- `WebCore::HTTPHeaderIdentifiers`
(`src/jsc/bindings/webcore/HTTPHeaderIdentifiers.{h,cpp}`, reached
through `clientData(vm)->httpHeaderIdentifiers()`) already caches one
`JSString` and one `Identifier` per known header name for the lifetime
of the VM, and `Bun__NodeHTTP__buildRawHeadersArray` in `NodeHTTP.cpp`
already uses it. The HTTP/2 path did not, and the cache had no
pseudo-header entries. Its `identifierFor()` was removed as dead code in
#39420 earlier today.

### Fix
- `HTTPHeaderIdentifiers`: add `HTTP2_PSEUDO_HEADERS_EACH_NAME`, a
second X-macro list with the five pseudo-headers, and feed it through
the same declaration / lazy-init / accessor / visitor macros as the
existing entries, so each pseudo-header gets the same
`LazyProperty<JSString>` + cached `Identifier` pair.
`HTTP2PseudoHeaderName` is generated from that list, so the enum and the
lookup tables cannot drift apart; `findHTTP2PseudoHeaderName()` maps a
wire name to it (one byte compare for names that do not start with `:`).
- Restore `identifierFor(VM&, HTTPHeaderName)` next to `stringFor()` and
add the `HTTP2PseudoHeaderName` overloads. The regular-name tables are
indexed by `HTTPHeaderName`, so a `static_assert` now checks that
`HTTP_HEADERS_EACH_NAME` has one entry per enum value and in enum order
(previously nothing checked either; a misplaced entry would have
silently shifted every later slot).
- `H2HeadersMaterializer.cpp`: names found in either list take the
cached `JSString` and `Identifier`, so they allocate nothing and are
never atomized per request. Any other name is atomized straight from the
wire bytes (the same atom-table lookup as before, minus the intermediate
`StringImpl` copy) and its rawHeaders `JSString` wraps that atom, so the
unknown path goes from two allocations to one. The `:status`, `cookie`
and `set-cookie` special cases compare the enums instead of re-comparing
the name bytes. Behavior of the produced rawHeaders array, headers
object and sensitive list is unchanged.
- `BunHttp2CommonStrings` is not brought back.
- Verified:
- `test/js/node/http2/node-http2.test.js`, `http2 hands out the cached
name strings for pseudo-headers and known header names`: after one round
trip, `jscDescribe()` from `bun:jsc` reports every name string in
rawHeaders, on both the server and the client side, as atomic, which is
only true of strings that wrap the atoms the headers object is keyed by
(the cache entries, or the per-block atom for unknown names). On main
all ten names come out non-atomic (a JSString over WebCore's static name
string, or a copy of the wire bytes), so the test fails there and passes
with this change.
- Same file, `http2 materializes pseudo, known, unknown and all-digit
header names the same way on every request`: pins the produced
rawHeaders / headers object / sensitive list for all four name paths on
both sides, including the cookie / set-cookie / `:status` special cases,
and checks that a second round trip on the same session (served from the
populated cache) materializes identically. Behavior-preservation test,
passes before and after.
- `bun bd test test/js/node/http2/` (all 9 files, including
`h2-conformance.test.ts`): 477 pass, 0 fail, on the debug + ASAN build.
- All 283 upstream `test/js/node/test/{parallel,sequential}/*http2*`
files pass with the debug build.
  - The new test also passes with `BUN_JSC_validateExceptionChecks=1`.
  - The three touched C++ files are clang-format clean.

### Benchmark
Local h2c loop in one process: a `node:http2` server and client on
127.0.0.1, 64 streams in flight on one session. Each round trip carries
10 regular fields on the request and 10 on the response (70% names from
`HTTPHeaderNames.in`, 30% `x-*`), so with the pseudo-headers it
materializes 25 fields per round trip (14 on the server, 11 on the
client). `bun run build:release` (non-LTO) of main at 9f67488 with and
without this diff, same machine (16 vCPU container on a shared, busy
host), 7 interleaved trials each, 60k timed round trips per trial after
a 5k warm-up.

| headers per message | metric | before | after | delta |
| --- | --- | ---: | ---: | ---: |
| 10 (25 fields/round trip) | header fields/sec, median | 576,473 |
589,189 | +2.2% |
| 10 | header fields/sec, best | 582,706 | 600,457 | +3.0% |
| 10 | round trips/sec, median | 23,059 | 23,568 | +2.2% |
| 50 (105 fields/round trip) | header fields/sec, median | 1,006,661 |
1,043,415 | +3.7% |
| 50 | header fields/sec, best | 1,032,850 | 1,062,902 | +2.9% |
| 50 | round trips/sec, median | 9,587 | 9,937 | +3.7% |

The loop measures the whole request path (stream objects, events,
framing, HPACK, loopback sockets), of which name materialization is a
small slice, so a few percent end to end is the expected size of this
change. Per round trip with the 10-header mix (5 pseudo-headers, 15
known names, 5 `x-*` names) it removes 20 `JSString` allocations, 10
`StringImpl` copies and 20 atom-table lookups; the 5 `x-*` names keep
their lookup and go from two allocations to one.

<details>
<summary>Benchmark script</summary>

```js
// usage: <bun> bench.mjs [regularHeadersPerMessage=10] [roundTrips=60000] [concurrency=64]
import http2 from "node:http2";
const regular = Number(process.argv[2] ?? 10), total = Number(process.argv[3] ?? 60000), concurrency = Number(process.argv[4] ?? 64);
const knownNames = ["content-type","cache-control","etag","vary","last-modified","accept","accept-encoding","user-agent",
  "authorization","accept-language","referer","origin","if-none-match","link","via"];
function buildHeaders(count, prefix) {
  const out = {}; let known = 0;
  for (let i = 0; i < count; i++) {
    if (i % 10 < 7 && known < knownNames.length) out[knownNames[known++]] = `${prefix}-value-${i}`;
    else out[`x-${prefix}-${i}`] = `${prefix}-value-${i}`;
  }
  return out;
}
const requestHeaders = { ":method": "POST", ":path": "/bench", ...buildHeaders(regular, "req") };
const responseHeaders = { ":status": 200, date: "Thu, 01 Jan 2026 00:00:00 GMT", ...buildHeaders(regular - 1, "res") };
const requestFields = 4 + regular, responseFields = 1 + regular;
const server = http2.createServer();
let serverFields = 0;
server.on("stream", stream => { serverFields += requestFields; stream.respond(responseHeaders, { endStream: true }); });
await new Promise(r => server.listen(0, "127.0.0.1", r));
const client = http2.connect(`http://127.0.0.1:${server.address().port}`);
await new Promise(r => client.once("connect", r));
function run(n) {
  return new Promise((resolve, reject) => {
    let started = 0, finished = 0, clientFields = 0;
    function one() {
      started++;
      const req = client.request(requestHeaders, { endStream: true });
      req.on("error", reject);
      req.on("response", h => { if (h[":status"] !== 200) reject(new Error("bad status")); clientFields += responseFields; });
      req.on("close", () => { if (++finished === n) resolve(clientFields); else if (started < n) one(); });
    }
    for (let i = 0; i < Math.min(concurrency, n); i++) one();
  });
}
await run(Math.min(total, 5000)); // warm-up
serverFields = 0;
const t0 = performance.now();
const clientFields = await run(total);
const s = (performance.now() - t0) / 1000;
console.log(JSON.stringify({ regular, roundTripsPerSec: Math.round(total / s), headerFieldsPerSec: Math.round((clientFields + serverFields) / s) }));
client.close(); server.close();
```

</details>

### Background
- `HTTPHeaderNames.in` is WebCore's list of ~94 well-known HTTP header
names. `findHTTPHeaderName()` maps a name to the `HTTPHeaderName` enum
with a perfect hash. HTTP/2 pseudo-headers are not HTTP header fields,
which is why they are not in that list.
- `WTF::AtomStringImpl` is the per-thread interning table for strings
used as property keys. A `JSC::Identifier` is a handle to an atom;
`Identifier::fromString` hashes the input and looks it up (allocating a
new atom on a miss). `JSString` is the GC cell that wraps a `StringImpl`
and is what JavaScript sees; several `JSString`s can share one
`StringImpl`, including an atom.
- `JSC::LazyProperty<Owner, T>` is JSC's lazily-initialized GC slot: it
stores an initializer until first use, and `visit()` marks the cell once
created. `HTTPHeaderIdentifiers` lives in the per-VM `JSVMClientData`
and is marked from `Zig::GlobalObject::visitChildren`, so the cached
strings stay alive for the VM's lifetime and adding entries to the
X-macro list extends both the initialization and the marking
automatically.
- Sharing one `JSString` cell across many rawHeaders arrays is not
observable from JavaScript: strings are compared by value, and the cells
are immutable.

<!-- robobun:evidence:begin -->

---

**[review]** gate passed · iteration 1 · 4 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 1 failed, 6 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/js/node/http2/node-http2.test.js"
bun test v1.4.0 (8326d1b)

test/js/node/http2/node-http2.test.js:
(pass) node none > Client Basics > should be able to send a GET request [842.82ms]
(pass) node none > Client Basics > should be able to send a POST request [564.28ms]
(pass) node none > Client Basics > constants [18.74ms]
(pass) node none > Client Basics > getDefaultSettings [7.14ms]
(pass) node none > Client Basics > getPackedSettings/getUnpackedSettings [22.56ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is too small [5.48ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a multiple of 6 bytes [3.35ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a buffer [5.25ms]
(pass) node none > Client Basics > should be able to send data using end [594.14ms]
(pass) node none > Client Basics > should be able to mutiplex GET requests [582.90ms]
(pass) node none > Client Basics > http2 should receive remoteSettings when receiving 
... (truncated)

release without fix: 6 skipped
bun test v1.4.0-canary.1 (b4a5e9a)

test/js/node/http2/node-http2.test.js:
(pass) node none > Client Basics > constants [0.91ms]
(pass) node none > Client Basics > getDefaultSettings [0.15ms]
(pass) node none > Client Basics > getPackedSettings/getUnpackedSettings [0.39ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is too small [0.11ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a multiple of 6 bytes [0.04ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a buffer [0.07ms]
(pass) node none > Client Basics > is possible to abort request [1.72ms]
(pass) node none > Client Basics > aborted event should work with abortController [0.72ms]
(pass) node none > Client Basics > aborted event should work with aborted signal [0.65ms]
(pass) node none > Client Basics > signal validation matches node: non-signal objects throw, duck-typed { aborted } is accepted [1.09ms]
(pass) node none > Client Basics > headers cannot be bigger than 65536 bytes [34.66ms]
(skip) node none > Client Basics > should not leak memory
(pass) node none > Client Basics > should fail to con
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: 6 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/js/node/http2/node-http2.test.js"
bun test v1.4.0 (8326d1b)

test/js/node/http2/node-http2.test.js:
(pass) node none > Client Basics > should be able to send a GET request [860.23ms]
(pass) node none > Client Basics > should be able to send a POST request [575.20ms]
(pass) node none > Client Basics > constants [18.83ms]
(pass) node none > Client Basics > getDefaultSettings [7.39ms]
(pass) node none > Client Basics > getPackedSettings/getUnpackedSettings [23.12ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is too small [5.63ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a multiple of 6 bytes [3.68ms]
(pass) node none > Client Basics > getUnpackedSettings should throw if buffer is not a buffer [5.08ms]
(pass) node none > Client Basics > should be able to send data using end [607.15ms]
(pass) node none > Client Basics > should be able to mutiplex GET requests [599.84ms]
(pass) node none > Client Basics > http2 should receive remoteSettings when receiving 
... (truncated)

release with fix: 6 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1049ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/126] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 241 extern-C blocks audited
[2/126] gen cpp.rs (cppbind)
[3/126] gen JSSink.{cpp,h,lut.h,rs}
generated_jssink.rs: 8 sinks, 96 exported symbols
Generating /workspace/bun/build/release/codegen/JSSink.lut.h from /workspace/bun/build/release/codegen/JSSink.lut.txt
[4/126] gen JS modules (bundle-modules)
Preprocess modules (8180ms)
Bundle modules (39ms)
Postprocesss modules (36ms)
Bundle Functions (629ms)
Generate Code (22ms)

[8.92s] Bundled "src/js" for production
  2630 kb
  197 internal modules
  13 native modules
  91 internal functions across 17 files
[4/125] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_highway v0.0.0 (/workspace/bun/src/highway)
�[1m�[92m   Compilin
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
src/jsc/bindings/H2HeadersMaterializer.cpp         |  36 +++--
 src/jsc/bindings/webcore/HTTPHeaderIdentifiers.cpp |  91 ++++++++++--
 src/jsc/bindings/webcore/HTTPHeaderIdentifiers.h   |  24 +++
 test/js/node/http2/node-http2.test.js              | 164 +++++++++++++++++++++
 4 files changed, 291 insertions(+), 24 deletions(-)
```

</details>

**gate history** · 1 passed · 1 rejected · iteration 1

<details><summary>evidence per changed file</summary>

```
file                                                reads  edits  tests
src/jsc/bindings/H2HeadersMaterializer.cpp              4     11      0
src/jsc/bindings/webcore/HTTPHeaderIdentifiers.cpp      5     13      0
src/jsc/bindings/webcore/HTTPHeaderIdentifiers.h        2      3      0
test/js/node/http2/node-http2.test.js                   4      8      0
```

</details>

**root cause** · written by the author bot

The inbound node:http2 header path in H2HeadersMaterializer.cpp built a
fresh JSString and then a separate Identifier for every header name on
every HEADERS frame, and the pseudo-headers were absent from
HTTPHeaderNames.in, so each request or response also paid for a new
StringImpl plus atomization for :method, :path, :scheme, :authority and
:status. The fix routes known names through the existing per-VM
HTTPHeaderIdentifiers cache, as NodeHTTP.cpp already does, and adds the
five pseudo-headers to that cache as lazily initialized strings with
cached identifiers, so steady-state materializat…

<!-- robobun:evidence:end -->
robobun pushed a commit that referenced this pull request Aug 18, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.
robobun pushed a commit that referenced this pull request Aug 18, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from
#39558 and the other-thread possibility the Send bounds rely on.
robobun pushed a commit that referenced this pull request Aug 19, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.
robobun pushed a commit that referenced this pull request Aug 19, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
#39547's shape (type check and dial/rejection before any listener is
stored, no trailing else) with the rejection and the new check spelled
through the scope.
robobun pushed a commit that referenced this pull request Aug 21, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.
robobun pushed a commit that referenced this pull request Aug 21, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
#39732's parameterless ReadableStream::done().
robobun pushed a commit that referenced this pull request Aug 21, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.
robobun pushed a commit that referenced this pull request Aug 21, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
#39922's bodies (callback handed to the native job, one from_js call)
under the scoped signatures.
robobun pushed a commit that referenced this pull request Aug 21, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.
robobun pushed a commit that referenced this pull request Aug 22, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
#39924's three thin host fns plus its run() helper, with the thin fns
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.
robobun pushed a commit that referenced this pull request Aug 22, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.
robobun pushed a commit that referenced this pull request Aug 22, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
#40051's comment sweep; verifySync keeps the deferred materialize and
NodeHTTPResponse's on_resolve keeps the scoped call, both without the
removed defer comments.
robobun pushed a commit that referenced this pull request Aug 23, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
NodeHTTPResponse's on_resolve keeps the scoped call, both without the
removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.
robobun pushed a commit that referenced this pull request Aug 23, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
NodeHTTPResponse's on_resolve keeps the scoped call, both without the
removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.
robobun pushed a commit that referenced this pull request Aug 23, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing from

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
stored, no trailing else) with the rejection and the new check spelled
through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
NodeHTTPResponse's on_resolve keeps the scoped call, both without the
removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.
robobun pushed a commit that referenced this pull request Aug 24, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing
(the deallocator can run before Err, per #39558) and the cross-thread
timing this PR's Send bounds rely on.

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
the structure #39547 gave it (channel type check first, dial plus
send_rejection() before a listener is stored, no trailing else) with the
rejection and the new check spelled through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
the parameterless ReadableStream::done() and is_some() guard from #39732.

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
the bodies #39922 gave them (from_js also returns the callback, pbkdf2
returns undefined, length 6) under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
the defer-comment sweep (#40051): PasswordObject's verifySync keeps this
PR's deferred materialize of both arguments, and NodeHTTPResponse's
on_resolve keeps the scoped call, both without the removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.

Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the
exception checks that follow already-checked calls and made
JSString::to_slice / view, JSValue::get_zig_string and
handle_ipc_message return JsResult. Eight files conflicted inside scoped
bodies (BunObject, CryptoHasher, PasswordObject, ipc_host,
node_util_binding, server_body, expect, ObjectURLRegistry); main's
control flow is kept (the guards go, the ? is added) under the scoped
spellings. The four has_exception checks left in BunObject.rs are the
ones main kept (print_table / format2 swallow nested throws).
robobun pushed a commit that referenced this pull request Aug 24, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing
(the deallocator can run before Err, per #39558) and the cross-thread
timing this PR's Send bounds rely on.

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
the structure #39547 gave it (channel type check first, dial plus
send_rejection() before a listener is stored, no trailing else) with the
rejection and the new check spelled through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
the parameterless ReadableStream::done() and is_some() guard from #39732.

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
the bodies #39922 gave them (from_js also returns the callback, pbkdf2
returns undefined, length 6) under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
the defer-comment sweep (#40051): PasswordObject's verifySync keeps this
PR's deferred materialize of both arguments, and NodeHTTPResponse's
on_resolve keeps the scoped call, both without the removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.

Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the
exception checks that follow already-checked calls and made
JSString::to_slice / view, JSValue::get_zig_string and
handle_ipc_message return JsResult. Eight files conflicted inside scoped
bodies (BunObject, CryptoHasher, PasswordObject, ipc_host,
node_util_binding, server_body, expect, ObjectURLRegistry); main's
control flow is kept (the guards go, the ? is added) under the scoped
spellings. The four has_exception checks left in BunObject.rs are the
ones main kept (print_table / format2 swallow nested throws).

Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted,
nearly all with #40238 (bun_core::String owns its WTF ref). Main's
ownership idioms replace this PR's: OwnedString / scopeguard deref
wrappers and manual .deref() calls go (String drops its ref), into_js
replaces transfer_to_js (Scope::transfer_string now consumes the
String), JSValue::get_zig_string is gone so Local::get_zig_string becomes
Local::to_js_string_view (the JSStringView guard keeps the cell alive),
and to_slice_or_null collapses into to_slice. OwnedUrl is retired:
main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and
js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's
text (the strings module is gone, names are &'static str) with the 14
host fns scoped and rustfmt applied; jest.rs and expect.rs take main's
literals under this PR's wrapping. CachedStructure keeps main's
assume_init_mut / drop_in_place sequence over this PR's slice-taking
create_structure. UDP address getters add the ? main's create_sock_addr
now needs. Scope-escape limits drop by one in BunObject, node_util_binding
and server_body and by two in FormData (hatches replaced by scoped calls).
robobun pushed a commit that referenced this pull request Aug 24, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing
(the deallocator can run before Err, per #39558) and the cross-thread
timing this PR's Send bounds rely on.

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
the structure #39547 gave it (channel type check first, dial plus
send_rejection() before a listener is stored, no trailing else) with the
rejection and the new check spelled through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
the parameterless ReadableStream::done() and is_some() guard from #39732.

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
the bodies #39922 gave them (from_js also returns the callback, pbkdf2
returns undefined, length 6) under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
the defer-comment sweep (#40051): PasswordObject's verifySync keeps this
PR's deferred materialize of both arguments, and NodeHTTPResponse's
on_resolve keeps the scoped call, both without the removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.

Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the
exception checks that follow already-checked calls and made
JSString::to_slice / view, JSValue::get_zig_string and
handle_ipc_message return JsResult. Eight files conflicted inside scoped
bodies (BunObject, CryptoHasher, PasswordObject, ipc_host,
node_util_binding, server_body, expect, ObjectURLRegistry); main's
control flow is kept (the guards go, the ? is added) under the scoped
spellings. The four has_exception checks left in BunObject.rs are the
ones main kept (print_table / format2 swallow nested throws).

Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted,
nearly all with #40238 (bun_core::String owns its WTF ref). Main's
ownership idioms replace this PR's: OwnedString / scopeguard deref
wrappers and manual .deref() calls go (String drops its ref), into_js
replaces transfer_to_js (Scope::transfer_string now consumes the
String), JSValue::get_zig_string is gone so Local::get_zig_string becomes
Local::to_js_string_view (the JSStringView guard keeps the cell alive),
and to_slice_or_null collapses into to_slice. OwnedUrl is retired:
main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and
js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's
text (the strings module is gone, names are &'static str) with the 14
host fns scoped and rustfmt applied; jest.rs and expect.rs take main's
literals under this PR's wrapping. CachedStructure keeps main's
assume_init_mut / drop_in_place sequence over this PR's slice-taking
create_structure. UDP address getters add the ? main's create_sock_addr
now needs. Scope-escape limits drop by one in BunObject, node_util_binding
and server_body and by two in FormData (hatches replaced by scoped calls).

Twenty-first rebase (7 more commits, onto 8335017): one import-line
conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc
trait import next to this PR's scoped imports. Both kept; no inventory
changes.
robobun pushed a commit that referenced this pull request Aug 25, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing
(the deallocator can run before Err, per #39558) and the cross-thread
timing this PR's Send bounds rely on.

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
the structure #39547 gave it (channel type check first, dial plus
send_rejection() before a listener is stored, no trailing else) with the
rejection and the new check spelled through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
the parameterless ReadableStream::done() and is_some() guard from #39732.

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
the bodies #39922 gave them (from_js also returns the callback, pbkdf2
returns undefined, length 6) under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
the defer-comment sweep (#40051): PasswordObject's verifySync keeps this
PR's deferred materialize of both arguments, and NodeHTTPResponse's
on_resolve keeps the scoped call, both without the removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.

Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the
exception checks that follow already-checked calls and made
JSString::to_slice / view, JSValue::get_zig_string and
handle_ipc_message return JsResult. Eight files conflicted inside scoped
bodies (BunObject, CryptoHasher, PasswordObject, ipc_host,
node_util_binding, server_body, expect, ObjectURLRegistry); main's
control flow is kept (the guards go, the ? is added) under the scoped
spellings. The four has_exception checks left in BunObject.rs are the
ones main kept (print_table / format2 swallow nested throws).

Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted,
nearly all with #40238 (bun_core::String owns its WTF ref). Main's
ownership idioms replace this PR's: OwnedString / scopeguard deref
wrappers and manual .deref() calls go (String drops its ref), into_js
replaces transfer_to_js (Scope::transfer_string now consumes the
String), JSValue::get_zig_string is gone so Local::get_zig_string becomes
Local::to_js_string_view (the JSStringView guard keeps the cell alive),
and to_slice_or_null collapses into to_slice. OwnedUrl is retired:
main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and
js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's
text (the strings module is gone, names are &'static str) with the 14
host fns scoped and rustfmt applied; jest.rs and expect.rs take main's
literals under this PR's wrapping. CachedStructure keeps main's
assume_init_mut / drop_in_place sequence over this PR's slice-taking
create_structure. UDP address getters add the ? main's create_sock_addr
now needs. Scope-escape limits drop by one in BunObject, node_util_binding
and server_body and by two in FormData (hatches replaced by scoped calls).

Twenty-first rebase (7 more commits, onto 8335017): one import-line
conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc
trait import next to this PR's scoped imports. Both kept; no inventory
changes.

Twenty-second rebase (6 more commits, onto 0823e50): 39 files
conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's
types replace the PR's spellings inside scoped bodies: Local::to_slice is
now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and
create_utf8_for_js calls become scope.string_utf8 / scope.string, and
ScopedStringOrBuffer names StringOrBuffer<'static>. Main's
owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so
src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and
TextDecoder.rs are main's again. Scope-escape limits drop in
filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to
9).
robobun pushed a commit that referenced this pull request Aug 25, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing
(the deallocator can run before Err, per #39558) and the cross-thread
timing this PR's Send bounds rely on.

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
the structure #39547 gave it (channel type check first, dial plus
send_rejection() before a listener is stored, no trailing else) with the
rejection and the new check spelled through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
the parameterless ReadableStream::done() and is_some() guard from #39732.

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
the bodies #39922 gave them (from_js also returns the callback, pbkdf2
returns undefined, length 6) under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
the defer-comment sweep (#40051): PasswordObject's verifySync keeps this
PR's deferred materialize of both arguments, and NodeHTTPResponse's
on_resolve keeps the scoped call, both without the removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.

Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the
exception checks that follow already-checked calls and made
JSString::to_slice / view, JSValue::get_zig_string and
handle_ipc_message return JsResult. Eight files conflicted inside scoped
bodies (BunObject, CryptoHasher, PasswordObject, ipc_host,
node_util_binding, server_body, expect, ObjectURLRegistry); main's
control flow is kept (the guards go, the ? is added) under the scoped
spellings. The four has_exception checks left in BunObject.rs are the
ones main kept (print_table / format2 swallow nested throws).

Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted,
nearly all with #40238 (bun_core::String owns its WTF ref). Main's
ownership idioms replace this PR's: OwnedString / scopeguard deref
wrappers and manual .deref() calls go (String drops its ref), into_js
replaces transfer_to_js (Scope::transfer_string now consumes the
String), JSValue::get_zig_string is gone so Local::get_zig_string becomes
Local::to_js_string_view (the JSStringView guard keeps the cell alive),
and to_slice_or_null collapses into to_slice. OwnedUrl is retired:
main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and
js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's
text (the strings module is gone, names are &'static str) with the 14
host fns scoped and rustfmt applied; jest.rs and expect.rs take main's
literals under this PR's wrapping. CachedStructure keeps main's
assume_init_mut / drop_in_place sequence over this PR's slice-taking
create_structure. UDP address getters add the ? main's create_sock_addr
now needs. Scope-escape limits drop by one in BunObject, node_util_binding
and server_body and by two in FormData (hatches replaced by scoped calls).

Twenty-first rebase (7 more commits, onto 8335017): one import-line
conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc
trait import next to this PR's scoped imports. Both kept; no inventory
changes.

Twenty-second rebase (6 more commits, onto 0823e50): 39 files
conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's
types replace the PR's spellings inside scoped bodies: Local::to_slice is
now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and
create_utf8_for_js calls become scope.string_utf8 / scope.string, and
ScopedStringOrBuffer names StringOrBuffer<'static>. Main's
owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so
src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and
TextDecoder.rs are main's again. Scope-escape limits drop in
filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to
9).

Twenty-third rebase (9 more commits, onto adc354d): two files.
FileSystemRouter::routes takes #40410's fallible JSValue::from_entries
(mapped into the scope), and advanceTimersByTime keeps #40414's NaN
check and main's message text under the scoped throws. The
jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers
entry).
robobun pushed a commit that referenced this pull request Aug 25, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing
(the deallocator can run before Err, per #39558) and the cross-thread
timing this PR's Send bounds rely on.

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
the structure #39547 gave it (channel type check first, dial plus
send_rejection() before a listener is stored, no trailing else) with the
rejection and the new check spelled through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
the parameterless ReadableStream::done() and is_some() guard from #39732.

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
the bodies #39922 gave them (from_js also returns the callback, pbkdf2
returns undefined, length 6) under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
the defer-comment sweep (#40051): PasswordObject's verifySync keeps this
PR's deferred materialize of both arguments, and NodeHTTPResponse's
on_resolve keeps the scoped call, both without the removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.

Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the
exception checks that follow already-checked calls and made
JSString::to_slice / view, JSValue::get_zig_string and
handle_ipc_message return JsResult. Eight files conflicted inside scoped
bodies (BunObject, CryptoHasher, PasswordObject, ipc_host,
node_util_binding, server_body, expect, ObjectURLRegistry); main's
control flow is kept (the guards go, the ? is added) under the scoped
spellings. The four has_exception checks left in BunObject.rs are the
ones main kept (print_table / format2 swallow nested throws).

Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted,
nearly all with #40238 (bun_core::String owns its WTF ref). Main's
ownership idioms replace this PR's: OwnedString / scopeguard deref
wrappers and manual .deref() calls go (String drops its ref), into_js
replaces transfer_to_js (Scope::transfer_string now consumes the
String), JSValue::get_zig_string is gone so Local::get_zig_string becomes
Local::to_js_string_view (the JSStringView guard keeps the cell alive),
and to_slice_or_null collapses into to_slice. OwnedUrl is retired:
main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and
js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's
text (the strings module is gone, names are &'static str) with the 14
host fns scoped and rustfmt applied; jest.rs and expect.rs take main's
literals under this PR's wrapping. CachedStructure keeps main's
assume_init_mut / drop_in_place sequence over this PR's slice-taking
create_structure. UDP address getters add the ? main's create_sock_addr
now needs. Scope-escape limits drop by one in BunObject, node_util_binding
and server_body and by two in FormData (hatches replaced by scoped calls).

Twenty-first rebase (7 more commits, onto 8335017): one import-line
conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc
trait import next to this PR's scoped imports. Both kept; no inventory
changes.

Twenty-second rebase (6 more commits, onto 0823e50): 39 files
conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's
types replace the PR's spellings inside scoped bodies: Local::to_slice is
now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and
create_utf8_for_js calls become scope.string_utf8 / scope.string, and
ScopedStringOrBuffer names StringOrBuffer<'static>. Main's
owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so
src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and
TextDecoder.rs are main's again. Scope-escape limits drop in
filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to
9).

Twenty-third rebase (9 more commits, onto adc354d): two files.
FileSystemRouter::routes takes #40410's fallible JSValue::from_entries
(mapped into the scope), and advanceTimersByTime keeps #40414's NaN
check and main's message text under the scoped throws. The
jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers
entry).

Twenty-fourth rebase (9 more commits, onto 82123d3): six files, all
with #40478 (RefPtr releases on Drop). This PR's StoreRef::adopt is
retired: main's RefPtr<Store> is the same owning handle, so
webcore_types.rs is main's again and store_backed_buffer_to_js moves a
RefPtr<Store> into the JS object as the *_from_owner owner (the view
closure reaches the bytes through Store::data_mut). The sql event-loop
guard keeps this PR's safe EventLoop::scope under main's renamed ref
guard; expect.rs keeps this PR's wrapping over main's RefPtr comments.
The vm-thread-door inventory follows main's StoreRef-to-Store rename.
robobun pushed a commit that referenced this pull request Aug 27, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing
(the deallocator can run before Err, per #39558) and the cross-thread
timing this PR's Send bounds rely on.

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
the structure #39547 gave it (channel type check first, dial plus
send_rejection() before a listener is stored, no trailing else) with the
rejection and the new check spelled through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
the parameterless ReadableStream::done() and is_some() guard from #39732.

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
the bodies #39922 gave them (from_js also returns the callback, pbkdf2
returns undefined, length 6) under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
the defer-comment sweep (#40051): PasswordObject's verifySync keeps this
PR's deferred materialize of both arguments, and NodeHTTPResponse's
on_resolve keeps the scoped call, both without the removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.

Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the
exception checks that follow already-checked calls and made
JSString::to_slice / view, JSValue::get_zig_string and
handle_ipc_message return JsResult. Eight files conflicted inside scoped
bodies (BunObject, CryptoHasher, PasswordObject, ipc_host,
node_util_binding, server_body, expect, ObjectURLRegistry); main's
control flow is kept (the guards go, the ? is added) under the scoped
spellings. The four has_exception checks left in BunObject.rs are the
ones main kept (print_table / format2 swallow nested throws).

Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted,
nearly all with #40238 (bun_core::String owns its WTF ref). Main's
ownership idioms replace this PR's: OwnedString / scopeguard deref
wrappers and manual .deref() calls go (String drops its ref), into_js
replaces transfer_to_js (Scope::transfer_string now consumes the
String), JSValue::get_zig_string is gone so Local::get_zig_string becomes
Local::to_js_string_view (the JSStringView guard keeps the cell alive),
and to_slice_or_null collapses into to_slice. OwnedUrl is retired:
main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and
js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's
text (the strings module is gone, names are &'static str) with the 14
host fns scoped and rustfmt applied; jest.rs and expect.rs take main's
literals under this PR's wrapping. CachedStructure keeps main's
assume_init_mut / drop_in_place sequence over this PR's slice-taking
create_structure. UDP address getters add the ? main's create_sock_addr
now needs. Scope-escape limits drop by one in BunObject, node_util_binding
and server_body and by two in FormData (hatches replaced by scoped calls).

Twenty-first rebase (7 more commits, onto 8335017): one import-line
conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc
trait import next to this PR's scoped imports. Both kept; no inventory
changes.

Twenty-second rebase (6 more commits, onto 0823e50): 39 files
conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's
types replace the PR's spellings inside scoped bodies: Local::to_slice is
now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and
create_utf8_for_js calls become scope.string_utf8 / scope.string, and
ScopedStringOrBuffer names StringOrBuffer<'static>. Main's
owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so
src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and
TextDecoder.rs are main's again. Scope-escape limits drop in
filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to
9).

Twenty-third rebase (9 more commits, onto adc354d): two files.
FileSystemRouter::routes takes #40410's fallible JSValue::from_entries
(mapped into the scope), and advanceTimersByTime keeps #40414's NaN
check and main's message text under the scoped throws. The
jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers
entry).

Twenty-fourth rebase (9 more commits, onto 82123d3): six files, all
with #40478 (RefPtr releases on Drop). This PR's StoreRef::adopt is
retired: main's RefPtr<Store> is the same owning handle, so
webcore_types.rs is main's again and store_backed_buffer_to_js moves a
RefPtr<Store> into the JS object as the *_from_owner owner (the view
closure reaches the bytes through Store::data_mut). The sql event-loop
guard keeps this PR's safe EventLoop::scope under main's renamed ref
guard; expect.rs keeps this PR's wrapping over main's RefPtr comments.
The vm-thread-door inventory follows main's StoreRef-to-Store rename.

Twenty-fifth rebase (23 more commits, onto 0e395c2): four files, all
with #40511 (async fs calls no longer pin Buffer paths). pbkdf2 and
scrypt take main's from_js_async parsers (ThreadIsolated params) under
the scoped signatures, StringOrBuffer keeps main's from_js_async next to
this PR's from_js_scoped / from_js_deferred, and the
BlobOrStringOrBuffer::from_js_async this PR's insertion sat beside is
gone with main. Import merges in node.rs and MarkdownObject.rs. The
vm-thread-door inventory follows main's ThreadSafe-to-ThreadIsolated
rename.
robobun pushed a commit that referenced this pull request Aug 27, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing
(the deallocator can run before Err, per #39558) and the cross-thread
timing this PR's Send bounds rely on.

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
the structure #39547 gave it (channel type check first, dial plus
send_rejection() before a listener is stored, no trailing else) with the
rejection and the new check spelled through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
the parameterless ReadableStream::done() and is_some() guard from #39732.

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
the bodies #39922 gave them (from_js also returns the callback, pbkdf2
returns undefined, length 6) under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
the defer-comment sweep (#40051): PasswordObject's verifySync keeps this
PR's deferred materialize of both arguments, and NodeHTTPResponse's
on_resolve keeps the scoped call, both without the removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.

Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the
exception checks that follow already-checked calls and made
JSString::to_slice / view, JSValue::get_zig_string and
handle_ipc_message return JsResult. Eight files conflicted inside scoped
bodies (BunObject, CryptoHasher, PasswordObject, ipc_host,
node_util_binding, server_body, expect, ObjectURLRegistry); main's
control flow is kept (the guards go, the ? is added) under the scoped
spellings. The four has_exception checks left in BunObject.rs are the
ones main kept (print_table / format2 swallow nested throws).

Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted,
nearly all with #40238 (bun_core::String owns its WTF ref). Main's
ownership idioms replace this PR's: OwnedString / scopeguard deref
wrappers and manual .deref() calls go (String drops its ref), into_js
replaces transfer_to_js (Scope::transfer_string now consumes the
String), JSValue::get_zig_string is gone so Local::get_zig_string becomes
Local::to_js_string_view (the JSStringView guard keeps the cell alive),
and to_slice_or_null collapses into to_slice. OwnedUrl is retired:
main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and
js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's
text (the strings module is gone, names are &'static str) with the 14
host fns scoped and rustfmt applied; jest.rs and expect.rs take main's
literals under this PR's wrapping. CachedStructure keeps main's
assume_init_mut / drop_in_place sequence over this PR's slice-taking
create_structure. UDP address getters add the ? main's create_sock_addr
now needs. Scope-escape limits drop by one in BunObject, node_util_binding
and server_body and by two in FormData (hatches replaced by scoped calls).

Twenty-first rebase (7 more commits, onto 8335017): one import-line
conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc
trait import next to this PR's scoped imports. Both kept; no inventory
changes.

Twenty-second rebase (6 more commits, onto 0823e50): 39 files
conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's
types replace the PR's spellings inside scoped bodies: Local::to_slice is
now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and
create_utf8_for_js calls become scope.string_utf8 / scope.string, and
ScopedStringOrBuffer names StringOrBuffer<'static>. Main's
owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so
src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and
TextDecoder.rs are main's again. Scope-escape limits drop in
filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to
9).

Twenty-third rebase (9 more commits, onto adc354d): two files.
FileSystemRouter::routes takes #40410's fallible JSValue::from_entries
(mapped into the scope), and advanceTimersByTime keeps #40414's NaN
check and main's message text under the scoped throws. The
jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers
entry).

Twenty-fourth rebase (9 more commits, onto 82123d3): six files, all
with #40478 (RefPtr releases on Drop). This PR's StoreRef::adopt is
retired: main's RefPtr<Store> is the same owning handle, so
webcore_types.rs is main's again and store_backed_buffer_to_js moves a
RefPtr<Store> into the JS object as the *_from_owner owner (the view
closure reaches the bytes through Store::data_mut). The sql event-loop
guard keeps this PR's safe EventLoop::scope under main's renamed ref
guard; expect.rs keeps this PR's wrapping over main's RefPtr comments.
The vm-thread-door inventory follows main's StoreRef-to-Store rename.

Twenty-fifth rebase (23 more commits, onto 0e395c2): four files, all
with #40511 (async fs calls no longer pin Buffer paths). pbkdf2 and
scrypt take main's from_js_async parsers (ThreadIsolated params) under
the scoped signatures, StringOrBuffer keeps main's from_js_async next to
this PR's from_js_scoped / from_js_deferred, and the
BlobOrStringOrBuffer::from_js_async this PR's insertion sat beside is
gone with main. Import merges in node.rs and MarkdownObject.rs. The
vm-thread-door inventory follows main's ThreadSafe-to-ThreadIsolated
rename.

Twenty-sixth rebase (8 more commits, onto 72ffcd8): one import-line
conflict in ffi_body.rs, where #40592 added ErrorCode next to this PR's
scoped imports. Both kept; no inventory changes.
robobun pushed a commit that referenced this pull request Aug 28, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing
(the deallocator can run before Err, per #39558) and the cross-thread
timing this PR's Send bounds rely on.

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
the structure #39547 gave it (channel type check first, dial plus
send_rejection() before a listener is stored, no trailing else) with the
rejection and the new check spelled through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
the parameterless ReadableStream::done() and is_some() guard from #39732.

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
the bodies #39922 gave them (from_js also returns the callback, pbkdf2
returns undefined, length 6) under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
the defer-comment sweep (#40051): PasswordObject's verifySync keeps this
PR's deferred materialize of both arguments, and NodeHTTPResponse's
on_resolve keeps the scoped call, both without the removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.

Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the
exception checks that follow already-checked calls and made
JSString::to_slice / view, JSValue::get_zig_string and
handle_ipc_message return JsResult. Eight files conflicted inside scoped
bodies (BunObject, CryptoHasher, PasswordObject, ipc_host,
node_util_binding, server_body, expect, ObjectURLRegistry); main's
control flow is kept (the guards go, the ? is added) under the scoped
spellings. The four has_exception checks left in BunObject.rs are the
ones main kept (print_table / format2 swallow nested throws).

Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted,
nearly all with #40238 (bun_core::String owns its WTF ref). Main's
ownership idioms replace this PR's: OwnedString / scopeguard deref
wrappers and manual .deref() calls go (String drops its ref), into_js
replaces transfer_to_js (Scope::transfer_string now consumes the
String), JSValue::get_zig_string is gone so Local::get_zig_string becomes
Local::to_js_string_view (the JSStringView guard keeps the cell alive),
and to_slice_or_null collapses into to_slice. OwnedUrl is retired:
main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and
js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's
text (the strings module is gone, names are &'static str) with the 14
host fns scoped and rustfmt applied; jest.rs and expect.rs take main's
literals under this PR's wrapping. CachedStructure keeps main's
assume_init_mut / drop_in_place sequence over this PR's slice-taking
create_structure. UDP address getters add the ? main's create_sock_addr
now needs. Scope-escape limits drop by one in BunObject, node_util_binding
and server_body and by two in FormData (hatches replaced by scoped calls).

Twenty-first rebase (7 more commits, onto 8335017): one import-line
conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc
trait import next to this PR's scoped imports. Both kept; no inventory
changes.

Twenty-second rebase (6 more commits, onto 0823e50): 39 files
conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's
types replace the PR's spellings inside scoped bodies: Local::to_slice is
now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and
create_utf8_for_js calls become scope.string_utf8 / scope.string, and
ScopedStringOrBuffer names StringOrBuffer<'static>. Main's
owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so
src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and
TextDecoder.rs are main's again. Scope-escape limits drop in
filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to
9).

Twenty-third rebase (9 more commits, onto adc354d): two files.
FileSystemRouter::routes takes #40410's fallible JSValue::from_entries
(mapped into the scope), and advanceTimersByTime keeps #40414's NaN
check and main's message text under the scoped throws. The
jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers
entry).

Twenty-fourth rebase (9 more commits, onto 82123d3): six files, all
with #40478 (RefPtr releases on Drop). This PR's StoreRef::adopt is
retired: main's RefPtr<Store> is the same owning handle, so
webcore_types.rs is main's again and store_backed_buffer_to_js moves a
RefPtr<Store> into the JS object as the *_from_owner owner (the view
closure reaches the bytes through Store::data_mut). The sql event-loop
guard keeps this PR's safe EventLoop::scope under main's renamed ref
guard; expect.rs keeps this PR's wrapping over main's RefPtr comments.
The vm-thread-door inventory follows main's StoreRef-to-Store rename.

Twenty-fifth rebase (23 more commits, onto 0e395c2): four files, all
with #40511 (async fs calls no longer pin Buffer paths). pbkdf2 and
scrypt take main's from_js_async parsers (ThreadIsolated params) under
the scoped signatures, StringOrBuffer keeps main's from_js_async next to
this PR's from_js_scoped / from_js_deferred, and the
BlobOrStringOrBuffer::from_js_async this PR's insertion sat beside is
gone with main. Import merges in node.rs and MarkdownObject.rs. The
vm-thread-door inventory follows main's ThreadSafe-to-ThreadIsolated
rename.

Twenty-sixth rebase (8 more commits, onto 72ffcd8): one import-line
conflict in ffi_body.rs, where #40592 added ErrorCode next to this PR's
scoped imports. Both kept; no inventory changes.

Twenty-seventh rebase (24 more commits, onto 49ff888): five files, all
with #40516 (refcounted types own their teardown). The serve-plugins
.then callbacks adopt their ref through main's RefPtr::from_raw under the
scoped argument spellings (this PR's ServePluginsRef guard is gone with
main's newtypes), FileSink keeps this PR's with_mut spelling over main's
RefPtr<FileSink> construction (create is main's one-liner), the
StatWatcher deinit hook stays deleted next to the scoped do_ref, and
ipc_host.rs / socket_body.rs are import and return-spelling merges. No
inventory changes.
robobun pushed a commit that referenced this pull request Aug 28, 2026
…rship contracts compile errors

Rebased onto main as a single commit; the branch history (with its merge
commits) is not preserved.

Adds a zero-cost branded scope layer over the raw JSC FFI (`src/jsc/scope.rs`:
`Scope<'s>`, `Local<'s>`) that makes two boundary bug classes compile
errors for code that stays on the scoped API:

- a JS value escaping its host call unrooted (persisting requires the
  explicit `Scope::persist` -> `Strong`);
- a JS-heap view (`Local::array_buffer_bytes`) held across an operation
  that can re-enter user JS (`&mut Scope`), e.g. a coercion that detaches
  the buffer.

Codegen integration:

- `#[bun_jsc::host_fn(scoped)]`: functions written as
  `fn(scope: &mut Scope, callframe) -> JsResult<Local>` get a
  macro-synthesized wrapper under their original name and unscoped
  signature, so js2native / `.classes.ts` / direct-call wiring stays
  byte-compatible. User `cfg`/doc/lint attributes propagate to the public
  wrapper and the extern shims.
- `ZIG_EXPORT(tag, reenters_js | no_user_js)` effect markers on the C++
  declarations; explicitly classified functions get branded wrappers
  generated into `bun_jsc::cpp::scoped` (`&mut Scope` / `&Scope`),
  unclassified and `null_is_throw` functions get none. All classified
  exports are verified against their C++ (`toMatch` and `putMayBeIndex`
  are `reenters_js`: a non-uint32 `lastIndex` goes through ToNumber, and
  index puts on exotic receivers reach `defineOwnProperty` traps).

Migration: ~470 host functions are converted to the scoped form
(behavior-preserving); the remaining escape hatches
(`unscoped_global()` / `unscoped_bun_vm()` / `.unscoped()` and unscoped
`#[host_fn]`s) are pinned per file by
`test/internal/source-lints/scope-escapes.test.ts`.

Also expresses the `make_*_with_bytes_no_copy` "pointer stays valid until
the deallocator runs" contracts as ownership transfer
(`typed_array_from_owned_slice` / `_from_vec` / `_from_owner`,
`ForeignBytes`, `external_string_from_utf16`, `OwnedUrl`,
`OwnedTextCodec`, `StoreRef::adopt`, `bun_sys::Mmap`,
`EventLoop::scope`), replacing hand-paired create/destroy and
leak-and-remember-to-free code paths.

The static `Bun.CryptoHasher.hash` / `Bun.password.verifySync` argument
detach bugs that motivated the layer were fixed independently on main
(#36165) by coercing every argument first; here the same behavior is
expressed through deferred `materialize` under the shared scope borrow,
so reordering the view capture before a coercion is a borrow error. Both
main's regression tests and the layer's suites pass.

Rebase onto main (461 commits): 40 files conflicted; resolved by taking
main's text and re-applying only the scope transformation. Changes that
main made obsolete were dropped (TextEncoderStreamEncoder host fns,
EventLoop::with_pipe_read_buffer, JSC__JSMap__size global arg, the
sendHelperChild scoping). Follow-ups main's newer code required:
JSValue::create_buffer_from_foreign now returns JsResult (the binding
became fallible on main), ArrayBufferSink::end_from_js uses
or_pending_exception (empty-jsvalue-laundering lint), TextDecoder
createForStream uses struct update syntax (clippy, since the PR removes
TextDecoder's Drop impl), ForeignBytes recorded in the vm-thread-door
inventory, scope-escape limits regenerated, and the ratchet's
regeneration mode is gated on an explicit --update flag.

Second rebase (30 more commits, onto 8bc4d2a): two conflicts from the
zero-fill removal (#39417). The zstd sync functions keep main's Failure
enum and create_buffer_from_box behind the scoped signatures, and the
latin1 TextDecoder path keeps main's uninitialized Vec but hands it to
JSC through external_string_from_utf16_vec instead of the raw
to_external_u16, matching the file's other two decode paths.

Third rebase (6 more commits, onto 0002bf8): conflicts were all with the
dead-code sweeps (#39420, #39448). Dropped the scoping of things main
deleted (Bun.nanoseconds' host fn, ArrayBufferSink::to_js, the
unreachable csrf error arm, three unused node:: re-exports) and kept the
ownership refactor of ArrayBufferSink::end_from_js. Inventories
regenerated; the jsresult-swallow one also picks up a count #39448 left
stale on main.

Fourth rebase (17 more commits, onto 258517a): do_publish keeps #39389's
shape (topic JSString held and ensure_still_alive'd across the message
conversion) under the scoped signature, and the valkey publish scoping
sits after the command block #29339 added.

Fifth rebase (42 more commits, onto 6948a12): OwnedTextCodec is gone
with the WebKit codecs (#39485); TextDecoder keeps main's encoding_rs
path and this PR's external_string_from_utf16_vec hand-offs, including
on the new path. ArrayBuffer::from_owned_bytes stays deleted (no callers;
its u32 cast that #39558 fixed never existed in the replacements), and
the no-copy deallocator contract now states both the Err-path timing
(the deallocator can run before Err, per #39558) and the cross-thread
timing this PR's Send bounds rely on.

Sixth rebase (18 more commits, onto 681a49b): three dead-code conflicts.
The scoped js_assert_settings goes away with the native assertSettings
(#38900, node:http2 validates in JS now), ParseArgumentsCfg's unused
Default impl stays removed (#39585), and TimeoutObject keeps main's
generated cached-accessor import next to the scoped imports.

Seventh rebase (14 more commits, onto 32e8703): valkey subscribe() keeps
the structure #39547 gave it (channel type check first, dial plus
send_rejection() before a listener is stored, no trailing else) with the
rejection and the new check spelled through the scope.

Eighth rebase (36 more commits, onto 56c4e3d): the three expect matcher
utils take #36912's propagating print_value; the conflict was only the
line wrapping. memory_pressure.rs (new on main) is added to the
scope-escape limits.

Ninth rebase (17 more commits, onto 72ec6e2, which includes the #39839
build fix): FileSink::on_close combines this PR's with_mut probe with
the parameterless ReadableStream::done() and is_some() guard from #39732.

Tenth rebase (5 more commits, onto 4448a2e): the Windows cluster handle
path keeps #39804's `?` on attach_windows_socket_payload under the scoped
argument spelling.

Eleventh rebase (7 more commits, onto a21f02a): pbkdf2/pbkdf2Sync take
the bodies #39922 gave them (from_js also returns the callback, pbkdf2
returns undefined, length 6) under the scoped signatures.

Twelfth rebase (16 more commits, onto 6fb7102): the safe vm_loop_ctx is
re-applied onto #40002's Cell-based upgrade client, including inside the
new clear_data's with_mut.

Thirteenth rebase (15 more commits, onto d0f6486): the assert binding is
scoped (5 hatches now, was 1); #39995's named-pipe live-count testing fn
is scoped like its neighbours.

Fourteenth rebase (12 more commits, onto e8300da): cron_remove's tail
takes #40024's safe ThisPtr start_linux call under the scoped return; the
rest of cron.rs's Cell/ThisPtr rewrite auto-merged. node_crypto_binding's
scope-escape limit rises by the two unscoped argon2 host fns #37015 added.

Fifteenth rebase (6 more commits, onto 1423031): two conflicts with
the defer-comment sweep (#40051): PasswordObject's verifySync keeps this
PR's deferred materialize of both arguments, and NodeHTTPResponse's
on_resolve keeps the scoped call, both without the removed defer comments.

Sixteenth rebase (31 more commits, onto 01008f8): the socket ref()/unref()
host fns take #39856's bodies (hold the loop while connecting, apply the
recorded state on open) under the scoped signatures.

Seventeenth rebase (8 more commits, onto 4bb20e5): #40055 made the
websocket upgrade client's loop-context plumbing safe itself (and dropped
the adapter), so this PR's vm_loop_ctx change there is retired and both
http_jsc files are main's.

Eighteenth rebase (8 more commits, onto 3e347b3): the rootError crash
hook keeps #37181's one-argument handle_root_error under the scoped
signature.

Nineteenth rebase (6 more commits, onto 7a5d837): #40251 removed the
exception checks that follow already-checked calls and made
JSString::to_slice / view, JSValue::get_zig_string and
handle_ipc_message return JsResult. Eight files conflicted inside scoped
bodies (BunObject, CryptoHasher, PasswordObject, ipc_host,
node_util_binding, server_body, expect, ObjectURLRegistry); main's
control flow is kept (the guards go, the ? is added) under the scoped
spellings. The four has_exception checks left in BunObject.rs are the
ones main kept (print_table / format2 swallow nested throws).

Twentieth rebase (9 more commits, onto f2fe7d3): 32 files conflicted,
nearly all with #40238 (bun_core::String owns its WTF ref). Main's
ownership idioms replace this PR's: OwnedString / scopeguard deref
wrappers and manual .deref() calls go (String drops its ref), into_js
replaces transfer_to_js (Scope::transfer_string now consumes the
String), JSValue::get_zig_string is gone so Local::get_zig_string becomes
Local::to_js_string_view (the JSStringView guard keeps the cell alive),
and to_slice_or_null collapses into to_slice. OwnedUrl is retired:
main's whatwg::Parsed is the same RAII handle, so src/jsc/URL.rs and
js_valkey.rs are main's again. ScopeFunctions.rs is rebuilt from main's
text (the strings module is gone, names are &'static str) with the 14
host fns scoped and rustfmt applied; jest.rs and expect.rs take main's
literals under this PR's wrapping. CachedStructure keeps main's
assume_init_mut / drop_in_place sequence over this PR's slice-taking
create_structure. UDP address getters add the ? main's create_sock_addr
now needs. Scope-escape limits drop by one in BunObject, node_util_binding
and server_body and by two in FormData (hatches replaced by scoped calls).

Twenty-first rebase (7 more commits, onto 8335017): one import-line
conflict in node_fs_binding.rs, where #38383 added the SystemErrorJsc
trait import next to this PR's scoped imports. Both kept; no inventory
changes.

Twenty-second rebase (6 more commits, onto 0823e50): 39 files
conflicted, all with #40374 (Utf8Bytes<'a> / EncodedSlice<'a>). Main's
types replace the PR's spellings inside scoped bodies: Local::to_slice is
now Local::to_utf8 (Utf8Bytes<'static>), ZigString::init(..).to_js and
create_utf8_for_js calls become scope.string_utf8 / scope.string, and
ScopedStringOrBuffer names StringOrBuffer<'static>. Main's
owned_utf16_into_js supersedes this PR's external_string_from_utf16*, so
src/jsc/ZigString.rs stays deleted and bun_string_jsc.rs and
TextDecoder.rs are main's again. Scope-escape limits drop in
filesystem_router (13 to 7), server_body (17 to 15) and Listener (11 to
9).

Twenty-third rebase (9 more commits, onto adc354d): two files.
FileSystemRouter::routes takes #40410's fallible JSValue::from_entries
(mapped into the scope), and advanceTimersByTime keeps #40414's NaN
check and main's message text under the scoped throws. The
jsresult-swallow inventory is main's again (#40410 fixed the FakeTimers
entry).

Twenty-fourth rebase (9 more commits, onto 82123d3): six files, all
with #40478 (RefPtr releases on Drop). This PR's StoreRef::adopt is
retired: main's RefPtr<Store> is the same owning handle, so
webcore_types.rs is main's again and store_backed_buffer_to_js moves a
RefPtr<Store> into the JS object as the *_from_owner owner (the view
closure reaches the bytes through Store::data_mut). The sql event-loop
guard keeps this PR's safe EventLoop::scope under main's renamed ref
guard; expect.rs keeps this PR's wrapping over main's RefPtr comments.
The vm-thread-door inventory follows main's StoreRef-to-Store rename.

Twenty-fifth rebase (23 more commits, onto 0e395c2): four files, all
with #40511 (async fs calls no longer pin Buffer paths). pbkdf2 and
scrypt take main's from_js_async parsers (ThreadIsolated params) under
the scoped signatures, StringOrBuffer keeps main's from_js_async next to
this PR's from_js_scoped / from_js_deferred, and the
BlobOrStringOrBuffer::from_js_async this PR's insertion sat beside is
gone with main. Import merges in node.rs and MarkdownObject.rs. The
vm-thread-door inventory follows main's ThreadSafe-to-ThreadIsolated
rename.

Twenty-sixth rebase (8 more commits, onto 72ffcd8): one import-line
conflict in ffi_body.rs, where #40592 added ErrorCode next to this PR's
scoped imports. Both kept; no inventory changes.

Twenty-seventh rebase (24 more commits, onto 49ff888): five files, all
with #40516 (refcounted types own their teardown). The serve-plugins
.then callbacks adopt their ref through main's RefPtr::from_raw under the
scoped argument spellings (this PR's ServePluginsRef guard is gone with
main's newtypes), FileSink keeps this PR's with_mut spelling over main's
RefPtr<FileSink> construction (create is main's one-liner), the
StatWatcher deinit hook stays deleted next to the scoped do_ref, and
ipc_host.rs / socket_body.rs are import and return-spelling merges. No
inventory changes.

Twenty-eighth rebase (36 more commits, onto 69c6138): one import-line
conflict in csrf_jsc.rs, where #40697 added IntegerRange next to this
PR's scoped imports. Both kept; no inventory changes.
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.

2 participants