Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions crates/perry-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,22 @@ crate-type = ["rlib"]
default = ["full", "regex-engine", "temporal", "url-engine", "string-normalize", "intl-segmenter", "intl-namespace", "global-math", "global-json", "global-reflect", "global-atomics", "global-url", "global-text", "global-websocket", "global-webcrypto", "global-webfetch", "proc-ipc", "intl-locale", "intl-datetime", "diagnostics", "mod-dgram", "mod-http2-constants", "mod-node-test", "dyn-eval", "keepalive-anchors", "alloc-mimalloc"]
# Compile the ~490 `#[used]` keep-alive anchor statics (`KEEP_*`, `keep!`,
# `K*`/`G*` fn-pointer statics) that pin codegen-facing `#[no_mangle]` entry
# points as dead-strip roots. They exist for the whole-program bitcode-LTO
# link (`PERRY_LLVM_BITCODE_LINK=1`), where the merged module could otherwise
# internalize + drop symbols only referenced from the user's separate `.o`.
# The CLASSIC link path never needs them: every reachable runtime symbol is
# kept by a real undefined reference from the program's objects, and the
# anchors only defeat `-dead_strip` (measured: a hello-world binary carries
# the entire fs/child_process/vm/node_submodules surface through ONE anchored
# chain — KEEP_JS_MODULE_DYNAMIC_IMPORT_DEFERRED → dynamic-import fallback →
# js_nm_install_all → every dispatch bucket). Kept in `default` so the
# prebuilt/full archives and `cargo test` are unchanged; the auto-optimize
# rebuild omits it unless the bitcode link was requested.
# points so they survive into the staticlib archive. Without these anchors,
# rustc dead-code-eliminates `#[no_mangle] pub extern "C" fn` symbols that are
# only called from the program's generated object files (not from within the
# perry-runtime crate itself), producing an incomplete `libperry_runtime.a`
# that fails to link with "Undefined symbols" when the codegen emits calls to
# them (e.g. `js_box_release`, `js_bool_box_release`,
# `js_closure_set_box_capture_ptr`, `js_link_path_module_parent`).
#
# In a staticlib archive (`.a`) the linker only pulls in object files that
# resolve an undefined reference, so `#[used]` anchors only become
# `-dead_strip` roots when their object file is pulled in — i.e. when the
# program actually references a symbol from that unit. The size cost is
# therefore limited to the transitive callees of symbols the program uses
# (which would be kept anyway), not the entire runtime surface. Kept in
# `default` so the prebuilt/full archives and `cargo test` are unchanged;
# the auto-optimize rebuild always re-adds it (see optimized_libs.rs).
keepalive-anchors = []
# Route every Rust heap allocation through mimalloc (#62) — ~3-5× faster
# per-alloc than macOS `malloc` on hot paths, at ~140 KB of binary. In
Expand Down
40 changes: 30 additions & 10 deletions crates/perry/src/commands/compile/optimized_libs/freshness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ pub(crate) fn auto_optimized_cache_key(
""
}
),
std::env::var("PERRY_LLVM_BITCODE_LINK").ok().as_deref() == Some("1"),
// keepalive-anchors is now always enabled (see auto_optimized_cross_features),
// so the cache key always reflects anchors=true. The field stays in the key
// so a future change to gate it again would get its own cache dir.
true,
env!("CARGO_PKG_VERSION"),
)
}
Expand Down Expand Up @@ -286,15 +289,32 @@ pub(crate) fn auto_optimized_cross_features(
// system-allocator ranges, the feature stays force-on; the cfg gate in
// perry-runtime remains for that future audit.
cross_features.push("perry-runtime/alloc-mimalloc".to_string());
// The `#[used]` keep-alive anchors exist for the whole-program bitcode
// LTO path only (see perry-runtime's `keepalive-anchors` feature docs).
// The classic link keeps every reachable symbol via real undefined
// references, so the anchors are omitted there — that is what lets
// `-dead_strip` drop the never-imported node-module surface from small
// programs. Re-enable them whenever the bitcode link was requested.
if std::env::var("PERRY_LLVM_BITCODE_LINK").ok().as_deref() == Some("1") {
cross_features.push("perry-runtime/keepalive-anchors".to_string());
}
// `#[used]` keep-alive anchors must be present on EVERY auto-optimize
// rebuild, not just the bitcode-LTO path. The anchors pin codegen-only
// `#[no_mangle] pub extern "C" fn` symbols (e.g. `js_box_release`,
// `js_bool_box_release`, `js_closure_set_box_capture_ptr`,
// `js_link_path_module_parent`) that are never called from within the
// perry-runtime crate itself — only from the program's generated object
// files. Without `#[used]`, rustc dead-code-eliminates these unreferenced
// symbols during staticlib archive creation, so they are absent from
// `libperry_runtime.a` and the final link fails with "Undefined symbols
// for architecture arm64" when the program's codegen emits calls to them.
//
// #6917 gated the anchors behind `keepalive-anchors` (bitcode-LTO only)
// under the assumption that "the classic link keeps every reachable
// runtime symbol via real undefined references from the program's
// objects." That assumption is wrong: the program's undefined references
// can only resolve symbols that are IN the archive, and the archive
// doesn't contain symbols that were DCE'd during archive creation.
//
// In a staticlib archive (`.a`) the linker only pulls in object files
// that resolve an undefined reference, so `#[used]` anchors only become
// `-dead_strip` roots when their object file is pulled in — i.e. when
// the program actually references a symbol from that unit. The size
// regression is therefore limited to the transitive callees of symbols
// the program uses (which would be kept anyway), not the entire runtime
// surface. Re-enable unconditionally; the bitcode-LTO path already did.
cross_features.push("perry-runtime/keepalive-anchors".to_string());
// Compile OUT perry-runtime's no-op fetch stubs (`js_fetch_with_options` /
// `js_headers_new` / `js_request_new`, gated `#[cfg(not(feature =
// "external-fetch-symbols"))]`) whenever the program uses fetch — perry-stdlib's
Expand Down
54 changes: 54 additions & 0 deletions crates/perry/src/commands/compile/optimized_libs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,60 @@ fn retain_workspace_declared_features_keeps_all_without_manifests() {
assert_eq!(cross_features.len(), 2);
}

/// Regression: the auto-optimize rebuild must always include
/// `perry-runtime/keepalive-anchors` in the cross-feature set. #6917 gated
/// the `#[used]` keepalive anchors behind the feature and only enabled it for
/// the bitcode-LTO path, under the assumption that the classic link path
/// "keeps every reachable runtime symbol via real undefined references from
/// the program's objects." That assumption is wrong: `#[no_mangle] pub extern
/// "C" fn` symbols that are only called from codegen (not from within the
/// perry-runtime crate) are dead-code-eliminated during staticlib archive
/// creation when no `#[used]` anchor pins them. The resulting
/// `libperry_runtime.a` is missing core symbols (`js_box_release`,
/// `js_bool_box_release`, `js_closure_set_box_capture_ptr`,
/// `js_link_path_module_parent`) and programs whose codegen emits calls to
/// them fail to link with "Undefined symbols for architecture arm64."
#[test]
fn auto_optimize_always_includes_keepalive_anchors() {
let dir = tempfile::tempdir().expect("tempdir");
let empty_features: std::collections::BTreeSet<&'static str> =
std::collections::BTreeSet::new();
let ctx = CompilationContext::new(dir.path().to_path_buf());
let cross = auto_optimized_cross_features(&ctx, &empty_features, &[]);
assert!(
cross.iter().any(|f| f == "perry-runtime/keepalive-anchors"),
"auto_optimized_cross_features must always include \
perry-runtime/keepalive-anchors so codegen-only #[no_mangle] symbols \
survive into the staticlib archive, got {cross:?}"
);
}

/// The `keepalive-anchors` feature must NOT be conditional on
/// `PERRY_LLVM_BITCODE_LINK` — the classic link path needs it too.
#[test]
fn auto_optimize_keepalive_anchors_not_bitcode_only() {
let _guard = env_lock();
let old_bitcode = std::env::var_os("PERRY_LLVM_BITCODE_LINK");
std::env::remove_var("PERRY_LLVM_BITCODE_LINK");

let dir = tempfile::tempdir().expect("tempdir");
let empty_features: std::collections::BTreeSet<&'static str> =
std::collections::BTreeSet::new();
let ctx = CompilationContext::new(dir.path().to_path_buf());
let cross = auto_optimized_cross_features(&ctx, &empty_features, &[]);

set_env_var(
"PERRY_LLVM_BITCODE_LINK",
old_bitcode.as_deref().and_then(|v| v.to_str()),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

assert!(
cross.iter().any(|f| f == "perry-runtime/keepalive-anchors"),
"keepalive-anchors must be present even without PERRY_LLVM_BITCODE_LINK, \
got {cross:?}"
);
}

/// The well-known flip drops `compression-brotli`/`compression-zstd` from the
/// stdlib rebuild on a stated premise: "The ext crate carries all codecs, so
/// nothing is lost by dropping them here."
Expand Down
Loading