Skip to content

runtime: new Blob(x) where x is an Array subclass stringifies the constructor instead of iterating elements #6232

Closed

Description

@proggeramlug

Summary

new Blob(x) where x is an instance of a subclass of Array (e.g. class MyArray extends Array {}) does not iterate the elements — it treats the instance as a single non-array part and stringifies its constructor. A plain array of the same values works. Wrong result, no error.

Repro

class MyArray extends Array {}
const arr = MyArray.from(["hello", " ", "derived"]);
const blob = new Blob(arr as any);
console.log(blob.size, await blob.text());
// perry:    36  "function MyArray() { [native code] }"
// expected: 13  "hello derived"

Likely cause

The BlobPart-array detection appears to check for the exact Array and misses Array subclasses. It should use the iterable/Symbol.iterator path (the constructor takes any iterable of parts).

Related: #2959 (Blob/File constructor input coercion).

Environment

perry release binary dated 2026-07-05, PERRY_NO_AUTO_OPTIMIZE=1, with libperry_stdlib.a. Found via secret-tests/bun-suite; from Bun's web/fetch/blob-array-fast-path.test.ts ("DerivedArray").

Activity

  1. added
    bugConfirmed defect or regression
    parityCompatibility gap with Node.js, ECMAScript, or the supported ecosystem
    on Jul 10, 2026
  2. proggeramlug commented on Jul 10, 2026

    @proggeramlug
    ContributorAuthor

    Update after digging in: this is not a Blob-part-detection bug — it's that subclassing Array is broadly unsupported, and the Blob path just surfaced it. On the current build:

    class MyArray extends Array {}
    MyArray.from(["x","y","z"])   // → typeof "function", not an array (returns garbage, not an instance)
    const a = new MyArray(); a.push("p")   // → TypeError: value is not a function

    So MyArray.from(...) never produces an array instance for new Blob(...) to iterate; the Blob code then ToStrings whatever it got. Fixing the Blob branch would not help until class extends Array (subclassing the builtin — cf. #6105) produces real array-backed instances with working inherited statics (from) and methods (push). Recommend tracking this under the subclass-builtin work rather than as a Blob issue.

  3. proggeramlug commented on Jul 10, 2026

    @proggeramlug
    ContributorAuthor

    Confirmed this is a feature gap, not a targeted bug — closing the loop on scope for whoever picks it up.

    class extends Array is essentially unimplemented. Even the explicit-super() path is broken:

    class A extends Array { constructor(){ super(); } }
    const a = new A(); a.push(1);   // TypeError: value is not a function
    Array.isArray(a)                // false

    Root cause: js_array_subclass_init (perry-runtime/.../node_stream_constructors/builders.rs) only sets a length field and installs a single fill method on a plain GC_TYPE_OBJECT; the instance is never array-backed and gets none of push/pop/map/index behavior, and Array.isArray doesn't recognize it. Inherited statics are also broken (A.from(...) returns a non-array).

    A real fix needs exotic-array-object semantics for subclass instances (array-backed storage or full method-inheritance + length exotic behavior) plus inherited-static construction — i.e. proper builtin-subclassing, adjacent to #6105. Recommend tracking as a feature rather than a patch. (The Blob symptom that filed this is downstream and resolves for free once instances are real arrays.)

  4. proggeramlug commented on Jul 10, 2026

    @proggeramlug
    ContributorAuthor

    Investigated with a build (perry @ current main). The diagnosis in this issue is wrong — the Blob part-walker is fine; the real bug is upstream in Array.from/Array.of static dispatch on a subclass.

    Probe:

    class MyArray extends Array {}
    const arr = MyArray.from(["hello", " ", "derived"]);
    typeof arr            // perry: "function"   node: "object"
    Array.isArray(arr)    // perry: false        node: true
    (arr as any).length   // perry: undefined    node: 3
    [...arr]              // perry: throws "value is not iterable"
    typeof MyArray.from   // perry: "undefined"  node: "function"
    typeof MyArray.of     // perry: "undefined"  node: "function"
    Array.from.call(MyArray, ["m","n"])   // perry: object len 2 (elements ARE built)

    So MyArray.from(...) returns the constructor function, not an array (Blob then stringifies that function — hence the "36 / function MyArray()" output). Root cause: static methods are not inherited from a builtin Array parent — MyArray.from/MyArray.of resolve to undefined, and the MyArray.from(...) call falls back to yielding the class ref. Array.from.call(MyArray, …) shows the underlying array_from_full does honor a subclass this (it built len 2), so the missing piece is wiring the builtin constructor's statics onto subclasses of Array (see install_builtin_constructor_statics in object/global_this/install_static.rs, and the no-own-ctor/builtin_parent_runtime path in lower_call/new.rs), not the Blob walker.

    (Also independent: new MyArray(3) returns a plain object with isArray:false, length:0 — the documented "subclass modeled as a plain object" limitation — so even a fixed .from would produce an array-like object, not a genuine exotic Array. Full class X extends Array exotic-array behavior is a larger effort; wiring inherited statics from/of is the scoped fix that resolves this repro.)

    Retitling/repurposing this toward "Array subclass does not inherit static from/of from the builtin Array" would be accurate. Not shipping a Blob patch since the Blob code is correct.

  5. proggeramlug commented on Jul 19, 2026

    @proggeramlug
    ContributorAuthor

    Data point from the pi bring-up (wall #7 stretch assessment, see #6658 / PR #6659): the class extends Set family is further along than the class extends Array case this issue tracks, and the remaining gap is precisely scoped to dynamic construction.

    Probed on current main + #6659 (node v26 parity):

    class A extends Set {}
    new A().add("x");                    // WORKS (static new, decl)
    
    const C: any = class extends Set {};
    new C().add("x");                    // WORKS (static new, class expr)
    
    const obj: any = {}; obj.A = A;
    new obj.A().add("x");                // FAILS: "add is not a function"
    
    // the gaxios/webpack-CJS shape (same failure):
    exportsObj.GaxiosInterceptorManager = class extends Set {};
    new exportsObj.GaxiosInterceptorManager().add(...);

    Root cause of the failing shapes: the Map/Set backing init (js_map_set_subclass_init, which allocates the real SetHeader/MapHeader and stashes it on the instance) is emitted by codegen at static new sites only — lower_call/new_helpers.rs::emit_native_instance_base_init. A new through a runtime value (property read, laundered local, cross-module export object) takes the dynamic-construct path, which never installs the backing for ANY native instance base (Set, Map, Event, CustomEvent, EventEmitter — the whole NativeInstanceBase enum), so the instance has no collection storage and every method read misses.

    A general fix needs: a class-id → native-base registry populated at class registration (codegen knows the base statically), the dynamic-new runtime path consulting it and running the same base init with forwarded ctor args (seeding semantics: new Seeded([["k",9]])), for the full native-base family — i.e. this issue's feature, not a bounded Set-only patch. Method inheritance itself is NOT the problem: once the backing exists, dispatch already serves the whole surface via subclass_backing_of (methods, for…of, .size).

    Practical impact: gaxios instantiates class extends Set interceptor managers per Gaxios instance through exactly the failing shape — this is the projected pi bring-up wall #8 once execution goes past --help/--version (those two are clean as of #6659, GATE 1 reached).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regressionparityCompatibility gap with Node.js, ECMAScript, or the supported ecosystem

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions