Skip to content

class X extends Array in an T[]-annotated binding takes the raw ArrayHeader fast paths (sibling of #7570) #7574

Description

@proggeramlug

Sibling of #7570 (fixed by #7573) on a different family. Filed from the sweep
that PR did of every declared-type-keyed lowering; not fixed there because it is
a separate set of code paths with a different guard already half-present.

Shape

class X extends Array produces a plain ObjectHeader, not an
ArrayHeader — stated outright in crates/perry-runtime/src/array/subclass.rs:1-7
("perry has no exotic array-object representation"), and js_array_subclass_init
(node_stream_constructors/builders.rs:98) installs the elements as ordinary
object properties, with no ArrayHeader backing (unlike
js_map_set_subclass_init, which does allocate a real hidden Map/Set).

ObjectHeader and ArrayHeader overlay:

ArrayHeader offset reads on an ObjectHeader
length: u32 0 object_type (= 1)
capacity: u32 4 class_id
(elements begin) 8 parent_class_idfield_count, then keys_array, then meta

is_array_expr (crates/perry-codegen/src/type_analysis/predicates.rs:401) is
satisfied by a declared Type::Array(_) / Generic { base: "Array" }, so a
binding annotated with the base type and holding a subclass instance takes the
raw lowering — the same "a declared type is a hint, never a layout fact" premise
as #7570.

class MyArr<T> extends Array<T> {}
const a: number[] = new MyArr<number>();
a.push(1);
console.log(a.length, a[0]);

Tiers that do NOT brand-check

  • crates/perry-codegen/src/expr/index_get.rs:531lower_bounded_array_index_get
    (the hoisted-arr.length loop fast path) tests only GC_TYPE_LAZY_ARRAY,
    GC_FLAG_FORWARDED and OBJ_FLAG_ARRAY_DESCRIPTORS, then does a raw
    gep + load double at handle + 8 + idx*8 — straight into
    parent_class_id ‖ field_count, then keys_array, then meta.
  • crates/perry-codegen/src/expr/property_get.rs:236 — inline arr.length is a
    bare safe_load_i32_from_ptr(recv) at offset 0, no GC-type test, so it reports
    object_type (= 1).
  • crates/perry-codegen/src/lower_call/property_get.rs:215lower_array_method
    (js_array_pop_f64, js_array_join, js_array_sort, js_array_reverse, …).
  • crates/perry-codegen/src/expr/property_set.rs:383js_array_set_length_strict.
  • crates/perry-codegen/src/expr/index_set.rs — the guard-free
    Ptr<NumArray> store arm (try_lower_num_array_guard_free_set).

The tier that already does it right

crates/perry-codegen/src/expr/index_get/guarded_array.rs:22-100 loads the GC
header byte at handle - 8 and tests icmp eq i8 %gc_type, 1 (GC_TYPE_ARRAY)
before any slot load, branching to a generic fallback otherwise. An Array
subclass instance takes the fallback there and behaves correctly.

The recognition machinery also already exists on the runtime side and is used by
js_native_call_methodarray::subclass::is_array_subclass_instance (:46,
which requires GC_TYPE_OBJECT via try_read_gc_header) and
array_subclass_dense_snapshot (:77, used by iteration/spread) — so
class X extends Array works fine through the generic dispatch. It is only the
declared-type fast paths that are unsafe.

Suggested fix

Whichever of the two #7573 considered:

  • extend the guarded_array.rs GC_TYPE_ARRAY test to the other tiers (keeps
    the fast path, costs one header-byte load + branch), or
  • give the raw js_array_* entries a receiver resolution the way fix(runtime): a Map/Set subclass in a base-typed binding was read as a raw header (#7570) #7573 gave
    clean_map_ptr / clean_set_ptr one, so it is fail-closed for every future
    caller. Note this family has no hidden backing to redirect to — an Array
    subclass keeps its elements as object properties — so the runtime-side answer
    is "fall back to the generic array-like path", not "redirect".

The second is the one that cannot be forgotten at a new call site; the first is
the smaller diff.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions