Close 33.5% of the interp↔compiled Test262 divergence (#1279) - #1322
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Progress on the standing task in #1279. Divergence between the two execution
modes drops 2236 → 1486 (−33.5%).
Divergence histogram
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/lengthon built-in functions weren't deletable and read back as"".Numeric built-in arguments were read with
RuntimeValue.AsNumber(), a kindassertion —
Math.abs("-5")crashed with a host message that reached guestcatchas a bare string; all 106 argument reads now run ToNumber, andToNumber(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.parsethrew a host Exception.2.
1b2ba989— prototype identity,Promise.prototype, iterable combinators (interpreter, 170 tests)X.prototypeminted a fresh wrapper per read, soTypeError.prototype !== TypeError.prototype.Object.getPrototypeOfreportednullfor plain objectliterals.
Promise.prototyperead asundefined. The Promise combinatorsdemanded a literal array rather than any iterable.
3.
d16da1db—Date.prototype, descriptor-bag getters, array-index accessors (compiler, 85 tests)Date.prototyperead asundefined— Date instance calls compile inline, sothe prototype object was never materialized and every reflective use of it died
on a null receiver. The compiled
Object.createwalked the descriptor bag'sbacking 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.prototypewas backed by a value-onlyDictionaryand supportednone of
defineProperty/ index assignment /delete/for...in. Also:obj[k]andobj.kdisagreed — indexed reads skipped getters, the__proto__walk and the
Object.prototypefallback.5.
8b66c01f— per-realm constructor objects (interpreter, 6 tests)Small delta, but closes a real gap (
Number.foo = 1threw) and abaseline-stability bug: the
Object.prototypemethods were handed to guestcode as process-wide statics carrying mutable §17 metadata, so one program's
delete fn.lengthwas visible to every later program in the same process.Results were order-dependent —
Object/prototype/toString/length.jspassedalone 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:
ObjectCreatekept a thinner copy ofObjectDefineProperties'props loop; it now delegates.
Object.hasOwnwas a thinner reimplementation ofHasOwnProperty; they nowshare one implementation.
through the dotted one.
which is how
Object.prototypecame to be missing from one andNumber.prototypefrom another. IntroducedISharpTSMutableBuiltInandcollapsed them.
Verification
dotnet testgreen: 16,279 / 16,279.Issue1279ParityTests(33 new), all green.SharpTS.TypeScriptConformancebaseline unchanged.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
instead of interleaving with
C.resolve, so 13 Test262 cases with infiniteiterators still fail. A documented cap (
MaxCombinatorElements) turns whatwas a hang into a prompt rejection — they fail fast rather than burning the
per-test timeout. Making the combinators lazy would fix them properly.
Array/prototype/lastIndexOftests build 2^32-element sparse arraysand take ~4.5s each. One regen flaked them to
Timeout; timed with andwithout these changes and they're unaffected, but they sit close enough to the
per-test budget to destabilize the committed baseline.
DnsRecordTypeTests.LiveSmoke_ResolveNs_Promisehits real DNS and flakedonce mid-session. Pre-existing and unrelated, but it will flake in CI.
Array/from/iter-map-fn-err.jstrips a 2.5 GB memory watchdog in compiledmode 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 SameValuespread across adozen 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
Parseleaking throughlengthcoercion (7).Refs #1279.