Skip to content

Commit 40f1d47

Browse files
author
Ralph Küpper
committed
fix(runtime): dispatch inherited Array statics on a subclass constructor (#7541)
1 parent 0fc7d7e commit 40f1d47

3 files changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
### fix(runtime): dispatch inherited Array statics on a subclass constructor (#7541)
2+
3+
`[...MyArr.from([1, 2, 3])]` (where `class MyArr extends Array {}`) threw
4+
`TypeError: value is not iterable`. The spread was never at fault — a
5+
directly-constructed subclass instance has always spread correctly. **`MyArr.from`
6+
resolved to nothing**: `Array.from` / `Array.of` / `Array.isArray` are folded in
7+
the HIR on the *literal identifier* `Array`, so a subclass receiver matched no
8+
fold, fell through to `js_class_static_method_call`, and hit its documented
9+
miss-fallback — which returns the RECEIVER. The call therefore evaluated to the
10+
class ref, which is genuinely not iterable. Same "keyed on a literal `extends`
11+
name" weak area as the instance-side native-base gaps, on the static side.
12+
13+
`js_class_static_method_call` now has an Array arm beside the existing
14+
`class X extends Promise` and `class X extends Buffer` ones, gated on the same
15+
bounded class-chain walk (`is_array_subclass_class_id`). Both spec statics were
16+
already implemented constructor-aware — `array_from_full` / `array_of_full` run
17+
`Construct(C, …)` when `IsConstructor(this)`, and a class ref answers true — so
18+
passing the subclass receiver through builds a real subclass instance, matching
19+
`Array.from.call(MyArr, x)` in node rather than degrading to a plain `Array`.
20+
21+
Depends on #7574's funnel: `array_from_full` installs elements through
22+
`CreateDataPropertyOrThrow`, which branches on `js_array_is_array` — true for a
23+
subclass instance — and so reaches `js_array_set_f64_extend` on what is
24+
physically an `ObjectHeader`. Without that fix this change would construct the
25+
right object and then write into its header.
26+
27+
Validated by `test-files/test_gap_7541_array_subclass_inherited_statics.ts`
28+
(byte-identical to node, exit 0; covers `from` with a mapFn / from a `Set` /
29+
from an array-like, `of`, `isArray`, an indirect subclass, and the full
30+
iteration surface on a static-produced instance), plus a full runtime revert
31+
reproducing the reported `TypeError` verbatim.
32+
33+
Still open, deliberately: the property-GET form (`typeof MyArr.from` reports
34+
`undefined` — only the call form is dispatched, as is already the case for the
35+
Promise/Buffer subclass statics), `sub instanceof MyArr` (the class-registry
36+
parent-edge gap, Array sibling of #7575), and `ArraySpeciesCreate` on
37+
`sub.map(f)`.

crates/perry-runtime/src/object/class_registry/parent_static.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,48 @@ pub unsafe extern "C" fn js_class_static_method_call(
15231523
return result;
15241524
}
15251525
}
1526+
// #7541: `class X extends Array` — inherited builtin static
1527+
// (`X.from(...)`, `X.of(...)`, `X.isArray(...)`).
1528+
//
1529+
// `Array.from` / `Array.of` are folded in the HIR on the LITERAL identifier
1530+
// `Array` (`lower/expr_call/array_only_methods.rs`), so a subclass receiver
1531+
// never matched and `MyArr.from([1, 2, 3])` resolved to nothing. The
1532+
// fallback at the end of this function returns the RECEIVER unchanged, so
1533+
// the call evaluated to the class ref — and `[...MyArr.from([1,2,3])]` threw
1534+
// `TypeError: value is not iterable` (#7541's report), which looked like a
1535+
// spread bug but is a missing static.
1536+
//
1537+
// Both spec statics are already implemented constructor-aware
1538+
// (`array::{array_from_full, array_of_full}` run `Construct(C, …)` when
1539+
// `IsConstructor(this)`, and `class_ref_id` makes a class ref answer true),
1540+
// so passing the subclass receiver as `this` builds a subclass instance —
1541+
// matching `Array.from.call(MyArr, …)`. Mirrors the Promise arm above.
1542+
if crate::array::is_array_subclass_class_id(class_id) {
1543+
let arg = |i: usize| -> f64 {
1544+
if i < args_len && !args_ptr.is_null() {
1545+
*args_ptr.add(i)
1546+
} else {
1547+
f64::from_bits(crate::value::TAG_UNDEFINED)
1548+
}
1549+
};
1550+
match name {
1551+
"from" => {
1552+
return crate::array::array_from_full(receiver, arg(0), arg(1), arg(2));
1553+
}
1554+
"of" => {
1555+
let vals: &[f64] = if args_ptr.is_null() || args_len == 0 {
1556+
&[]
1557+
} else {
1558+
std::slice::from_raw_parts(args_ptr, args_len)
1559+
};
1560+
return crate::array::array_of_full(receiver, vals);
1561+
}
1562+
"isArray" => {
1563+
return crate::array::js_array_is_array(arg(0));
1564+
}
1565+
_ => {}
1566+
}
1567+
}
15261568
// #6475: `class X extends <function value>() {}` — a static member
15271569
// INHERITED from the parent FUNCTION's own properties, invoked as a call
15281570
// (`X.use(f)`, effect's `HttpRouter.Tag(id)().use`/`unwrap`/`serve`). The
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// #7541 — `[...MyArr.from([1,2,3])]` threw `TypeError: value is not iterable`.
2+
//
3+
// The spread was never the problem: `Array.from` / `Array.of` / `Array.isArray`
4+
// are folded in the HIR on the LITERAL identifier `Array`, so a SUBCLASS
5+
// receiver matched nothing, and `js_class_static_method_call`'s miss-fallback
6+
// returns the RECEIVER — making `MyArr.from([1,2,3])` evaluate to the class ref,
7+
// which is genuinely not iterable. Directly-constructed instances always
8+
// spread fine; only the inherited-static-produced ones failed.
9+
//
10+
// KNOWN GAP, deliberately not asserted here: the property-GET form
11+
// (`typeof MyArr.from`) still reports `undefined` — only the CALL form is
12+
// dispatched. Same for `sub instanceof MyArr`, a pre-existing class-registry
13+
// parent-edge gap (#7575's Map/Set sibling).
14+
15+
class MyArr extends Array {}
16+
class Indirect extends MyArr {}
17+
18+
// The issue's exact repro.
19+
const sub = MyArr.from([1, 2, 3]);
20+
console.log([...sub]);
21+
console.log(Array.isArray([...sub]));
22+
23+
// The statics themselves.
24+
console.log("from ", Array.isArray(sub), sub.length, sub.join(","));
25+
const mapped = MyArr.from([1, 2, 3], (v: number) => v * 10);
26+
console.log("from+map ", mapped.length, mapped.join(","));
27+
const fromSet = MyArr.from(new Set([4, 5, 6]));
28+
console.log("from set ", fromSet.length, fromSet.join(","));
29+
const fromLike = MyArr.from({ length: 2, 0: "a", 1: "b" } as ArrayLike<string>);
30+
console.log("from like", fromLike.length, fromLike.join(","));
31+
const ofd = MyArr.of(7, 8, 9);
32+
console.log("of ", ofd.length, ofd.join(","));
33+
console.log("isArray ", MyArr.isArray([]), MyArr.isArray(1));
34+
35+
// An INDIRECT subclass resolves through the same chain walk.
36+
const ind = Indirect.from([1, 2]);
37+
console.log("indirect ", Array.isArray(ind), ind.length, ind.join(","));
38+
39+
// Every iteration surface on a static-produced instance.
40+
const it = MyArr.from([10, 20, 30]);
41+
const acc: number[] = [];
42+
for (const v of it) {
43+
acc.push(v);
44+
}
45+
console.log("for-of ", acc.join(","));
46+
console.log("spread ", [...it].join(","));
47+
console.log("Array.from", Array.from(it).join(","));
48+
const [a0, a1] = it;
49+
console.log("destr ", a0, a1);
50+
console.log("map ", it.map((v) => v + 1).join(","));
51+
console.log("index ", it[0], it[2], it.length);
52+
53+
// Controls: the base intrinsic and a non-Array class are untouched.
54+
console.log("base from", Array.from([1, 2]).join(","));
55+
console.log("base of ", Array.of(3, 4).join(","));
56+
class Other {
57+
static make(): string {
58+
return "other";
59+
}
60+
}
61+
class OtherSub extends Other {}
62+
console.log("user stat", OtherSub.make());
63+
console.log("done");

0 commit comments

Comments
 (0)