|
1 | 1 | # Interface vtable simplification: design |
2 | 2 |
|
3 | | -Status: **PR 1 (§4 + §5) implemented, full suite green (716/716)**. §3 |
4 | | -(constant vtables for capture-free literal methods) not yet started. Follow-up |
5 | | -to PR #251 (heap-allocated patched vtable) and PR #252 (canonical slot |
6 | | -numbering, `fix-interface-vtable-index-mismatch@be2c9620`). All anchors below |
7 | | -verified by code inspection on that branch, except where PR 1's implementation |
8 | | -notes (end of §4) correct them. Goal: remove the last *runtime-patched* vtable |
9 | | -path for the common case, make slot numbering single-sourced, and fix one |
10 | | -latent cast-order miscompile found during review. |
| 3 | +Status: **PR 1 (§4 + §5) and PR 2 (§3) both implemented, full suite green |
| 4 | +(718/718)**. Follow-up to PR #251 (heap-allocated patched vtable) and PR #252 |
| 5 | +(canonical slot numbering, `fix-interface-vtable-index-mismatch@be2c9620`). |
| 6 | +All anchors below verified by code inspection at the time each PR started; |
| 7 | +see each section's "Implementation notes" subsection for what changed, or was |
| 8 | +found to be wrong, while building it. Goal: remove the last *runtime-patched* |
| 9 | +vtable path for the common case, make slot numbering single-sourced, and fix |
| 10 | +latent bugs found during review and implementation. |
11 | 11 |
|
12 | 12 | ## 1. Current architecture: what a vtable slot means |
13 | 13 |
|
@@ -120,6 +120,78 @@ the #251 heap machinery becomes dead on the main path (kept for the fallback); |
120 | 120 | the vtable global can be const-qualified; cross-module behavior stops depending |
121 | 121 | on which module happened to run a cast. |
122 | 122 |
|
| 123 | +### Implementation notes (PR 2, as landed) |
| 124 | + |
| 125 | +Two corrections to the plan above, both found by testing rather than |
| 126 | +inspection - the "captures need a fallback" premise was actually wrong in the |
| 127 | +*helpful* direction (one fewer case to handle), and a second, unrelated |
| 128 | +distinction turned out to be load-bearing that the plan didn't mention at all. |
| 129 | + |
| 130 | +**Captures don't need the fallback.** Rereading `addObjectFuncFieldInfo` |
| 131 | +(`MLIRGenImpl.h`) shows the func-typed field's value is |
| 132 | +`FlatSymbolRefAttr(funcName)` unconditionally - captures-with-methods land in |
| 133 | +`methodInfosWithCaptures` *in addition to*, not *instead of*, the same |
| 134 | +symbol-valued field. What captures actually add is a separate `.captured` |
| 135 | +data field the (single, shared) lifted function reads via `this` at call time |
| 136 | +(`mlirGenObjectLiteralCaptures`) - not a different function per instance. So |
| 137 | +the side table (`objectLiteralMethodSymbolsMap`, keyed on the object literal's |
| 138 | +own `ObjectStorageType`, populated in `addObjectFuncFieldInfo` for every |
| 139 | +method, captures or not) needed no captures-awareness at all. The imported- |
| 140 | +object-type fallback (side-table miss, since there's no local `funcOp` to |
| 141 | +name) is the only case actually exercised in practice today - no test yet |
| 142 | +covers it explicitly (see §7's test matrix, still open). |
| 143 | + |
| 144 | +**A vtable-identity subtlety this plan didn't anticipate, but had to solve |
| 145 | +to key the side table at all:** vtable globals are named by |
| 146 | +`hash_value(objectType)` (`interfaceVTableNameForObject`) - structural, not |
| 147 | +nominal. The worry this raises - two differently-implemented, same-shape |
| 148 | +literals colliding on one vtable global and a baked-in constant only being |
| 149 | +correct for one of them - does not happen, verified by compiling a |
| 150 | +counterexample (`{count:0,inc(){...+1}}` vs `{count:0,inc(){...+100}}`): |
| 151 | +they get different vtable globals. Why: a method field's `FunctionType` |
| 152 | +carries `this` as an explicit first parameter typed `object<object_storage<@literal's-own-symbol>>`, |
| 153 | +and that symbol embeds the literal's own source location - so the "same |
| 154 | +shape" premise never actually holds once the method's own signature is |
| 155 | +counted. This is also why the side table has to be keyed on that *nested* |
| 156 | +`ObjectStorageType` (recovered at the vtable-build site by reading |
| 157 | +`FunctionType.getInputs().front()` off the object's own field, cast to |
| 158 | +`ObjectType`, `.getStorageType()`), not on the boxed `ObjectType` directly, |
| 159 | +since the boxed type wraps a converted plain `TupleType` (`mlirGen(ObjectLiteralExpression)`, |
| 160 | +`convertConstTupleTypeToTupleType`), a *different* MLIR `Type` value than |
| 161 | +`oli.objThis` captured when the field was added, even though one is derived |
| 162 | +from the other. |
| 163 | + |
| 164 | +**The bug that actually broke the regression suite (22 failures on the first |
| 165 | +pass): `PropertySignature` with a function type is not a `MethodSignature`.** |
| 166 | +`interface Something { toString: () => string; }` categorizes `toString` as |
| 167 | +an interface **field** (`InterfaceInfo::fields`), not a method |
| 168 | +(`InterfaceInfo::methods`) - `getVirtualTable`'s `methodsAsFields=true` only |
| 169 | +governs how the *object's own* func-typed field gets resolved into the local |
| 170 | +`virtualTable` snapshot; it collapses every entry to `isField=true` |
| 171 | +regardless of which list the *interface* actually declared the member in. The |
| 172 | +access site, however, dispatches on the interface's real categorization |
| 173 | +(`InterfaceMembers` tries `findField` before `findMethod`) - so |
| 174 | +`toString`'s access always goes through `InterfaceFieldAccess` |
| 175 | +(`thisVal + slotValue`, offset semantics), never `InterfaceMethodAccess` |
| 176 | +(raw function-pointer slot, `BoundFunctionType`), no matter how the object |
| 177 | +stores it. The first version of this change substituted a constant |
| 178 | +`SymbolRefOp` into *any* func-typed slot, including `toString`'s - handing a |
| 179 | +raw function pointer to code that adds it to `thisVal` expecting an offset, |
| 180 | +producing a garbage call target. Crash confirmed via WinDbg |
| 181 | +(`WinDbgX.exe -g -c ".dump /ma ..."` then offline `!analyze -v`; see |
| 182 | +[[windbg-available-for-debugging]]): faulting instruction `call qword ptr |
| 183 | +[rax+rdx]`, both registers holding `thisVal`/slot-sum garbage. Fix: gate the |
| 184 | +symbol substitution on `newInterfacePtr->findMethod(fieldName)` actually |
| 185 | +finding the member in the interface's own (extends-inclusive) method list, |
| 186 | +not merely on the vtable entry being func-typed |
| 187 | +(`mlirGenObjectVirtualTableDefinitionForInterface`). Regression test: |
| 188 | +`00interface_function_typed_field.ts` - `00interface_object.ts` (pre-existing) |
| 189 | +already covered this pattern and is what surfaced the bug, but a minimal, |
| 190 | +purpose-labeled test makes the invariant explicit for future readers. |
| 191 | + |
| 192 | +718/718 tests green (716 from PR 1 + this section's new regression test +1 |
| 193 | +already counted, plus the incidental fix above). |
| 194 | + |
123 | 195 | ## 4. Change 2: a single writer for `virtualIndex` |
124 | 196 |
|
125 | 197 | Slot numbering is currently embodied in four places: |
|
0 commit comments