feat(#4513): IR-adopt statically-foldable computed object keys - #4617
Merged
Conversation
Every `ComputedPropertyName` in an object literal used to reject the whole
containing function at `objectlit-computed-key` — including `{ ["a"]: 1 }`,
whose key is a string literal and is indistinguishable from `{ a: 1 }` after
folding. Legacy already folds (`resolveComputedKeyExpression` →
`resolveConstantExpression`) and compiles the result to the same closed struct.
The IR object shape is static, so the adoptable set is exactly the keys that
resolve to a string during selection. The selector is checker-free (bare
`SourceFile`; its `scope` is a name set, not a value environment), so the fold
is purely syntactic: string literal, no-substitution template, numeric literal,
and parenthesised wrappers of those. Keys needing a value environment —
`const k = "a"`, `Symbol.iterator`, template substitution, arithmetic — keep
rejecting at the same arm. No new reject code minted.
New leaf module `src/ir/property-key-fold.ts` holds the single fold that both
`select.ts` and `from-ast.ts` call. The two files cannot import each other, and
a duplicated *widening* fold could drift into a post-claim `invariant` (selector
claims, lowerer cannot deliver). It is applied at the object-literal
data-property site only, not to `phase1PropertyName`, whose 7 other call sites
are class-member / method naming where a computed name means something else.
Measured, not assumed:
- A `String(Number(text)) === text` canonicality guard was written first, on the
premise that `.text` for `{ [0x10]: v }` is raw `"0x10"`. It is not —
TypeScript's scanner already stores the canonical decimal form (16 spellings
verified: `0x10`→`16`, `0b101`→`5`, `0.50`→`0.5`, `1e3`→`1000`,
`1e21`→`1e+21`, …). The guard was dead code documenting a rejection that
never happened; the sweep caught it because three cases drafted as negatives
came back claimed. Removed, and replaced by a runtime assertion that
`{ [0x10]: v }` / `{ 0x10: v }` / `{ 16: v }` are the same key.
- Evaluation order: `lowerObjectLiteral` sorts the field list by name, but only
after every initializer has been lowered in source order. A computed key makes
that distinction visible, since a folded name need not sort in source
position. Pinned with the #4459 counter idiom — `{ ["b"]: p(1), a: p(2) }`
reads 12, not 21.
- `{ a: 1, ["a"]: v }` now rejects at `objectlit-duplicate-key` rather than
`objectlit-computed-key`: still rejected, more precisely labelled.
Acceptance sweep: 30 shapes, each claimed-and-emitted with legacy/IR parity or
cleanly rejected; a single `invariant` fails. PASS — 0 bad of 30, 18 claimed
(12 before). `check:ir-fallbacks` and `check:ir-only` A/B'd against base copies:
unchanged in both directions. tests/issue-4511.test.ts (28), neighbours #4471 /
#4459 (62), equivalence shards 1–8, and the `quality` ratchets all pass.
Co-Authored-By: Claude <noreply@anthropic.com>
✓
…ject-keys # Conflicts: # plan/log/ir-adoption.md
…ision ✓ `claim-issue.mjs --allocate` reserved #4511 with `pr_scan="degraded"` (gh unavailable in this container, so the open-PR id universe was not consulted). While this branch was in flight, `1efe399b` landed the session usage-limit monitor on main as #4511 — exactly the collision the degraded scan warns about, and the one `check:issue-ids:against-main` exists to catch before the merge queue does. Renumbered to a freshly reserved #4513: issue file, test file, and every `#4511` reference in `src/ir/property-key-fold.ts`, `src/ir/select.ts`, `src/ir/from-ast.ts` and the `ObjectLiteralExpression` row in `scripts/gen-ir-adoption.mjs` (table regenerated). No behaviour change. Co-Authored-By: Claude <noreply@anthropic.com> ✓
…ision ✓ - `check:ir-only` on the merged tip reads 22 emitted / 15 unsupported on the standalone lane vs 19 / 18 before the merge. That delta is main's, not this branch's: the pre-merge A/B against the `.tmp/4513/base/` revert copies read 19 / 18 both with and without the change. Recording which run attributes what, rather than quoting the larger number as if this slice produced it. - New section on the #4511 collision: the id was reserved with `pr_scan="degraded"` (no `gh` in this container, so the open-PR universe was never consulted), main then landed the usage-limit monitor as #4511, and `check:issue-ids:against-main` caught it at the pre-push hook. A degraded-scan reservation is provisional — re-check before pushing, not only before creating the file. Co-Authored-By: Claude <noreply@anthropic.com> ✓
…guard ✓ `isPhase1ObjectLiteral` still pointed at "the numeric-canonicality guard" in `property-key-fold.ts`, and both `phase1PropertyName` copies described a numeric key's `.text` as "raw". Measurement removed the guard and disproved the "raw" description: TypeScript's scanner already stores the canonical decimal form, so `.text` IS the spec key. Comments only — no behaviour change. Co-Authored-By: Claude <noreply@anthropic.com> ✓
ttraenkler
added a commit
that referenced
this pull request
Aug 22, 2026
ttraenkler
added a commit
that referenced
this pull request
Aug 22, 2026
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.
Description
Implements #4513 (issue file in this PR): object literals with a statically foldable computed key — string/numeric/template-with-no-substitution/parenthesized-literal — now claim and emit in the IR instead of rejecting the whole function with
objectlit-computed-key.(Reserved as #4511 first;
--allocateranpr_scan="degraded"becauseghis unavailable in-container, and main landed the usage-limit monitor as #4511 concurrently. Thecheck:issue-ids:against-mainpre-push gate caught it — renumbered to #4513, the #4511 reservation released. Working exactly as designed.)Claims now:
{ ["a"]: v },{ [`a`]: v },{ [0]: v },{ [0x10]: v },{ [("a")]: v }→emitted(body=true). Still demotes (unchanged): runtime/variable keys,{ [${p}b]: v },{ [1+1]: v },Symbol.iteratorkeys, computed method/accessor names ({ ["m"]() {} },{ get ["a"]() {} }), andclass A { [k]() {} }(different gate, different node kind).{ a:1, ["a"]:v }now rejects with the more preciseobjectlit-duplicate-key. No new reject code minted.Two things measurement corrected: (1) a numeric-canonicality guard (
String(Number(text)) === text) was drafted then deleted as dead code — TS's scanner already stores the canonical decimal form (0x10→16, verified across 16 spellings), so the guard documented a rejection that never fired; replaced by a runtime assertion that{[0x10]}/{[16]}are one key. (2){ ["m"]() {} }is a legacy backend bug (emits a non-callablem) — recorded so nobody "fixes" the IR to match it.Evaluation order pinned with the #4459 counter (
t = t*10 + k), asserted on both paths and against V8:{ ["b"]: p(1), a: p(2) }→ 12 (not 21, thoughasorts first) — initializers run in source order, name-sort applies only after.Gates (on the merged tip):
tests/issue-4513.test.ts28/28; neighbours #4471/#4459 62/62;check:ir-fallbacksOK;check:ir-only(+--policy=hybrid) READY;check:ir-adoptionclean (ObjectLiteralExpression row regenerated); typecheck 0 (base 0); lint/format clean; equivalence shards 1–8 all exit 0, no new regressions; loc-budgetselect.ts+17 /from-ast.ts+9 granted. The single fold lives in a new leaf modulesrc/ir/property-key-fold.tsso selector and lowerer consult one function and cannot drift into a post-claim invariant; each god-file takes a one-call-site swap at the data-property site only.Two measured caveats, both not this branch's:
check:ir-onlystandalone reads 22/15 vs 19/18 pre-merge — that delta is main's (A/B against revert copies reads 19/18 with and without the change); andcheck:godfilesfails identically without this change on files the branch doesn't touch (and no workflow runs it).CLA
🤖 Generated with Claude Code
https://claude.ai/code/session_01G8EzRKZovuUremQM9dfJAC
Generated by Claude Code