Skip to content

fix(hir,codegen,runtime): class elements/subclass/dstr test262 tail (native-region-safe)#4997

Merged
proggeramlug merged 1 commit into
mainfrom
class-elements-finish-parity
Jun 11, 2026
Merged

fix(hir,codegen,runtime): class elements/subclass/dstr test262 tail (native-region-safe)#4997
proggeramlug merged 1 commit into
mainfrom
class-elements-finish-parity

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Summary

Targets the biggest remaining class-leaf test262 gaps: language/{statements,expressions}/class/elements (+ subclass, subclass-builtins, dstr). Verified with scripts/test262_subset.py against the node v26 oracle on the 48-core box.

Class dirs (6 target dirs, 4201 judged): 3968 → 4035 pass, parity 94.5% → 96.0% (+67), zero regressions (per-round failure-set diffs).
Broad regression shard (--shard 0/12 built-ins language, 2645 judged): +8 fixed, ZERO regressed vs a from-scratch merge-base baseline build (set-diff, both runs on an idle box).

Three shared root causes

1. GetIterator ignored a patched/deleted Array.prototype[Symbol.iterator] (~50 tests)

js_get_iterator short-circuited every array to the builtin values iterator, so the whole async-gen dstr *-array-prototype / iter-get-err family diverged (sync variants are filtered out by features-applicable.txt, so only async showed). Fix: sticky ARRAY_PROTO_ITERATOR_MODIFIED flag (same shape as ARRAY_PROTO_HAS_INDEX) noted by the symbol-property set and delete paths; when flipped, GetIterator reads the patched method off the prototype, calls it with this === array, and throws TypeError when deleted. Also fixes array_prototype_addr() permanently caching 0 when first probed during runtime init (before global Array materializes).

2. Static-method this was a compile-time class-ref literal (~30 tests)

C.m.call({}) and inherited D.m() ran with this === C baked into the LLVM body, so the (already-correct!) js_private_guard brand checks could never fire, and C.g.call(new C()) for an instance-private read wrongly threw. Fix — no ABI change: a one-shot STATIC_THIS_OVERRIDE thread-local, armed by js_class_static_method_call (arm-if-unarmed so an outer call/apply thisArg wins), the Function.prototype.call/apply arms (only when the target is a static bound-method value), the codegen static-dispatch tower, and inherited direct calls; consumed by a new js_static_this_resolve(lexical_classref) prologue call in compile_static_method. Direct calls keep the lexical class-ref fallback. GC-scanned like IMPLICIT_THIS.

3. this in static field initializers saw the module's implicit this (~10 tests)

The HIR's inline StaticFieldSet stmts (the spec-position evaluation) run in module-init context where Expr::This reads implicit this; the correctly-seeded init_static_fields_late pass ran AFTER user code (too late) and double-evaluated every initializer. Fix: substitute lexical thisExpr::ClassRef in place on class.static_fields at class lowering (in-place matters: closure bodies compile from the class's copy — substituting a stmt-level clone desyncs the closure creation site from its compiled body), and dedup the late pass against inline-emitted fields (also stops initializer side effects firing twice / clobbering user reassignments).

Stragglers fixed

  • Static private getters/setters now register on the static-accessor side; the instance-ABI direct accessor call is gated off for static accessors (was an undefined-symbol link error).
  • Static fields are real own properties: CLASS_DYNAMIC_PROPS registration at init + getOwnPropertyDescriptor support on class refs (verifyProperty family); computed string-keyed static fields become real props, with the spec TypeError for a static field named prototype.
  • NamedEvaluation for anonymous fn initializers (static f = function(){}.name === "f", incl. #field).
  • Annex B __lookupGetter__/__lookupSetter__/__defineGetter__/__defineSetter__ dispatch on class instances.
  • for (o.#f of iter) loop heads compile with the brand-guarded private write (was a compile error).

Verification

  • Class-dirs sweep: 96.0%, set-diffed each round, 0 regressions.
  • Broad shard 0/12 vs merge-base baseline: +8 / −0 (152 → 144 failures).
  • cargo test --release -p perry-runtime (RUST_TEST_THREADS=1): 1023 passed, 0 failed.
  • native-region gate: compiler_output_regression.py suite --suite native-region-proof → status pass, all 7 workloads, no failed_workloads.
  • cargo fmt --all clean, file-size gate OK. No version/changelog edits (maintainer folds at merge).

Deferred tail (~144 remaining in the dirs)

async-gen yield* return/throw protocol (~46, state-machine risk), subclass-builtins exotic constructors (~25), private-on-proxy, per-evaluation private brands (multiple-evaluations family), [self.#f] computed-key visibility, static constructor(){} SWC parse (TS1089), private-method-value .call(plainObj) (BOUND_METHOD by-name re-resolution), static h; descriptor.

Three shared root causes plus stragglers, +67 tests (94.5% -> 96.0%) on the
class elements/subclass/dstr dirs, zero regressions:

1. GetIterator ignored a patched/deleted Array.prototype[Symbol.iterator]:
   js_get_iterator short-circuited every array to the builtin values
   iterator. Sticky ARRAY_PROTO_ITERATOR_MODIFIED flag (noted by the
   symbol set/delete paths) gates the fast path; when modified, the
   patched method is read off the prototype and called with this=array,
   and a deleted method throws TypeError. Also fixes array_prototype_addr
   caching 0 when first probed during runtime init. Covers the async-gen
   dstr *-array-prototype / iter-get-err families (~50 tests).

2. Static-method 'this' was a compile-time class-ref literal, so
   C.m.call({}) / inherited D.m() never saw the real receiver and static
   private brand checks could not fire. New one-shot STATIC_THIS_OVERRIDE
   (object/mod.rs): armed by js_class_static_method_call, the
   Function.prototype call/apply arms (for static bound-method values),
   the codegen static-dispatch tower, and inherited direct calls
   (js_static_this_arm_classref); consumed by js_static_this_resolve in
   the compile_static_method prologue, falling back to the lexical
   class-ref for direct calls. Covers the static-private-* brand-check
   family (~30 tests) and the wrongly-throwing
   private-method-referenced-from-static-method cases.

3. 'this' in static field initializers evaluated in module-init context
   (implicit this) instead of the class constructor: substitute lexical
   this (including arrow bodies, which compile from these exprs) with
   ClassRef at class lowering; dedup init_static_fields_late against the
   inline spec-position StaticFieldSet stmts so initializers no longer
   run twice nor clobber user reassignments.

Stragglers: static private getters/setters register on the static-accessor
side (and the instance-ABI direct accessor call is gated off for them);
static fields get real own-property descriptors (CLASS_DYNAMIC_PROPS
registration + getOwnPropertyDescriptor support, computed string keys
become real static props with the 'prototype' TypeError per spec);
NamedEvaluation for anonymous functions in field initializers;
__lookupGetter__/__lookupSetter__/__defineGetter__/__defineSetter__
dispatch on class instances; for (o.#f of iter) loop heads compile with
the brand-guarded private write.
@proggeramlug proggeramlug force-pushed the class-elements-finish-parity branch from 862a55e to d4fb220 Compare June 11, 2026 11:33
@proggeramlug proggeramlug merged commit 6511817 into main Jun 11, 2026
12 of 13 checks passed
@proggeramlug proggeramlug deleted the class-elements-finish-parity branch June 11, 2026 11:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant