Skip to content

BuiltinShape Factory slots: shape-path intrinsic references and throwers; flip Function.prototype - #2590

Merged
lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf-builtin-shape-slot-kinds
Jul 6, 2026
Merged

lahma merged 2 commits into
sebastienros:mainfrom
lahma:perf-builtin-shape-slot-kinds

Conversation

@lahma

@lahma lahma commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Adds a Factory slot kind to BuiltinShape — the descriptor is produced by a process-shared static factory over the host instance — and teaches the source generator to emit [JsIntrinsicReference] / [JsThrowerAccessor] members on UseShape hosts as Factory slots (previously they hit the UnsupportedShapeMember diagnostic and forced the whole host onto the generated-dictionary path).

  • Intrinsic references emit the same lazy per-realm LazyPropertyDescriptor the dictionary path uses, so neither slot materialization nor a deopt forces the realm intrinsic into existence. This is a hard requirement for the upcoming global-object flip: globalThis deopts on any top-level var, and its 56 intrinsic-reference slots must stay lazy through that (the deopt itself stays lazy per Keep builtin-shape deopt lazy: unmaterialized function slots stay deferred #2588).
  • Throwers emit realm-pinned %ThrowTypeError% accessor descriptors — materialization may run under a foreign active realm (same class of issue as the cross-realm fix in Serve arguments reads virtually; defer property materialization #2589).
  • Emission order interleaves with the dictionary-path order (properties, functions, intrinsic references, accessors, throwers, aliases, instance slots) so own-key enumeration stays byte-identical when a host flips.
  • DeoptBuiltinShape stores factory results for untouched slots (cheap/lazy by contract).

The second commit flips Function.prototype to shape storage as the first consumer: its two thrower accessors become Factory slots and its per-realm property dictionary disappears. Both descriptors resolve the same per-realm %ThrowTypeError% singleton, so the spec getter identity (gOPD(fp,"arguments").get === gOPD(fp,"caller").get) holds.

Measurements

FunctionPrototype is a small host, so this is gated as no-regression; the init win arrives with the GlobalObject flip this unblocks. BenchmarkDotNet default jobs, A/B vs main from separate worktrees: EngineConstructionBenchmark (8 cases), MinimalScriptBenchmark, BuiltinShapeBenchmark — times within noise, allocations byte-identical (±0.01 KB). Two new Verify snapshots cover the Factory emissions.

Verification

  • Jint.Tests green (net10.0 + net472), Jint.Tests.PublicInterface green, Jint.Tests.SourceGenerators 32/32
  • Full test262: 99,260 passed, 0 failed (function-prototype property attributes, thrower semantics, %ThrowTypeError% identity)
  • Live-engine checks: thrower getter identity across arguments/caller, TypeError on access, own-key enumeration order, deopt via Function.prototype.x = 1 with post-deopt thrower materialization

🤖 Generated with Claude Code

lahma and others added 2 commits July 6, 2026 20:55
Adds a Factory slot kind whose descriptor a process-shared static factory
produces from the host instance, and teaches the generator to emit
[JsIntrinsicReference] / [JsThrowerAccessor] members on UseShape hosts as
Factory slots (previously an UnsupportedShapeMember diagnostic):

- intrinsic references emit the same lazy per-realm LazyPropertyDescriptor
  the dictionary path uses, so neither slot materialization nor a deopt
  forces the realm intrinsic into existence — a hard requirement for the
  upcoming global-object flip, where any top-level var deopts the shape;
- throwers emit realm-pinned ThrowTypeError accessor descriptors
  (materialization may run under a foreign active realm);
- emission order interleaves with the dictionary path order (properties,
  functions, intrinsic references, accessors, throwers, aliases, instance
  slots) so own-key enumeration stays byte-identical;
- DeoptBuiltinShape stores factory results for untouched slots (cheap/lazy
  by contract).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First consumer of the Factory slot kinds: the two thrower accessors become
Factory slots and the per-realm property dictionary disappears. Both
descriptors resolve the same per-realm %ThrowTypeError% singleton, so the
spec getter identity (fp.arguments.get === fp.caller.get) holds with
separate descriptor instances.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma enabled auto-merge (squash) July 6, 2026 18:26
@lahma
lahma merged commit 58a380b into sebastienros:main Jul 6, 2026
7 of 8 checks passed
lahma added a commit that referenced this pull request Jul 7, 2026
…hapes; escalate obsolete Generators flag (#2597)

The three remaining generator-emitted dictionary hosts whose members became
expressible with Factory slots (#2590): Intl (10 intrinsic refs + 2 functions),
Temporal (9 intrinsic refs) and %AsyncFromSyncIteratorPrototype% (3 functions).
Intl cold-start allocations drop ~1.1 KB per fresh-engine format call (-4.3%);
engine construction is unaffected (hosts are lazy).

Audited and skipped: RegExpConstructor (21 hand-rolled legacy accessors like
$1-$9 would need instance slots; low value), JsSegments (per-call instance,
no representative traffic). Math/JSON/Reflect/Atomics/Generator prototypes
already use BuiltinShapeObject.

Rider: ExperimentalFeature.Generators [Obsolete] escalated to error: true
(flag has been a no-op; removal candidate for next major).


Claude-Session: https://claude.ai/code/session_01S2U2y9voMQV9rvCkYeQbka

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@lahma
lahma deleted the perf-builtin-shape-slot-kinds branch July 13, 2026 06:52
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