diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 77202c4f38..22c02475fb 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -1760,6 +1760,19 @@ pub fn compile_module(hir: &HirModule, opts: CompileOptions) -> Result> // disqualifies every type-directed unboxed access on a boxed slot in // one place; consumers fall back to the generic (box-aware) paths. 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 — + // `closure.rs` filters module globals OUT of `closure_captures`, so the + // closure is `alloc_singleton` with no capture slots. But advertising the + // local's type here made the typed-ABI specialization + // (`__typed_f64`/i32/…) read `js_closure_get_capture_bits(this, 0)` — an + // UNSET slot (0) — while the generic variant correctly loads the global; + // the dispatcher picked the typed body, so every closure returned 0. + // Repro (bisected to #5466 representation lowering): + // for (let i=0;i<5;i++){ const c=i; fns.push(()=>c); } // → 0,0,0,0,0 + // A module-global capture has no capture-slot representation, so — like a + // boxed slot — it must not feed the type-directed unboxed capture path. + module_local_types.retain(|id, _| !module_globals.contains_key(id)); // Cross-module function declares are emitted lazily by `lower_call` // via `FnCtx.pending_declares` (drained back into `llmod` at the diff --git a/crates/perry/tests/issue_5982_loop_capture_typed.rs b/crates/perry/tests/issue_5982_loop_capture_typed.rs new file mode 100644 index 0000000000..fde4ac5f79 --- /dev/null +++ b/crates/perry/tests/issue_5982_loop_capture_typed.rs @@ -0,0 +1,78 @@ +//! Regression test for #5982 (a #5466 representation-lowering regression): +//! a closure capturing a MODULE-LEVEL `const` bound to a typed value read +//! the wrong slot. +//! +//! `for (let i…) { const c = i; fns.push(() => c); }` returned `0,0,0,0,0` +//! instead of `0,1,2,3,4`. The captured module-level `c` is read by the +//! closure through `@perry_global_*` (module globals are filtered out of the +//! capture array), but its declared numeric type made the typed-ABI closure +//! specialization read `js_closure_get_capture_bits(this, 0)` — an unset slot +//! (0) — and the dispatcher picked that typed body. Module-global captures no +//! longer feed the type-directed unboxed capture path. + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +fn compile_and_run(dir: &std::path::Path, src: &str) -> String { + let entry = dir.join("main.ts"); + let out = dir.join("main_bin"); + std::fs::write(&entry, src).expect("write"); + let c = Command::new(perry_bin()) + .current_dir(dir) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&out) + .arg("--no-cache") + .output() + .expect("compile"); + assert!( + c.status.success(), + "compile failed\n{}", + String::from_utf8_lossy(&c.stderr) + ); + let r = Command::new(&out).current_dir(dir).output().expect("run"); + assert!( + r.status.success(), + "run failed\n{}", + String::from_utf8_lossy(&r.stderr) + ); + String::from_utf8_lossy(&r.stdout).into_owned() +} + +#[test] +fn loop_captured_module_const_reads_own_iteration_value() { + let dir = tempfile::tempdir().expect("tempdir"); + let out = compile_and_run( + dir.path(), + r#" +const fns: Array<() => number> = []; +for (let i = 0; i < 5; i++) { + const captured = i; + fns.push(() => captured); +} +console.log(fns[0](), fns[1](), fns[2](), fns[3](), fns[4]()); +"#, + ); + assert_eq!(out, "0 1 2 3 4\n"); +} + +#[test] +fn loop_direct_capture_of_let_var_still_works() { + let dir = tempfile::tempdir().expect("tempdir"); + let out = compile_and_run( + dir.path(), + r#" +const fns: Array<() => number> = []; +for (let i = 0; i < 5; i++) { + fns.push(() => i); +} +console.log(fns[0](), fns[1](), fns[2](), fns[3](), fns[4]()); +"#, + ); + assert_eq!(out, "0 1 2 3 4\n"); +}