test(bench): canonical shape axis across all 14 languages (#2766)

The shape arm was three languages with an ad-hoc shape list each, so
"covers all supported languages" was an assertion rather than a
measurement. Replace it with a canonical 10-shape axis every language
must answer for, and add two states so a hole stops masquerading as a
result:

- `N/A` — the grammar does not admit the spelling. A reason is REQUIRED;
  an omitted cell and an inapplicable one are otherwise identical in a
  diff, which is how coverage rots.
- `GRAMMAR-UNAVAILABLE` — the parser could not load, so nothing was
  measured. `drift` skips it on BOTH sides, so the gate cannot fail for
  the environment it ran in. Dart, Kotlin and Swift are vendored
  OPTIONAL grammars, absent under GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 or
  when no vendored prebuild matches the host. All 14 load on glibc
  linux-x64, so this state has no producer in the committed baseline —
  it guards the skip-flag and unsupported-host cases.

`assertMatrixComplete` throws on a missing cell, an unknown id, or a
reasonless N/A. It caught four real ids on its first run; those became
declared `extraShapeIds`, because PHP's annotated/unannotated pair and
C++'s pointer/value pair discriminate things the axis cannot express.

164 cells: 42 RESOLVES, 22 VISIBLE-GAP, 31 INVISIBLE-GAP, 69 N/A. Count
arm unmoved at 101 call drops — shape fixtures build in temp directories
and never touch the committed corpus.

The measurement contradicts what was assumed about the decoration bug:

- Go isolates it to ONE cell. Varying receiver and field decoration
  independently: value/value RESOLVES, value/pointer RESOLVES,
  pointer/value is a VISIBLE-GAP. Only the pointer receiver fails; field
  bindings already go through normalizeGoTypeName, so the step lookup is
  sound and the defect is entirely the base.
- Go is the ONLY language whose receiver decoration defeats the lookup.
  Rust `&mut self` resolves.
- The field-type gap is real in rust (Box<User>), typescript
  (User|null), csharp (User?), swift (User?) and cpp (User*) — but php,
  python, kotlin and dart already resolve theirs.
- PHP's sigil hypothesis is dead: `$svc->getUser()->save()` (unannotated
  return) is an INVISIBLE-GAP while `$svc->getUserTyped()->save()`
  (annotated) RESOLVES. Same chain, same `->`, same base.

Also surfaced, previously unknown: Swift resolves almost no chained
receiver; Ruby's chains are VISIBLE-GAPs; C++ `this->` field receivers
are INVISIBLE; `indexElement` is INVISIBLE-GAP in all 14; and Dart is
the only language where `awaitParen` already resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-07-31 10:59:20 +00:00
parent 1147646518
commit b442e04bf4
3 changed files with 1348 additions and 11 deletions

View file

@ -1,5 +1,123 @@
# Receiver-resolution baseline
## U2 — shape matrix expanded to a canonical axis
The shape arm was three languages with an ad-hoc shape list each. It is now a
**canonical 10-shape axis** (`SHAPE_IDS`) that every language must answer for,
with two states added so a hole cannot masquerade as a measurement:
- `N/A` — the grammar does not admit this spelling. **A reason is required.** An
omitted cell and a genuinely inapplicable cell look identical in a diff
otherwise, which is how coverage rots.
- `GRAMMAR-UNAVAILABLE` — the parser could not be loaded, so nothing was
measured. Neither passes nor fails the gate, and `drift` skips it on **both**
sides so the gate cannot fail for the environment it ran in. `tree-sitter-dart`,
`-kotlin` and `-swift` are vendored *optional* grammars: absent when a run sets
`GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1`, and soft-failing when no vendored prebuild
matches the host (the set covers darwin/linux arm64+x64 and win32-arm64 — a
win32-x64 or musl host has none). **All 14 load on a glibc linux-x64 host, so
this state has no producer in the committed baseline** — it guards the
skip-flag and unsupported-host cases rather than a condition seen here.
`assertMatrixComplete` throws when a language omits a cell, declares an unknown
id, or writes an `N/A` with no reason. Languages may declare `extraShapeIds` for
diagnostics the canonical axis cannot express (PHP's annotated/unannotated
return-type pair, C++'s pointer/value base pair) — an extra must be declared, so
it stays a deliberate diagnostic rather than a typo'd canonical id.
**Vue and COBOL** are language-level `N/A` rows: their emitters never call
`synthesizeReceiverChainCapture`, so there is nothing to measure — but the
language axis now obeys the same no-omitted-cells rule as the shape axis.
### What the first expanded run found
Three results that redirected the plan they were built to serve:
**Go — the root cause, isolated to one cell.** Three rows vary receiver
decoration and field decoration independently:
| Cell | Receiver | Field | State |
|---|---|---|---|
| `fieldReceiverCall` | value | value | RESOLVES |
| `decoratedFieldType` | value | **pointer** | RESOLVES |
| `decoratedReceiverBase` | **pointer** | value | **VISIBLE-GAP** |
Only the pointer *receiver* fails. Go already normalizes field type bindings
through `normalizeGoTypeName`, so the step lookup is sound and the defect is
entirely the base — `synthesizeGoReceiverBinding` stores `typeNode.text` raw, so
`func (h *Host)` binds `h` to the literal `*Host`, which
`findClassBindingInScope` cannot resolve.
**PHP — the sigil hypothesis is dead.** The two rows differ only in whether the
called method declares a return type:
| Cell | Return type | State |
|---|---|---|
| `arrowCallChain``$svc->getUser()->save()` | unannotated | INVISIBLE-GAP |
| `plainChain``$svc->getUserTyped()->save()` | **annotated** | **RESOLVES** |
Same chain, same `->`, same base. PHP chains resolve when the return type is
declared; the `$` sigil is not involved. `decoratedFieldType` (`?User $repo`)
also resolves, so PHP nullable field types already work.
**C++ — the base already resolves, but `this->` field receivers do not.**
`pointerArrowChain` and `valueDotChain` both RESOLVE, so a decorated C++ base is
not a gap. But `this->repo.save()` and `this->repo->save()` are both
INVISIBLE-GAP — a distinct defect, not a decoration one.
**Rust — the decorated receiver is NOT a gap.** `&mut self` resolves, so Go is
the only language whose method receiver decoration defeats the lookup. Rust's
gap is the field: `Box<User>` is INVISIBLE-GAP.
### The decoration cells, across all 14
The rows U1 exists to fix. Everything else is a different defect.
| Language | `decoratedReceiverBase` | `decoratedFieldType` |
|---|---|---|
| go | **VISIBLE-GAP** (`*Host`) | RESOLVES |
| rust | RESOLVES (`&mut self`) | **INVISIBLE-GAP** (`Box<User>`) |
| typescript | N/A | **INVISIBLE-GAP** (`User \| null`) |
| csharp | N/A | **VISIBLE-GAP** (`User?`) |
| swift | N/A | **INVISIBLE-GAP** (`User?`) |
| cpp | N/A | **INVISIBLE-GAP** (`User*`) |
| python, php, kotlin, dart | N/A | RESOLVES |
| java, c, javascript, ruby | N/A | N/A |
So U1's measured scope is **Go's receiver base**, plus the field-type gap in
**Rust, TypeScript, C#, Swift and C++** — and *not* PHP, Python, Kotlin, Dart or
Java, whose decoration handling already works or does not exist. Five of the
seven hooks the plan speculatively listed were aimed at languages that need
none; three languages that do need one were not on the list at all.
### Other gaps this run surfaced, not in the plan
- **Swift resolves almost nothing.** `plainChain`, `plainDeepChain`,
`optionalChain` and `nonNullAssert` are all INVISIBLE-GAP, while
`fieldReceiverCall` resolves. Chained receivers are essentially unsupported.
- **Ruby chains are VISIBLE-GAPs** (`plainChain`, `plainDeepChain`,
`optionalChain`) and `fieldReceiverCall` on `@repo` is INVISIBLE.
- **C++ `this->` field receivers** are INVISIBLE-GAP in both the value and
pointer form.
- **C# has four gaps** beyond the field one: `optionalChain`, `nonNullAssert`,
`awaitParen`, `explicitTypeArgs`.
- **Dart `await` already resolves** — the only language where `awaitParen` is
green, which makes it the reference for U4's unwrap direction.
- **`indexElement` is INVISIBLE-GAP in all 14** — uniform, and exactly what U5
targets.
### Coverage status
All 14 languages measured, plus `vue` and `cobol` as language-level `N/A` rows.
164 cells: 42 RESOLVES, 22 VISIBLE-GAP, 31 INVISIBLE-GAP, 69 N/A, 0
GRAMMAR-UNAVAILABLE.
The **count arm is unchanged at 101 call drops** — shape fixtures are built in
temp directories and never touch the committed corpus, so expanding the shape
axis moves the shape arm only.
---
> **Updated after U10** (structural receiver typing wired into Case 0). Three
> TypeScript shapes flipped to `RESOLVES``svc?.getUser().save()`,
> `svc!.getUser().save()`, `svc.getTyped<User>().save()` — and the call-drop

View file

@ -1,5 +1,29 @@
{
"shapeArm": {
"vue": {
"plainChain": "N/A",
"plainDeepChain": "N/A",
"optionalChain": "N/A",
"nonNullAssert": "N/A",
"awaitParen": "N/A",
"explicitTypeArgs": "N/A",
"indexElement": "N/A",
"fieldReceiverCall": "N/A",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "N/A"
},
"cobol": {
"plainChain": "N/A",
"plainDeepChain": "N/A",
"optionalChain": "N/A",
"nonNullAssert": "N/A",
"awaitParen": "N/A",
"explicitTypeArgs": "N/A",
"indexElement": "N/A",
"fieldReceiverCall": "N/A",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "N/A"
},
"typescript": {
"plainChain": "RESOLVES",
"plainDeepChain": "RESOLVES",
@ -7,15 +31,170 @@
"nonNullAssert": "RESOLVES",
"awaitParen": "VISIBLE-GAP",
"explicitTypeArgs": "RESOLVES",
"indexElement": "INVISIBLE-GAP"
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "RESOLVES",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "INVISIBLE-GAP"
},
"php": {
"arrowCallChain": "INVISIBLE-GAP",
"arrowPropertyPath": "RESOLVES"
"arrowPropertyPath": "RESOLVES",
"plainChain": "RESOLVES",
"plainDeepChain": "INVISIBLE-GAP",
"optionalChain": "INVISIBLE-GAP",
"nonNullAssert": "N/A",
"awaitParen": "N/A",
"explicitTypeArgs": "N/A",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "N/A",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "RESOLVES"
},
"cpp": {
"pointerArrowChain": "RESOLVES",
"valueDotChain": "RESOLVES"
"valueDotChain": "RESOLVES",
"plainChain": "N/A",
"plainDeepChain": "RESOLVES",
"optionalChain": "N/A",
"nonNullAssert": "N/A",
"awaitParen": "N/A",
"explicitTypeArgs": "VISIBLE-GAP",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "INVISIBLE-GAP",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "INVISIBLE-GAP"
},
"go": {
"plainChain": "RESOLVES",
"plainDeepChain": "RESOLVES",
"optionalChain": "N/A",
"nonNullAssert": "N/A",
"awaitParen": "N/A",
"explicitTypeArgs": "N/A",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "RESOLVES",
"decoratedReceiverBase": "VISIBLE-GAP",
"decoratedFieldType": "RESOLVES"
},
"javascript": {
"plainChain": "VISIBLE-GAP",
"plainDeepChain": "VISIBLE-GAP",
"optionalChain": "VISIBLE-GAP",
"nonNullAssert": "N/A",
"awaitParen": "VISIBLE-GAP",
"explicitTypeArgs": "N/A",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "RESOLVES",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "N/A"
},
"python": {
"plainChain": "RESOLVES",
"plainDeepChain": "RESOLVES",
"optionalChain": "N/A",
"nonNullAssert": "N/A",
"awaitParen": "VISIBLE-GAP",
"explicitTypeArgs": "N/A",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "RESOLVES",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "RESOLVES"
},
"java": {
"plainChain": "RESOLVES",
"plainDeepChain": "RESOLVES",
"optionalChain": "N/A",
"nonNullAssert": "N/A",
"awaitParen": "N/A",
"explicitTypeArgs": "RESOLVES",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "RESOLVES",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "N/A"
},
"csharp": {
"plainChain": "RESOLVES",
"plainDeepChain": "RESOLVES",
"optionalChain": "INVISIBLE-GAP",
"nonNullAssert": "VISIBLE-GAP",
"awaitParen": "VISIBLE-GAP",
"explicitTypeArgs": "VISIBLE-GAP",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "RESOLVES",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "VISIBLE-GAP"
},
"ruby": {
"plainChain": "VISIBLE-GAP",
"plainDeepChain": "VISIBLE-GAP",
"optionalChain": "VISIBLE-GAP",
"nonNullAssert": "N/A",
"awaitParen": "N/A",
"explicitTypeArgs": "N/A",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "INVISIBLE-GAP",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "N/A"
},
"rust": {
"plainChain": "RESOLVES",
"plainDeepChain": "RESOLVES",
"optionalChain": "N/A",
"nonNullAssert": "N/A",
"awaitParen": "N/A",
"explicitTypeArgs": "INVISIBLE-GAP",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "RESOLVES",
"decoratedReceiverBase": "RESOLVES",
"decoratedFieldType": "INVISIBLE-GAP"
},
"c": {
"plainChain": "N/A",
"plainDeepChain": "INVISIBLE-GAP",
"optionalChain": "N/A",
"nonNullAssert": "N/A",
"awaitParen": "N/A",
"explicitTypeArgs": "N/A",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "INVISIBLE-GAP",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "N/A"
},
"kotlin": {
"plainChain": "RESOLVES",
"plainDeepChain": "RESOLVES",
"optionalChain": "RESOLVES",
"nonNullAssert": "VISIBLE-GAP",
"awaitParen": "VISIBLE-GAP",
"explicitTypeArgs": "RESOLVES",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "RESOLVES",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "RESOLVES"
},
"swift": {
"plainChain": "INVISIBLE-GAP",
"plainDeepChain": "INVISIBLE-GAP",
"optionalChain": "INVISIBLE-GAP",
"nonNullAssert": "INVISIBLE-GAP",
"awaitParen": "VISIBLE-GAP",
"explicitTypeArgs": "N/A",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "RESOLVES",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "INVISIBLE-GAP"
},
"dart": {
"plainChain": "VISIBLE-GAP",
"plainDeepChain": "VISIBLE-GAP",
"optionalChain": "VISIBLE-GAP",
"nonNullAssert": "VISIBLE-GAP",
"awaitParen": "RESOLVES",
"explicitTypeArgs": "N/A",
"indexElement": "INVISIBLE-GAP",
"fieldReceiverCall": "RESOLVES",
"decoratedReceiverBase": "N/A",
"decoratedFieldType": "RESOLVES"
}
},
"countArm": {

File diff suppressed because it is too large Load diff