@asmlift/core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/contracts.ts +60 -11
  5. package/src/frontend/ssa.ts +1 -1
  6. package/src/frontend/thumb.ts +2 -2
  7. package/src/ir/alias.ts +24 -0
  8. package/src/ir/core.ts +8 -0
  9. package/src/ir/opcodes.ts +43 -7
  10. package/src/ir/simplify.ts +1 -1
  11. package/src/l3/address.ts +2 -2
  12. package/src/l3/advance.ts +373 -0
  13. package/src/l3/argbase.ts +4 -4
  14. package/src/l3/ast.ts +65 -21
  15. package/src/l3/basecse.ts +48 -28
  16. package/src/l3/coalesce.ts +9 -9
  17. package/src/l3/gates.ts +75 -1
  18. package/src/l3/hoist.ts +1 -1
  19. package/src/l3/homesplit.ts +13 -13
  20. package/src/l3/initfirst.ts +3 -3
  21. package/src/l3/inlinebase.ts +16 -16
  22. package/src/l3/mentions.ts +68 -5
  23. package/src/l3/mulfirst.ts +3 -3
  24. package/src/l3/nearbase.ts +4 -4
  25. package/src/l3/offmember.ts +5 -5
  26. package/src/l3/parkfirst.ts +6 -6
  27. package/src/l3/pollguard.ts +3 -3
  28. package/src/l3/ptrfield.ts +4 -4
  29. package/src/l3/regspell.ts +8 -8
  30. package/src/l3/reindex.ts +22 -17
  31. package/src/l3/scopebase.ts +28 -25
  32. package/src/l3/sinkinit.ts +7 -7
  33. package/src/l3/slotorder.ts +3 -3
  34. package/src/l3/storage.ts +1 -1
  35. package/src/l3/tailmerge.ts +2 -2
  36. package/src/l3/typing.ts +3 -3
  37. package/src/l3/unmerge.ts +483 -59
  38. package/src/l3/unreduce.ts +13 -13
  39. package/src/l3/volatileptr.ts +11 -11
  40. package/src/l3/volatileval.ts +11 -11
  41. package/src/l3/volstore.ts +16 -16
  42. package/src/l3/zerosub.ts +6 -6
  43. package/src/pattern/engine.ts +4 -4
  44. package/src/pipeline.ts +17 -5
  45. package/src/proto.ts +2 -2
  46. package/src/raise/const.ts +203 -3
  47. package/src/raise/divpow2.ts +2 -2
  48. package/src/raise/extscale.ts +342 -0
  49. package/src/raise/globalshape.ts +32 -12
  50. package/src/raise/gvn.ts +2 -2
  51. package/src/raise/magicdiv.ts +2 -2
  52. package/src/raise/memberarrays.ts +4 -4
  53. package/src/raise/narrowlocal.ts +18 -2
  54. package/src/raise/paramwidth.ts +24 -2
  55. package/src/raise/pre-recovery.ts +90 -25
  56. package/src/raise/retsink.ts +381 -15
  57. package/src/raise/shortcircuit.ts +595 -34
  58. package/src/raise/structs.ts +4 -4
  59. package/src/raise/tailsink.ts +126 -0
  60. package/src/rank-declare.ts +4 -4
  61. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  62. package/src/rank.ts +1148 -803
  63. package/src/structure/analysis.ts +87 -90
  64. package/src/structure/bitfields.ts +130 -30
  65. package/src/structure/globalaccess.ts +30 -4
  66. package/src/structure/namecoalesce.ts +32 -13
  67. package/src/structure/structure.ts +1415 -200
  68. package/src/structure/switch-recover.ts +100 -7
  69. package/src/symbols.ts +127 -6
  70. package/src/target.ts +155 -35
  71. package/src/trace.ts +1 -1
  72. package/src/variation-definitions.ts +1540 -0
  73. package/src/variation-gates.ts +89 -0
  74. package/src/variation-tokens.ts +355 -0
package/README.md CHANGED
@@ -85,8 +85,8 @@ injected via hooks, never copied. `verify()` runs after every IR-mutating pass;
85
85
  | `frontend/{thumb,mips,ppc}.ts` | ISA frontends: decode → CFG → L1 with **Braun-2013 block-arg SSA** (`ssa.ts`), incl. loops, calls (signature-driven arity), memory, jump tables. Shared scaffolding: `disasm.ts` (objdump parsing), `splat.ts` (Splat-dialect MIPS → objdump-shaped instrs), `format.ts` (input-format classification), `emit.ts` (per-block emitter kit + `switch_br`), `opaque.ts` (the unmodelled-op → loud-`opaque` contract), `errors.ts` (`FrontendUnsupportedError`; PPC's subclass), `registry.ts`, `asmdata.ts` (Regime-B jump-table side-table) |
86
86
  | `pattern/engine.ts` | Idiom layer: **rewrite patterns as data** + greedy driver + DCE; `patternApplies` gates on Target capabilities |
87
87
  | `raise/*.ts` | The pre-recovery recognizers, in ONE ordered list (`pre-recovery.ts`): const materialize → magic division (`magicdiv.ts`, Hacker's Delight inverse) → soft division → array legalize → struct-array → member-array (`memberarrays.ts`, a constant offset feeding a variable-index walk is a struct's ARRAY member) → struct-pointer → short-circuit → narrow-reads (`narrow.ts`) → narrow locals (`narrowlocal.ts`, a carrier read only through its own extension IS that width) → parameter width (`paramwidth.ts`, a parameter extended in the prologue is DECLARED at that width); plus `recover.ts` (L1→L2 type recovery), `retsink.ts` (return-sinking), `latch.ts` (empty-latch folding), `errors.ts` (`RaiseUnsupportedError`) |
88
- | `structure/*.ts` | L2→L3 in six modules: `loops.ts` (natural-loop discovery, over `ir/core.ts`'s dominators), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `hazards.ts` (the checks a loop emitter runs before committing to a loop form), `namecoalesce.ts` (copy coalescing over the interference graph — the `/merge-names` axis), `structure.ts` (SSA-destruction coalescing with interference checks + emission: if/while/do-while/for/switch, break/early-return) |
89
- | `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the shared hoist mechanism `hoist.ts`, the differ-ranked re-spelling levers `regspell.ts` + `reindex.ts` + `argbase.ts` + `scopebase.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share) |
88
+ | `structure/*.ts` | L2→L3 in six modules: `loops.ts` (natural-loop discovery, over `ir/core.ts`'s dominators), `analysis.ts` (use registry, liveness, C4 materialization), `switch-recover.ts` (Regime-A comparison-tree recovery), `hazards.ts` (the checks a loop emitter runs before committing to a loop form), `namecoalesce.ts` (copy coalescing over the interference graph — the `/merge-names` variation), `structure.ts` (SSA-destruction coalescing with interference checks + emission: if/while/do-while/for/switch, break/early-return) |
89
+ | `l3/*.ts` | `ast.ts`: language-**neutral** structured AST, the one traversal vocabulary (`exprChildren` etc.), and the `LanguageBackend` seam. Post-structure passes `dce.ts` + `basecse.ts`, the shared hoist mechanism `hoist.ts`, the differ-ranked respell variations `regspell.ts` + `reindex.ts` + `argbase.ts` + `scopebase.ts`, and `typing.ts` (the rendered-expression C type the backends and contracts share) |
90
90
  | `backend/{c,cpp,cfamily,pascal}.ts` | Three backends: C and C++ (CodeWarrior mangling via `mangle.ts`) over the shared `cfamily.ts` substrate, and Pascal (`:=`, `div`, tail-position returns; unspellable constructs throw) |
91
91
  | `pipeline.ts` | `decompile()` + the shared tower spine + annotate-mode stubs/diagnostics |
92
92
  | `trace.ts` | `decompileTraced` — the traced tower (per-stage IR dumps + pattern before/after events), browser-pure; @asmlift/cli's `report.ts` enriches it with objdiff scores/candidates, the playground's Pipeline tab renders it directly |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asmlift/core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Match decompile an assembly function to C or Pascal",
@@ -80,10 +80,18 @@ export function cType(t: IrType): string {
80
80
 
81
81
  /** Declare a name of a given type, C declarator rules: an array puts its length AFTER the name
82
82
  * (`u8 _pad[4]`), a pointer binds its `*` to the declarator (`void *p`), everything else is
83
- * the prefix `cType name`. */
83
+ * the prefix `cType name`. A NESTED array spells every extent after the name in declaration
84
+ * order (`u8 unk8[6][8]`) — one declarator, not an element type that is itself an array, which
85
+ * C has no syntax for and `cType` marks ill-formed as a prefix. */
84
86
  function cDeclare(t: IrType, name: string): string {
85
87
  if (t.kind === 'array') {
86
- return `${cType(t.elem)} ${name}[${t.count}]`;
88
+ const extents: number[] = [];
89
+ let e: IrType = t;
90
+ while (e.kind === 'array') {
91
+ extents.push(e.count);
92
+ e = e.elem;
93
+ }
94
+ return `${cType(e)} ${name}${extents.map((n) => `[${n}]`).join('')}`;
87
95
  }
88
96
  if (t.kind === 'ptr') {
89
97
  return `${cType(t.to)} *${name}`;
@@ -136,7 +144,19 @@ export type LeafHook = (e: Expr, rec: (e: Expr, p: number) => string) => string
136
144
  * -fprologue-bugfix`), and referring to a volatile object through a non-volatile lvalue is
137
145
  * undefined behaviour (C99 6.7.3p5). */
138
146
  function legalizedIndexBase(ix: Extract<Expr, { k: 'index' }>, vt: PrintEnv): Expr {
139
- return derefStrideOk(exprCType(ix.base, vt.type), ix.width, ix.signed)
147
+ // `baseElem` states the element type the base's own DECLARATION gives it, where the type walk
148
+ // cannot reconstruct one (a map-declared array MEMBER — see l3/ast.ts). It is a FALLBACK, not an
149
+ // override, and the order is the whole guard: the walk reads the tree in front of it, so
150
+ // wherever it answers at all it is the better answer and the stated one is not consulted.
151
+ // Inverted (`declared ?? walked`), a stale statement would stand — `derefStrideOk` cannot catch
152
+ // one, because it tests the STATED type against the access width and never against the base.
153
+ // No pass carries a statement onto a foreign base today (each refuses a `field` base by its own
154
+ // predicate: basecse `isHoistableBase`, scopebase/argbase `eligible`, nearbase's untouched
155
+ // `field` subtree, reindex's `base.k === 'var'`), but `mapExprChildren` spreads the field across
156
+ // an arbitrary base substitution, so that is five accidents rather than a check. This ordering
157
+ // is the check. Measured: zero test moves either way.
158
+ const declared = ix.baseElem !== undefined ? T.ptr(ix.baseElem) : undefined;
159
+ return derefStrideOk(exprCType(ix.base, vt.type) ?? declared, ix.width, ix.signed)
140
160
  ? ix.base
141
161
  : {
142
162
  k: 'cast',
@@ -226,11 +246,19 @@ function printExpr(e: Expr, parentPrec: number, vt: PrintEnv, leaf?: LeafHook):
226
246
  // Leading constant subscripts (a multidimensional array global's bare spelling) keep the
227
247
  // postfix form whatever `idx` is: `g[0][0]` is the element, `*g[0]` would be its ROW.
228
248
  //
229
- // `lead` implies the base already strides the access width its only producers register a
230
- // matching element type for the global (structure/globalaccess.ts `bareArrayLead` and
231
- // `declaredSubscripts`, both through structure.ts's `noteGlobal`). Nothing
232
- // else enforced that, and the failure would be quiet-ish: legalization would wrap the base,
233
- // spelling `((u16 *)g)[0][i]`, which subscripts a `u16` twice. Check it rather than assume.
249
+ // `lead` implies the base already strides the access width, and its producers reach that by
250
+ // TWO different mechanisms. The two GLOBAL ones register a matching element type for the
251
+ // global (structure/globalaccess.ts `bareArrayLead` and `declaredSubscripts`, both through
252
+ // structure.ts's `noteGlobal`, so the type walk finds it); the MEMBER one
253
+ // (structure.ts `pointeeElement`, `gPtr->grid[0][i]`) has no global to register and states
254
+ // the element type on the node instead (`baseElem`). Nothing else enforced either, and the
255
+ // failure would be quiet-ish: legalization would wrap the base, spelling `((u16 *)g)[0][i]`,
256
+ // which subscripts a `u16` twice. Check it rather than assume.
257
+ //
258
+ // This throw covers rank >= 2 ONLY, because only a rank >= 2 access carries `lead`. A rank-1
259
+ // member (the large majority of the corpus's dims-carrying members) has no guard here: a
260
+ // missing or stale `baseElem` there degrades silently to the cast form the rule replaces,
261
+ // which is a lost spelling rather than a wrong address.
234
262
  if (e.lead && e.lead.length > 0) {
235
263
  if (base !== e.base) {
236
264
  throw new Error(
@@ -396,8 +424,8 @@ function printSwitchStmt(s: Extract<Stmt, { k: 'switch' }>, indent: string, vt:
396
424
  // arm first, loudly), so it is a contract on the next one.
397
425
  //
398
426
  // `continue;` is deliberately NOT refused: C binds it to the smallest enclosing ITERATION
399
- // statement, which a `switch` is not, so it already means what L3 means. What a loop-respelling
400
- // pass must preserve is exactly that — the three that can re-spell a loop into one whose
427
+ // statement, which a `switch` is not, so it already means what L3 means. What a pass that re-spells
428
+ // a loop must preserve is exactly that — the three that can re-spell a loop into one whose
401
429
  // `continue` would run a different increment (`recognizeForLoops` in structure.ts,
402
430
  // `respellCountdown` in l3/reindex.ts, and l3/unreduce.ts) each scan switch arms for the node
403
431
  // before firing, and a fourth must too.
@@ -539,7 +567,7 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
539
567
  // differently: on a scalar the qualifier binds to the object (`volatile u16 sp0`), on a
540
568
  // pointer declarator to the pointee — the INNERMOST one for a multi-level pointer
541
569
  // (`volatile u16 ** p`). An object-volatile POINTER (`u16 *volatile p`) has no inhabitant —
542
- // no lever or recognizer produces one.
570
+ // no variation or recognizer produces one.
543
571
  lines.push(` ${l.volatile || l.pointeeVolatile ? 'volatile ' : ''}${cType(l.type)} ${l.name};`);
544
572
  }
545
573
  for (const s of fn.body) {
package/src/contracts.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  stmtLists,
17
17
  walkExprs,
18
18
  } from './l3/ast';
19
+ import { mentionedLocals } from './l3/mentions';
19
20
  import { declaredTypes, exprCType } from './l3/typing';
20
21
 
21
22
  export class ContractError extends Error {
@@ -277,10 +278,58 @@ export function assertLocalsWritten(sfn: SFn): void {
277
278
  }
278
279
  }
279
280
 
280
- /** Post-lever: every read of a MINTED local its ADDRESS being taken included must sit where
281
+ /** After a respell variation: a local a pass DELETED from the declaration list is named nowhere in the tree it
282
+ * produced.
283
+ *
284
+ * THE FAILURE THIS CATCHES is the mirror of `assertLocalsWritten` above, and the three contracts
285
+ * beside it do not see it. A pass that consumes a local — l3/unmerge.ts substituting a merge temp
286
+ * into the arms, l3/coalesce.ts folding two names into one, l3/inlinebase.ts deleting a
287
+ * const-address pointer — drops the name from `sfn.locals` on the strength of an in-pass count
288
+ * that it is no longer mentioned. If that count is ever wrong the result is not a loud variation
289
+ * error: it is a candidate handed to the compiler with an undeclared identifier. Normally that is
290
+ * a dropped candidate, but in the REAL tier the candidate is compiled inside the project's
291
+ * vendored translation unit, where an orphaned name that collides with a context symbol compiles
292
+ * and scores. Measured on the shape that inhabits it — `locals = [p]`, body `p = 0; v16 = 1;
293
+ * *p = v16;` — `assertResolved`, `assertDerefsTyped` and `assertLocalsWritten` all pass: the name
294
+ * is neither `?` nor `undefined`, it is well-typed, and the question the third one asks is the
295
+ * OPPOSITE one (read but never written).
296
+ *
297
+ * A DIFFERENTIAL, and that is what makes it safe to run on every respelled tree. "Every name the tree
298
+ * mentions is declared" is NOT the invariant and would refuse correct output everywhere:
299
+ * structure.ts spells a write to a scalar global as a bare `assign` whose name is declared in the
300
+ * project's headers and nowhere in the tree (`gBlendValue = v;` — 71 such occurrences across 22
301
+ * winning sources, per l3/unmerge.ts's own note), and `SFn.globals` is the symbol-map-shaped
302
+ * subset, not that population. So the check speaks only about names the tree ITSELF declared a
303
+ * moment ago and the pass then removed — a set with no legitimate inhabitant, because a pass that
304
+ * drops a declaration is asserting exactly this.
305
+ *
306
+ * `addr` counts, like everywhere else: `&v` names the object as surely as a read does. */
307
+ export function assertNoOrphanedLocals(before: SFn, after: SFn): void {
308
+ const kept = new Set(after.locals.map((l) => l.name));
309
+ const dropped = new Set(before.locals.map((l) => l.name).filter((n) => !kept.has(n)));
310
+ if (!dropped.size) {
311
+ return;
312
+ }
313
+ // `l3/mentions.ts`'s walk, not a third copy of the node vocabulary — this is the LOUD BACKSTOP
314
+ // for the mistake that predicate guards, so it is the last place that should own its own.
315
+ // Locals only, deliberately: no L3 respell variation drops `SFn.params` (`pruneDeadParams` is L1 block
316
+ // params, ir/simplify.ts), so a params arm here would be a refusal with no inhabitant.
317
+ const found = mentionedLocals(after.body, dropped);
318
+ if (found.size) {
319
+ throw new ContractError(
320
+ `a respell variation deleted the declaration of ${[...found]
321
+ .sort()
322
+ .map((n) => `'${n}'`)
323
+ .join(', ')} in '${after.name}' ` +
324
+ `while the tree still names ${found.size > 1 ? 'them' : 'it'} — an undeclared identifier in the candidate`,
325
+ );
326
+ }
327
+ }
328
+
329
+ /** After a respell variation: every read of a MINTED local — its ADDRESS being taken included — must sit where
281
330
  * that local's assignment has already run.
282
331
  *
283
- * THE failure a placing lever can ship, and the only one the byte differ rewards: a base local whose
332
+ * THE failure a placing variation can ship, and the only one the byte differ rewards: a base local whose
284
333
  * assignment does not reach a use is a DIFFERENT VARIABLE — C that compiles, scores, and can win
285
334
  * (the shape #106 shipped). `contracts.ts`'s `assertLocalsWritten` does not see it: it accumulates
286
335
  * reads and writes as SETS over the whole body, so a local assigned in one arm and read after the
@@ -288,16 +337,16 @@ export function assertLocalsWritten(sfn: SFn): void {
288
337
  *
289
338
  * Checked on the EMITTED tree rather than argued from the plan, because the plan is what a bug
290
339
  * would be in. `rank.ts`'s `respell` catches the throw and drops the candidate, so the wrong
291
- * answer becomes a reported lever error instead of a scored spelling.
340
+ * answer becomes a reported variation error instead of a scored spelling.
292
341
  *
293
342
  * IT LIVES HERE, beside `assertLocalsWritten`, because it has that check's population and that
294
- * check's call site: levers that place a def — l3/sinkinit.ts, l3/basecse.ts's first-use policy,
343
+ * check's call site: respell variations that place a def — l3/sinkinit.ts, l3/basecse.ts's first-use policy,
295
344
  * l3/nearbase.ts, l3/reindex.ts, l3/scopebase.ts, l3/argbase.ts — are the population that can
296
- * produce the failure, so the check belongs on every lever tree rather than on one lever's.
345
+ * produce the failure, so the check belongs on every respelled tree rather than on one variation's.
297
346
  *
298
- * Called ABSOLUTELY by the placing levers that put an init inside a nested list, each over its own
347
+ * Called ABSOLUTELY by the placing variations that put an init inside a nested list, each over its own
299
348
  * plan; everywhere else it is reached through `assertPlacementSurvives` below, which is a
300
- * DIFFERENTIAL — so a placement no lever's tree ever satisfied is not judged, and a lever that
349
+ * DIFFERENTIAL — so a placement no respelled tree ever satisfied is not judged, and a variation that
301
350
  * mints nothing is not judged at all.
302
351
  *
303
352
  * A nested list gets a COPY of the reaching set, so an assignment inside one arm does not count as
@@ -357,13 +406,13 @@ export function assertHoistsDominate(sfn: SFn, minted: ReadonlySet<string>): voi
357
406
  }
358
407
 
359
408
  /** The same guarantee across a re-spelling that MOVES statements over a placement another pass
360
- * already made — `rank.ts`'s statement shapes (`/initfirst`, `/pollguard`, `/pollread`), derived
361
- * onto every lever tree after the lever placed its defs, and the lever-on-lever compositions in
409
+ * already made — `rank.ts`'s stacked variations (`/initfirst`, `/pollguard`, `/pollread`), derived
410
+ * onto every respelled tree after the variation placed its defs, and the variation-on-variation compositions in
362
411
  * the same file where a def-moving pass (`sinkInitsToFirstUse`, `nearBaseClusters`,
363
- * `reindexWalks`) runs on a tree a placing lever built. `pollReads` folds a materialized re-read
412
+ * `reindexWalks`) runs on a tree a placing variation built. `pollReads` folds a materialized re-read
364
413
  * back into a loop condition, which is exactly such a move.
365
414
  *
366
- * A DIFFERENTIAL, which is what makes it safe on every lever: the walk judges the reshaped tree
415
+ * A DIFFERENTIAL, which is what makes it safe on every variation: the walk judges the reshaped tree
367
416
  * only where it already described the unshaped one, so a placement it cannot model (a def inside
368
417
  * a loop body read earlier in the same body is assigned on every iteration but the first) is not
369
418
  * judged either way. `minted` may name a local `before` does not carry — a mover mints its own —
@@ -742,7 +742,7 @@ export function trimClobberedCallArgs(inp: CallArgTrim): void {
742
742
  * an attr only one of an otherwise-matching pair carries would cost a recovery. */
743
743
  const setupArgc = new WeakMap<Op, number>();
744
744
 
745
- /** Whether anything in `fn` HAS the narrower reading — the lever's gate, so the ~99% of functions
745
+ /** Whether anything in `fn` HAS the narrower reading — the variation's gate, so the ~99% of functions
746
746
  * with no narrowable call cost no re-lift. Read it off the lift itself: a later pipeline stage may
747
747
  * replace a `call` op (softdiv rewrites one to a division), and the table is keyed by op. */
748
748
  export function hasSetupArgsNarrowing(fn: Fn): boolean {
@@ -932,7 +932,7 @@ function decode(
932
932
  const ad = rest.match(/^\.(align|balign|p2align)([wl]?)\b\s*(.*)$/i);
933
933
  if (ad) {
934
934
  const kind = ad[1].toLowerCase();
935
- const patternFill = ad[2] !== ''; // the `w`/`l` variants
935
+ const patternFill = ad[2] !== ''; // the `w`/`l` forms
936
936
  const am = ad[3].trim().match(/^(\d{1,3})(?:\s*,\s*([^,]*?))?\s*$/); // N [, fill]; max-skip → no match
937
937
  const n = am ? Number(am[1]) : NaN;
938
938
  const pow = kind === 'balign' ? Math.log2(n) : n;
@@ -3416,7 +3416,7 @@ export function lift(
3416
3416
  // handed to a callee arrives there through a frame this conjunct never looks at, and what bounds
3417
3417
  // its extent is the audit's frame-accounting rule rather than anything here.
3418
3418
  //
3419
- // WHY IT IS NOT WIDENED anyway, since a wider frame is the obvious next lever. Three shapes,
3419
+ // WHY IT IS NOT WIDENED anyway, since a wider frame is the obvious next change. Three shapes,
3420
3420
  // each compiled with agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit -fhex-asm
3421
3421
  // -fprologue-bugfix`, and only the first is about extent at all:
3422
3422
  //
package/src/ir/alias.ts CHANGED
@@ -86,6 +86,30 @@ export function globalCellOf(defs: Map<Value, Op>, addr: Value, off: number): Gl
86
86
  return null;
87
87
  }
88
88
 
89
+ /**
90
+ * The NUMERIC address an access denotes, resolved through defs alone — a literal base, or the sum
91
+ * of two literals — plus the access's own `off`. Null when the address
92
+ * does not reduce to a literal: a `gaddr` (ask {@link globalCellOf} instead), a parameter, a
93
+ * materialized base, anything carrying a runtime term.
94
+ *
95
+ * The caller that has one asks a question about the BOARD — "is this cell a hardware register" —
96
+ * so a null here must be read as "unknown", never as "ordinary memory".
97
+ */
98
+ export function constAddressOf(defs: Map<Value, Op>, addr: Value, off: number): number | null {
99
+ const d = defs.get(addr);
100
+ if (d?.opcode === 'const') {
101
+ return (d.attrs.value as number) + off;
102
+ }
103
+ if (d?.opcode === 'add' && d.operands.length === 2) {
104
+ const a = defs.get(d.operands[0]);
105
+ const b = defs.get(d.operands[1]);
106
+ if (a?.opcode === 'const' && b?.opcode === 'const') {
107
+ return (a.attrs.value as number) + (b.attrs.value as number) + off;
108
+ }
109
+ }
110
+ return null;
111
+ }
112
+
89
113
  /**
90
114
  * Are these two accesses through ONE base provably different byte cells — same base value, both at
91
115
  * a constant offset and width, ranges non-overlapping? The everyday struct interleave
package/src/ir/core.ts CHANGED
@@ -117,6 +117,14 @@ export type SlotHomes = Map<Value, Set<number>>;
117
117
  * param have no common scale. LAST write, not first: a predecessor commonly writes one key several
118
118
  * times (1,867 of 5,283 records over three checkouts), and the edge carries what the last left.
119
119
  *
120
+ * A SECOND CONSUMER READS THE ABSENCE, not the order. `structure.ts`'s `enclosingCarrierName` takes
121
+ * a missing `lastWrite` entry on a measured block as the licence to give a nested loop's parameter
122
+ * the enclosing header's NAME: the key was not written, so the machine carried the value into the
123
+ * inner loop in the register it had — one variable, not a copy. There a stale or over-eager "not
124
+ * written" is not a mis-sorted copy but a copy the emitted C no longer makes, so the absence
125
+ * semantics below are load-bearing for naming, and a pass that drops a record it should have
126
+ * folded changes spellings, not just orders.
127
+ *
120
128
  * Keyed by OBJECTS (the predecessor block, the destination param), never by arg position, so the
121
129
  * param splices in `ir/simplify.ts` cannot leave it stale. A pass that moves one block's ops into
122
130
  * another owes `foldWriteOrder`.
package/src/ir/opcodes.ts CHANGED
@@ -237,11 +237,11 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
237
237
  /** Ops with an observable side effect: the flag on the signature, derived rather than re-listed.
238
238
  * `isDceSafe` asks the same question of the FLAG through `opSig` rather than of this set, so the
239
239
  * two cannot disagree. The SET's own consumers are `HOIST_UNSAFE_OPS` below, structure.ts's
240
- * `sideEffects` walk (an effectful op whose result nobody reads is still an execution),
240
+ * block-purity tests (may this block be folded into a loop header, is this exit owned),
241
241
  * analysis.ts's memory-write barrier, divpow2's bias block (which is DELETED rather than moved),
242
- * and the idiom layer's de-sequencing guard (pattern/engine.ts). Three of them — the
243
- * `sideEffects` walk, the barrier and the bias block — each carried a hand-written copy of this
244
- * membership, which is how the models drifted apart before. */
242
+ * and the idiom layer's de-sequencing guard (pattern/engine.ts). Derived rather than re-listed
243
+ * because three of those consumers each carried a hand-written copy of this membership, which is
244
+ * how the models drifted apart. */
245
245
  export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
246
246
  (Object.keys(OPCODES) as Opcode[]).filter((k) => (OPCODES[k] as OpSig).effects),
247
247
  );
@@ -254,8 +254,9 @@ export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
254
254
  * is raise/shortcircuit.ts, which hoists an arm's body into the block above, and the structurer
255
255
  * inlines an unnamed value back into the `&&`/`||` right-hand side, where C's own short-circuit
256
256
  * re-guards it. Adding the two reads
257
- * here costs three byte-matches (kleod:UpdateHUDCounterDisplay, synthetic:breakloop,
258
- * synthetic:strcmp1), so the argument is load-bearing rather than merely plausible.
257
+ * here cost three byte-matches (kleod:UpdateHUDCounterDisplay, retired 2026-09-13 with its source,
258
+ * plus synthetic:breakloop and synthetic:strcmp1), so the argument was load-bearing rather than
259
+ * merely plausible. Not re-measured since the kleod row's retirement.
259
260
  *
260
261
  * KNOWN GAP: the trapping divides are absent too, and there the re-guard argument does NOT carry
261
262
  * — a hoisted `sdiv` that the structurer NAMES becomes an unconditional statement. Left as it is
@@ -271,9 +272,20 @@ export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
271
272
  * re-guard at the new point — actually holds at your call site. */
272
273
  export const HOIST_UNSAFE_OPS: ReadonlySet<string> = EFFECTFUL_OPS;
273
274
 
275
+ /** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
276
+ * `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
277
+ * Authored rather than derived — no registry field records the operand ROLE, and the only other
278
+ * memory-touching opcode is `call`, whose variadic operands are arguments and not a base.
279
+ *
280
+ * Two consumers ask two different questions of it and both need the same answer, which is why it
281
+ * is here and not next to either: `structure/analysis.ts` uses it for the address-home variation's slot
282
+ * model, and `raise/const.ts` to recognise a folded literal that IS an address. */
283
+ export const MEM_BASE_OPS: ReadonlySet<string> = new Set(['load', 'store', 'aload', 'astore']);
284
+
274
285
  /** Ops whose answer depends on WHERE they run: an effect (its order against other effects is
275
286
  * observable) or a memory read (it answers whichever stores ran before it). The question a pass
276
- * asks before moving a computation to another point on the SAME path. */
287
+ * asks before moving a computation to another point on the SAME path. `SPELLED_WHEN_DEAD_OPS`
288
+ * below asks a DIFFERENT question and derives, today, the same set. */
277
289
  export const ORDER_SENSITIVE_OPS: ReadonlySet<string> = new Set(
278
290
  (Object.keys(OPCODES) as Opcode[]).filter((k) => {
279
291
  const sig = OPCODES[k] as OpSig;
@@ -281,6 +293,30 @@ export const ORDER_SENSITIVE_OPS: ReadonlySet<string> = new Set(
281
293
  }),
282
294
  );
283
295
 
296
+ /** Ops the structurer must still SPELL when nothing consumes their result — an effect, or a memory
297
+ * READ. Consumer: structure.ts's `sideEffects` walk.
298
+ *
299
+ * The read half is the entry worth arguing, because `reads` documents the OPPOSITE about C — a
300
+ * load nobody reads is deletable, nothing observes it. That is the C claim. The COMPILER claim
301
+ * points the other way: an optimizing compiler deletes every dead read it is ALLOWED to delete, so
302
+ * one still in the target is evidence the source's access was `volatile`. Membership here only
303
+ * says the structurer may not drop the op silently; whether a `volatile` can actually reach the
304
+ * access is a second, ADDRESS-level question the call site asks separately
305
+ * (structure.ts `volatileQualifiable`), and a read it answers no to is dropped as before.
306
+ *
307
+ * DERIVED FROM THE REGISTRY, not aliased to `ORDER_SENSITIVE_OPS`, even though the two are
308
+ * extensionally identical today and `HOIST_UNSAFE_OPS` above does alias `EFFECTFUL_OPS`. An alias
309
+ * makes two DIFFERENT questions incapable of ever differing, so the day one of them acquires an
310
+ * opcode the other should not have, the edit lands on both silently. The identity is pinned by a
311
+ * test instead (test/pattern.test.ts), where a future divergence surfaces as a decision to make
312
+ * rather than a coupling nobody sees. */
313
+ export const SPELLED_WHEN_DEAD_OPS: ReadonlySet<string> = new Set(
314
+ (Object.keys(OPCODES) as Opcode[]).filter((k) => {
315
+ const sig = OPCODES[k] as OpSig;
316
+ return sig.effects || sig.reads;
317
+ }),
318
+ );
319
+
284
320
  /** Ops that may not be RE-EVALUATED at another program point — order-sensitive, or trapping. The
285
321
  * trap half is what separates this from `ORDER_SENSITIVE_OPS`: it only matters when the new point
286
322
  * can be reached on a path the old one was not, so a consumer that merely re-orders on one path
@@ -105,7 +105,7 @@ function trivialPhiValue(incoming: readonly Successor[], i: number, param: Value
105
105
  export function firstTrivialPhi(fn: Fn): { block: Block; param: Value } | null {
106
106
  // ONE pass over the successor edges, indexed by target — `simplifyTrivialPhis` rescans the
107
107
  // whole function per block, which is fine for a mutating fixpoint and not for a check on the
108
- // raising tower's hot path (a candidate fan re-raises the same function once per lift variant).
108
+ // raising tower's hot path (a candidate fan re-raises the same function once per lift).
109
109
  const incomingOf = new Map<Block, Successor[]>();
110
110
  for (const pb of fn.blocks) {
111
111
  for (const op of pb.ops) {
package/src/l3/address.ts CHANGED
@@ -7,8 +7,8 @@
7
7
  // declares which one it means instead of restating four lines and drifting.
8
8
  //
9
9
  // • baseConst — a deref BASE, through SCALAR pointer casts only. A cast to a STRUCT pointer is
10
- // the dot-form's base and is refused, because a lever that re-spells THROUGH it collapses the
11
- // stride (`((struct S *)K)[i].f` is not `((u8 *)K)[…]`). This is the reading a lever that
10
+ // the dot-form's base and is refused, because a respell variation that re-spells THROUGH it collapses the
11
+ // stride (`((struct S *)K)[i].f` is not `((u8 *)K)[…]`). This is the reading a respell variation that
12
12
  // REWRITES the base needs: l3/nearbase.ts's clusters, l3/volstore.ts's qualifier.
13
13
  // • addrConst — the address an expression IS, through ANY pointer cast. Wider, and safe
14
14
  // because nothing re-spells through it: l3/volatileptr.ts counts volatility claims with it.