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
10 changes: 9 additions & 1 deletion crates/perry-codegen/src/codegen/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,14 @@ pub(super) struct ModuleArtifactsCtx<'a> {
pub func_signatures: &'a HashMap<u32, (usize, bool, bool, bool)>,
pub func_synthetic_arguments: &'a std::collections::HashSet<u32>,
pub module_boxed_vars: &'a std::collections::HashSet<u32>,
/// Typed-ABI capture-representation oracle: module-wide `Stmt::Let` types
/// MINUS boxed ids (#5869). Only the typed closure clones read this.
pub module_local_types: &'a HashMap<u32, perry_types::Type>,
/// #6369: receiver-type oracle for closure bodies — the same module-wide
/// `Stmt::Let` types with no representation filtering, mirroring the
/// `module_global_types` seed that `compile_function` / `compile_method`
/// already use. Feeds `FnCtx.local_types` only.
pub module_receiver_types: &'a HashMap<u32, perry_types::Type>,
pub closure_rest_params: &'a HashMap<u32, usize>,
pub closure_synthetic_arguments: &'a std::collections::HashSet<u32>,
pub closure_rest_and_arguments: &'a std::collections::HashSet<u32>,
Expand Down Expand Up @@ -194,6 +201,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
func_synthetic_arguments,
module_boxed_vars,
module_local_types,
module_receiver_types,
closure_rest_params,
closure_synthetic_arguments,
closure_rest_and_arguments,
Expand Down Expand Up @@ -277,7 +285,7 @@ pub(super) fn emit_module_artifacts(c: ModuleArtifactsCtx<'_>) -> Result<()> {
func_synthetic_arguments,
module_prefix,
module_boxed_vars,
module_local_types,
module_receiver_types,
closure_rest_params,
cross_module,
)
Expand Down
10 changes: 8 additions & 2 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,11 @@ pub(super) fn compile_closure(
func_synthetic_arguments: &std::collections::HashSet<u32>,
module_prefix: &str,
module_boxed_vars: &std::collections::HashSet<u32>,
module_local_types: &HashMap<u32, perry_types::Type>,
// #6369: receiver-type oracle (module-wide `Stmt::Let` types, unfiltered).
// Seeds `FnCtx.local_types` so a binding captured from an enclosing scope
// keeps its declared type at its read sites. NOT the typed-ABI capture
// map — the typed closure clones take `module_local_types` instead.
module_receiver_types: &HashMap<u32, perry_types::Type>,
closure_rest_params: &HashMap<u32, usize>,
cross_module: &CrossModuleCtx,
) -> Result<()> {
Expand Down Expand Up @@ -618,7 +622,7 @@ pub(super) fn compile_closure(
// typed fast path and return undefined.
let mut local_types: HashMap<u32, perry_types::Type> =
params.iter().map(|p| (p.id, p.ty.clone())).collect();
for (id, ty) in module_local_types.iter() {
for (id, ty) in module_receiver_types.iter() {
local_types.entry(*id).or_insert_with(|| ty.clone());
}

Expand Down Expand Up @@ -736,6 +740,8 @@ pub(super) fn compile_closure(
&cross_module.clamp3_functions,
&closure_boxed_vars,
module_globals,
// #6369: declared types of module-scope bindings this closure captures.
&local_types,
classes,
&cross_module.compile_time_constants,
&cross_module.module_dispatch,
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,9 @@ pub(super) fn compile_module_entry(
&cross_module.clamp3_functions,
&main_boxed_vars,
module_globals,
// Module scope IS this body: its `Stmt::Let`s are walked directly, so
// there is nothing to seed from an enclosing scope (#6369).
&HashMap::new(),
classes,
&cross_module.compile_time_constants,
&cross_module.module_dispatch,
Expand Down Expand Up @@ -1127,6 +1130,8 @@ pub(super) fn compile_module_entry(
&cross_module.clamp3_functions,
&init_boxed_vars,
module_globals,
// Module scope IS this body — see the `main` fact graph above (#6369).
&HashMap::new(),
classes,
&cross_module.compile_time_constants,
&cross_module.module_dispatch,
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,8 @@ pub(super) fn compile_function(
&cross_module.clamp3_functions,
&boxed_vars,
module_globals,
// #6369: declared types of module-scope bindings this body reads through.
&local_types,
classes,
&cross_module.compile_time_constants,
&cross_module.module_dispatch,
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ pub(super) fn compile_method(
&cross_module.clamp3_functions,
&method_boxed_vars,
module_globals,
// #6369: declared types of module-scope bindings this body reads through.
&local_types,
classes,
&cross_module.compile_time_constants,
&cross_module.module_dispatch,
Expand Down Expand Up @@ -1302,6 +1304,8 @@ pub(super) fn compile_static_method(
&cross_module.clamp3_functions,
&static_boxed_vars,
module_globals,
// #6369: declared types of module-scope bindings this body reads through.
&local_types,
classes,
&cross_module.compile_time_constants,
&cross_module.module_dispatch,
Expand Down
28 changes: 25 additions & 3 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1758,7 +1758,24 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>

// Module-wide boxed-var union + LocalId→Type map. See `boxed_locals`.
let module_boxed_vars = boxed_locals::collect_module_boxed_vars(hir);
let mut module_local_types = boxed_locals::collect_module_local_types(hir);
// #6369: the *receiver-type oracle* for closure bodies — every module-wide
// `Stmt::Let` type, with NO representation-driven filtering. `FnCtx.
// local_types` is what `static_type_of` / `is_array_expr` /
// `receiver_class_name` read to pick a specialized (guarded) access path,
// and a binding's declared type is a fact about its VALUE — it holds no
// matter whether the slot backing it is a plain alloca, a box cell, or a
// module global (every read routes through the matching load, and every
// specialized path is a runtime-guarded fast path with the generic
// fallback intact). `compile_function` / `compile_method` already seed
// `local_types` from the unfiltered `module_global_types`; closures are
// the outlier, and the two filters below (both aimed squarely at the
// typed-ABI *capture representation*) were silently dropping the type of
// every captured binding from the closure oracle too — so a captured
// `number[]` reached `arr[i]` as an unknown receiver and fell all the way
// to `js_dyn_index_get` (27× slower than the same array passed as a
// parameter, and no faster than an untyped array).
let module_receiver_types = boxed_locals::collect_module_local_types(hir);
let mut module_local_types = module_receiver_types.clone();
// #5869 residual: a BOXED local's slot holds a BOX POINTER, never the
// typed value — advertising its declared type to the typed-ABI layer
// made the typed closure specializations (typed_f64/i1/i32/string
Expand All @@ -1772,6 +1789,10 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// for closures inside labeled blocks.) Removing boxed ids here
// disqualifies every type-directed unboxed access on a boxed slot in
// one place; consumers fall back to the generic (box-aware) paths.
//
// #6369: scoped to the typed-ABI copy (like the module-globals filter
// below). The hazard is the unboxed *capture representation*, not the
// receiver type — see `module_receiver_types` above.
module_local_types.retain(|id, _| !module_boxed_vars.contains(id));
// #5982 (#5466 regression): a MODULE-GLOBAL captured local is read by a
// closure through `@perry_global_*`, NOT the closure's capture array —
Expand All @@ -1796,8 +1817,8 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
// `assert.throws(TypeError, () => arr.every())` then saw no exception
// (24 regressions: Array HOF + symbol-strict [[Set]], all via harness
// closures). Only the typed-ABI *specialization* decision needs the
// module-globals removed, so scope the filter to a dedicated copy and
// leave `module_local_types` (the receiver oracle) module-global-inclusive.
// module-globals removed, so scope the filter to a dedicated copy — the
// receiver oracle is `module_receiver_types` (#6369).
let typed_abi_local_types: std::collections::HashMap<u32, perry_types::Type> =
module_local_types
.iter()
Expand Down Expand Up @@ -2258,6 +2279,7 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result<Vec<u8>>
func_synthetic_arguments: &func_synthetic_arguments,
module_boxed_vars: &module_boxed_vars,
module_local_types: &module_local_types,
module_receiver_types: &module_receiver_types,
closure_rest_params: &closure_rest_params,
closure_synthetic_arguments: &closure_synthetic_arguments,
closure_rest_and_arguments: &closure_rest_and_arguments,
Expand Down
86 changes: 67 additions & 19 deletions crates/perry-codegen/src/collectors/hir_facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ pub(crate) fn collect_type_facts(
arg_dependent_clamp_fn_ids: &HashSet<u32>,
boxed_vars: &HashSet<u32>,
module_globals: &HashMap<u32, String>,
binding_types: &HashMap<u32, perry_types::Type>,
classes: &HashMap<String, &perry_hir::Class>,
compile_time_constants: &HashMap<u32, f64>,
module_dispatch: &super::ModuleDispatchFacts,
Expand All @@ -315,7 +316,8 @@ pub(crate) fn collect_type_facts(
arg_dependent_clamp_fn_ids,
);
let unsigned_i32_locals = super::i32_locals::collect_unsigned_i32_locals(stmts);
let (array_facts, effect_facts, materialization_hazards) = collect_array_facts(stmts, params);
let (array_facts, effect_facts, materialization_hazards) =
collect_array_facts(stmts, params, module_globals, binding_types);
let index_used_locals = super::index_uses::collect_index_used_locals(stmts);
let strictly_i32_bounded_locals = super::i32_locals::collect_strictly_i32_bounded_locals(
stmts,
Expand Down Expand Up @@ -419,6 +421,7 @@ pub(crate) fn collect_native_region_fact_graph(
arg_dependent_clamp_fn_ids: &HashSet<u32>,
boxed_vars: &HashSet<u32>,
module_globals: &HashMap<u32, String>,
binding_types: &HashMap<u32, perry_types::Type>,
classes: &HashMap<String, &perry_hir::Class>,
compile_time_constants: &HashMap<u32, f64>,
module_dispatch: &super::ModuleDispatchFacts,
Expand All @@ -431,6 +434,7 @@ pub(crate) fn collect_native_region_fact_graph(
arg_dependent_clamp_fn_ids,
boxed_vars,
module_globals,
binding_types,
classes,
compile_time_constants,
module_dispatch,
Expand All @@ -455,6 +459,7 @@ pub(crate) fn collect_hir_facts(
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
// No class table here, so no scalar-method summary can apply; the
// conservative default keeps it that way if one ever could.
&super::ModuleDispatchFacts::default(),
Expand Down Expand Up @@ -602,9 +607,12 @@ fn is_fresh_uint8array_length_literal(expr: &Expr) -> bool {
fn collect_array_facts(
stmts: &[Stmt],
params: &[perry_hir::Param],
module_globals: &HashMap<u32, String>,
binding_types: &HashMap<u32, perry_types::Type>,
) -> (ArrayFacts, EffectFacts, MaterializationHazardFacts) {
let mut collector = ArrayFactCollector::default();
collector.seed_params(params);
collector.seed_module_bindings(module_globals, binding_types);
collector.collect_stmts(stmts);
collector.finish()
}
Expand All @@ -616,11 +624,14 @@ struct ArrayFactCollector {
aliased_locals: HashSet<u32>,
length_mutation_locals: HashSet<u32>,
materialization_hazard_locals: HashSet<u32>,
/// #6011: param ids seeded purely from a declared `Packed*` array type.
/// A param can receive ANY array at runtime, so these seeds are only
/// versioning hints for the runtime-guard-validated packed loop matcher;
/// body-observed mutations still downgrade them like any tracked local.
param_seeded_locals: HashSet<u32>,
/// #6011/#6369: ids seeded purely from a *declared* `Packed*` array type —
/// function params, and module-scope bindings read from an enclosing scope.
/// Neither can be proven packed from this body alone (a param receives any
/// array at runtime; a module global can be rewritten by another function),
/// so these seeds are only versioning hints for the runtime-guard-validated
/// packed loop matcher; body-observed mutations still downgrade them like
/// any tracked local.
declared_type_seeded_locals: HashSet<u32>,
unknown_call_escape: bool,
async_microtask_escape: bool,
}
Expand All @@ -638,17 +649,51 @@ impl ArrayFactCollector {
if param.is_rest {
continue;
}
let kind = array_kind_from_declared_type(&param.ty);
if matches!(
kind,
ArrayKindFact::PackedI32 | ArrayKindFact::PackedU32 | ArrayKindFact::PackedF64
) {
self.local_kinds.insert(param.id, kind);
self.param_seeded_locals.insert(param.id);
self.seed_declared_array_type(param.id, &param.ty);
}
}

/// #6369: same declared-type seed for a MODULE-SCOPE binding this body only
/// *reads through* — `const prices: number[] = […]` at module scope, used
/// inside a function or closure. Without it the binding has no array fact at
/// all in this body, so the packed-numeric loop matchers reject it and a
/// captured `number[]` is stuck one tier below the identical array passed as
/// a parameter (which #6011 already seeds this exact way).
///
/// Soundness is the same as the param seed's, and rests on the same two
/// pillars: (1) the loop's entry guard
/// (`js_typed_feedback_packed_*_loop_guard`) re-validates the *actual*
/// runtime array — kind, packed-ness and the whole index window — and side
/// exits to the generic loop when it does not hold, so a wrong seed costs a
/// guard, never correctness; (2) the matched loop body is walked and admits
/// only pure element reads/writes and scalar updates — no call, no `await`,
/// no closure — so nothing reachable from the body can rebind the global or
/// change the array's shape between the guard and the last iteration. Any
/// mutation the body walk *does* see (push, alias, `length` write, identity
/// escape) downgrades the seed exactly like a `Stmt::Let`-declared array.
fn seed_module_bindings(
&mut self,
module_globals: &HashMap<u32, String>,
binding_types: &HashMap<u32, perry_types::Type>,
) {
for id in module_globals.keys() {
if let Some(ty) = binding_types.get(id) {
self.seed_declared_array_type(*id, ty);
}
}
}

fn seed_declared_array_type(&mut self, id: u32, ty: &perry_types::Type) {
let kind = array_kind_from_declared_type(ty);
if matches!(
kind,
ArrayKindFact::PackedI32 | ArrayKindFact::PackedU32 | ArrayKindFact::PackedF64
) {
self.local_kinds.insert(id, kind);
self.declared_type_seeded_locals.insert(id);
}
}

fn collect_stmts(&mut self, stmts: &[Stmt]) {
for stmt in stmts {
self.collect_stmt(stmt);
Expand Down Expand Up @@ -1179,15 +1224,16 @@ impl ArrayFactCollector {
self.unknown_call_escape = true;
let ids: Vec<u32> = self.local_kinds.keys().copied().collect();
for id in ids {
// #6011: param-seeded facts still lose their packed kind on an
// unknown call (conservative for every fact consumer), but do NOT
// gain a materialization hazard — params were never hazard-tracked
// before seeding existed, and hazards feed non-fact consumers
// #6011: declared-type-seeded facts still lose their packed kind on
// an unknown call (conservative for every fact consumer), but do NOT
// gain a materialization hazard — params (and, #6369, module-scope
// bindings) were never hazard-tracked before seeding existed, and
// hazards feed non-fact consumers
// (`array_length_receiver_is_loop_local`'s length-hoist gate) that
// must not regress for `i < param.length` loops in call-bearing
// bodies. Explicit hazards (freeze/defineProperty/identity escape
// on the param itself) still mark normally.
if !self.param_seeded_locals.contains(&id) {
// on the binding itself) still mark normally.
if !self.declared_type_seeded_locals.contains(&id) {
self.mark_array_materialization_hazard(id);
}
self.update_array_kind_for_local(id, ArrayKindFact::Unknown);
Expand Down Expand Up @@ -1663,6 +1709,7 @@ mod tests {
&HashSet::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&constants,
&crate::collectors::ModuleDispatchFacts::default(),
);
Expand Down Expand Up @@ -1754,6 +1801,7 @@ mod tests {
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&crate::collectors::ModuleDispatchFacts::default(),
);

Expand Down
16 changes: 15 additions & 1 deletion crates/perry-codegen/src/expr/literals_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,21 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
}
// Boxed local in enclosing function: load the slot (box
// pointer), deref via js_box_get_bits.
if ctx.boxed_vars.contains(id) {
//
// #6369: never for a MODULE GLOBAL. Its storage is the
// `@perry_global_*` cell, which holds the VALUE — `Stmt::Let`
// stores it there directly and never allocates a box — so a
// box deref here would reinterpret e.g. an array pointer as a
// box pointer. `LocalSet` and `Update` (below) already carry
// this exclusion; the read path was the odd one out, and it
// only stayed latent because a module global normally has no
// `ctx.locals` slot to find. The packed-loop invariant-global
// read cache installs exactly such a slot (`loops.rs` aliases
// the global into `ctx.locals` for the duration of the loop),
// so any module global that landed in the module-wide boxed
// union — which every closure inherits wholesale — was read
// back as garbage (`NaN`) there.
if ctx.boxed_vars.contains(id) && !ctx.module_globals.contains_key(id) {
if let Some(slot) = ctx.locals.get(id).cloned() {
let blk = ctx.block();
let box_ptr = blk.load(I64, &slot);
Expand Down
Loading
Loading