Merge train: #9703, #9705 (+plugin-lifetime fix), #9715 - #9723
Merged
Conversation
added 4 commits
September 4, 2026 13:10
…orrowing entry points (#9188) Registering function metadata runs once per function a bundle CONTAINS — 72,713 of them on the compiled claude-code TUI — so what one call costs is a startup cost every program pays whether or not it ever reads a name. The copy had already been removed by storing `(ptr, len)` and borrowing the program image, which is sound only if the bytes outlive the PROCESS. That is strictly stronger than the "outlives the call" these entry points published, and `js_register_function_name` / `js_register_function_source` are `#[no_mangle] pub extern "C"` symbols reachable from separately-loaded provider images and from FFI, so it is not a promise that can be imposed on callers that already exist. #9188 was filed to make that a deliberate decision rather than a side effect of a perf commit. This is option 2 from the issue — split the entry points instead of retightening the contract: * `js_register_function_name` / `js_register_function_source` are back to their original contract: the bytes need only outlive the call, because the registry copies them. Every caller that is not codegen uses these. * `js_register_function_name_static` / `js_register_function_source_static` require process lifetime and store the borrowed slice. Codegen emits these, and only these, from `__perry_init_strings_<prefix>`, where the bytes are `@.str.N` `private unnamed_addr constant` globals in the image. All the volume is on the borrowing side, so the startup copy stays gone from the path that had it, and no published contract was tightened underneath a caller. Borrowed and owned bytes live in separate maps rather than one map of an enum: an enum value would add 8 bytes to every one of the ~60,000 borrowed entries to carry the handful of owned ones, which measured as a net loss (+0.31 MB). Owned entries take precedence on read, and the two locks are never held at the same time, so there is no acquisition order to get wrong. The registries move out of `formatting.rs` (26 lines under the 2,000-line cap) into a new `builtins/fn_metadata.rs`. The two copy tests are sabotage tests: they register from a heap buffer, overwrite it in place while it is still alive, and assert the registry still returns what was registered. Rewiring the copying entry points to borrow fails 3 of the 6 deterministically, instead of producing latent UB in a provider image. `codegen/emission_order_tests.rs`'s IR-text matchers were updated to the emitted spelling — left on the old name they would have matched nothing and passed vacuously. Both new symbols are added to `check_runtime_symbols.sh`, so a runtime archive predating the split fails there rather than at link time on a build worker. Claude-Session: https://claude.ai/code/session_01KL1tsB4oYnxRzF533NzHJF
Nested property receivers were optimistically treated as arrays based on the method name. React.Children.map(children, callback) therefore became an ArrayLikeMethod call and interpreted children as the callback.\n\nDefer callback-taking methods on unproven nested property receivers to dynamic dispatch while retaining specialization for statically typed Array fields.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge train: #9703, #9705, #9715. Validated once as a tree, rebase-merged so each commit keeps its author.
#9705 — function-metadata registration split, plus a follow-up fix. The PR restores the published contract of two
#[no_mangle] pub extern "C"symbols by adding separate borrowing spellings, rather than tightening the existing symbols in place — the right call, and the separate borrowed/owned maps are well argued with a measurement.Its
_staticspelling was unsound for plugin dylibs, and that is fixed here. The borrow rests on "those globals live for the life of the image", which equals process lifetime for an executable but not for a plugin: perry compiles TypeScript to adylib(codegen/entry.rsemits itsperry_plugin_abi_version/plugin_activateshim) andperry_plugin_unloadends indlclose. The emission was unconditional —string_pool.rshad no notion of output kind — and neither registry has an unregister path (perry_plugin_unloadclears plugin hook registrations only). After an unload the maps retained(ptr, len)into unmapped memory, and the nextfn.name/Function.prototype.toString()/ stack frame resolving one would read it; being address-keyed, a later image mapped over the range would collide silently instead of faulting.emit_string_poolnow picks the spelling fromoutput_type— executables keep the borrow (all the volume: 72,713 registrations on the compiled cc TUI),dylibandstaticlibcopy.staticlibis included because its objects link into whatever consumes them, which may be a plugin.registration_spelling_follows_output_kindpins both directions and was verified to fail against the unconditional emission before being committed.#9703 — extension keepalive/pump registry as the sole seam. Deletes the parallel
#[cfg(feature = "external-*-pump")]arms so a prebuilt stdlib no longer needs to know whichperry-ext-*archives a program links.Because the failure mode is a silent hang rather than an error, this was held for end-to-end evidence beyond its registry unit tests. That evidence: one process running a zlib stream and a net server+client, both drained through the registry, printing
gzip:2:true/net:pongand terminating — a missing registration hangs.Scope of that evidence, stated plainly: it covers 2 of 5 registration paths (zlib, net). http-server, http-client and ws are not covered, because
node:httpdoes not link from a cold auto-optimize cache — filed as #9719, pre-existing and unrelated (bun_server.rsuntouched by this PR, identical reference counts on both branches, feature definitions identical, driver diff comment-only).#9715 —
React.Children.map(children, callback)miscompiled asArray.prototype.map(#9701). The fast path was selected on method name and arity alone, sochildrenwas read as the callback and threwobject is not a function. Unknown nested-property receivers now decline. The enumerated ambiguity list is a superset of the nine methods the guarded fast path can actually select.Validation
64/64 lint gates; release build;
perry-runtime,perry-stdlib,perry-codegen,perry-hir(allRUST_TEST_THREADS=1), andissue_9701_nested_object_map— all green.