@asmlift/core 0.6.0 → 0.8.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 (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
package/src/l3/reindex.ts CHANGED
@@ -1,11 +1,11 @@
1
- // asmlift L3 — the walk→index RE-SPELLING, a differ-ranked representation lever.
1
+ // asmlift L3 — the walk→index RESPELL VARIATION, differ-ranked.
2
2
  //
3
3
  // A compiler strength-reduces a source-level `arr[i]` loop into a pointer WALK (`*p; p += 1`),
4
4
  // so asmlift's faithful lift of the machine form emits the walk — but recompiling the walk
5
5
  // rarely reproduces the bytes the INDEXED source produced (different induction variable,
6
6
  // different regalloc). Which representation the source used is genuinely ambiguous from asm —
7
7
  // exactly the class of ambiguity asmlift resolves by CANDIDATES, not guesses (rank.ts: "types
8
- // are differ-ranked levers"). This module produces the indexed re-spelling of a structured
8
+ // are differ-ranked variations"). This module produces the indexed re-spelling of a structured
9
9
  // function; enumerateCandidates emits BOTH and the objdiff score referees.
10
10
  //
11
11
  // v1 SCOPE (decline over approximate): a loop is re-spelled only when ALL hold —
@@ -29,7 +29,7 @@
29
29
  // it (its statements are the loop-preceding subset of the else arm, now unconditionally hoisted).
30
30
  // The walk pointers KEEP their init and lose their step — gcc folds a loop-invariant pointer
31
31
  // local into addressing, so `p = B; … p[i]` and `B[i]` compile identically, and keeping the
32
- // local is what lets the `/volatile` lever qualify a numeric B. Several walk pointers share the
32
+ // local is what lets the `/volatile` variation qualify a numeric B. Several walk pointers share the
33
33
  // one counter (`dotprod`'s a/b pair). Its OWN rules, on top of the shared table below —
34
34
  // • the guard tests THE SAME var the counter is initialised from, against 0, in the sense that
35
35
  // skips the loop; the do-while exit is exactly `k != 0`;
@@ -227,7 +227,7 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
227
227
  },
228
228
  {
229
229
  id: 'global-counter',
230
- why: 'a global counter’s final value is observable to every other caller, ISR and translation unit',
230
+ why: 'a global counter’s final value is observable to every other caller, interrupt handler and translation unit',
231
231
  sound: true,
232
232
  guardedBy: 'reindex.test.ts: the counter may not be a GLOBAL',
233
233
  rejects: (c) => !c.kIsDeclared,
@@ -255,14 +255,14 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
255
255
  },
256
256
  {
257
257
  id: 'body-exit',
258
- why: 'the steps sat in the body tail, which a `continue` skips and a `for`’s inc does not',
258
+ why: 'the steps sat at the end of the body, which a `continue` skips and a `for`’s increment does not',
259
259
  sound: true,
260
260
  guardedBy: 'reindex.test.ts: a `break` in the body declines',
261
261
  rejects: (c) => c.coreHasExit,
262
262
  },
263
263
  {
264
264
  id: 'walk-base',
265
- why: 'the kept init must be a value the rewrite can leave standing a var, or a rematerializable address',
265
+ why: 'the kept init must be a value the rewrite can leave standing: a variable, or an address that can be loaded again',
266
266
  sound: true,
267
267
  guardedBy: 'reindex.test.ts: a walk pointer with no init ahead of the loop declines',
268
268
  rejects: (c) => c.badBases.length > 0,
@@ -290,7 +290,7 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
290
290
  },
291
291
  {
292
292
  id: 'leftover-walk',
293
- why: 'a leftover outlives the deleted step, and its skip-arm twin reads a pointer that path never set',
293
+ why: 'a statement left outside the deleted step still reads the walk pointer, which a path that skipped the step never set',
294
294
  sound: true,
295
295
  guardedBy: 'reindex.test.ts: a leftover mentioning a walk pointer declines',
296
296
  rejects: (c) => c.leakyLeftovers > 0,
@@ -367,13 +367,18 @@ function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
367
367
  ? { k: 'var', name: iv }
368
368
  : { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: e.idx };
369
369
  // NOTE: this rebuilds the node from parts, so any field not named here is DROPPED — and the
370
- // `index` node has exactly three optional ones (ast.ts): `lead`, `operandOff`, `baseOrdered`.
371
- // `lead` is declined above (the deref side); it cannot arrive on the base side either, since
372
- // `walk.base` is a local pointer and structuring only ever puts `lead` on an array GLOBAL's
373
- // own name. Dropping the other two is right rather than merely harmless: `baseOrdered` is
374
- // stamped per SYMBOL on an order-licensed GLOBAL, which a local var base never is, and
375
- // `operandOff`'s readers (l3/basecse.ts, l3/offmember.ts) ask about a CONSTANT subscript,
376
- // which the rewritten `i`/`i + k` index never is.
370
+ // `index` node has exactly five optional ones (ast.ts): `lead`, `baseElem`, `operandOff`,
371
+ // `baseOrdered`, `baseAdvanced`. `lead` is declined above (the deref side); it cannot arrive on
372
+ // the base side either, since `walk.base` is a local pointer and structuring only ever puts
373
+ // `lead` on an array GLOBAL's own name. Dropping the other four is right rather than merely
374
+ // harmless: `baseElem` states the element type of a base the C type walk cannot type, and the
375
+ // rebuilt base is `{k:'var', name: walk.base}` — a LOCAL pointer, whose declared type the walk
376
+ // reads straight out of the print env, so a statement about the base it replaced would describe
377
+ // a different base and the walk answers without one; `baseOrdered` is stamped per SYMBOL on an
378
+ // order-licensed GLOBAL, which a local var base never is; `operandOff`'s readers
379
+ // (l3/basecse.ts, l3/offmember.ts) ask about a CONSTANT subscript, which the rewritten
380
+ // `i`/`i + k` index never is; and `baseAdvanced` describes an address the machine reached from
381
+ // a CONSTANT one, which a walk pointer's is not.
377
382
  return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
378
383
  }
379
384
  let failed = false;
@@ -653,8 +658,8 @@ function tryExprWalk(
653
658
  * one loop re-spelled, or null (no candidate) when nothing fired — callers emit the extra
654
659
  * candidate only on non-null. Pure: never mutates the input SFn. `keptWalks` collects the names
655
660
  * of the pointers each fired loop kept as its base — v1 the walk's base (a param lands in the
656
- * set too, inertly: the volatile lever marks only declared locals), v2 the walk pointers
657
- * themselves — the locals the /indexed/volatile product (rank.ts) narrows the volatile lever
661
+ * set too, inertly: the volatile variation marks only declared locals), v2 the walk pointers
662
+ * themselves — the locals the /indexed/volatile composition (rank.ts) narrows the volatile variation
658
663
  * to. A v3 loop contributes nothing: it DELETES its pointer, and its base is qualified through
659
664
  * the /livebase pairings instead. `gates` is the shared countdown admission table — a parameter
660
665
  * so a test can ablate one entry and re-run the real pass. */
@@ -666,7 +671,7 @@ export function reindexWalks(
666
671
  const ptrVars = new Map<string, IrType>();
667
672
  const declTypes = declaredTypes(sfn);
668
673
  // BOTH volatility facts (ast.ts SFn.locals): the object-volatile counter, and the pointer whose
669
- // POINTEE is volatile — which is the one the `/volatile` lever mints, and the one a walk carries.
674
+ // POINTEE is volatile — which is the one the `/volatile` variation mints, and the one a walk carries.
670
675
  const volatileLocals = new Set(
671
676
  sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
672
677
  );
@@ -1,4 +1,4 @@
1
- // L3 re-spelling lever: name a reused global base in a pointer local placed by SCOPE rather than at
1
+ // L3 respell variation: name a reused global base in a pointer local placed by SCOPE rather than at
2
2
  // the function top. Two region rules ship, one pass and one collected index behind them:
3
3
  //
4
4
  // `/scopebase` ONE local for a key, at the innermost list holding all of its uses (else the
@@ -9,13 +9,15 @@
9
9
  // and the three-block-scoped one assemble byte-identically. So there is no nested
10
10
  // declaration block here and none is needed — the locals are declared at function
11
11
  // top and only their ASSIGNMENTS are placed per region. That compiler fact is
12
- // PINNED rather than asserted: packages/cli/test/matching/decl-scope-axis.test.ts
12
+ // PINNED rather than asserted: packages/cli/test/matching/decl-scope-variation.test.ts
13
13
  // compiles both spellings through the project's own agbcc and compares the object
14
14
  // bytes, and compiles a count-collapsed third spelling to show the COUNT is not
15
15
  // free either.
16
16
  //
17
- // The lever earns its place: returning `null` from `hoistScopedBases` costs
18
- // kleod:UpdateHUDCounterDisplay its match, so the benchmark's zero-lost gate guards this file.
17
+ // The variation earned its place on kleod's kl-eod-decomp rows: returning `null` from `hoistScopedBases`
18
+ // cost kleod:UpdateHUDCounterDisplay its match. That row was retired on 2026-09-13 with its source,
19
+ // so no current benchmark row is known to guard this file; re-measure by ablation on the current
20
+ // rows before naming one.
19
21
  //
20
22
  // `l3/basecse.ts` already hoists a reused leaf base — at three positions now, of which two are in
21
23
  // the TOP-LEVEL statement list (the function top, or an init's first use where a roster row asks
@@ -28,8 +30,8 @@
28
30
  // narrows that range and does not close it: the init still lands ABOVE the `if`. `l3/hoist.ts`'s
29
31
  // third placement, `scope`, now does close it for the run basecse places, so "into a nested list"
30
32
  // is no longer this file's alone; what stays here is the base this file can SEE (below) and the
31
- // COUNT question (`REGION_RULES`), which no placement answers. That argument is why the lever is
32
- // scope-aware; it is NOT a claim about what the lever achieves, and no committed measurement
33
+ // COUNT question (`REGION_RULES`), which no placement answers. That argument is why the variation is
34
+ // scope-aware; it is NOT a claim about what the variation achieves, and no committed measurement
33
35
  // separates basecse's two flat placements (the one that did edited a reference source by hand and
34
36
  // cannot be re-run). On kleod:UpdateHUDCounterDisplay the primary path declines outright (a later
35
37
  // pass retired the phi it keyed on, so the base's uses span the function body), and the cluster
@@ -47,7 +49,7 @@
47
49
  // shape. The decomp author's alternative is a no-op read-modify-write (`g[0][K] += 0;`) purely to
48
50
  // force that materialization; naming the base is the same codegen without the quirk.
49
51
  //
50
- // A LEVER, not a rewrite: both region rules are emitted as ADDITIONAL candidates (rank.ts
52
+ // A VARIATION, not a committed rewrite: both region rules are emitted as ADDITIONAL candidates (rank.ts
51
53
  // `/scopebase`, `/regionbase`) with the differ refereeing, so the un-hoisted spelling is always
52
54
  // still in the list and neither can cost a match.
53
55
  //
@@ -60,19 +62,19 @@
60
62
  // sees it (they check resolution, deref typing, and whether a local is written ANYWHERE).
61
63
  //
62
64
  // ORDERING: `hoistBaseLocals` (basecse) runs unconditionally in `structureChecked`, BEFORE
63
- // rank's levers see the tree. So this pass's `addr`/`const` input is what basecse's DEFAULT table
65
+ // rank's variations see the tree. So this pass's `addr`/`const` input is what basecse's DEFAULT table
64
66
  // refused — EVERY gate in it, single-use bases as much as loop and repeated-constant-offset ones —
65
67
  // which is why `SCOPEBASE_GATES` re-states basecse's rules rather than assuming those bases never
66
68
  // arrive.
67
69
  import { assertHoistsDominate } from '../contracts';
68
70
  import { type IrType, T, scalarTypeForAccess } from '../ir/types';
69
71
  import type { Expr, SFn, Stmt } from './ast';
70
- import { mapExprChildren, stmtExprs, stmtLists } from './ast';
72
+ import { isLoop, mapExprChildren, stmtExprs, stmtLists } from './ast';
71
73
  import { type Gate, ablateHeuristic, firstRejection } from './gates';
72
74
  import { nameAllocator, takenNames } from './hoist';
73
75
  import { addressableGlobals } from './storage';
74
76
 
75
- /** A base this lever may name: a leaf whose value is a fixed address. */
77
+ /** A base this variation may name: a leaf whose value is a fixed address. */
76
78
  type LeafBase = Extract<Expr, { k: 'addr' } | { k: 'const' } | { k: 'var' }>;
77
79
 
78
80
  /** THE identity of a base — what makes two accesses "the same address".
@@ -89,7 +91,7 @@ export interface AccessCtx {
89
91
  readonly addressable: ReadonlySet<string>;
90
92
  }
91
93
 
92
- /** Which accesses this lever may re-point. BOTH rules are SOUND: each one, removed, makes the
94
+ /** Which accesses this variation may re-point. BOTH rules are SOUND: each one, removed, makes the
93
95
  * rewrite name DIFFERENT BYTES — C that compiles, type-checks and scores, which is the failure
94
96
  * mode nothing downstream catches.
95
97
  *
@@ -117,14 +119,14 @@ export const SCOPEBASE_ELIGIBILITY: readonly Gate<AccessCtx>[] = [
117
119
  },
118
120
  {
119
121
  id: 'shadowed-or-nonarray-base',
120
- why: '`&name` on a local or a pointer-shaped global names a different object',
122
+ why: 'taking `&name` of a local or a pointer-shaped global names a different object',
121
123
  sound: true,
122
124
  guardedBy: 'addr-placement.test.ts: scopebase declines the shadowed name rather than take its address',
123
125
  rejects: (c) => c.base.k === 'var' && !c.addressable.has(c.base.name),
124
126
  },
125
127
  ];
126
128
 
127
- /** An access this lever may re-point, or null. */
129
+ /** An access this variation may re-point, or null. */
128
130
  function eligible(
129
131
  e: Expr,
130
132
  globals: ReadonlySet<string>,
@@ -225,7 +227,7 @@ function collect(body: Stmt[], path: Stmt[][], loop: boolean[], idxPath: number[
225
227
  };
226
228
  for (const [i, s] of body.entries()) {
227
229
  at = i;
228
- const isLoop = s.k === 'while' || s.k === 'dowhile' || s.k === 'for';
230
+ const repeats = isLoop(s);
229
231
  // A loop's OWN condition runs every iteration — a base there is loop-invariant exactly as a
230
232
  // body use is, and it lives at THIS list, which does not. basecse.ts and argbase.ts treat the
231
233
  // CONDITION the same way. They do NOT agree about a `for`'s `init`: basecse counts it in-loop
@@ -233,7 +235,7 @@ function collect(body: Stmt[], path: Stmt[][], loop: boolean[], idxPath: number[
233
235
  // it at the enclosing cadence, which is the truthful reading — it runs once. Recorded because
234
236
  // the divergence is real and an extraction has to pick one; both readings are pinned in
235
237
  // test/addr-placement.test.ts so the pick is deliberate rather than whichever survives.
236
- stmtExprs(s).forEach((e) => visit(e, isLoop));
238
+ stmtExprs(s).forEach((e) => visit(e, repeats));
237
239
  if (s.k === 'for') {
238
240
  // `init`/`inc` are typed as the full Stmt union, so a COMPOUND one is type-legal. `stmtExprs`
239
241
  // reaches only its own expressions while `rewriteStmt` descends into any nested list — the
@@ -255,7 +257,7 @@ function collect(body: Stmt[], path: Stmt[][], loop: boolean[], idxPath: number[
255
257
  stmtExprs(s.inc).forEach((e) => visit(e, true));
256
258
  }
257
259
  for (const child of stmtLists(s)) {
258
- collect(child, [...path, child], [...loop, isLoop], [...idxPath, i], st);
260
+ collect(child, [...path, child], [...loop, repeats], [...idxPath, i], st);
259
261
  }
260
262
  }
261
263
  }
@@ -320,7 +322,7 @@ function deepestCluster(all: Site[]): { scope: Stmt[]; depth: number; uses: Site
320
322
  return best;
321
323
  }
322
324
 
323
- /** How a key's uses are cut into REGIONS — the one axis this pass varies. Names a `REGION_RULES`
325
+ /** How a key's uses are cut into REGIONS — the one dimension this pass varies. Names a `REGION_RULES`
324
326
  * entry; it is the only field a production caller passes. */
325
327
  export type RegionSelector = 'whole' | 'per-region';
326
328
 
@@ -414,13 +416,13 @@ export interface RegionCtx {
414
416
  const COUNTING_RULES: readonly Gate<RegionCtx>[] = [
415
417
  {
416
418
  id: 'single-use',
417
- why: 'one access re-materializes as cheaply as a named local',
419
+ why: 'a base accessed once is as cheap to load again as to hold in a named local',
418
420
  sound: false,
419
421
  rejects: (c) => c.uses < 2,
420
422
  },
421
423
  {
422
424
  id: 'repeated-const-offset',
423
- why: 'a fixed offset touched twice is a scalar RMW, which the compiler re-materializes',
425
+ why: 'a fixed offset read and then written is one scalar update, and the compiler loads its address again for it',
424
426
  sound: false,
425
427
  rejects: (c) => c.repeatedConstOffset,
426
428
  },
@@ -431,7 +433,7 @@ const COUNTING_RULES: readonly Gate<RegionCtx>[] = [
431
433
  const LOOP_RULES: readonly Gate<RegionCtx>[] = [
432
434
  {
433
435
  id: 'per-iteration-use',
434
- why: 'no scope reachable from the use runs at a loop condition or `for` inc cadence',
436
+ why: 'no scope reachable from the use runs as often as a loop condition or a `for` increment',
435
437
  sound: false,
436
438
  rejects: (c) => c.perIteration,
437
439
  },
@@ -443,7 +445,7 @@ const LOOP_RULES: readonly Gate<RegionCtx>[] = [
443
445
  },
444
446
  ];
445
447
 
446
- /** The admission rules. NONE is sound: a wrong choice here names the same address in a different
448
+ /** The admission rules. NONE is sound: a wrong decision here names the same address in a different
447
449
  * place, so it costs bytes and a match, never meaning — the eligibility table above is where
448
450
  * meaning is at stake, and `rank.ts` keeps the un-hoisted spelling beside every candidate.
449
451
  *
@@ -504,7 +506,7 @@ export const SCOPEBASE_GATES: readonly Gate<RegionCtx>[] = [...COUNTING_RULES, .
504
506
  const perRegionReading = (g: Gate<RegionCtx>): Gate<RegionCtx> => ({
505
507
  ...g,
506
508
  id: `region-${g.id}`,
507
- why: `${g.why} — judged over ONE region's direct uses`,
509
+ why: `counted over one regions own uses: ${g.why}`,
508
510
  });
509
511
 
510
512
  /** `/regionbase`'s admission (rank.ts): the per-region readings of `SCOPEBASE_GATES`, MINUS the one
@@ -531,7 +533,7 @@ const perRegionReading = (g: Gate<RegionCtx>): Gate<RegionCtx> => ({
531
533
  * admits.
532
534
  *
533
535
  * ONE RULE HERE IS PRICED BY A ROW; three are not. Ablating `region-single-use` moves
534
- * `synthetic:dmascope` — the lever's own row — while `region-repeated-const-offset`,
536
+ * `synthetic:dmascope` — the variation's own row — while `region-repeated-const-offset`,
535
537
  * `per-iteration-use` and `regions-degenerate` each leave all five gating rows exactly where they
536
538
  * stand. NO SCORE PAIR IS QUOTED for that move: `dmascope` is MATCH in the committed artifact, so
537
539
  * a pair whose unablated endpoint is a nonmatch score describes a corpus state that no longer
@@ -546,7 +548,7 @@ export const REGIONBASE_GATES: readonly Gate<RegionCtx>[] = [
546
548
  ...ablateHeuristic(LOOP_RULES, 'nested-loop-use'),
547
549
  {
548
550
  id: 'regions-degenerate',
549
- why: 'one region is the function-top hoist basecse and /livebase already offer',
551
+ why: 'a single region is the function-top local that the default hoist and `livebase` already offer',
550
552
  sound: false,
551
553
  rejects: (c) => c.siblingRegions < 2,
552
554
  },
@@ -554,7 +556,7 @@ export const REGIONBASE_GATES: readonly Gate<RegionCtx>[] = [
554
556
 
555
557
  /** THE REGION RULE, as a value. A third rule is one entry here — a partition, a gate table, and
556
558
  * the population its counting rules are judged over — rather than three hand-edited branches in
557
- * three functions, which is the same doctrine `rank.ts` states for its own admissions ("one entry
559
+ * three functions, which is the same doctrine `rank-variations.ts` states for its hoist roster ("one entry
558
560
  * here, one gate table, and that table's line in the gate-contract roster — not nine hand-edited
559
561
  * sites that can drift"). */
560
562
  export interface RegionRule {
@@ -675,9 +677,10 @@ export function planScopedBases(sfn: SFn, opts: ScopeBaseOpts = {}): ScopedBaseP
675
677
  * PER KEY, not per function, and the difference is not hypothetical. Nothing in the L3 contract
676
678
  * forbids the sharing — `l3/pollguard.ts` already emits it (`{ k: 'if', cond: s.cond, then: [s] }`
677
679
  * puts one `cond` object at two tree positions), and it is harmless today only because the shapes
678
- * are derived AFTER this lever in `rank.ts`, an ordering nothing pins. A whole-function decline
680
+ * are derived AFTER this variation in `rank.ts`, an ordering nothing pins. A whole-function decline
679
681
  * would make a future producer that shares one node silently delete every base this pass names —
680
- * including `kleod:UpdateHUDCounterDisplay`'s match, which returning `null` costs. Refusing the
682
+ * including, until that row was retired (2026-09-13), `kleod:UpdateHUDCounterDisplay`'s match,
683
+ * which returning `null` cost. Refusing the
681
684
  * key that actually shares costs that key's spelling and nothing else, and the differ still has
682
685
  * every other spelling in the list.
683
686
  *
@@ -757,14 +760,14 @@ export function planScopedBases(sfn: SFn, opts: ScopeBaseOpts = {}): ScopedBaseP
757
760
 
758
761
  /**
759
762
  * The re-spelling `regions` asks for — `/scopebase` or `/regionbase` — or null when nothing
760
- * qualifies (the caller then adds no candidate rather than a duplicate of the primary).
763
+ * qualifies (the caller then adds no candidate rather than a duplicate of the default).
761
764
  */
762
765
  export const hoistScopedBases = (sfn: SFn, opts: ScopeBaseOpts = {}): SFn | null =>
763
766
  applyScopedBasePlan(sfn, planScopedBases(sfn, opts));
764
767
 
765
768
  /**
766
769
  * `plan` applied to the tree it was planned over — null when it decided nothing (the caller then
767
- * adds no candidate rather than a duplicate of the primary).
770
+ * adds no candidate rather than a duplicate of the default).
768
771
  *
769
772
  * IDENTITY-BOUND to that tree, and not by the type: `scope` is matched against statement LISTS and
770
773
  * `repoint` against access NODES, both by reference. A plan from a DIFFERENT tree therefore splices
@@ -1,18 +1,18 @@
1
- // L3 re-spelling lever: sink each leading pointer-base INIT to the statement that first uses it.
1
+ // L3 respell variation: sink each leading pointer-base INIT to the statement that first uses it.
2
2
  //
3
3
  // `l3/basecse.ts`'s COMMITTED call emits every base hoist at the head of `sfn.body`, so a base
4
4
  // first touched halfway down the function is live across everything above it — a live range the
5
- // original never had, and agbcc pays for it with a callee-saved register. (Its roster admissions
6
- // ask for this placement directly, through the same `l3/hoist.ts` mechanism this lever uses; what
7
- // the lever adds is reaching the run on a tree the roster did not build — one NOTHING re-hoisted,
5
+ // original never had, and agbcc pays for it with a callee-saved register. (Its roster's hoists
6
+ // ask for this placement directly, through the same `l3/hoist.ts` mechanism this variation uses; what
7
+ // the variation adds is reaching the run on a tree the roster did not build — one NOTHING re-hoisted,
8
8
  // or one `l3/nearbase.ts` prepended into, which is the `/nearbase/sinkinit` pairing.)
9
9
  // Compiled pair on `synthetic:basehome`:
10
10
  // assigning at the top adds `push {r4, lr}` / `pop {r4}` / `pop {r0}` / `bx r0` where assigning at
11
- // the first use keeps a plain `bx lr`. The ladder that row records is the argument for the lever
11
+ // the first use keeps a plain `bx lr`. The ladder that row records is the argument for the variation
12
12
  // being a PLACEMENT rather than a wider hoist — top-placed 11, not hoisted at all 9, placed at
13
13
  // first use 0. A hoist at the wrong place is worse than no hoist.
14
14
  //
15
- // A LEVER, NOT THE DEFAULT. Which placement the source used is per-function knowledge the asm does
15
+ // A VARIATION, NOT THE DEFAULT. Which placement the source used is per-function knowledge the asm does
16
16
  // not carry: moving basecse's own head placement to first use moves 8 benchmark rows, 4 better and
17
17
  // 4 worse, two of the losses being matches. The UNSUNK spelling always rides beside this one —
18
18
  // head-placed wherever basecse built the run, prepend-placed under `/nearbase` — and the differ
@@ -25,7 +25,7 @@
25
25
  // immediately before the first TOP-LEVEL statement mentioning its name and never INTO a nested
26
26
  // scope, so it still dominates every use — planning a hoist inside a scope is `l3/scopebase.ts`'s
27
27
  // job and it does the domination work. `placeBaseLocals` carries the refusals; a run where none of
28
- // them moves is this lever declining.
28
+ // them moves is this variation declining.
29
29
  //
30
30
  // SEMANTICS BY CONSTRUCTION: the moved value is a pure address leaf — it reads nothing, writes its
31
31
  // own plain cell and cannot fault — and every statement it crosses mentions the name nowhere, an
@@ -7,13 +7,13 @@
7
7
  // LOWER `[sp,#k]`, and a source whose two spilled locals are declared the other way round compiles
8
8
  // to the same object with those two operands swapped and nothing else moved.
9
9
  //
10
- // A PER-COMPILER DEFAULT, NOT A RANKED AXIS. The asm does not underdetermine the answer: a
10
+ // A COMPILER BEHAVIOR, NOT A RANKED VARIATION. The asm does not underdetermine the answer: a
11
11
  // `[sp,#k]` operand NAMES the slot, and slot → declaration rank is a FUNCTION once the compiler is
12
- // fixed. An axis exists where two source spellings collapse to the same object and only the differ
12
+ // fixed. A variation exists where two source spellings collapse to the same object and only the differ
13
13
  // can choose between them; here the object chooses. The fan cost is zero — this rewrites the one
14
14
  // tree every candidate already carries, adding no candidate, no `structure()` call and no compile.
15
15
  //
16
- // WHY `emit` OWNS IT. Two reasons, and they point the same way. It must run AFTER every L3 lever,
16
+ // WHY `emit` OWNS IT. Two reasons, and they point the same way. It must run AFTER every L3 respell variation,
17
17
  // because each of those rebuilds the declaration list (appends: basecse, scopebase, argbase,
18
18
  // nearbase, regspell, reindex; filters: unmerge, coalesce, dce, inlinebase, pollguard, unreduce),
19
19
  // and `emit` is last by construction. And it must NOT run at a `.emit(` call site: there are seven
package/src/l3/storage.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  // SHADOWING is what the copies disagreed on, and the disagreement is REAL rather than drift: a
10
10
  // name can be declared both a global and a local, because `SFn.globals` records what the code
11
11
  // REFERENCES (structure.ts `noteGlobal`) independently of what it declares. Which answer is right
12
- // depends on how the asking lever will SPELL the name, so the two questions are two functions
12
+ // depends on how the asking pass will SPELL the name, so the two questions are two functions
13
13
  // here instead of one set each caller filters its own way:
14
14
  // • it keeps the reference verbatim (`(u8 *)g`) — the shadow is harmless, the spelling denotes
15
15
  // whatever the original access denoted: `declaredGlobals`;
@@ -25,7 +25,7 @@
25
25
  // above covers only below-vs-in-arms.
26
26
  //
27
27
  // KNOWN INTERACTIONS, both byte-level rather than soundness. This pass is unconditional like
28
- // `dce.ts` and `basecse.ts` rather than a differ-refereed lever, and the argument those files each
28
+ // `dce.ts` and `basecse.ts` rather than a differ-refereed variation, and the argument those files each
29
29
  // state for themselves applies here too and was missing: a wrong merge changes recompiled bytes and
30
30
  // surfaces as a LOST match under the zero-lost gate, never as wrong C.
31
31
  //
@@ -49,7 +49,7 @@
49
49
  // ways in `test/tailmerge.test.ts`.
50
50
  // A ranked row is not stuck with either order: `/copy-defpos` (rank.ts) enumerates the
51
51
  // def-position spelling beside the record's, so the merged form is a candidate the differ can
52
- // pick on bytes — on `CountCollectedGems` its gate admits the axis on the map-ful and map-less
52
+ // pick on bytes — on `CountCollectedGems` its gate admits the variation on the map-ful and map-less
53
53
  // lifts alike. What no arm of the fan spells is the third form, the hidden statement merged
54
54
  // while KEEPING the record's order.
55
55
  //
@@ -0,0 +1,70 @@
1
+ // L3 spelling pass: drop a void `return;` the assembly says the source never wrote.
2
+ //
3
+ // `structure/retspell.ts` marks a return `unspelled` when the machine reached that epilogue without
4
+ // the `b <epilogue>` a source `return;` compiles to — the reading, and what makes it decidable, is
5
+ // stated there. This pass is the other half: a mark alone is not a licence to delete, because a
6
+ // `return` is still a control transfer in the STATEMENT tree. Deleting one that something follows
7
+ // lets control run on into it, and that is a semantic change, not a spelling one.
8
+ //
9
+ // So the licence is TAIL POSITION and nothing weaker: nothing executes after the statement on any
10
+ // path, all the way up to the end of the function. A return ending a loop body, a `switch` arm, or
11
+ // an `if` arm the function continues past is left exactly where it is, whatever the mark says.
12
+ //
13
+ // TWO SHAPES REFUSE EVEN IN TAIL POSITION, both because removing the statement would leave a
14
+ // statement list that has to be re-spelled rather than shortened:
15
+ //
16
+ // - the sole statement of a `then` arm. `if (c) { }` is not the answer — `if (!c) { … }` is, and
17
+ // which sense a compiler emits is a per-SITE question this pass holds nothing to decide.
18
+ // - the whole function body. A body is not a place a statement can vanish from.
19
+ //
20
+ // An `else` arm IS allowed to empty: an `if` with no else is the same statement, and both the
21
+ // printer (`backend/cfamily.ts`) and `l3/dce.ts` already spell `else: []` that way.
22
+ //
23
+ // THE REFUSALS ARE PROSE, NOT A `Gate` TABLE, and that is the second of the three answers
24
+ // `docs/level-tower.md` sanctions rather than an omission. A table buys the ablation — drop this
25
+ // rule and something breaks — as a test instead of a claim, and it is worth building for a refusal a
26
+ // round has HAD to instrument. None of these was: each is a property of one candidate's own
27
+ // position, and `test/tailret.test.ts` holds a test per refusal, which is the ablation the table
28
+ // would have bought. Convert them when a round has to argue about one.
29
+ //
30
+ // Ordering: after `l3/tailmerge.ts`, whose peel moves `assign`/`store`/`exprstmt` only, so a
31
+ // `return` ending an arm blocks it — run first and this pass hands tailmerge arms it could not
32
+ // otherwise peel, changing rows that have nothing to do with returns. Before `l3/dce.ts`, so its
33
+ // branch peephole sees the `else` this pass empties. `pipeline.ts` commits that order.
34
+ import type { SFn, Stmt } from './ast';
35
+
36
+ /** Can this statement be deleted outright — a void return the asm did not spell? */
37
+ const isDroppable = (s: Stmt): boolean => s.k === 'return' && s.value === undefined && s.unspelled === true;
38
+
39
+ /** `stmts` rewritten. `isTail` — control falls off the END of the function after this list, so its
40
+ * last statement is in tail position. `mayEmpty` — the list is allowed to come back empty. */
41
+ function walk(stmts: Stmt[], isTail: boolean, mayEmpty: boolean): Stmt[] {
42
+ const out = stmts.map((s, i) => {
43
+ const tail = isTail && i === stmts.length - 1;
44
+ switch (s.k) {
45
+ case 'if':
46
+ return { ...s, then: walk(s.then, tail, false), else: walk(s.else, tail, true) };
47
+ case 'while':
48
+ case 'dowhile':
49
+ case 'for':
50
+ // A return inside a loop is never in tail position: the statement after it is the next
51
+ // iteration.
52
+ return { ...s, body: walk(s.body, false, false) };
53
+ case 'switch':
54
+ // Nor inside a `switch`: dropping an arm's return diverts it into the arm below.
55
+ return {
56
+ ...s,
57
+ cases: s.cases.map((c) => ({ ...c, body: walk(c.body, false, false) })),
58
+ ...(s.default ? { default: walk(s.default, false, false) } : {}),
59
+ };
60
+ default:
61
+ return s;
62
+ }
63
+ });
64
+ const last = out[out.length - 1];
65
+ return isTail && last !== undefined && isDroppable(last) && (out.length > 1 || mayEmpty) ? out.slice(0, -1) : out;
66
+ }
67
+
68
+ export function dropUnspelledReturns(sfn: SFn): SFn {
69
+ return { ...sfn, body: walk(sfn.body, true, false) };
70
+ }
package/src/l3/typing.ts CHANGED
@@ -21,7 +21,7 @@ import { IrType, T, scalarTypeForAccess } from '../ir/types';
21
21
  import { type Expr, type SFn, exprChildren } from './ast';
22
22
 
23
23
  /** The declared type of a printed variable — the env `exprCType` judges rendered C against.
24
- * THE one copy of the SFn→env derivation — printers, contracts and L3 levers alike: each
24
+ * THE one copy of the SFn→env derivation — printers, contracts and L3 respell variations alike: each
25
25
  * consumer judging against anything but the declarations it emits would let them disagree. */
26
26
  export type VarTypes = (name: string) => IrType | undefined;
27
27
 
@@ -133,7 +133,7 @@ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes):
133
133
  *
134
134
  * THE one rendered-signedness judgment, and it lives beside the declarations it judges against
135
135
  * because every consumer shares it: the C-family backend's operand pin, structure.ts's
136
- * unsigned-compare gate (the /uns-cmp axis) and its signed-compare pin, and — through
136
+ * unsigned-compare gate (the /uns-cmp variation) and its signed-compare pin, and — through
137
137
  * `arithConversionSignedness` above — initfirst's compare-meaning gate. Two of those models
138
138
  * disagreeing about one expression is the drift this placement prevents.
139
139
  *
@@ -149,7 +149,7 @@ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes):
149
149
  * instruction-for-instruction the same where `f` really returns `int`); a missing one is a
150
150
  * miscompile — with `u32 f(void);` in scope `f() / a` calls `__udivsi3` where the machine called
151
151
  * `__divsi3`, and `if (f() >= 0)` compiles to `bl f; mov r0, #0`, comparison and both arms gone.
152
- * The one consumer that reads `undefined` the other way is the /uns-cmp axis's own gate, which is
152
+ * The one consumer that reads `undefined` the other way is the /uns-cmp variation's own gate, which is
153
153
  * asking whether an operand is ALREADY unsigned.
154
154
  *
155
155
  * Anything narrower than 32 bits promotes to `int` and is therefore SIGNED, whatever it was