Skip to content

Close 33.5% of the interp↔compiled Test262 divergence (#1279) - #1322

Merged
nickna merged 5 commits into
mainfrom
codex/issue-1279-parity-fourth
Jul 29, 2026
Merged

Close 33.5% of the interp↔compiled Test262 divergence (#1279)#1322
nickna merged 5 commits into
mainfrom
codex/issue-1279-parity-fourth

Conversation

@nickna

@nickna nickna commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Progress on the standing task in #1279. Divergence between the two execution
modes drops 2236 → 1486 (−33.5%).

before after Δ
Divergence 2236 1486 −750
Interpreted Pass 6929 7642 +713
Compiled Pass 8003 8089 +86
Interpreted Timeout 1 0 −1

Divergence histogram

count interp → compiled before
654 Fail → Pass 984
270 Pass → Fail (compiled bug) 263
155 RuntimeError → Pass 475
148 RuntimeError → Fail 227
146 Fail → RuntimeError 145
92 Pass → RuntimeError (compiled bug) 121
18 Fail → HarnessError 18
1 Pass → Timeout 1
1 Fail → Timeout 1

Track A (interpreter deficits) went 1459 → 809. Track B (compiled deficits)
went 384 → 363 — it dipped to 346 after the compiled batch, then rose slightly
as the interpreter became correct in places the compiler still isn't
(Object.getPrototypeOf(RangeError), non-configurable instance properties).
Those are genuine compiler bugs the interpreter now exposes.


Commits

Each is one coherent root-cause group, with before/after numbers in its message.

1. cfea32b5 — built-in property & coercion gaps (interpreter, 476 tests)

Seven root causes. Unbound prototype methods lost their receiver on a member
call, so arr.getClass = Object.prototype.toString; arr.getClass() threw.
Built-in prototypes and constructors didn't inherit Object.prototype.
name/length on built-in functions weren't deletable and read back as "".
Numeric built-in arguments were read with RuntimeValue.AsNumber(), a kind
assertionMath.abs("-5") crashed with a host message that reached guest
catch as a bare string; all 106 argument reads now run ToNumber, and
ToNumber(Symbol) raises a guest TypeError. 13 Math functions and the Annex B
accessors were missing entirely. The primitive prototypes didn't carry their own
primitive value. JSON.parse threw a host Exception.

2. 1b2ba989 — prototype identity, Promise.prototype, iterable combinators (interpreter, 170 tests)

X.prototype minted a fresh wrapper per read, so TypeError.prototype !== TypeError.prototype. Object.getPrototypeOf reported null for plain object
literals. Promise.prototype read as undefined. The Promise combinators
demanded a literal array rather than any iterable.

3. d16da1dbDate.prototype, descriptor-bag getters, array-index accessors (compiler, 85 tests)

Date.prototype read as undefined — Date instance calls compile inline, so
the prototype object was never materialized and every reflective use of it died
on a null receiver. The compiled Object.create walked the descriptor bag's
backing dictionary directly and dropped accessor entries. Array indices couldn't
carry accessor descriptors.

4. a23c90f3 — built-in prototypes as ordinary objects (interpreter, 61 tests)

Object.prototype was backed by a value-only Dictionary and supported
none of defineProperty / index assignment / delete / for...in. Also:
obj[k] and obj.k disagreed — indexed reads skipped getters, the __proto__
walk and the Object.prototype fallback.

5. 8b66c01f — per-realm constructor objects (interpreter, 6 tests)

Small delta, but closes a real gap (Number.foo = 1 threw) and a
baseline-stability bug: the Object.prototype methods were handed to guest
code as process-wide statics carrying mutable §17 metadata, so one program's
delete fn.length was visible to every later program in the same process.
Results were order-dependent — Object/prototype/toString/length.js passed
alone and failed in a full run.


A note on method

Several of these bugs were duplication bugs, and the fixes remove the
duplication rather than patching the copy:

  • Compiled ObjectCreate kept a thinner copy of ObjectDefineProperties'
    props loop; it now delegates.
  • Object.hasOwn was a thinner reimplementation of HasOwnProperty; they now
    share one implementation.
  • Indexed and dotted property reads had separate paths; indexed now routes
    through the dotted one.
  • Six near-identical switch arms named each prototype singleton individually —
    which is how Object.prototype came to be missing from one and
    Number.prototype from another. Introduced ISharpTSMutableBuiltIn and
    collapsed them.

Verification

  • dotnet test green: 16,279 / 16,279.
  • 339 Test262 parity tests in Issue1279ParityTests (33 new), all green.
  • SharpTS.TypeScriptConformance baseline unchanged.
  • Both Test262 baselines regenerated together against this commit.
  • Zero regressions, verified test-by-test rather than in aggregate: every
    one of the 7,636 previously-passing interpreted tests and all 8,003
    previously-passing compiled tests re-run individually after each batch.

Known gaps, deliberately left

  • Promise combinators are eager. They materialize the iterable up front
    instead of interleaving with C.resolve, so 13 Test262 cases with infinite
    iterators still fail. A documented cap (MaxCombinatorElements) turns what
    was a hang into a prompt rejection — they fail fast rather than burning the
    per-test timeout. Making the combinators lazy would fix them properly.
  • Three Array/prototype/lastIndexOf tests build 2^32-element sparse arrays
    and take ~4.5s each. One regen flaked them to Timeout; timed with and
    without these changes and they're unaffected, but they sit close enough to the
    per-test budget to destabilize the committed baseline.
  • DnsRecordTypeTests.LiveSmoke_ResolveNs_Promise hits real DNS and flaked
    once mid-session. Pre-existing and unrelated, but it will flake in CI.
  • Array/from/iter-map-fn-err.js trips a 2.5 GB memory watchdog in compiled
    mode on an error path. Pre-existing.

What's next

Per-batch yield fell 646 → 170 → 85 → 61 → 6 as the fat clusters cleared.
Track A's largest remaining group is 57 Expected SameValue spread across a
dozen folders; nothing left resembles the single-root-cause wins above.
Track B's 363 is now the better seam — Expected a $TypeError (16),
array-index descriptor value drift (11), and a host Parse leaking through
length coercion (7).

Refs #1279.

nickna added 5 commits July 28, 2026 20:49
Track A of #1279: 476 Test262 tests where the compiled backend passed and the
interpreter did not. Divergence between the two modes drops 2236 -> 1798
(-19.6%); interpreted Pass 6929 -> 7405, compiled unchanged at 8003.

Seven root causes, each interpreter-only:

* Unbound built-in prototype methods lost their receiver on a member call, so
  the Sputnik idiom `arr.getClass = Object.prototype.toString; arr.getClass()`
  threw "requires a receiver". Member calls now bind `this` from the Reference
  Record (§13.3.6.1) via a call-site-only binder, kept separate from the
  read-time binder so `obj.toString === Object.prototype.toString` still holds.

* Built-in prototype objects and constructors did not inherit Object.prototype,
  so `Array.prototype.hasOwnProperty` / `.isPrototypeOf` were undefined. Each
  early-returning arm of the fallback dispatcher now consults Object.prototype
  before yielding undefined, and `Object.getPrototypeOf` reports Array.prototype
  for arrays and Function.prototype for callables (it returned null).

* `name`/`length` on built-in function objects were synthesized but not
  deletable, and read back as "" for the wrapper types. Hoisted the §17
  bookkeeping into IBuiltInFunctionMetadata, implemented on every built-in
  callable wrapper, and routed delete / hasOwnProperty /
  getOwnPropertyDescriptor / Function.prototype reads through it.

* Numeric built-in arguments were read with RuntimeValue.AsNumber(), a kind
  assertion — `Math.abs("-5")` crashed with a host "RuntimeValue has Kind
  String" message that reached guest `catch` as a bare string. All 106 built-in
  argument reads now run ToNumber, and ToNumber(Symbol) raises a guest TypeError
  per §7.1.4.

* Math.sinh/cosh/tanh/asinh/acosh/atanh/log1p/expm1/fround/f16round/clz32/imul/
  sumPrecise and Object.prototype.__define{Getter,Setter}__ /
  __lookup{Getter,Setter}__ were missing entirely; the compiled backend has
  emitted all of them for some time.

* Number/String/Boolean.prototype are themselves wrapper objects (+0 / "" /
  false), so their own prototype methods now work on them; Number.prototype
  also gained the delete support String/Array.prototype already had.

* JSON.parse threw a host Exception, so `catch (e) { e instanceof SyntaxError }`
  saw a string.

Interpreter-only: no compiled-mode outcome changed. Every previously-passing
interpreted test still passes (verified test-by-test, not just in aggregate).
Second pass on Track A of #1279: another 170 tests the compiled backend passed
and the interpreter did not. Divergence 1798 -> 1647; interpreted Pass
7405 -> 7575, compiled unchanged at 8003. Cumulative for the branch:
2236 -> 1647 (-26.3%), interpreted Pass 6929 -> 7575.

* Prototype objects had no identity. Each read of `X.prototype` minted a fresh
  wrapper, so `TypeError.prototype !== TypeError.prototype`; the object is now
  created once and cached on the class. Object.getPrototypeOf gained the arms
  it was missing: a plain object literal reports Object.prototype (it reported
  null), a class instance reports its constructor's prototype, a native error
  resolves through the built-in Error-class registry, and a derived constructor
  reports its base constructor (§10.2.5).

* `Promise.prototype` read as undefined, so every access through it threw. It
  is now a real object exposing then/catch/finally in unbound form, with the
  §27.2.5.4 step-2 receiver check running synchronously in the species pre-step
  — `Promise.prototype.then.call({}, …)` throws a TypeError out of the call
  instead of surfacing an InvalidCastException as a bare string.

* The Promise combinators demanded a literal array. They now accept any
  iterable, and an abrupt completion from GetIterator rejects the returned
  promise (IfAbruptRejectPromise) rather than throwing synchronously. A cap on
  non-array iterables keeps a non-terminating iterator from hanging — see the
  KNOWN LIMITATION note on MaxCombinatorElements: those cases still fail, but
  fail fast. Interpreted Timeout count is 0 (was 1).

* BuiltInAsyncMethod carries an ECMA-262 §17 spec length and the name/length
  metadata contract, so `Promise.prototype.then.length === 2`.

* `Object.hasOwn` was a thinner reimplementation of HasOwnProperty and had
  drifted from it: it saw only data fields, missing accessors. Both now share
  one implementation, which in turn stops reporting inherited class methods as
  own properties.

* Resolving an unbound name throws a guest ReferenceError (§9.4.2); it produced
  a generic Error.

* `delete` on a class instance ignored per-property configurability, so a
  `configurable: false` property vanished — and reappeared as configurable to
  propertyHelper.js's delete-based probe.

Interpreter-only: no compiled-mode outcome changed. Every previously-passing
interpreted test still passes (verified test-by-test).
…essors

Track B of #1279 — the compiled-mode deficits, where the interpreter is the
correct reference. 85 tests. Divergence 1647 -> 1563; compiled Pass
8003 -> 8088, interpreted unchanged at 7575. Cumulative for the branch:
2236 -> 1563 (-30.1%).

* `Date.prototype` read as undefined. Date instance calls are emitted inline
  by DateEmitter, so the prototype object was never materialized — and every
  reflective use of it ("Object.getOwnPropertyDescriptor(Date.prototype, …)")
  died on a null receiver. It is now a populated singleton carrying the §21.4.4
  method table as $TSFunction wrappers over the existing $Runtime.Date* helpers,
  each installed with §17 attributes, plus the constructor back-reference and
  the Object.prototype link. Borrowing works too
  ("Date.prototype.getTime.call(d)"). The method table is data, so a new Date
  helper is one row rather than another hand-written wiring call. A Date-free
  program gets an empty body — the helpers don't exist then, and neither can a
  reference to Date.prototype.

* The compiled `Object.create` walked the descriptor bag's backing dictionary
  directly, so a bag built with `Object.defineProperty(props, k, {get})`
  contributed nothing. §20.1.2.2 step 3 IS ObjectDefineProperties, whose step 4
  does `Get(props, key)` and therefore invokes the getter — and the compiled
  ObjectDefineProperties already had that right. ObjectCreate now delegates
  instead of keeping a second, thinner copy of the loop.

* An array index can carry an accessor descriptor (§10.4.2.1 routes an
  array-index [[DefineOwnProperty]] through OrdinaryDefineOwnProperty). Those
  live in the PDS, not element storage, so the compiled index read answered
  undefined; it now consults the PDS for a getter first, keyed through the same
  ToJsString the named-property route uses so `arr[0]` and `arr["0"]` agree.

Compiled-only: no interpreted outcome changed. Every previously-passing
compiled test still passes — verified test-by-test across all 8003, not just in
aggregate. TS-conformance baseline unchanged.
Track A of #1279, fourth pass: 61 tests. Divergence 1563 -> 1489; interpreted
Pass 7575 -> 7636, compiled unchanged at 8088. Cumulative for the branch:
2236 -> 1489 (-33.4%).

ECMA-262 makes every built-in prototype an ordinary object, so guest code may
define descriptors on it, index into it, delete from it and enumerate it.
Object.prototype alone was backed by a value-only Dictionary and supported none
of that: `Object.defineProperty(Object.prototype, …)` threw "called on
non-object", `Object.prototype[1] = 1` threw "Index assignment not supported",
`delete` silently no-oped, and `for...in` threw "requires an object". Test262
patches Object.prototype constantly to exercise inherited-property paths, so
this one gap accounted for several clusters at once. It now uses the same
descriptor-aware SharpTSObject storage its five siblings already had.

The switches that enumerate those prototypes type-by-type are what let this
drift — Object.prototype and Number.prototype were each missing from a
different one. Introduced ISharpTSBuiltInPrototype, implemented by all six
(plus SharpTSClassPrototype), and collapsed the property-set and for-in
switches onto it.

Also in this pass:

* `obj[k]` read own properties directly while `obj.k` walked getters, the
  __proto__ chain and the Object.prototype fallback, so an inherited property
  was visible to one form and not the other. Indexed reads now route through
  the same implementation.

* An accessor inherited from Object.prototype never ran its getter — the
  fallback returned the raw stored value. It now invokes the getter with the
  *receiver* as `this`.

* A class's `prototype` accepts descriptor definitions, including Symbol-keyed
  ones (`Object.defineProperty(Error.prototype, Symbol.toStringTag, …)`), via
  the same extras storage. The class's own method table stays read-only.

Interpreter-only: no compiled-mode outcome changed. Every previously-passing
interpreted test still passes — verified test-by-test across all 7575.
TS-conformance baseline unchanged.

Known gap, deferred: assignment onto the Number/String/Boolean *namespace*
objects still throws (`Number.MAX_VALUE = 1` should be a silent no-op in sloppy
mode; `Number.zz = 1` should take). Those are process-wide singletons, unlike
Math/JSON/Object which are per-realm, so giving them expando storage as-is
would leak state between programs sharing a process. The fix is to make them
per-realm first — see Interpreter.Realm.cs IsRealmIntrinsicName. ~19 tests.
…ween programs

Track A of #1279, fifth pass. Divergence 1489 -> 1486; interpreted Pass
7636 -> 7642, compiled 8088 -> 8089. Cumulative for the branch:
2236 -> 1486 (-33.5%).

Small test delta, but it closes a real correctness gap and a
baseline-stability bug.

The gap: `Number.foo = 1` threw "Only instances and objects have fields" and
`Number["foo"] = 1` threw "Index assignment not supported". ECMA-262 makes the
Number/String/Boolean constructor objects ordinary and extensible, so both must
work — and assigning to one of their non-writable statics (`Number.MAX_VALUE = 1`)
must be a silent no-op, not a throw and not a shadowing own property.

Giving process-wide singletons a mutable expando bag would leak state between
programs sharing a process, so the three are now per-realm, following the
pattern Math/JSON/Object already use. That required keeping every identity that
crosses resolution routes intact — verified: `Number === globalThis.Number`,
`Number.prototype.constructor === Number`, `(5).constructor === Number`,
`Number.isInteger === Number.isInteger`, and a static matching the `value` of
its own descriptor. Two of those needed work:

* Constant-wrapping statics (`Number.MAX_SAFE_INTEGER`) were materialized only
  by the namespace static fast-path in EvaluateGet, which a per-realm intrinsic
  deliberately bypasses. The member table now unwraps them itself, via a new
  BuiltInMethod.ConstantValue, so no read path has to remember to.

* A static reached through instance-member dispatch comes back freshly bound,
  so `String.fromCharCode` did not equal its own descriptor's value. These
  constructor objects now take the same early return the Object namespace
  already had — which documents exactly this hazard.

The stability bug: the Object.prototype methods were handed to guest code as
process-wide statics carrying mutable ECMA-262 §17 metadata. propertyHelper.js
proves `length`/`name` are configurable by deleting them, so one program's
delete was visible to every later program in the same process — Test262 results
became order-dependent (`Object/prototype/toString/length.js` passed alone and
failed in a full run). Each realm now gets its own copy.

Also renamed ISharpTSBuiltInPrototype to ISharpTSMutableBuiltIn, since it now
covers the constructor objects as well as the prototypes.

Interpreter-only. Every previously-passing interpreted test still passes —
verified test-by-test across all 7636. TS-conformance baseline unchanged.

Note: three Array/prototype/lastIndexOf tests build 2^32-element sparse arrays
and take ~4.5s each; they flaked to Timeout in one regen under parallel load.
Timed with and without this change and they are unaffected — but they sit close
enough to the per-test budget to destabilize the committed baseline.
@nickna
nickna merged commit ca495b9 into main Jul 29, 2026
2 checks passed
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