Skip to content

Choose record key order from the orders values are built in - #1398

Merged
ctate merged 2 commits into
mainfrom
opt/key-order-s1
Oct 19, 2026
Merged

ctate merged 2 commits into
mainfrom
opt/key-order-s1

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

Summary

A record enumerates its keys (Object.keys/values/entries, for…in, JSON.stringify, util.inspect, console.log, record→dyn conversion) through one table per shape, IrRecordShape.declaredOrder. Until now that table was the checker's property order of the first type interned to the shape, so every value built in another order printed differently from Node, silently in most cases (scriptc's own RuntimeFeatures record broke the self-hosting comparison this way in #1348).

This change picks each shape's table from the orders its values are actually created in. It changes table contents only: storage layout, shape identity, and the structure of every helper stay the same, so there is no new runtime work and no new per-object state.

Stage 1: construction witnesses.

  • An object literal contributes the order it writes its keys in.
  • A spread segment contributes its source shape's order. It is an inheritance edge, never the desugar's field list, which was the prototype's mistake that broke 1880/5330/5961. A key keeps the position of its first occurrence, so an override stays where the spread put it. A key whose earlier occurrence may be absent at run time contributes every position it can take.
  • Rest bindings and record→record conversions contribute their source's order.
  • Synthesized constructions (builtin results, runtime builders) and checked conversions (dynCheck: typed JSON.parse, values from JavaScript) pin today's table. Checked conversions already keep their own order through the record-source table, and a different table would make values parsed in today's order keep a source view.
  • The orders merge into a precedence graph per shape. It is sorted topologically, with ties broken by today's table, so a shape whose witnesses agree with the table keeps it untouched (the same array).

Stage 2: late-write witnesses.

  • An optional key that no construction creates goes after every key a construction can create.
  • Straight-line writes of several such keys keep their program order. The writes must go through one binding, initialized once, in the same function and loop body. Writes behind conditions count.
  • Any other write that can append a key goes after every key.

What keeps today's table. Shapes whose witnesses conflict keep today's table and today's SC6002 warnings, and so does every shape copied from them. Conflicts come from values built in two orders, structural interning of differently ordered types, delete then re-add, or late keys written through different bindings. Shapes that no order surface observes also keep today's table.

Placement. The pass (frontend/lowering/key-order-witness.ts) runs once retained reachability has settled, after the shape-order metadata finalizers and before the helper finalizers that rebuild the keys/values/inspect/merge helpers. It uses the typed IR traversal, and the self-hosted compiler lowers it.

SC6002 consults the re-chosen table, so warnings disappear exactly where the native order now matches Node.

Native inspect fix. lower-inspect.ts typed its helper function as an inferred object literal. Natively, pushing that into liftedFns: IrFunction[] copied it, so the shape-order finalizer replaced the body of the original instead of the listed copy. This was latent: the finalizer rarely changed an order before. It is now typed IrFunction. Without this fix, the native self-lowering diverged from the Node seed.

Evidence

Probes (design-key-order probes, Node 24.15 vs native):

  • 23 probes go from DIFF to MATCH, and no probe regresses.
  • 42 of 60 entries now match, against 19 on main.
Probe main this PR
p01 direct keys (SC6002 gone) DIFF MATCH
p02 factory DIFF MATCH
p04 for…in DIFF MATCH
p05 entries/values DIFF MATCH
p06 inspect DIFF MATCH
p09 structuredClone DIFF MATCH
p13 optional later DIFF MATCH
p13b optional later, direct (SC6002 gone) DIFF MATCH
p19 widen to unknown/any DIFF MATCH
p20 Map values DIFF MATCH
p21 union of records DIFF MATCH
p25 rest destructuring DIFF MATCH
p29 generic factory / map DIFF MATCH
p31 nested records DIFF MATCH
p34 two late keys DIFF MATCH
p44 explicit undefined DIFF MATCH
p52 push in a loop DIFF MATCH
p55 replacer array / indent DIFF MATCH
p57 replacer function DIFF MATCH
p58 util.inspect / util.format DIFF MATCH
p59 sequential option fills DIFF MATCH
p62 RuntimeFeatures pattern DIFF MATCH
p63 frontend-cache spread writer DIFF MATCH

These still differ, as on main, and are stage 3 or out of scope: p03, p07, p08, p10, p11, p14, p23, p24, p32, p33, p40, p41, p50, p53, p60. p21b and p48 stay refused (SC1052, SC1090). p08 and p40 go through a checked conversion, which pins the table on purpose. p60 builds one JSDoc type in two orders.

Corpus (Node vs native, byte-identical, both lanes): key-order-literal-flows.ts, key-order-spread-copies.ts, key-order-late-optional.ts. All three fail on main.

Unit tests: key-order-witness.test.ts covers agreement, conflict, pins, inheritance, late writes, loops and re-adds.

Zero-cost gate (scripts/zero-cost-gate.mjs, bench, self and the whole corpus, baseline = origin/main):

  • Bench. All 30 bench programs (the runtime benchmark workloads and the 2 build applications) are identical in IR, LLVM and executable.
    • The gate's one failure, bench/thread-slots linking dynamic-tier symbols, is not from this change: that executable is byte-identical to the baseline's.
  • Corpus. 3,115 of 3,117 corpus programs (TypeScript and JavaScript; the three new programs are not in the baseline set) are identical. Two change, and both still match Node in both lanes:
    • 5047-generator-delegation-iterables-typed.ts: the iterator-result shape {done, value} is built as { done, value } by the program's next() methods, and its table goes from value,done to done,value. Nine functions change: JSON writers, one record→dyn converter and the record-view materializers.
    • 6183-interface-overload-flow.ts: the shape {emit, label} goes from emit,label to label,emit, and one JSON writer changes. emit is a function, so the output is identical.
    • Neither change touches storage layout, field access or allocation.
  • Self-hosted compiler. 18 of 91,279 LLVM functions change, all intended:
    • 8 JSON writers: diagnostic records (code, category, severity, scope, file, line, column, …), compile-option records and lowering-position records.
    • 8 record-view materializers.
    • The %obj.keys and %obj.values helpers.
    • These orders now follow how the compiler builds those objects, which is Node's order. The Node-run and self-hosted compilers agree on the self-hosting comparison tests: runtime-features, native-frontend and its checks, native-cache, and native-driver (native self-lowering IR and LLVM byte-identical to the Node seed).
  • Compile time. The bench geomean is +0.25% (95% CI −0.38% to +1.13%) and the self-hosted compiler build is −0.15% (95% CI −0.32% to +0.65%). Both are within tolerance.

Test plan

  • probes before/after
  • new corpus programs, plain and sanitized lanes
  • pnpm test:ts7 --baselines-only (3 new baselines recorded)
  • coverage.test.ts (divergence snapshot unchanged; every corpus program static, 3 shards)
  • diagnostics.test.ts, envelope.test.ts
  • self-host IR build (no diagnostics)
  • self-hosting-runtime-features, native-frontend(-checks), native-cache, native-driver
  • zero-cost gate (bench, self, whole corpus incl. JS)
  • docs gate (NEXT_DIST_DIR=.next-check pnpm check)
  • lint, format

🤖 Generated with Claude Code

Each record shape's enumeration table now follows the key order its object
literals, spreads, rest bindings and record copies create, and appends
optional keys assigned after construction. Shapes whose values are built
in more than one order keep their declaration order and SC6002.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 19, 2026 9:51pm UTC

@ctate
ctate merged commit 0df30d4 into main Oct 19, 2026
66 checks passed
@ctate
ctate deleted the opt/key-order-s1 branch October 19, 2026 22:15

This branch was successfully deployed

1 active deployment
Preview — cf87b359 Deployed Oct 19, 2026 by vercel[bot]
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.

2 participants