Skip to content

Merge train: #9703, #9705 (+plugin-lifetime fix), #9715 - #9723

Merged
proggeramlug merged 4 commits into
mainfrom
train117
Sep 4, 2026
Merged

Merge train: #9703, #9705 (+plugin-lifetime fix), #9715#9723
proggeramlug merged 4 commits into
mainfrom
train117

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

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 _static spelling 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 a dylib (codegen/entry.rs emits its perry_plugin_abi_version / plugin_activate shim) and perry_plugin_unload ends in dlclose. The emission was unconditional — string_pool.rs had no notion of output kind — and neither registry has an unregister path (perry_plugin_unload clears plugin hook registrations only). After an unload the maps retained (ptr, len) into unmapped memory, and the next fn.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_pool now picks the spelling from output_type — executables keep the borrow (all the volume: 72,713 registrations on the compiled cc TUI), dylib and staticlib copy. staticlib is included because its objects link into whatever consumes them, which may be a plugin. registration_spelling_follows_output_kind pins 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 which perry-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:pong and 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:http does not link from a cold auto-optimize cache — filed as #9719, pre-existing and unrelated (bun_server.rs untouched by this PR, identical reference counts on both branches, feature definitions identical, driver diff comment-only).

#9715React.Children.map(children, callback) miscompiled as Array.prototype.map (#9701). The fast path was selected on method name and arity alone, so children was read as the callback and threw object 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 (all RUST_TEST_THREADS=1), and issue_9701_nested_object_map — all green.

Ralph Küpper 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant