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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 9 additions & 10 deletions benchmarks/compiler_output/workloads.toml
Original file line number Diff line number Diff line change
Expand Up @@ -863,14 +863,13 @@ regex_none = [
detail = "fast clone loads raw f64 array slots without per-access guards or numeric coercion"

[[workloads.packed_f64_loop_versioning.ir_checks]]
name = "packed_f64_store_loop_not_cloned"
contains = "call i32 @js_typed_feedback_numeric_array_index_set_guard"
regex_none = [
'''packed_f64_loop_store\.fast''',
'''packed_f64_loop_store\.fallback''',
'''array\[packed_f64_loop\]=''',
name = "packed_f64_store_loop_guarded_clone"
section = "llvm_before"
regex_all = [
'''packed_f64_loop_store\.fast\.\d+:\n(?:(?!\n[^\s].*:)[\s\S])*?\bstore double\b''',
'''packed_f64_loop_store\.fallback\.\d+:\n(?:(?!\n[^\s].*:)[\s\S])*?\bbr label %packed_f64\.loop\.slow''',
]
detail = "store-bearing loops stay out of the packed-f64 clone; guarded numeric store fallback handles invalidation"
detail = "eligible store loops contain a raw-f64 fast store and a side exit to the ordinary guarded fallback"

[[workloads.packed_f64_loop_versioning.stdout_checks]]
name = "packed_f64_loop_versioning_checksum"
Expand All @@ -895,9 +894,9 @@ min = { load_f64 = 1 }
detail = "fast clone contains raw double loads for read-only packed loops"

[[workloads.packed_f64_loop_versioning.named_regions.checks]]
name = "packed_f64_fast_loop_no_fp_int_conversions"
max = { fptosi = 0, sitofp = 0, ptrtoint = 0 }
detail = "fast clone does not perform per-access numeric conversions"
name = "packed_f64_fast_loop_bounded_fp_int_conversions"
max = { fptosi = 0, sitofp = 3, ptrtoint = 0 }
detail = "fast clones avoid lossy conversions while permitting the source-required integer-index-to-number conversion in each materialized copy"

[workloads.packed_f64_loop_versioning.native_rep_checks]
allow_materialization_reasons = ["runtime_api", "return_abi"]
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/results/public-node-bun-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"perry_version": "perry 0.5.1258",
"generated_at": "2026-07-13T14:10:04Z",
"freshness": {
"source_fingerprint": "4ff76a2ff69d1cde787770e9ae81f615be56c74e9a1bcb208890810fd079799d",
"source_fingerprint": "9713a2e1131dbc407e2f6e7b056b9e2e1092ed45be7ecd5bf88b3d5a9910405f",
"harness_fingerprint": "437f64a8020aebd4d677405b4e5c8d915b2c76b10977df397ed324eef7b3ae23"
},
"host": {
Expand Down
1 change: 1 addition & 0 deletions changelog.d/6810-aot-runtime-fast-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fix(codegen/runtime): validate single-candidate dynamic imports after loader hooks, harden GC address classification, add transactional mixed-array numeric range updates, admit safely guarded packed numeric store loops, and avoid allocating a duplicate JSON tree during syntax validation.
103 changes: 8 additions & 95 deletions crates/perry-codegen/src/expr/dyn_extern_i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,101 +544,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
));
}

// Single-target fast path. Skip the runtime string compare
// — the static resolver already proved this is the only
// possible target.
if paths.len() == 1 {
// Evaluate the arg for side effects (most are pure but
// a template literal with computed parts can have e.g.
// function calls) and let registered module loader hooks
// observe/delegate the import. The statically known target
// still determines the namespace in Perry's compile-time graph.
let path_val = lower_expr(ctx, arg)?;
let _ = ctx.block().call(
DOUBLE,
"js_module_dynamic_import_apply_hooks",
&[(DOUBLE, &path_val)],
);
let path = &paths[0];
let target_prefix = ctx.dynamic_import_path_to_prefix.get(path).cloned();
// #1671: a dynamic import of a known node-submodule
// (`await import('hono/jsx/server')`) carries the sentinel
// prefix `__node_submod__<key>` instead of a compiled-module
// prefix. Build its namespace via `js_node_submodule_namespace`
// and resolve the promise with it (mirrors the static
// namespace-import path).
if let Some(prefix) = &target_prefix {
if let Some(key) = prefix.strip_prefix("__node_submod__") {
let key = key.to_string();
let submod_label = emit_string_literal_global(ctx, &key);
let submod_len = key.len();
let install_sym = crate::nm_install::nm_submod_install_symbol(&key);
let blk = ctx.block();
if let Some(s) = install_sym {
blk.call_void(s, &[]);
}
let ns_val = blk.call(
DOUBLE,
"js_node_submodule_namespace",
&[(PTR, &submod_label), (I32, &submod_len.to_string())],
);
let promise = blk.call(I64, "js_promise_resolved", &[(DOUBLE, &ns_val)]);
return Ok(nanbox_pointer_inline(blk, &promise));
}
// #1673: a dynamic import of a general native builtin
// (`await import('node:crypto')`) carries the sentinel
// prefix `__native_mod__<name>`. Build its namespace via
// `js_create_native_module_namespace` — the same
// NATIVE_MODULE_CLASS_ID object `require('node:crypto')`
// produces, whose member access dispatches natively at
// runtime — and resolve the promise with it.
if let Some(name) = prefix.strip_prefix("__native_mod__") {
let name = name.to_string();
let mod_label = emit_string_literal_global(ctx, &name);
let mod_len = name.len();
let blk = ctx.block();
if let Some(s) = crate::nm_install::nm_install_symbol(&name) {
blk.call_void(s, &[]);
}
let ns_val = blk.call(
DOUBLE,
"js_create_native_module_namespace",
&[(PTR, &mod_label), (I64, &mod_len.to_string())],
);
let promise = blk.call(I64, "js_promise_resolved", &[(DOUBLE, &ns_val)]);
return Ok(nanbox_pointer_inline(blk, &promise));
}
}
let blk = ctx.block();
let ns_val = match target_prefix {
Some(prefix) => {
// Issue #753: trigger the target's init before
// loading its namespace. For Eager targets the
// guard short-circuits; for Deferred targets
// this is the only invocation that populates
// `@__perry_ns_<prefix>`.
blk.call_void(&format!("{}__init", prefix), &[]);
blk.load(DOUBLE, &format!("@__perry_ns_{}", prefix))
}
None => {
// Driver didn't resolve this path to a target module —
// route through the runtime fallback (#6660: builtin
// specifiers resolve like Node, everything else rejects
// with `ERR_MODULE_NOT_FOUND` instead of the old
// literal-`undefined` rejection).
return Ok(blk.call(
DOUBLE,
"js_module_dynamic_import_fallback",
&[(DOUBLE, &path_val)],
));
}
};
let promise = blk.call(I64, "js_promise_resolved", &[(DOUBLE, &ns_val)]);
return Ok(nanbox_pointer_inline(blk, &promise));
}

// Multi-target: evaluate the runtime path string, then
// emit a chain of `js_string_equals` compares. Each
// Evaluate the runtime path string, apply registered loader hooks,
// then emit a chain of `js_string_equals` compares. Do this even
// for a single statically-resolved candidate: TypeScript types are
// erased at runtime and a hook may rewrite the specifier, so the
// candidate count does not prove that the runtime value matches.
// Skipping the compare here used to silently initialize the sole
// candidate for `load("./other.ts" as any)` and for hook redirects.
// Each
// successful compare resolves to its corresponding
// namespace global. The final fallback emits a rejected
// promise.
Expand Down
10 changes: 10 additions & 0 deletions crates/perry-codegen/src/runtime_decls/arrays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ pub fn declare_phase_b_arrays(module: &mut LlModule) {
module.declare_function("js_array_fill_f64_iota_extend", I64, &[I64, I32]);
module.declare_function("js_array_fill_f64_const_len_extend", I64, &[I64, DOUBLE]);
module.declare_function("js_array_fill_f64_iota_len_extend", I64, &[I64]);
module.declare_function(
"js_array_numeric_range_add",
I64,
&[DOUBLE, DOUBLE, DOUBLE, DOUBLE],
);
module.declare_function(
"js_array_numeric_range_add_len",
I64,
&[DOUBLE, DOUBLE, DOUBLE],
);
module.declare_function("js_array_set_string_key", I64, &[I64, I64, DOUBLE]);
module.declare_function("js_array_set_index_or_string", I64, &[I64, DOUBLE, DOUBLE]);
module.declare_function("js_array_mark_arguments_object", I64, &[I64]);
Expand Down
Loading
Loading