diff --git a/benchmarks/compiler_output/workloads.toml b/benchmarks/compiler_output/workloads.toml index bf0ce9180f..a88eaafbaf 100644 --- a/benchmarks/compiler_output/workloads.toml +++ b/benchmarks/compiler_output/workloads.toml @@ -261,7 +261,10 @@ required = true no_runtime_calls = true [[workloads.loop_data_dependent.named_regions.selectors]] -label_prefix_any = ["for.body.4", "arr.merge."] +# The measured loop's FP work lives in the numeric-array merge blocks. Do not +# select a `for.body.*` by its generated suffix: the preceding data-setup loop +# can receive that suffix and legitimately contains the one seed `sitofp`. +label_prefix_any = ["arr.merge."] counter_any_min = { fmul = 1, fadd = 1 } [[workloads.loop_data_dependent.named_regions.checks]] diff --git a/changelog.d/8342-release-gate-regressions.md b/changelog.d/8342-release-gate-regressions.md new file mode 100644 index 0000000000..da22f1675c --- /dev/null +++ b/changelog.d/8342-release-gate-regressions.md @@ -0,0 +1,14 @@ +### Fixed + +- `Uint8Array.prototype.map` and `toSorted` now keep their newly allocated + output Buffer live while a user callback runs. A callback-triggered full + mark-sweep could otherwise reclaim the output because its address existed + only in a Rust local, producing corrupt results in the GC representation + matrix when generational collection or write barriers were disabled. +- The compiled-package ambient `require()` regression test now expects the + Node-compatible `MODULE_NOT_FOUND` code that the runtime intentionally + returns for unresolved modules. +- The compiler-output gate now scopes the data-dependent numeric loop to its + array merge blocks. Its previous generated-label prefix also selected the + preceding setup loop and rejected that loop's legitimate one-time integer + conversion. diff --git a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs index 2321da2c9d..4d9445b49c 100644 --- a/crates/perry-codegen/src/expr/dyn_extern_i18n.rs +++ b/crates/perry-codegen/src/expr/dyn_extern_i18n.rs @@ -63,8 +63,8 @@ pub(crate) fn namespace_value_for_prefix(ctx: &mut FnCtx<'_>, prefix: &str) -> S /// returns the target **namespace value directly** (no Promise wrap) and uses the /// ambient createRequire-backed require (`js_module_ambient_require_apply`) as the /// unresolved / no-match fallthrough instead of a rejected promise — so builtins -/// keep resolving by string and unknown packages throw the descriptive -/// `ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE`. `paths` is populated by the same +/// keep resolving by string and unknown packages throw Node-compatible +/// `MODULE_NOT_FOUND`. `paths` is populated by the same /// `collect_modules` resolver as `import()`. fn lower_dynamic_require(ctx: &mut FnCtx<'_>, paths: &[String], arg: &Expr) -> Result { // Empty `paths` → genuinely runtime-computed specifier (didn't const-fold). diff --git a/crates/perry-hir/src/lower/expr_call/intrinsics/require.rs b/crates/perry-hir/src/lower/expr_call/intrinsics/require.rs index 734b30b587..2e17563f2e 100644 --- a/crates/perry-hir/src/lower/expr_call/intrinsics/require.rs +++ b/crates/perry-hir/src/lower/expr_call/intrinsics/require.rs @@ -110,8 +110,8 @@ pub(crate) fn try_require_literal( /// import edge. Codegen then dispatches to the matching compiled-module /// namespace **synchronously** (no Promise), with the Tier-1 ambient /// createRequire-backed `require` as the no-match / unresolved fallthrough -/// (builtins resolve by string; unknown packages throw the descriptive -/// `ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE`). +/// (builtins resolve by string; unknown packages throw Node-compatible +/// `MODULE_NOT_FOUND`). /// /// Gated to external modules: in first-party source a bare `require` keeps the /// deliberate compile-time behavior (#668). Returns `Some(expr)` when matched. diff --git a/crates/perry-hir/src/lower/lower_expr/arm_ident.rs b/crates/perry-hir/src/lower/lower_expr/arm_ident.rs index a63fdc7ded..04228ae49f 100644 --- a/crates/perry-hir/src/lower/lower_expr/arm_ident.rs +++ b/crates/perry-hir/src/lower/lower_expr/arm_ident.rs @@ -203,8 +203,8 @@ pub(crate) fn lower_ident_expr(ctx: &mut LoweringContext, ident: &ast::Ident) -> // and throw `ReferenceError: require is not defined`. Bind a // bare unshadowed `require` to a real createRequire-backed // closure instead — builtins (`node:os`, …) resolve by string; - // package/file specifiers throw the descriptive - // ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE. Reaching this arm means + // unresolved package/file specifiers throw Node-compatible + // MODULE_NOT_FOUND. Reaching this arm means // `require` is unshadowed (a local/func/imported/native binding // would have matched an earlier arm). Gated to external modules: // in first-party source the bare-require compile error (#668) diff --git a/crates/perry-runtime/src/module_require.rs b/crates/perry-runtime/src/module_require.rs index 6b3f2ffe1f..ab4db40df7 100644 --- a/crates/perry-runtime/src/module_require.rs +++ b/crates/perry-runtime/src/module_require.rs @@ -1143,9 +1143,8 @@ pub extern "C" fn js_require_json_disk(specifier: f64) -> f64 { /// defined`. This returns the same `createRequire`-backed closure as /// `js_module_create_require`, but takes no base argument (it is produced where a /// bare `require` identifier appears, not from an explicit `createRequire(base)`). -/// Builtins resolve by string today; package/file specifiers throw the descriptive -/// `ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE` until Tier 2 lands static package -/// resolution. +/// Builtins resolve by string; unresolved package/file specifiers throw Node's +/// `MODULE_NOT_FOUND` error code. #[no_mangle] pub extern "C" fn js_module_ambient_require() -> f64 { let base = std::env::current_dir() @@ -1167,9 +1166,8 @@ static KEEP_JS_MODULE_AMBIENT_REQUIRE: extern "C" fn() -> f64 = js_module_ambien /// not const-fold to a compiled-module target, the dynamic-require dispatch calls /// this with the runtime specifier value: it resolves exactly like a /// createRequire-backed `require(spec)` — builtins (`node:os`, …) by string, -/// unknown package/file specifiers throw the descriptive -/// `ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE`. Returns the required value directly -/// (no Promise). +/// unknown package/file specifiers throw Node-compatible `MODULE_NOT_FOUND`. +/// Returns the required value directly (no Promise). #[no_mangle] pub extern "C" fn js_module_ambient_require_apply(spec: f64) -> f64 { require_thunk(std::ptr::null(), spec) diff --git a/crates/perry-runtime/src/object/typed_array_proto_thunks.rs b/crates/perry-runtime/src/object/typed_array_proto_thunks.rs index 480efc99a5..bbefd1c703 100644 --- a/crates/perry-runtime/src/object/typed_array_proto_thunks.rs +++ b/crates/perry-runtime/src/object/typed_array_proto_thunks.rs @@ -758,14 +758,18 @@ pub(crate) unsafe fn dispatch_uint8_buffer_method( let cb = RootedCallback3::new(&scope, validate_callback(args)); // `uint8_alloc_like` allocates, so the receiver read is // sequenced AFTER it. - let out = uint8_alloc_like(recv.live().0, len) as usize; + // Keep the fresh output live while the callback can run a full + // mark-sweep. Unlike a copying minor, the full collector can + // reclaim an old-arena Buffer that exists only in a Rust local. + let out = RootedUint8Buffer::new(&scope, uint8_alloc_like(recv.live().0, len) as usize); let (addr, receiver) = recv.live(); + let out_addr = out.live().0; for i in 0..len { let value = uint8_get(addr, i) as f64; let mapped = cb.call(value, i as f64, receiver); - uint8_set(out, i, to_uint8(mapped)); + uint8_set(out_addr, i, to_uint8(mapped)); } - pointer_value(out) + out.live().1 } "filter" => { let cb = RootedCallback3::new(&scope, validate_callback(args)); @@ -918,11 +922,11 @@ pub(crate) unsafe fn dispatch_uint8_buffer_method( "sort" | "toSorted" => { let cmp = validate_comparator(args); let (addr, receiver) = recv.live(); - let out_addr = if method == "sort" { - addr - } else { - uint8_copy_to_new(addr) as usize - }; + // `toSorted`'s fresh Buffer must remain a GC root while the user + // comparator runs. The receiver root covers in-place `sort`. + let out = (method == "toSorted") + .then(|| RootedUint8Buffer::new(&scope, uint8_copy_to_new(addr) as usize)); + let out_addr = out.as_ref().map_or(addr, |root| root.live().0); let mut values: Vec = (0..len).map(|i| uint8_get(out_addr, i)).collect(); if cmp.is_null() { values.sort_unstable(); @@ -945,7 +949,7 @@ pub(crate) unsafe fn dispatch_uint8_buffer_method( if method == "sort" { receiver } else { - pointer_value(out_addr) + out.as_ref().expect("toSorted output root").live().1 } } "toReversed" => { diff --git a/crates/perry/src/commands/compile/collect_modules.rs b/crates/perry/src/commands/compile/collect_modules.rs index e6f908dc01..436f2a17cb 100644 --- a/crates/perry/src/commands/compile/collect_modules.rs +++ b/crates/perry/src/commands/compile/collect_modules.rs @@ -840,7 +840,7 @@ fn collect_module_one( // doesn't const-fold (and didn't glob-match above) falls back // to the Tier-1 ambient createRequire-backed `require` at // codegen — builtins resolve by string, unknown packages throw - // the descriptive ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE. Leave + // Node-compatible MODULE_NOT_FOUND. Leave // `paths` empty (no `deferred_error`); the empty-paths + // synchronous codegen arm emits the ambient require. This // never participates in the strict-dynamic-import hard error. diff --git a/crates/perry/tests/create_require_package.rs b/crates/perry/tests/create_require_package.rs index f8aff28be0..4dc828ef46 100644 --- a/crates/perry/tests/create_require_package.rs +++ b/crates/perry/tests/create_require_package.rs @@ -148,8 +148,8 @@ console.log("file:", localValue, localCall("C")); /// compiled `compilePackages` module must bind to a createRequire-backed closure /// instead of throwing `ReferenceError: require is not defined`. Builtins resolve /// by string, `typeof require` is "function", a non-builtin package specifier -/// throws the descriptive ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE (not a -/// ReferenceError), and a shadowing local `require` still wins. +/// throws Node-compatible MODULE_NOT_FOUND (not a ReferenceError), and a +/// shadowing local `require` still wins. #[test] fn ambient_require_in_compiled_package_resolves_builtins_without_reference_error() { let dir = tempfile::tempdir().expect("tempdir"); @@ -247,7 +247,7 @@ console.log(shadowed()); let stdout = String::from_utf8_lossy(&run.stdout); assert_eq!( stdout, - "typeof=function | builtin-ok | Error:ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE\nshadow:zzz\n" + "typeof=function | builtin-ok | Error:MODULE_NOT_FOUND\nshadow:zzz\n" ); } @@ -257,7 +257,7 @@ console.log(shadowed()); /// target namespace — reusing the dynamic-`import()` resolver but returning the /// value directly (no Promise). A specifier that does not const-fold falls back /// to the Tier-1 ambient require (builtins resolve by string; unknown packages -/// throw `ERR_PERRY_UNSUPPORTED_CREATE_REQUIRE`). +/// throw Node-compatible `MODULE_NOT_FOUND`). #[test] fn computed_require_const_folds_to_compiled_package_modules() { let dir = tempfile::tempdir().expect("tempdir"); diff --git a/tests/test_compiler_output_regression.py b/tests/test_compiler_output_regression.py index e40a7c408b..d60184ac20 100644 --- a/tests/test_compiler_output_regression.py +++ b/tests/test_compiler_output_regression.py @@ -758,11 +758,11 @@ def test_numeric_loop_does_not_require_typed_buffer_metadata(self): numeric_ir = """ define i32 @main() { entry: - br label %for.body.4 -for.body.4: + br label %arr.merge.4 +arr.merge.4: %x = fmul double %a, %b %y = fadd double %x, %c - br label %for.body.4 + br label %arr.merge.4 } """ report = HARNESS.verify_artifacts( @@ -784,11 +784,11 @@ def test_native_region_workloads_require_native_rep_artifacts(self): numeric_ir = """ define i32 @main() { entry: - br label %for.body.4 -for.body.4: + br label %arr.merge.4 +arr.merge.4: %x = fmul double %a, %b %y = fadd double %x, %c - br label %for.body.4 + br label %arr.merge.4 } """ cases = [ @@ -2816,11 +2816,11 @@ def test_loop_data_dependent_allows_setup_conversion_only(self): ir = """ define double @main() { entry: - br label %for.body.2 -for.body.2: - %setup = sitofp i32 %seed to double br label %for.body.4 for.body.4: + %setup = sitofp i32 %seed to double + br label %arr.merge.24 +arr.merge.24: %mul = fmul double %sum, %x %add = fadd double %mul, %y br label %exit @@ -2852,6 +2852,41 @@ def test_loop_data_dependent_allows_setup_conversion_only(self): ) ) + def test_loop_data_dependent_rejects_conversion_inside_numeric_merge(self): + ir = """ +define double @main() { +entry: + br label %arr.merge.24 +arr.merge.24: + %converted = sitofp i32 %index to double + %mul = fmul double %sum, %x + %add = fadd double %mul, %converted + ret double %add +} +""" + report = HARNESS.verify_artifacts( + workload="loop_data_dependent", + ir_before=ir, + ir_after=ir, + assembly="main:\n retq\n", + benchmark=None, + vectorization={ + "vectorized_count": 0, + "missed_count": 0, + "analysis_count": 0, + "missed_reason_kinds": {}, + }, + target="x86_64-unknown-linux-gnu", + native_reps=[{"records": loop_data_dependent_native_records()}], + ) + self.assertEqual(report["status"], "fail") + self.assertTrue( + any( + "named_region_numeric_loop_no_fp_int_conversions" in error + for error in report["errors"] + ) + ) + if __name__ == "__main__": unittest.main()