Repository navigation
Web API: give WinterTC §5.1 its interface objects, on the principal realm and in workers - #3250
Merged
Merged
Conversation
… Crypto/SubtleCrypto/Performance Item 4 of sebastienros#3195, widened as the owner ruled: expose as real globals every §5.1 interface object that was reachable only through an instance's prototype, or not reachable at all. Eight Streams interfaces — ReadableStreamDefaultReader, ReadableStreamBYOBReader, the four controllers, ReadableStreamBYOBRequest and WritableStreamDefaultWriter — already existed as genuine interface objects; they were simply not installed. The Streams Standard declares every one of them [Exposed=*], and the reduction cost seven excluded WPT rows plus a file that could not be vendored at all. All thirteen are globals now. Crypto, SubtleCrypto and Performance had no interface object at all: their members were own properties of the one instance, so `crypto instanceof Crypto` could not be written and Crypto.prototype did not exist. Each is now the usual triple — a non-constructible Constructor, a Prototype carrying the members, and a JsX instance whose [[Prototype]] is that prototype — so instanceof is answered by a real prototype chain rather than by a brand shim. No Symbol.hasInstance anywhere. The members' bodies are unchanged; what moved is where they live and what the brand check names. Performance's entry buffer moved with the instance, which is the split WebIDL draws. Deliberately not claimed: Performance does not inherit from EventTarget. The specification says it does, but nothing here fires an event at it, and asserting the inheritance would be exactly the half-truth this change exists to remove. WPT accounting: the seven NeedsStreamInterfaceGlobals rows of default-reader.any.js now pass, so the category and its rows are gone; construct-byob-request.any.js is vendored (16 tests, byte-identical to the pinned SHA) and leaves the not-vendored table. Going the other way, WebCryptoAPI/historical.any.js's SubtleCrypto row asserts what a *non-secure* context sees and is now excluded beside its two siblings under NeedsSecureContextModel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
…rpus that needed them The second half of sebastienros#3195 item 4. A worker's global object now inherits from DedicatedWorkerGlobalScope.prototype, which inherits from WorkerGlobalScope.prototype, and both interface objects are installed on the worker global — [Exposed=Worker] and [Exposed=DedicatedWorker], so the engine that *created* the worker carries neither. So the canonical worker feature-detect, if ('DedicatedWorkerGlobalScope' in self && self instanceof DedicatedWorkerGlobalScope) takes the branch it takes in a browser, and it does so because the prototype chain says so: neither interface object owns a Symbol.hasInstance, which is pinned. The chain stops one link early, deliberately. HTML has WorkerGlobalScope : EventTarget, and Jint's global is not one — addEventListener is an ordinary function bound to the synthetic listener list, the "suitable alternative mechanism" WinterTC §6 blesses — so WorkerGlobalScope.prototype inherits from %Object.prototype% and `self instanceof EventTarget` stays false rather than becoming an instanceof that lies. The per-connection members (postMessage, close, importScripts, name, the event-handler attributes) stay own properties of the global, because they close over this worker's link while an interface prototype is a realm intrinsic; what the pair adds is the brand, which is what a script asks for. That unlocks workers/modules/dedicated-worker-import.any.js, which could not be vendored at all: every fixture registers its onmessage inside that sniff, so the file used to stall rather than fail. Twelve files vendored, each byte-verified against the pinned SHA. Seven of its nine cases pass — static, nested static, dynamic, nested dynamic, both orders of the mixed pair, and eval(import()). The two that need a wpt server (a cross-origin .sub.js fixture and a redirect.py handler) fail as tests rather than stalling, because the worker's module loader refuses the specifier and the parent hears the startup failure as an error event; they are named individually under NeedsWptServer. Driver changes this needed, all of them in Jint.Tests/Wpt: * A workers/ file with no `// META: global=` key is a file about a page creating workers, so the top-level lane gets a WptWorkerProvider for it. _topLevelWorkersFiles names which files are meant to do that, so the lane-selection invariant still pins both directions. * WptWorkerProvider grew a second shape: with a null module source it serves vendored corpus modules, resolving specifiers through WptCorpus.ResolveReference exactly as the shim's fetch does. * The drive loop pumps live workers, and NextDue now reads Advanced.TimeUntilNextScheduledWork rather than the internal clocks-only TimeUntilNextPumpScheduledWork. That is what makes a two-engine loop terminate for the right reason: a message crossing from a worker to its parent enqueues with no due time, so the old question answered "nothing is left to pump" while the answer sat in the parent's queue. Also: `self instanceof WorkerGlobalScope` in workers/interfaces/WorkerGlobalScope/self.any.js now passes and leaves the table. Worker-replace-self.any.js's row moves from NeedsDeclinedWorkerGlobals to NeedsTriage — what is left failing there is only sebastienros#3224, `self` installed writable on a worker global. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
Found while measuring the exposure decision above rather than looked for. The probe that keeps an install non-clobbering needs a JsValue for the name, and JsString.Create allocates a fresh object for anything longer than one character — so every engine that enabled the web APIs was building one throwaway string object per installed global, whether or not the name was already taken. There are about seventy on a WebApiFeatures.Default engine. Measured with GC.GetAllocatedBytesForCurrentThread over 2,000 constructions: 26,472 -> 24,264 bytes for `new Engine(o => o.UseWebApis())`, exactly 2,208 bytes and identical on every run. The time difference is not resolvable — 28.8/29.6 and 28.4/28.6 us on interleaved pairs, against a run-to-run spread of about 2 us on a machine that was not idle — so this is an allocation change and is reported as one. The keys are the string literals at the call sites, so the table is bounded by the number of globals the file can install and is complete after the first engine of a given feature set. It is static because a JsString is immutable and realm-independent; the value factories, which produce realm-owned objects, stay per engine as they were. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
README.md: the WinterTC §5.1 table loses all eleven of its "not a global" / "absent" rows — the eight Streams interfaces and Crypto, SubtleCrypto and Performance — and the two bullets that explained them are gone with them, leaving three where there were five. The Streams section's "one deliberate reduction" paragraph is replaced by what is true now: all thirteen interface objects are globals, each is what its instances inherit from, three are constructible and the rest refuse `new`. The crypto and performance sections each gain a sentence on their interface object, including the one link Performance does not claim. The workers section gains a paragraph on the worker global's prototype chain: what is claimed, that instanceof is answered by walking it rather than by a Symbol.hasInstance, why the chain stops short of EventTarget, and why the per-connection members stay own properties of the global. Vendor/README.md: the two not-vendored rows this unlocks are gone, replaced by narrower rows for what a wpt server would have produced; the streams, WebCryptoAPI and workers sections are rewritten around what actually moved; and the two-lanes section documents the third shape, a workers/ file that runs in the top-level lane and creates real workers there. The inventory table was re-censused whole rather than adjusted, because four of its rows had gone stale between the changes that moved them: it read 269 files / 40,617 assertions / 2,980 not passing where the true figures before this change were 270 / 40,631 / 2,907, with Streams (16 against 11), Compression (84 against 22), User Timing (9 against 5) and DOM (2 against 0) each carrying a number a later fix had already improved. It now reads 272 / 40,656 / 2,902, and says how to take the census again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
The eleven new interface objects and the worker global's prototype chain are a public-surface commitment, so they are pinned in the project without InternalsVisibleTo, where an assertion proves a third party can actually reach the thing. WebApiInterfaceObjectTests is new: each of the eleven arrives with its own flag and never on its own, never reaches a ShadowRealm, and leaves a host's own lazy global unmaterialized; each is the object its instances inherit from and owns no Symbol.hasInstance, so the instanceof is the chain's answer; and each is constructible exactly where the standard gives the interface a constructor operation. WebApiWorkerTests gains the worker half, written through the shipped WorkerProvider extension point: the sniff every worker library opens with takes the dedicated branch, the chain is real in both links, neither interface object is planted with a Symbol.hasInstance, the parent carries neither name, and the chain's end at %Object.prototype% is asserted rather than only documented. Two existing pins moved with the decision: WebApiStreamsTests' "the helper interfaces are not globals" becomes "each helper global is what its instances inherit from", and WebApiSubtleCryptoTests reads the `subtle` accessor off Crypto.prototype, where a browser's is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
…ry name beside it WorkerGlobalScope.Install treats the global object's [[Prototype]] the way it treats every name it puts on that object: it inserts the chain only where the object still has the [[Prototype]] an ordinary global starts with. A provider that built its worker engine on a Host.CreateGlobalObject of its own, and gave that object a chain, keeps the chain it chose — and `self instanceof DedicatedWorkerGlobalScope` then answers false, which is the truth about that object rather than a claim planted on it. The two names are still installed, because they are non-clobbering against the global's own properties and such a host has taken neither. Pinned by WorkerMechanismTests.AHostThatGaveItsGlobalAPrototypeKeepsIt, through Options.UseHostFactory and a Host subclass — the same door a provider would use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
TimestampOfMark moved to JsPerformance with the entry buffer, and it was the only member reaching for a DOMException. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JmK1CzJSgMd1LyTEMX3yiE
lahma
force-pushed
the
feat/3195-interface-globals
branch
from
August 22, 2026 18:13
7ea4a8a to
16b4490
Compare
This was referenced Aug 22, 2026
Closed
Merged
lahma
added a commit
that referenced
this pull request
Aug 23, 2026
…A-262's (#3272) * WebApi: every operation carries WebIDL's property attributes, not ECMA-262's WebIDL gives a regular operation `{ [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }` (https://webidl.spec.whatwg.org/#es-operations), and the same "define the operations" steps cover a static operation, a stringifier, a `toJSON`, an iterable declaration's `entries`/`keys`/`values`/`forEach`, and a namespace object's members. Enumerable is the half that matters: it is the opposite of ECMA-262's rule for a built-in function property, which is what the source generator defaults to because every host under `Jint/Native/` wants it. 183 `[JsFunction]` declarations across 46 files now opt in. Five already did — the `next`/`return` of the four iterator prototype objects, fixed by #3238 and #3247 when a corpus row happened to assert them — so 188 of 188 operations are now right. Two `[JsAccessor]` declarations join them, `Crypto.prototype.subtle` and `Performance.prototype.timeOrigin`, the last two attributes in the subtree that were not already `Configurable | Enumerable`. This removes an incoherence rather than creating one. The attribute and constant rules were already implemented deliberately — 184 of 187 accessors and all 72 constants were correct — so `Object.keys(Response.prototype)` answered 9 while `getOwnPropertyNames` answered 17, and a `for...in` over a `Response` yielded nine attribute names and not one operation. Node 24 is the oracle throughout and agrees member for member: `SubtleCrypto.prototype.digest`, `TextDecoder.prototype.decode`, `Performance.prototype.now`, `Response.json`, `URL.canParse`, `ReadableStream.from` and `Storage.prototype`'s six are all enumerable there. `navigator` and `scheduler` are deliberately untouched; the next commit says why. `Jint/Native/` is untouched, and test262 is unmoved at 102,495 passed / 0 failed. Cost is zero, measured rather than assumed: `new Engine()` allocates 9,256.0 B, `new Engine(o => o.UseWebApis())` 24,280.0 B, and the same engine with fifteen web-API objects' property tables materialized 60,912.0 B — bit-identical before and after, three rounds each. The flags are one enum argument on the same call in both storage paths, and the one enumerability-gated fast path (`BuiltinShape.FixedSlotsAllNonEnumerable` via `HasNoEnumerableOwnStringKeysCore`) is unreachable here: `HasNoEnumerableOwnStringKeys` is overridden only by `ArrayInstance`, `JsObject` and `ObjectPrototype`. The twenty "one documented simplification against WebIDL: the operations are non-enumerable" paragraphs those host files carried are removed, because they no longer describe anything. What survives is the part of each that is still true — `console`'s and `crypto`'s and `performance`'s data-property install where WebIDL asks for a `[Replaceable]` accessor pair, and `Storage.prototype.length` being a real accessor pair. Closes #3239. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd * WebApi: navigator and scheduler keep ECMAScript's attributes, and the reason is in the code The one part of the sweep that must not be automated. `NavigatorInstance` and `SchedulerInstance` have no interface object and no interface prototype object, so `userAgent`, `postTask` and `yield` are own properties of the singleton rather than of a prototype. WebIDL's enumerability rule assumes the member is where the specification puts it, and there the enumerability is invisible from the instance: Node 24 reports `userAgent` as an enumerable accessor on `Navigator.prototype` and `Object.keys(navigator)` as `[]`, because the instance has no own properties at all; a browser answers `Object.keys(scheduler)` the same way for the same reason. So flipping these three would make `Object.keys(navigator)` report `["userAgent"]` and `Object.keys(scheduler)` report `["postTask", "yield"]`, which no implementation does. Non-enumerable here is what reproduces the browser answer, and what fixes them properly is the interface object — as #3250 did for `Crypto`, `SubtleCrypto` and `Performance` — not a flag. That is a separate change, and this is exactly why the sweep is a reviewed per-declaration opt-in rather than a namespace-scoped generator default: a blanket flip would move these two objects further from a browser rather than closer. `console` is the counter-example that proves the rule rather than a precedent against it: it is a WebIDL namespace, whose members genuinely are own properties of the namespace object, so its eighteen operations are enumerable here exactly as they are in Node, where `Object.keys(console).length` is 25. Both exemptions are recorded in `WebIdlPropertyAttributeTests.Divergences`, which fails on an entry that has stopped diverging — so whoever gives these two an interface object has to delete the entry rather than inherit a stale exemption. The stale "the same pair console, crypto and performance carry" sentence both files opened with goes too: console got a private prototype in #3266, and crypto and performance got real interface objects in #3250. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd * Tests: a structural WebIDL attribute matrix, and the nine pins it moves The artefact that keeps the sweep true. `WebIdlPropertyAttributeTests` builds an engine with every web-API feature on, walks every object reachable from the principal realm's global — 131 objects, 816 members — classifies each member by the WebIDL member kind it is, and holds it to that kind's property attributes. Nothing is enumerated by hand, so a member added tomorrow is checked tomorrow; against the previous commits reverted it reports exactly the 185 members that changed, 183 operations and 2 attributes. Four assertions, three of which exist to stop the matrix rotting: * the sweep itself; * every `[JsObject]` host under `Jint/WebApi/` must be visited — found by reflecting for the method the generator emits on one, so the list cannot go stale, with the two worker global scopes recorded as out of reach from the principal realm and re-checked in both directions; * every recorded divergence must still diverge, the WPT exclusion table's discipline: a fix cannot leave an exemption behind; * `Object.keys(navigator)` and `Object.keys(scheduler)` are `[]` while `Object.keys(console)` is not — the property the two divergences rest on, stated where a reader will find it. The class doc is the table to read when adding a member: WebIDL member kind, attributes, and the declaration that produces them, with the spec anchor for each. A green matrix is not a conformance certificate and says so — it checks that a member carries its kind's attributes, never that the member is on the right object, which is the very distinction the two divergences turn on. Nine existing pins move to the browser-correct expectation. Six asserted an operation or an attribute was non-enumerable and now assert the opposite — `CryptoTests.GivesItsMembersTheAttributesWebIdlGivesAnOperation`, `SubtleCryptoTests.IsAReadOnlyEnumerableAttributeOnTheCryptoPrototype`, `SubtleCryptoTests.ExposesAllTwelveOperations`, `SubtleCryptoAesWrapTests.ExposesAllTwelveOperationsWithTheirIdlNameAndArity`, `PerformanceTests.ExposesTimeOriginAsAReadOnlyAccessor`, `EncodingTests.ExposesAttributesAndOperationsWithTheirWebIdlPropertyAttributes` — three of them renamed, because the old names asserted the old answer. Each keeps its `Object.keys(instance)` is `[]` half, which is what shows the enumerability lands on the prototype and not on the object a script holds. `StorageTests.ForInVisitsTheStoredKeysAndTheEnumerableAttribute` now sees the five operations beside `length`, which is what Node 24 sees; the two visit the same six names in a different order, because Node builds its prototype in IDL declaration order where Jint's generator sorts a host's members by JS name. In `Jint.Tests.PublicInterface`, which has no `InternalsVisibleTo` and therefore proves the surface from a third party's side, `WebApiEventTests.GivesEveryMemberTheAttributesWebIdlAsksFor` had pinned the old answer with "changing it is a deliberate act". This is that act — and it gains the `Object.keys(EventTarget.prototype)` reading plus the `constructor` back-reference, which stays non-enumerable and is why that list is three names and not four. One comment correction rides along: `ReadableStreamAsyncIterationTests` said `ReadableStream.prototype[@@asynciterator]` is non-enumerable "because it is an interface member, and those stay non-enumerable". Interface members no longer stay non-enumerable; that symbol is non-enumerable because WebIDL's iterable declarations say so, which is now the whole of the reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd * Wpt: correct the reason @@asynciterator is non-enumerable in the #3195 triage record The record explaining why the async-iterator corpus row moved ended by noting that `ReadableStream.prototype[@@asynciterator]` was already right, "non-enumerable because it is an interface member rather than an iterator-prototype method". The first half is still true and the reason is not: interface members are enumerable now, and that symbol is non-enumerable because WebIDL's iterable declarations give it `{ [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }`. The corpus itself is untouched and unmoved — 422/422 before and after the sweep, no row in either direction, so no exclusion-table entry changes either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd * AGENTS.md: WebIDL's property attributes are the fourth Web APIs convention The subtree already had three conventions written down — the whole-file `#if NET8_0_OR_GREATER`, the install from `Options.Apply` as lazy globals, and the principal realm only. The fourth is now true of every member and worth stating where the next web-API PR will read it: WebIDL's attributes rather than ECMA-262's, the four flag spellings that produce them, the matrix test that enforces them, and — the part that is not mechanical — why this is deliberately not a namespace-scoped generator default, with `navigator` and `scheduler` named as the two objects a blanket flip would get wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
lahma
added a commit
that referenced
this pull request
Aug 23, 2026
…type (#3270) (#3275) Both were interface *instances* built without either, so their members were own properties of the singleton — the last two [JsObject] hosts under Jint/WebApi/ that were neither an interface prototype object, an interface object, nor a WebIDL namespace. Three consequences closed at once. The WebIDL attribute exemption. #3239 had to leave navigator.userAgent, scheduler.postTask and scheduler.yield with an ECMAScript built-in's attributes, because WebIDL's enumerability rule assumes the member sits on the interface prototype object, where the enumerability is invisible from the instance. Flipping the flags where they were would have made Object.keys(navigator) answer ["userAgent"], which no implementation does. Moving the members is what fixes it, so the three entries in WebIdlPropertyAttributeTests.Divergences are deleted and that list is now empty; EveryRecordedDivergenceIsReal is what would have caught a stale one. The containment hazard #3266 identified for these two and #3257 left open. Measured before the change, in the REPL: navigator.__proto__.poisoned = 42 ({}).poisoned 42 Object.prototype own property: true scheduler.__proto__.poisoned = 43 ({}).poisoned 43 Object.prototype own property: true and after: undefined and false for both, with the value still reachable through the singleton, which is what a patching library is after. The own-key lists. Reflect.ownKeys(navigator) was ["userAgent", Symbol(Symbol.toStringTag)]; it is [] now, as it is in Node 24 and a browser. Same for scheduler. Node 24 is the oracle for Navigator and agrees line for line — typeof Navigator "function", name/length Navigator/0, the prototype chain, the constructor property's attributes, userAgent an enumerable accessor with no setter on Navigator.prototype, and "TypeError: Illegal constructor" from new Navigator(). Two lines differ, both deliberately: Jint's Navigator.prototype carries only userAgent, everything else being absent rather than faked; and Object.prototype.toString.call(navigator) stays [object Navigator] as WebIDL's class-string rule and a browser require, where Node answers [object Object] because its Navigator is an ordinary class. Node has no scheduler at all, so for that one the oracle is the WICG draft's IDL plus Chrome. Constructibility, checked per spec rather than assumed. Navigator is [Exposed=Window] and declares no constructor operation (https://html.spec.whatwg.org/multipage/system-state.html#the-navigator-object); Scheduler is [Exposed=(Window, Worker)] and declares none either (https://wicg.github.io/scheduling-apis/#sec-scheduler), unlike TaskController and TaskPriorityChangeEvent beside it. Neither constructs. Both interface objects are exposed as globals, following #3250's pattern exactly: a lazy, non-clobbering descriptor on the principal realm's global object, WebIDL's interface-object attributes, never in a ShadowRealm. Navigator's [Exposed=Window] is not a reason to withhold it — the instance carries the same declaration and is a Jint global anyway because WinterTC asks for globalThis.navigator.userAgent, and Node's global is not a Window either yet exposes both. Scheduler joins the three interface objects its own feature already installs. Cost, measured with GC.GetAllocatedBytesForCurrentThread over 2,000 constructions, bit-identical on every repeat: new Engine() 9,256 -> 9,272 B +16 new Engine(o => o.UseWebApis()) 24,280 -> 24,392 B +112 UseWebApis() + Execute("1+1") 26,464 -> 26,576 B +112 UseWebApis(Navigator) 11,176 -> 11,288 B +112 UseWebApis(Scheduler) 12,032 -> 12,144 B +112 UseWebApis() + read navigator.userAgent 27,440 -> 27,912 B +472 UseWebApis() + read scheduler.postTask 27,584 -> 28,056 B +472 The +16 B on a default engine is two reference fields on Intrinsics. The +112 B is the two extra lazy global descriptors and the storage they land in; nothing materializes until a script names one, which InterfaceObjectExposureTests.CostsNothingUntilItIsRead now pins for both names and NavigatorTests pins for the singleton's own descriptor. The +472 B is what a script that actually reads a member pays for the interface object and prototype it did not have before. WPT: 422/422 before, 422/422 after, corpus untouched. No row moved, and none could have moved silently — the exclusion table refuses an entry that has started passing, so a green run is the assertion. Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Closes #3195 — item 4, the §5.1 interface-globals exposure decision. Items 1–3 landed in #3238.
The decision taken was widen fully: expose what browsers expose. This does that with real interface objects and real prototype chains — no
Symbol.hasInstanceshim anywhere.What is exposed
Eight Streams interfaces (flag
Streams), matching the issue's list exactly:newReadableStreamDefaultReader,ReadableStreamBYOBReader,WritableStreamDefaultWriterReadableStreamBYOBRequestTypeError(whatwg/streams#870 removed the last one's constructor)Three singletons —
Crypto,SubtleCrypto(flagCrypto),Performance(flagPerformance), none constructible. These had no interface object at all: their members were own properties of the one instance. Each became the usual triple — a non-constructibleXConstructor, anXPrototypeholding the members verbatim (the brand check now names the instance type), and aJsXinstance whose[[Prototype]]is that prototype.Performance's entry buffer and its four buffer operations moved toJsPerformance, which is the split WebIDL draws.Two worker-scope interfaces, worker global only per
[Exposed=Worker]/[Exposed=DedicatedWorker]:WorkerGlobalScopeandDedicatedWorkerGlobalScope. The parent engine carries neither name.Two links deliberately not claimed
Performancedoes not inheritEventTarget, andWorkerGlobalScope.prototypeinherits%Object.prototype%rather thanEventTarget.prototype. Jint's global is genuinely not anEventTarget, so claiming either would makeinstanceoftrue while the brand check failed — precisely the half-truth this exercise exists to remove. Both are documented and asserted.Tests assert
Object.prototype.hasOwnProperty.call(C, Symbol.hasInstance) === falseon all thirteen, so everyinstanceofanswer comes from the chain.The cost is not "near zero", and the report should say so
The issue predicted lazy non-clobbering installs would cost nothing. Measured, they do not —
GC.GetAllocatedBytesForCurrentThreadover 2,000 constructions against agit archiveexport ofmain, allocation figures bit-identical on every repeat:new Engine()Intrinsics)new Engine(o => o.UseWebApis())UseWebApis()+Execute("1+1")A default engine is untouched, and a script that never names one of these pays the slots and nothing more. But the cost is proportional to the number of names: a
Defaultengine installs ~69 globals for 22.4 µs, ~0.32 µs each, and 11 more cost 11 × that.An isolation experiment confirms it is the names, not this implementation: adding 11 arbitrary lazy globals to the baseline costs +6,848 B, nearly twice this change's +3,368, because
AddLazyGlobalallocates a closure and a configuration action per name whereWebApiRegistrationuses cached statics. Most of the step is one growth of the global's property storage rather than per-name cost.Nothing is materialized — all 11 descriptors stay
LazyPropertyDescriptor<Engine>with_value == null, pinned per name byCostsNothingUntilItIsRead.An unrelated allocation waste found while measuring, and fixed: the non-clobbering probe called
JsString.Create(name)per install, allocating a throwaway string object for every global — ~69 per web-API engine. Interned: −2,208 B exactly, every run. The +3,368 B above is after that credit; the raw exposure was +5,576 B. Committed as an allocation change and described as one, since the time difference was not resolvable against a ~2 µs spread.The worker lane
self instanceof DedicatedWorkerGlobalScopeandself instanceof WorkerGlobalScopeare both true inside a worker, by setting the global's[[Prototype]]. Non-clobbering in both respects: a provider whoseHost.CreateGlobalObjectgave the global a prototype of its own keeps it, andinstanceofthen honestly answers false.The worker global's own names (
postMessage,close,importScripts,name, the event-handler attributes) deliberately stay own properties of the global rather than moving to the prototypes: they are per connection — they close over theWorkerLink— while an interface prototype is a realm intrinsic. Documented in three places.Vendored: 13 blobs, every one byte-verified at the pin
streams/readable-byte-streams/construct-byob-request.any.js— floor 16, all 16 pass. This is the file that readReadableByteStreamController.prototypeat file scope and so could not be vendored at all.workers/modules/dedicated-worker-import.any.js+ 11 fixtures — floor 9, 7 pass. The two failures need a wpt server (a cross-origin.sub.jsfixture, and one importing throughredirect.py); they reach the file's ownonerrorreject path rather than stalling, so they are per-test exclusions underNeedsWptServerrather than a not-vendored row.Driver changes this required: a
workers/file with no// META: global=key now runs in the top-level lane with aWptWorkerProvider; the provider grew a second shape serving vendored corpus modules; andNextDuemoved from the internal clocks-onlyTimeUntilNextPumpScheduledWork()to publicAdvanced.TimeUntilNextScheduledWork— without which a worker→parent message enqueues with no due time and the loop declares "nothing left to pump" while the answer sits in the parent's queue.One row regresses, and it is a real consequence of the decision
WebCryptoAPI/historical.any.js'sSubtleCryptorow used to pass only because there was no such interface object. Exposing it makes the test fail, because it asserts the[SecureContext]model Jint does not have. Excluded underNeedsSecureContextModelwith that reasoning recorded — worth stating plainly, since it is the one place "expose what browsers expose" costs something rather than buying something.Accounting
NeedsStreamInterfaceGlobalsand its 7 rows deleted; theself instanceof WorkerGlobalScoperow deleted;Worker-replace-self.any.jsmoved toNeedsTriage, where #3224 is now the only thing left._notVendoredloses theconstruct-byob-requestand blanketworkers/modules/*rows, replaced by narrower ones for what genuinely needs a server, a browsing context or aSharedWorker.README: 11 rows move in the §5.1 table (the issue comment's figure was right), the "five entries need more than a flag name" list is now three, and the Streams reduction paragraph is gone.
Vendor/README.md's inventory was stale — it read 269 files / 40,617 assertions / 2,980 not-passing where the true pre-change figures were 270 / 40,631 / 2,907, with Streams, Compression, User Timing and DOM each carrying a number a later fix had already improved. Re-censused whole to 272 / 40,656 / 2,902, with a note on method.Verification
Release solution build clean on all five Jint TFMs.
Jint.Tests10,344 (net10.0) / 7,147 (net472), 0 failed, plain and verifying.Jint.Tests.PublicInterface2,463 / 1,879 (2,467 / 1,883 verifying).CommonScripts28+28. test262 102,495 passed, 0 failed.WPT before → after: 270 → 272 files, 40,631 → 40,656 assertions, 2,907 → 2,902 not passing. Only intended rows moved, and the arithmetic closes exactly: +16 (byob) +7 (default-reader) +1 (worker self) +7 (worker modules) −1 (SubtleCrypto) = +30 passes; −7 −1 +2 +1 = −5 not-passing. Every one of the twenty other corpora is unchanged.
Sequencing note for #3224
This touches
WorkerGlobalScope.Install, which is where #3224's fix goes, but they do not overlap textually — this adds a prototype assignment and two installs at the top; #3224 redefinesselfnon-writable further down. What has changed is the accounting: #3224 is now the only thing standing between two exclusion rows and green, and one of them has already been re-pointed at it. Sequence #3224 after this and its PR deletes two rows instead of one.🤖 Generated with Claude Code