Skip to content

Ask an exotic global prototype for a bare identifier through [[Get]] - #2946

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:fix/global-prototype-get-trap
Aug 10, 2026
Merged

lahma merged 1 commit into
sebastienros:mainfrom
lahma:fix/global-prototype-get-trap

Conversation

@lahma

@lahma lahma commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

GlobalEnvironment.TryGetFromGlobalPrototype resolved the direct prototype with TryGetOwnPropertyValue — that is [[GetOwnProperty]], so a Proxy installed as the global's prototype saw its getOwnPropertyDescriptor trap and never its get. Levels two and deeper were already spec-shaped (HasProperty + Get(name, global)), so the defect was exactly one level deep — and it made HasBinding and the value read contradict each other:

var p = new Proxy({}, { has: (t, k) => k === 'virt', get: () => 'VIRTUAL' });
Object.setPrototypeOf(globalThis, p);
typeof virt           // node: "string"    Jint: "undefined"
virt                  // node: "VIRTUAL"   Jint: ReferenceError: virt is not defined
'virt' in globalThis  // both: true
globalThis.virt       // both: "VIRTUAL"

Pre-existing, but aggravated by #2928: before it, typeof also answered "undefined", so the engine was at least self-consistent. Now it reports the binding exists and the read throws.

test262 covers this and we do not run it. test/staging/sm/Proxy/global-receiver.js asserts the global is the receiver handed to a Proxy's get and set traps when the Proxy is the global's prototype. Run by hand through the REPL: before, ReferenceError: bareword is not defined at the first assertion; after, PASS gets=1 sets=1. The harness emits only annexB, built-ins, intl402 and language, so staging/ never reaches the suite. It is now pinned in-repo as ExoticGlobalPrototypeTests.Test262StagingGlobalReceiver.

The gate is InternalTypes.ExoticGet — the bit behind PropertyAccessSemantics.Exotic — not a is JsProxy type test, so a host object that overrides Get gets the same answer, and one that overrides Get but declares Ordinary keeps the shortcut, which is exactly what that declaration promises. Level 1 and levels 2+ now share one body.

The shortcut is kept for ordinary prototypes because it is load-bearing: it lets a host prototype answer without building a descriptor it would throw away. An ordinary prototype now additionally executes a load of prototype._type, an AND with a constant and a not-taken branch — a same-cache-line load feeding a predictable branch, on a method that is already NoInlining and is reached only after the global's own lookup and its shape lookup have both missed. No new call, no new allocation, no change in virtual dispatch count.

8 new tests, 4 red on main on both net10.0 and net472. The 4 that passed cold were the set-trap and level-2 cases, isolating the defect. Verification leg green.

🤖 Generated with Claude Code

GlobalEnvironment's identifier lane resolved the global's direct prototype
with TryGetOwnPropertyValue, whose base body is [[GetOwnProperty]]. For a
Proxy that fires the getOwnPropertyDescriptor trap and never get, so a name
the handler only produces from its get trap resolved to nothing - while
HasBinding, a real [[HasProperty]] chain walk, had already said the binding
exists. A host object that synthesises a value in Get has the same shape and
was answered the same way. Reading the identical name two levels down worked,
because the deeper levels were already spec-shaped [[HasProperty]] + [[Get]].

The direct prototype now takes that same pair whenever it carries
InternalTypes.ExoticGet - the flag behind PropertyAccessSemantics.Exotic,
which the exotic built-ins state outright and which the engine derives for a
host type that overrides Get. Everything else keeps the shortcut and the
reason it exists: a host prototype answering from its own state without
building a descriptor it would only throw away. The ordinary path gains one
test on a field it already loads.

test262's staging/sm/Proxy/global-receiver.js is the specification for this
and now passes (gets 1, sets 1, both receivers the global). It is not in our
generated suite - the harness generates annexB, built-ins, intl402 and
language only - so it is pinned from the embedder's side instead, together
with the host-object shape the flag covers and the ordinary prototypes that
must keep the short path.

The write lane needed nothing: SetFromMutableBinding already routed an
inherited name through [[HasProperty]] + [[Set]] with the global as receiver.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lahma
lahma merged commit d18e108 into sebastienros:main Aug 10, 2026
5 checks passed
@lahma
lahma deleted the fix/global-prototype-get-trap branch August 10, 2026 17:39
lahma added a commit that referenced this pull request Aug 15, 2026
* Run test262's staging/ directory too

We generate annexB, built-ins, intl402 and language and have never generated
staging/, and the reason was never a decision: Test262Harness.Console's settings
file had no way to name the set. Test262StreamOptions.SubDirectories existed,
EnumerateTestFiles already recursed and TestSuiteGenerator already iterated the
option -- but TestSuiteGeneratorOptions had no matching property and the console's
configureOptions lambda set only the loggers, so the stream kept the built-in
default whatever the settings file said. lahma/test262-harness-dotnet#173 binds
the three together; this bumps to the 1.1.2 that carries it and opts in.

It is worth having because staging/ carries coverage that exists nowhere else in
the tree we already pin. Both defects fixed in 4.16.0 have their only upstream
test there:

- staging/sm/Proxy/global-receiver.js asserts the receiver a Proxy sees when it
  is the global's prototype -- the [[Get]] defect fixed in #2946.
- staging/sm/global/parseInt-01.js asserts parseInt("-0x10", 16) === -16 and
  that parseInt("-0", 10) is -0 -- both fixed in #2945. All 55 files under
  built-ins/parseInt/ use unsigned hex, so neither was reachable for us.

Both pass.

The SpiderMonkey ports include their helpers as "sm/non262-Set-shell.js" from
harness/sm/, but Test262Stream.GetHarnessFiles() enumerates only the top level of
harness/, so TestHarness now walks the tree itself and keys State.Sources on the
path relative to harness/. A top-level file's relative path is its file name, so
every existing include resolves byte-identically -- which the run proves: all
99,901 pre-existing cases still pass.

99,901 -> 102,664 generated cases; 2m48s -> 3m00s. 131 of the 1,483 new files are
excluded, grouped under a banner with a prose reason each: the ones that hang or
exhaust memory, and the conformance gaps the SpiderMonkey suite reaches that the
stable directories do not (cross-realm error identity, legacy Function.caller,
Annex B block-scoped function hoisting, Set-method operation order, Math and
Number precision, function source text, ...). Four pre-existing language/ tests
join the _slowTestFiles list: they eval() a regexp literal per code unit and the
extra parallel load pushes them past the 30-second wall clock, though the whole
group still runs in 6 seconds on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Re-run CI now that Test262Harness.Console 1.1.2 is published

The previous run failed only on 'Version 1.1.2 of package test262harness.console
is not found in NuGet feeds' - the tool was tagged but had not propagated yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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