From 978bdb5f2eac50b36c134bed48a7bec263e4e0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 28 Aug 2026 19:07:30 +0200 Subject: [PATCH 1/2] fix(runtime): Array-subclass fill honours start and end `js_array_subclass_init` installs `fill` on the instance, and that stub had arity 1: every `start` / `end` argument was dropped, so `sub.fill(8, 1)` overwrote the whole array instead of the tail from index 1 (node prints `7|8|8`, perry printed `8|8|8`). The stub now takes both, treating an omitted (`undefined`) argument as the spec default. e2e test covers value-only, a start, a start and end, and a negative start against node's output. Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ --- changelog.d/0000-array-subclass-fill-args.md | 3 + .../src/node_stream_constructors/builders.rs | 29 +++++++-- .../perry/tests/array_subclass_fill_args.rs | 62 +++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 changelog.d/0000-array-subclass-fill-args.md create mode 100644 crates/perry/tests/array_subclass_fill_args.rs diff --git a/changelog.d/0000-array-subclass-fill-args.md b/changelog.d/0000-array-subclass-fill-args.md new file mode 100644 index 0000000000..5b07067f66 --- /dev/null +++ b/changelog.d/0000-array-subclass-fill-args.md @@ -0,0 +1,3 @@ +### Fixed + +- `fill(value, start, end)` on a `class X extends Array` instance ignored `start` and `end` — the instance-installed stub had arity 1, so `sub.fill(8, 1)` overwrote the whole array instead of the tail from index 1. diff --git a/crates/perry-runtime/src/node_stream_constructors/builders.rs b/crates/perry-runtime/src/node_stream_constructors/builders.rs index de5a937947..c1b8956409 100644 --- a/crates/perry-runtime/src/node_stream_constructors/builders.rs +++ b/crates/perry-runtime/src/node_stream_constructors/builders.rs @@ -211,15 +211,15 @@ pub extern "C" fn js_array_subclass_init(this: f64, n: f64) -> f64 { // divergence tracked in #8953, unchanged by the elements store. let this = this_root.get_nanbox_f64(); let obj = raw_ptr_from_value(this) as *mut ObjectHeader; - crate::closure::js_register_closure_arity(ns_array_fill as *const u8, 1); - let methods: [(&str, StubFn); 1] = [("fill", super::cast1(ns_array_fill))]; + crate::closure::js_register_closure_arity(ns_array_fill as *const u8, 3); + let methods: [(&str, StubFn); 1] = [("fill", super::cast3(ns_array_fill))]; install_methods_on_existing_object(obj, this, &methods, &[]); return this_root.get_nanbox_f64(); } let length_key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6); js_object_set_field_by_name(obj, length_key, len); - crate::closure::js_register_closure_arity(ns_array_fill as *const u8, 1); - let methods: [(&str, StubFn); 1] = [("fill", super::cast1(ns_array_fill))]; + crate::closure::js_register_closure_arity(ns_array_fill as *const u8, 3); + let methods: [(&str, StubFn); 1] = [("fill", super::cast3(ns_array_fill))]; install_methods_on_existing_object(obj, this, &methods, &[]); this } @@ -261,8 +261,25 @@ pub unsafe extern "C" fn js_array_subclass_init_args( /// `Array.prototype.fill`-equivalent installed on an Array-subclass instance: /// fills the receiver's own indexed slots `0..length` with `value`. Delegates /// to the generic array-like fill (which reads `length` off the receiver). -pub(super) extern "C" fn ns_array_fill(closure: *const ClosureHeader, value: f64) -> f64 { - crate::array::js_array_fill_generic(super::this_value(closure), value, 0, 0.0, 0, 0.0) +pub(super) extern "C" fn ns_array_fill( + closure: *const ClosureHeader, + value: f64, + start: f64, + end: f64, +) -> f64 { + // `fill(value, start?, end?)`. An omitted argument arrives as `undefined` + // and selects the spec default (`0` / `length`); before this the stub had + // arity 1, so `sub.fill(8, 1)` filled the WHOLE array instead of the tail + // from index 1 (node: `7|8|8`, perry: `8|8|8`). + let present = |v: f64| i32::from(!JSValue::from_bits(v.to_bits()).is_undefined()); + crate::array::js_array_fill_generic( + super::this_value(closure), + value, + present(start), + start, + present(end), + end, + ) } #[no_mangle] diff --git a/crates/perry/tests/array_subclass_fill_args.rs b/crates/perry/tests/array_subclass_fill_args.rs new file mode 100644 index 0000000000..805fe3e86e --- /dev/null +++ b/crates/perry/tests/array_subclass_fill_args.rs @@ -0,0 +1,62 @@ +//! `sub.fill(value, start?, end?)` on a `class X extends Array` instance. +//! +//! `js_array_subclass_init` installs `fill` on the instance (node inherits it +//! from `Array.prototype`; perry has no such prototype object for these), and +//! that stub had arity 1 — so every `start`/`end` argument was dropped and the +//! whole array was overwritten. +use std::path::PathBuf; +use std::process::{Command, Output}; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +fn compile_and_run(source: &str) -> Output { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.js"); + let output = dir.path().join("main_bin"); + std::fs::write(&entry, source).expect("write entry"); + let compile = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-auto-optimize") + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + Command::new(&output).output().expect("run compiled binary") +} + +/// Every `fill` form node accepts, on a subclass instance: value only, a start, +/// a start and end, and a negative start. +#[test] +fn array_subclass_fill_honours_start_and_end() { + let run = compile_and_run( + r#" +class A extends Array {} +const out = []; +const a = new A(); a.push(1, 2, 3, 4); out.push("all=" + a.fill(9).join("|")); +const b = new A(); b.push(1, 2, 3, 4); out.push("from1=" + b.fill(9, 1).join("|")); +const c = new A(); c.push(1, 2, 3, 4); out.push("1to3=" + c.fill(9, 1, 3).join("|")); +const d = new A(); d.push(1, 2, 3, 4); out.push("neg=" + d.fill(9, -2).join("|")); +console.log(out.join(" ")); +"#, + ); + assert!( + run.status.success(), + "the program must exit cleanly\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&run.stdout), + "all=9|9|9|9 from1=1|9|9|9 1to3=1|9|9|4 neg=1|2|9|9\n" + ); +} From 9c1b9567d960f52b969c64aacaf5ac21f1a9bd68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 28 Aug 2026 20:16:43 +0200 Subject: [PATCH 2/2] chore(changelog): name the fragment for its PR Fragment filenames are PR-keyed so in-flight PRs cannot collide; 0000 collides with every other placeholder. --- ...ray-subclass-fill-args.md => 8972-array-subclass-fill-args.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{0000-array-subclass-fill-args.md => 8972-array-subclass-fill-args.md} (100%) diff --git a/changelog.d/0000-array-subclass-fill-args.md b/changelog.d/8972-array-subclass-fill-args.md similarity index 100% rename from changelog.d/0000-array-subclass-fill-args.md rename to changelog.d/8972-array-subclass-fill-args.md