Repository navigation
WebApi: every operation carries WebIDL's property attributes, not ECMA-262's - #3272
Merged
Merged
Conversation
…A-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 sebastienros#3238 and
sebastienros#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 sebastienros#3239.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd
… 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 sebastienros#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 sebastienros#3266, and crypto and performance got real interface objects in sebastienros#3250. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd
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
…stienros#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
…ntion 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
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 #3239, on the terms settled in its adjudication comment: a deliberate per-declaration sweep with an explicit decision for the two singletons, not a blanket generator default.
WebIDL §3.7.6 gives a regular operation
{ writable, enumerable, configurable }. Jint's web-API operations carried ECMA-262's rule for a built-in function instead — not by choice, but becauseJsFunctionAttributehad noFlagsproperty until #3238.The census was wrong twice, and this is the corrected one
The issue said 188 operations of 193.
grepcounted three hits inside doc comments: there are 190 real declarations, so the sweep is 183.[JsFunction])scheduler)[JsAccessor])navigator.userAgent)constructor,@@toStringTag,@@iterator185 members across 46 files. Largest movers:
console18,SubtleCrypto.prototype12,URLSearchParams.prototype12,Headers/FormData10 each. Static operations moved too —Response.error/json/redirect,AbortSignal.abort/any/timeout,URL.canParse/parse,ReadableStream.from.Node 24 agrees member for member wherever it implements the interface. One deviation reported honestly rather than glossed:
Object.keys(AbortSignal)is[]in Node (hand-written internals; its WebIDL-generated bindings do follow the rule), and Node omitsEvent.prototype's constants entirely.schedulerandnavigatorkeep ECMAScript's attributesBoth are interface instances with no interface object and no interface prototype object, so their members are own properties of the singleton. WebIDL's rule presumes the member sits on the prototype, where it is invisible from the instance.
Node is decisive:
Object.keys(navigator)→[],Reflect.ownKeys(navigator)→[], anduserAgentis an enumerable accessor onNavigator.prototype. Flipping Jint's would answer["userAgent"]and["postTask","yield"]— strictly less browser-accurate.consoleis the counter-example proving the rule: a WebIDL namespace, whose members genuinely are own properties, so its 18 operations did flip.The reasoning now lives in the two classes' doc comments, which also lose a stale claim — that they carry "the same pair
console,cryptoandperformancecarry", false since #3250 and #3266. The real fix is filed as #3270, which is also what would close the__proto__containment hazard #3266 identified for these two.The structural test is the artefact that keeps this true
WebIdlPropertyAttributeTestsbuilds an engine with everyWebApiFeaturesflag plus a refusingWorkerProvider, diffs the global against a bare engine's, and walks by CLR namespace — 131 objects, 816 members — classifying each by WebIDL member kind and holding it to that kind's attributes. Four facts:CreateProperties_Generated, so the list cannot go stale; 79 of 81, with the two worker global scopes recorded as unreachable and re-checked in both directions; plus achecked > 500non-vacuity floor;Adding a web-API member needs no edit here. The class doc is a table of kind → attributes → declaration spelling with a spec anchor each. Only an iterable declaration (4 seeds today) or a deliberate divergence (3 today) ever adds a line.
Verified against unfixed code: 185 failures, exactly the 183 + 2 that changed.
Nine existing tests updated, not the seven the census predicted
Seven in
Jint.Tests—CryptoTests,SubtleCryptoTests×2,SubtleCryptoAesWrapTests,PerformanceTests,EncodingTests,StorageTests(whosefor...innow yieldsone,two,clear,getItem,key,removeItem,setItem,length, the same six prototype names Node sees).And two the census missed, both in
Jint.Tests.PublicInterface:WebApiSubtleCryptoTests.IsAReadOnlyNonEnumerableAttribute, andWebApiEventTests.GivesEveryMemberTheAttributesWebIdlAsksFor— which had pinned the old answer with "Pinned so that changing it is a deliberate act." This is that deliberate act.Twenty host-class doc paragraphs claiming "one documented simplification: the operations are non-enumerable" are removed, along with the matching clause in the WPT triage record.
Cost: 0 B, bit-identical
1000 iterations after 200 warmup, three rounds each side, identical to the decimal:
new Engine()new Engine(o => o.UseWebApis())The mechanism was re-verified rather than taken on trust:
HasNoEnumerableOwnStringKeys()isvirtual → false, overridden only byArrayInstance,JsObjectandObjectPrototype, soFixedSlotsAllNonEnumerableis unreachable for everyJint/WebApi/type, and its only consumer already answeredfalsefor these.Verification
Zero files under
Jint/Native/are touched — and test262 proves it: 102,495 passed / 0 failed on both this branch and anupstream/mainbaseline on the same machine. WPT 422/422 both ways, not one row moved, exclusion table untouched.Solution builds Release across all TFMs including
net462/netstandard2.x.Jint.Tests10,514 (net10.0) / 7,256 (net472);Jint.Tests.PublicInterface2,493 / 1,899; both repeated withJINT_HOST_CONTRACT_VERIFICATION=1.One caveat reported rather than smoothed: of five test262 runs, four were clean and one intermediate run reported 2 failures whose identity was not captured, with the two runs immediately after clean. Two other agents were running. The change cannot reach test262 — it enables no web APIs and
Jint/WebApi/is unreferenced there — so this is the known load flake, but it is an observation and not nothing.Two follow-ups filed
NavigatorandSchedulerreal interface objects, which deletes all three divergence entries and closes the containment hazard.Object.keys(Storage.prototype)is["clear","getItem","key","removeItem","setItem","length"]here against["length","clear",…]in Node, because WebIDL defines attributes → operations → iteration methods → constants in IDL order while the generator sorts by name. Membership matches; order does not; no corpus row asserts it.JsFunctionAttribute's doc was deliberately not expanded — it lives in a source-generator raw string reproduced in 54 Verify snapshots, so a 17-line addition would have been ~1,200 lines of duplicated comment churn dominating the diff. The guidance went toAGENTS.mdas a fourth Web APIs convention instead.🤖 Generated with Claude Code