Host objects: the enumeration hook is the one enumeration uses, and two names say what they do - #3461
Merged
lahma merged 4 commits intoAug 27, 2026
Conversation
Collaborator
Author
|
Rebased onto latest §2.6 and §3.16 were also claimed by #3459 (limits as properties), and §4.44 by #3429 (calendar arithmetic). Assigning a definite merge order — #3459 and #3458, then #3429, then this — puts this PR at:
Anchors and the one cross-reference repointed with them. Two other things fixed in the rebase:
Build clean, and |
lahma
force-pushed
the
v5-b5-enumeration-hook-and-unchecked-defines
branch
5 times, most recently
from
August 27, 2026 04:32
e213d62 to
c5eecdf
Compare
…wo names say what they do `ObjectInstance.GetOwnProperties` was a second `virtual` whose name read like the enumeration hook and which nothing script-visible called. It is derived from `GetOwnPropertyKeys` + `GetOwnProperty` now, and non-virtual, so a host declares its keys once and every consumer — script, the CLR conversion behind `ToObject()`, the debugger, the debug view — reads the same answer. Thirteen in-box overrides go with it. Two of them disagreed with their own key enumeration: a function reported `prototype` first where `GetOwnPropertyKeys` reports `length`, `name`, `prototype`, and a `String` object omitted its character indices. A lazily created `f.prototype` declared `constructor` to `GetOwnProperties` alone, so `Object.getOwnPropertyNames(f.prototype)` answered `[]`; it now declares it through `GetInitialOwnStringPropertyKeys` and answers `["constructor"]`. `FastSetProperty` / `FastSetDataProperty` are `DefineOwnPropertyUnchecked` / `DefineOwnDataPropertyUnchecked`. The old names claimed a speed the methods do not have — a loop of them is the slow way to project host records — and hid what they do: always an own property, shadowing the prototype chain, no inherited setter, no `[[DefineOwnProperty]]` validation and therefore no possible `TypeError`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
sebastienros#3429 merged with 4.44 while this branch carried 4.45, and the rebase kept this section where it had been -- so the file read 4.43, 4.45, 4.44 and the ordering check added by sebastienros#3454 failed: line 2217: 4.44 follows 4.45 at line 2187 The numbers are right; the position was not. Moved this section to the end of part 4 rather than renumbering, so the section that merged first keeps the number it merged with. That is the rule the guide states, applied the way round it is meant: whoever lands second moves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
force-pushed
the
v5-b5-enumeration-hook-and-unchecked-defines
branch
from
August 27, 2026 04:52
ade36b0 to
f061f4f
Compare
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.
Two names that lied, in one theme.
1.
GetOwnPropertiesis not the enumeration hook — so it is not a hook any moreObject.keys/values/entries,for..in, object spread and rest,Object.assign,JSON.stringifyandJsonSerializerall list keys throughGetOwnPropertyKeysand filter them withProbeOwnProperty. None of them calledGetOwnProperties, whose name is the one that reads like the hook. Its real callers were the CLR conversion path (ToObjectunderOptions.Interop.CreateClrObject),GetSmallestIndex, the debugger'sGetAllBindingNamesand the debug view.So the trap ran both ways. A host that overrode
GetOwnPropertiesshipped an object script could not enumerate — a real integrator did exactly that. A host that did the right thing and overrodeGetOwnPropertyKeys+ProbeOwnPropertyshipped one whose propertiesToObject()and the debugger could not see.GetOwnPropertiesis now derived and non-virtual: keys fromGetOwnPropertyKeys, each descriptor fromGetOwnProperty, a key whose descriptor is absent skipped. One pair of overrides answers everything.Every override audited, and what the derived version answers for each
Thirteen in-box overrides removed. Eleven were already "the same order as
GetOwnPropertyKeys, with the descriptors materialized" — several said exactly that in their own doc comments.ArrayInstancelength, bag, symbols;GetOwnPropertymaterializes the same_sparsedescriptors the override didJsRegExplastIndex, then baseIteratorResultvalue,done, then base;GetOwnPropertycaches the same two descriptorsJsArgumentsEnsureInitialized()+ base, and itsGetOwnPropertyKeysoverride already does thatJsErrormessageinto the bag as a side effect of being enumeratedArrayLikeObject,NamedPropertyObject,JsStorage,JsEventObjectWrapperforeach (key in EnumerateOwnPropertyKeys) yield (key, GetOwnProperty(key))FunctionStringInstanceFunction.ObjectInstanceWithConstructorconstructor— fixed on the key side, see belowTest and benchmark overrides removed with them:
EnvelopeHostObject,ProjectedHostObject, andHostObjectAccessBenchmark's projected host — all three already agreed with their ownGetOwnPropertyKeys.Order changes, decided deliberately
In every case the derived order is the one
Object.getOwnPropertyNamesalready reported, i.e. the specification's[[OwnPropertyKeys]]order:prototype,length,name,[arguments, caller], bag → nowlength,name,prototype,[arguments, caller], bag. That is the order §10.2 creates them in, and the order the key enumeration always used.Stringobject: was bag, symbols,length→ now"0"…"n-1",length, bag, symbols. The character indices are own properties of aStringobject; the override never reported them.One in-box object had the exact defect this change exists to prevent
A lazily created
f.prototype(Function.ObjectInstanceWithConstructor) keepsconstructorin a field and declared it toGetOwnPropertiesalone. Onmaintoday:Deriving
GetOwnPropertiesfrom the key list would have propagated that omission rather than exposing it, so the fix belongs on the key side: it declaresconstructorthroughGetInitialOwnStringPropertyKeys, and both now answer["constructor"]. It stays non-enumerable, soObject.keysandfor..inare unchanged.Two callers dropped it entirely
GetSmallestIndexand the debugger'sGetAllBindingNamesonly ever wanted keys, so they askGetOwnPropertyKeys(Types.String). Strictly less work — no descriptor per key, and for a dense arrayGetSmallestIndexno longer materialized a CEW descriptor per element into_sparse— and it drops symbols fromDebugScope.BindingNames, where a symbol was never a binding name.One more debugger fix the new test forced out
ObjectEnvironment.HasBindings()read the engine's property tables directly, so awithscope over a host object whose properties live outside them was reported as empty and dropped from the scope chain. It now falls back to asking the object, after the two cheap storage tests that answer every in-box case without asking anything.2. Two names that describe an implementation, and a misleading one
FastSetProperty(string, PropertyDescriptor)DefineOwnPropertyUnchecked(string, PropertyDescriptor)FastSetProperty(JsValue, PropertyDescriptor)DefineOwnPropertyUnchecked(JsValue, PropertyDescriptor)FastSetDataProperty(string, JsValue)DefineOwnDataPropertyUnchecked(string, JsValue)Same bodies, mechanical rename, and the compiler finds every call site. The old doc comments spent a paragraph explaining that the methods are not fast; the new names say what they are —
[[DefineOwnProperty]]with the checks taken out. Always an own property, so it shadows the prototype chain; no inherited setter runs; no validation runs, so the call can never raise aTypeError; and a raw descriptor under a string key deopts a shape-mode receiver to dictionary mode permanently.ObjectInstance.Fast.csisObjectInstance.Unchecked.cs.Performance
GetOwnPropertiesis on the CLR conversion path, and deriving it does cost something. No measured number here — I did not run a gated benchmark — so, precisely:List<JsValue>and its backing array (about 56 + 8·N bytes on 64-bit), fromGetOwnPropertyKeys.GetOwnPropertycall plus one hash lookup, where the old dictionary path stepped an enumerator that already held the descriptor. TheJsStrings and thePropertyDescriptors are the same objects either way.Key[slotCount]the oldCollectKeysneeded. Roughly a wash there.ObjectWrapperadditionally materializes its key list instead of streaming it — oneList<JsValue>onToObject()of a wrapped object.GetSmallestIndexand the debugger's binding names materialize no descriptors at all now, where they used to materialize one per key.Nothing on a script-visible hot path moved: no script enumeration ever went through this method, and none does now.
Tests
Jint.Tests.PublicInterface/HostEnumerationHookTests.cs— a host declaring onlyGetOwnPropertyKeys+ProbeOwnPropertyis enumerable byObject.keys,for..in, spread,Object.assignandJSON.stringifyand throughGetOwnProperties,ToObject()andDebugScope.BindingNames; plus the order pin, the absent-descriptor filter, a reflection pin that the method is not virtual, and thef.prototyperegression.Against unmodified
main, 8 of its 9 tests fail — the ninth is the control, script enumeration, which already worked:Jint.Tests.PublicInterface/HostUncheckedDefineTests.cs— pins the four promises the new names make. It is new coverage rather than a regression net: the rename changes no behaviour, so these pass either way.Verification
dotnet build -c Release(solution) anddotnet test -c Release: green, no warnings.Jint.Tests.PublicInterfacewithJINT_HOST_CONTRACT_VERIFICATION=1: green on all three TFMs.docs/v5-migration.md§2 (table row plus 2.6), 3.16 and 4.44, all in this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S