@asmlift/core 0.5.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 (94) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +270 -171
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +243 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +78 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +345 -2
  16. package/src/ir/opcodes.ts +176 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/advance.ts +373 -0
  24. package/src/l3/argbase.ts +6 -5
  25. package/src/l3/ast.ts +510 -59
  26. package/src/l3/basecse.ts +686 -78
  27. package/src/l3/coalesce.ts +432 -46
  28. package/src/l3/dce.ts +31 -9
  29. package/src/l3/gates.ts +96 -1
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +176 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +114 -89
  42. package/src/l3/reindex.ts +722 -80
  43. package/src/l3/scopebase.ts +649 -220
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +16 -1
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +687 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +239 -16
  57. package/src/pipeline.ts +173 -60
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/const.ts +203 -3
  61. package/src/raise/divpow2.ts +4 -4
  62. package/src/raise/extscale.ts +342 -0
  63. package/src/raise/globalshape.ts +1058 -0
  64. package/src/raise/gvn.ts +33 -18
  65. package/src/raise/latch.ts +126 -0
  66. package/src/raise/magicdiv.ts +2 -2
  67. package/src/raise/memberarrays.ts +594 -0
  68. package/src/raise/narrow.ts +124 -0
  69. package/src/raise/narrowlocal.ts +572 -0
  70. package/src/raise/paramwidth.ts +201 -0
  71. package/src/raise/pre-recovery.ts +169 -21
  72. package/src/raise/recover.ts +56 -23
  73. package/src/raise/retsink.ts +585 -19
  74. package/src/raise/shortcircuit.ts +1050 -89
  75. package/src/raise/struct-arrays.ts +19 -2
  76. package/src/raise/structs.ts +34 -4
  77. package/src/raise/tailsink.ts +126 -0
  78. package/src/rank-declare.ts +256 -0
  79. package/src/rank-variations.ts +760 -0
  80. package/src/rank.ts +2122 -326
  81. package/src/structure/analysis.ts +1398 -150
  82. package/src/structure/bitfields.ts +432 -0
  83. package/src/structure/globalaccess.ts +300 -0
  84. package/src/structure/hazards.ts +411 -20
  85. package/src/structure/loops.ts +2 -49
  86. package/src/structure/namecoalesce.ts +454 -0
  87. package/src/structure/structure.ts +3979 -612
  88. package/src/structure/switch-recover.ts +710 -145
  89. package/src/symbols.ts +188 -6
  90. package/src/target.ts +495 -32
  91. package/src/trace.ts +112 -33
  92. package/src/variation-definitions.ts +1540 -0
  93. package/src/variation-gates.ts +89 -0
  94. package/src/variation-tokens.ts +355 -0
@@ -0,0 +1,227 @@
1
+ // L3 respell variation: declare a recovered WORD field a POINTER (`void *field_4;` rather than
2
+ // `s32 field_4;`), and cast at each read.
3
+ //
4
+ // raise/structs.ts recovers a field's type from the ACCESS WIDTH alone — a 4-byte load is `s32`,
5
+ // because that is all a load says. On a 32-bit target `void *` fits the same evidence exactly:
6
+ // the two spellings load the same word with the same instruction, so the asm cannot referee them
7
+ // and the struct recovery's own byte-neutrality note applies to the field TYPE as much as to the
8
+ // `->field_N` vs `[idx]` question it was written about.
9
+ //
10
+ // IT IS NOT NEUTRAL TO THE COMPILER, which is the whole reason to spell it. A pointer and an `s32`
11
+ // are different alias sets, so under strict aliasing the loop optimizer may hoist a pointer field's
12
+ // load past an `s32`-typed store it must otherwise keep behind — and on synthetic:dmaptrsrc that
13
+ // hoist is the difference between a byte-exact match and a 35-point diff, once the accumulator is
14
+ // un-reduced back into the loop (that row's fan: `/vol-store/unreduce/ptr-field` 0,
15
+ // `/vol-store/unreduce` 35). Which side matches is per-field knowledge nothing in the asm
16
+ // carries, so both are enumerated and the differ referees.
17
+ //
18
+ // WHERE THE FLAG IS, because two readers have now looked for `-fstrict-aliasing` in the benchmark's
19
+ // agbcc line and not found it: it is not there, and it does not need to be. `-O2` turns it on
20
+ // (gcc 2.9 sets `flag_strict_aliasing` from `optimize >= 2`), which is why the CITATION is a
21
+ // behaviour rather than a flag. Verified in BOTH directions on this row's own shape, objects
22
+ // compared byte-for-byte:
23
+ // `void *` field, harness flags → the field load is HOISTED above the loop label
24
+ // `s32` field, harness flags → it STAYS in the loop body
25
+ // `void *` field, + -fno-strict-aliasing → byte-IDENTICAL to the `s32` build
26
+ // `s32` field, + -fstrict-aliasing → byte-IDENTICAL to itself (the flag was already on)
27
+ // So adding the flag proves nothing and REMOVING it proves everything, which is the direction a
28
+ // reader checking this note should take. The stores the load is hoisted past are the loop's own
29
+ // `*(volatile s32 *)0x040000D4/D8/DC` device writes — `volatile` restricts what may be done with
30
+ // THOSE accesses and does not merge their type into the pointer's alias set.
31
+ //
32
+ // SEMANTICS ARE PRESERVED BY CONSTRUCTION on a target whose pointers are 32 bits: the declaration
33
+ // changes, and every read is wrapped in a cast back to the field's own recovered integer type, so
34
+ // each use computes the same value it did. Nothing else moves. THE 32-BIT ASSUMPTION IS ASSERTED,
35
+ // NOT CHECKED — there is no pointer-width field on TargetDescription to check it against, and the
36
+ // assumption is already tower-wide (`l3/typing.ts`'s `ptrElemBytes` returns 4 for any pointee). On
37
+ // a 64-bit target this variation would change a struct's LAYOUT rather than only its spelling, so the
38
+ // width field is what that target's first row must add, and this note is where to start.
39
+ //
40
+ // IT FLIPS EVERY ADMITTED FIELD AT ONCE, and its own paragraph above says the knowledge is
41
+ // PER-FIELD — so the subset enumeration `l3/volatileptr.ts` does for exactly this reason
42
+ // (`volatileSubsetCandidates`, capped at three locals) is the shape this variation will eventually
43
+ // want. It is not built yet because nothing demands it: swept over 834 corpus trees, the variation
44
+ // fires on 42, and 34 of those have a single admitted field. The six 2-field trees and the two
45
+ // 4-field ones (`sa3:sa2__sub_8083504` flips Struct0.field_8/12 and Struct2.field_8/12 together)
46
+ // are where 1 of 3 and 1 of 15 non-empty subsets is reachable. A row that needs one of the missing
47
+ // subsets is what earns the enumeration — and it will cost candidates, which is why "it might
48
+ // help" is not enough.
49
+ //
50
+ // GATE (PTR_FIELD_GATES, read once per field): the field's recovered type must be a 32-bit
51
+ // integer; it must never be a store LVALUE (the write side would need a cast on the value, which
52
+ // is a second question); and it must never stand as the BASE of another access (a field the tree
53
+ // already dereferences is one the recovery typed from its own use, not from a width). Nothing
54
+ // qualifying ⇒ decline (null).
55
+ //
56
+ // SCOPE, because both of this table's blind spots look exactly like a gate refusing. First, THERE
57
+ // IS NO PAD RULE, deliberately: `plan` builds a ctx only for fields the tree ACCESSES, so "a field
58
+ // nothing reaches" cannot arise — and a rule for it reads as one, then answers for the 78 corpus
59
+ // WRITE-ONLY fields that `written` owns. Second, a field whose
60
+ // BASE TYPE does not resolve is dropped by `plan` before any gate reads it — 151 of the corpus's
61
+ // 820 field nodes, on 20 trees, all of them `[map]` configurations (kleod:CheckTileCollisionVertical,
62
+ // FreeAllDecompBuffers, TransformSingleEntityToScreen and ConfigureEntityBehavior among them).
63
+ // Declining there is right; being unable to say which of the two happened is the defect.
64
+ import { type IrType, T } from '../ir/types';
65
+ import {
66
+ type Expr,
67
+ type SFn,
68
+ type Stmt,
69
+ type StructType,
70
+ mapExprChildren,
71
+ mapStmtExprs,
72
+ stmtChildren,
73
+ walkExprs,
74
+ } from './ast';
75
+ import { type Gate, firstRejection } from './gates';
76
+ import { declaredTypes, exprCType } from './typing';
77
+
78
+ /** One recovered field as the gates read it — built only for fields the tree accesses. */
79
+ interface FieldCtx {
80
+ /** the recovered type is a 32-bit integer — the width a pointer also fits */
81
+ word: boolean;
82
+ written: boolean;
83
+ /** the field's value stands as the base of an `index` or another `field` */
84
+ dereferenced: boolean;
85
+ }
86
+
87
+ export const PTR_FIELD_GATES: readonly Gate<FieldCtx>[] = [
88
+ {
89
+ id: 'not-word',
90
+ why: 'only a word-wide field fits a pointer as well as it fits an integer',
91
+ sound: true,
92
+ guardedBy: 'ptrfield.test.ts: a halfword field declines',
93
+ rejects: (c) => !c.word,
94
+ },
95
+ {
96
+ id: 'written',
97
+ why: 'the write side needs a cast on the stored value, which is a separate question',
98
+ sound: true,
99
+ guardedBy: 'ptrfield.test.ts: a field the tree stores through declines',
100
+ rejects: (c) => c.written,
101
+ },
102
+ {
103
+ id: 'dereferenced',
104
+ why: 'a field the tree already dereferences was typed from its use, not from a width',
105
+ sound: true,
106
+ guardedBy: 'ptrfield.test.ts: a field standing as an access base declines',
107
+ rejects: (c) => c.dereferenced,
108
+ },
109
+ ];
110
+
111
+ /** the struct a `field` node selects from, or null */
112
+ function structOf(base: Expr, vt: ReturnType<typeof declaredTypes>): Extract<IrType, { kind: 'struct' }> | null {
113
+ const t = exprCType(base, vt);
114
+ const st = t?.kind === 'ptr' ? t.to : t;
115
+ return st?.kind === 'struct' ? st : null;
116
+ }
117
+
118
+ /** `<struct>.<field>` as one key — two structs may both carry a `field_4`. */
119
+ const keyOf = (structName: string, field: string): string => `${structName}.${field}`;
120
+
121
+ /** The fields PTR_FIELD_GATES admits, keyed by struct and name, each with the integer type its
122
+ * reads cast back to. */
123
+ function plan(sfn: SFn): Map<string, IrType> {
124
+ const vt = declaredTypes(sfn);
125
+ const seen = new Map<string, FieldCtx & { type: IrType }>();
126
+ const note = (base: Expr, name: string, edit: (c: FieldCtx) => void): void => {
127
+ const st = structOf(base, vt);
128
+ const declared = st?.fields.find((f) => f.name === name)?.type;
129
+ if (st === null || declared === undefined) {
130
+ return;
131
+ }
132
+ const key = keyOf(st.name, name);
133
+ const cur = seen.get(key) ?? {
134
+ word: declared.kind === 'int' && declared.width === 32,
135
+ written: false,
136
+ dereferenced: false,
137
+ type: declared,
138
+ };
139
+ edit(cur);
140
+ seen.set(key, cur);
141
+ };
142
+ for (const e of walkExprs(sfn.body)) {
143
+ if (e.k === 'field') {
144
+ note(e.base, e.name, () => {});
145
+ }
146
+ // a field standing as an access base is one the recovery typed from its own use
147
+ const inner = e.k === 'index' ? e.base : e.k === 'field' ? e.base : null;
148
+ if (inner?.k === 'field') {
149
+ note(inner.base, inner.name, (c) => (c.dereferenced = true));
150
+ }
151
+ }
152
+ for (const s of stores(sfn.body)) {
153
+ if (s.lval.k === 'field') {
154
+ note(s.lval.base, s.lval.name, (c) => (c.written = true));
155
+ }
156
+ }
157
+ const out = new Map<string, IrType>();
158
+ for (const [key, ctx] of seen) {
159
+ if (firstRejection(PTR_FIELD_GATES, ctx) === null) {
160
+ out.set(key, ctx.type);
161
+ }
162
+ }
163
+ return out;
164
+ }
165
+
166
+ /** Every `store` in the tree. ORDER-FREE by construction, which is what lets it use the shared
167
+ * `stmtChildren` (a switch's default sits at `defaultAt`, not last): its one caller only sets
168
+ * `written` on entries the preceding `walkExprs` pass already created — `note` builds an entry
169
+ * from the DECLARED field type, a function of the key alone, so this loop mints no key. */
170
+ function* stores(body: readonly Stmt[]): Generator<Extract<Stmt, { k: 'store' }>> {
171
+ for (const s of body) {
172
+ if (s.k === 'store') {
173
+ yield s;
174
+ }
175
+ yield* stores(stmtChildren(s));
176
+ }
177
+ }
178
+
179
+ /** The `/ptr-field` candidate, or null when no field qualifies. Read-only: returns a fresh SFn.
180
+ *
181
+ * The struct type appears TWICE in a tree — inline in each `(struct S *)` cast and again in
182
+ * `SFn.structs`, which is what a backend prints — so both are re-typed here. Letting them drift
183
+ * would print a declaration the expressions' own types contradict. */
184
+ export function pointerFields(sfn: SFn): SFn | null {
185
+ const admitted = plan(sfn);
186
+ if (admitted.size === 0) {
187
+ return null;
188
+ }
189
+ const flipStruct = <S extends { name: string; fields: { name: string; type: IrType }[] }>(st: S): S => ({
190
+ ...st,
191
+ fields: st.fields.map((f) => (admitted.has(keyOf(st.name, f.name)) ? { ...f, type: T.ptr(T.void()) } : f)),
192
+ });
193
+ const flip = (t: IrType): IrType => {
194
+ switch (t.kind) {
195
+ case 'struct':
196
+ return flipStruct(t);
197
+ case 'ptr':
198
+ return T.ptr(flip(t.to));
199
+ case 'array':
200
+ return T.array(flip(t.elem), t.count);
201
+ default:
202
+ return t;
203
+ }
204
+ };
205
+ const vt = declaredTypes(sfn);
206
+ const sub = (e: Expr): Expr => {
207
+ if (e.k === 'field') {
208
+ const st = structOf(e.base, vt);
209
+ const back = st === null ? undefined : admitted.get(keyOf(st.name, e.name));
210
+ const inner = mapExprChildren(e, sub);
211
+ return back === undefined ? inner : { k: 'cast', to: back, e: inner };
212
+ }
213
+ const m = mapExprChildren(e, sub);
214
+ return m.k === 'cast' ? { ...m, to: flip(m.to) } : m;
215
+ };
216
+ return {
217
+ ...sfn,
218
+ params: sfn.params.map((p) => ({ ...p, type: flip(p.type) })),
219
+ locals: sfn.locals.map((l) => ({ ...l, type: flip(l.type) })),
220
+ ...(sfn.globals ? { globals: sfn.globals.map((g) => ({ ...g, type: flip(g.type) })) } : {}),
221
+ ...(sfn.structs ? { structs: sfn.structs.map((s): StructType => flipStruct(s)) } : {}),
222
+ // `mapStmtExprs` already recurses into nested statement lists, so ONE call per top-level
223
+ // statement rewrites the whole subtree — a second walk over its children would apply `sub`
224
+ // twice and cast each read back to an integer twice over.
225
+ body: sfn.body.map((s) => mapStmtExprs(s, sub)),
226
+ };
227
+ }
@@ -1,4 +1,4 @@
1
- // asmlift L3 — the REGISTER-COPY re-spelling, a differ-ranked representation lever (the fourth,
1
+ // asmlift L3 — the REGISTER-COPY respell variation, differ-ranked like the three before it (the fourth,
2
2
  // after signedness / branch sense / walk-vs-index).
3
3
  //
4
4
  // A compiler's register allocation leaves SOURCE-visible footprints the coalescing structurer
@@ -26,7 +26,7 @@
26
26
  // a bin operand materializes into its own fresh local first (`v = 128 << 9; … v / x …`) —
27
27
  // the register the compiler staged the constant in.
28
28
  // R3 — return assign-back: a non-var return expression lands in a fresh local first
29
- // (`r = E; return r`). Emitted as a SEPARATE variant (with/without R3) when R1/R2 fired:
29
+ // (`r = E; return r`). Emitted as a SEPARATE result (with/without R3) when R1/R2 fired:
30
30
  // which tail the source spelled is itself ambiguous.
31
31
  import { IrType, T } from '../ir/types';
32
32
  import { Expr, SFn, Stmt, exprEquals, mapExprChildren } from './ast';
@@ -117,9 +117,106 @@ function isConstExpr(e: Expr): boolean {
117
117
  }
118
118
  }
119
119
 
120
- /** Apply the register-copy re-spelling. Returns 0–2 variant SFns (without/with the R3 tail);
121
- * empty when nothing fired. Pure — never mutates the input. */
122
- export function registerishSpellings(sfn: SFn): SFn[] {
120
+ /** ONE R3 TAIL, NAMED BY WHAT IT DID rather than by where it landed in the list.
121
+ *
122
+ * The tail is the assign-back before the `return`, and there are two spellings of it: reuse R1's
123
+ * now-dead value var, or take a fresh one. R1 may not have fired, in which case there is no dead
124
+ * var and only the fresh spelling exists — so the tails are a 1- OR 2-element list and the SECOND
125
+ * spelling is not at a fixed index.
126
+ *
127
+ * This field exists because the caller names these, and a candidate's variations are the instrument
128
+ * every census in this repo reads (a `bench diff` FIELD). Index a `['/regcopy', '/regcopy-ret',
129
+ * '/regcopy-ret-fresh']` table by POSITION instead and an R1-less function publishes its fresh
130
+ * tail as `/regcopy-ret` — the dead-var-reuse name on the spelling that has no dead var to reuse
131
+ * — and a census over the token `/regcopy-ret-fresh` then censuses nothing wherever R1 declines,
132
+ * which is most of the corpus. */
133
+ export type RegcopyTail = 'none' | 'reuse' | 'fresh';
134
+
135
+ export interface RegcopySpelling {
136
+ /** which R3 tail this result carries — `none` is the un-tailed base */
137
+ tail: RegcopyTail;
138
+ sfn: SFn;
139
+ }
140
+
141
+ interface Diamond {
142
+ v: string;
143
+ E: Expr;
144
+ updArm: 'then' | 'else';
145
+ upd: Expr;
146
+ cond: Expr;
147
+ }
148
+ /** `if (cmp) { v = E } else { v = f(E) }` (or arms swapped), f = bin(E, const-ish). */
149
+ function matchDiamond(s: Extract<Stmt, { k: 'if' }>): Diamond | null {
150
+ if (s.then.length !== 1 || s.else.length !== 1) {
151
+ return null;
152
+ }
153
+ const a = s.then[0];
154
+ const b = s.else[0];
155
+ if (a.k !== 'assign' || b.k !== 'assign' || a.name !== b.name) {
156
+ return null;
157
+ }
158
+ const isUpdOf = (upd: Expr, base: Expr): boolean =>
159
+ upd.k === 'bin' && isPure(upd.r) && exprEq(upd.l, base) && isConstExpr(upd.r);
160
+ if (isUpdOf(b.value, a.value)) {
161
+ return { v: a.name, E: a.value, updArm: 'else', upd: b.value, cond: s.cond };
162
+ }
163
+ if (isUpdOf(a.value, b.value)) {
164
+ return { v: a.name, E: b.value, updArm: 'then', upd: a.value, cond: s.cond };
165
+ }
166
+ return null;
167
+ }
168
+ /** Does the cond's NON-E operand mention `v`? (The E side becomes the copy; the other side
169
+ * must be v-free or the hoisted assignment changes what it compares against.) */
170
+ function condOtherMentions(cond: Expr, E: Expr, v: string): boolean {
171
+ if (cond.k !== 'bin') {
172
+ return false;
173
+ }
174
+ const other = exprEq(cond.l, E) ? cond.r : exprEq(cond.r, E) ? cond.l : null;
175
+ const mentions = (e: Expr): boolean => {
176
+ if (e.k === 'var') {
177
+ return e.name === v;
178
+ }
179
+ let hit = false;
180
+ mapExprChildren(e, (c) => {
181
+ hit = hit || mentions(c);
182
+ return c;
183
+ });
184
+ return hit;
185
+ };
186
+ return other ? mentions(other) : false;
187
+ }
188
+ /** cond compares E against a pure operand → same comparison reading the named var. */
189
+ function rewriteCond(cond: Expr, E: Expr, name: string): Expr | null {
190
+ if (cond.k !== 'bin' || !(cond.op in FLIP)) {
191
+ return null;
192
+ }
193
+ if (exprEq(cond.l, E) && isPure(cond.r)) {
194
+ return { k: 'bin', op: cond.op, l: { k: 'var', name }, r: cond.r };
195
+ }
196
+ if (exprEq(cond.r, E) && isPure(cond.l)) {
197
+ return { k: 'bin', op: cond.op, l: cond.l, r: { k: 'var', name } };
198
+ }
199
+ return null;
200
+ }
201
+ function flipCmp(cond: Expr): Expr | null {
202
+ if (cond.k !== 'bin') {
203
+ return null;
204
+ }
205
+ const op = FLIP[cond.op];
206
+ return op ? { k: 'bin', op: op as Extract<Expr, { k: 'bin' }>['op'], l: cond.l, r: cond.r } : null;
207
+ }
208
+ /** in `upd`, the occurrence of subtree E replaced by var `name` (E was just assigned to it). */
209
+ function renameSubexpr(e: Expr, E: Expr, name: string): Expr {
210
+ if (exprEq(e, E)) {
211
+ return { k: 'var', name };
212
+ }
213
+ return mapExprChildren(e, (c) => renameSubexpr(c, E, name));
214
+ }
215
+
216
+ /** Apply the register-copy re-spelling. Returns 0–3 results — the base, plus the R3 tail in each
217
+ * spelling that exists (reuse needs R1 to have fired, fresh always does) — and an EMPTY list when
218
+ * nothing fired. Pure — never mutates the input. */
219
+ export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
123
220
  const locals = [...sfn.locals];
124
221
  const taken = new Set([...sfn.params, ...sfn.locals].map((x) => x.name));
125
222
  let fresh = 0;
@@ -150,7 +247,7 @@ export function registerishSpellings(sfn: SFn): SFn[] {
150
247
  if (m) {
151
248
  const { v, E, updArm, upd, cond } = m;
152
249
  // GUARDS BEFORE ALLOCATION (a declined shape must leave no residue — the leaked
153
- // dead `w` even perturbed the live-name count this lever exists to reproduce):
250
+ // dead `w` even perturbed the live-name count this variation exists to reproduce):
154
251
  // • E and the cond pure; the cond's non-E operand must NOT mention v (a clamp's
155
252
  // `if (a < v)` would compare against the POST-assignment v — reproduced);
156
253
  // • the copy w carries E's RENDERED type, not v's declared one (retyping a u32
@@ -188,81 +285,6 @@ export function registerishSpellings(sfn: SFn): SFn[] {
188
285
  return out;
189
286
  };
190
287
 
191
- interface Diamond {
192
- v: string;
193
- E: Expr;
194
- updArm: 'then' | 'else';
195
- upd: Expr;
196
- cond: Expr;
197
- }
198
- /** `if (cmp) { v = E } else { v = f(E) }` (or arms swapped), f = bin(E, const-ish). */
199
- function matchDiamond(s: Extract<Stmt, { k: 'if' }>): Diamond | null {
200
- if (s.then.length !== 1 || s.else.length !== 1) {
201
- return null;
202
- }
203
- const a = s.then[0];
204
- const b = s.else[0];
205
- if (a.k !== 'assign' || b.k !== 'assign' || a.name !== b.name) {
206
- return null;
207
- }
208
- const isUpdOf = (upd: Expr, base: Expr): boolean =>
209
- upd.k === 'bin' && isPure(upd.r) && exprEq(upd.l, base) && isConstExpr(upd.r);
210
- if (isUpdOf(b.value, a.value)) {
211
- return { v: a.name, E: a.value, updArm: 'else', upd: b.value, cond: s.cond };
212
- }
213
- if (isUpdOf(a.value, b.value)) {
214
- return { v: a.name, E: b.value, updArm: 'then', upd: a.value, cond: s.cond };
215
- }
216
- return null;
217
- }
218
- /** Does the cond's NON-E operand mention `v`? (The E side becomes the copy; the other side
219
- * must be v-free or the hoisted assignment changes what it compares against.) */
220
- function condOtherMentions(cond: Expr, E: Expr, v: string): boolean {
221
- if (cond.k !== 'bin') {
222
- return false;
223
- }
224
- const other = exprEq(cond.l, E) ? cond.r : exprEq(cond.r, E) ? cond.l : null;
225
- const mentions = (e: Expr): boolean => {
226
- if (e.k === 'var') {
227
- return e.name === v;
228
- }
229
- let hit = false;
230
- mapExprChildren(e, (c) => {
231
- hit = hit || mentions(c);
232
- return c;
233
- });
234
- return hit;
235
- };
236
- return other ? mentions(other) : false;
237
- }
238
- /** cond compares E against a pure operand → same comparison reading the named var. */
239
- function rewriteCond(cond: Expr, E: Expr, name: string): Expr | null {
240
- if (cond.k !== 'bin' || !(cond.op in FLIP)) {
241
- return null;
242
- }
243
- if (exprEq(cond.l, E) && isPure(cond.r)) {
244
- return { k: 'bin', op: cond.op, l: { k: 'var', name }, r: cond.r };
245
- }
246
- if (exprEq(cond.r, E) && isPure(cond.l)) {
247
- return { k: 'bin', op: cond.op, l: cond.l, r: { k: 'var', name } };
248
- }
249
- return null;
250
- }
251
- function flipCmp(cond: Expr): Expr | null {
252
- if (cond.k !== 'bin') {
253
- return null;
254
- }
255
- const op = FLIP[cond.op];
256
- return op ? { k: 'bin', op: op as Extract<Expr, { k: 'bin' }>['op'], l: cond.l, r: cond.r } : null;
257
- }
258
- /** in `upd`, the occurrence of subtree E replaced by var `name` (E was just assigned to it). */
259
- function renameSubexpr(e: Expr, E: Expr, name: string): Expr {
260
- if (exprEq(e, E)) {
261
- return { k: 'var', name };
262
- }
263
- return mapExprChildren(e, (c) => renameSubexpr(c, E, name));
264
- }
265
-
266
288
  // R2: stage const-expressions used as bin operands into fresh locals, at statement level.
267
289
  const r2Stmt = (s: Stmt): Stmt[] => {
268
290
  const staged: Stmt[] = [];
@@ -305,27 +327,30 @@ export function registerishSpellings(sfn: SFn): SFn[] {
305
327
  }
306
328
  const base: SFn = { ...sfn, locals: [...locals], body: afterR2 };
307
329
 
308
- // R3 variants: the tail assign-back — a non-var return lands in a local first. WHICH local is
330
+ // R3 results: the tail assign-back — a non-var return lands in a local first. WHICH local is
309
331
  // itself allocator-ambiguous (gcc 2.9 wanted R1's dead value var — live-name-count sensitive;
310
332
  // another allocator may want the fresh one), so BOTH tails are emitted as candidates rather
311
333
  // than asserting one compiler's preference; the source dedupe collapses them when identical.
312
- const tails: SFn[] = [];
334
+ const tails: RegcopySpelling[] = [];
313
335
  const last = base.body[base.body.length - 1];
314
- if (last?.k === 'return' && last.value && last.value.k !== 'var') {
336
+ if (last?.k === 'return' && last.value !== undefined && last.value.k !== 'var') {
337
+ // bound here rather than re-read inside `mk`: a PROPERTY's narrowing does not survive into a
338
+ // closure, and re-asserting it is what the three casts this replaces were doing
339
+ const retVal: Expr = last.value;
315
340
  const mk = (name: string): SFn => ({
316
341
  ...base,
317
342
  locals: [...locals],
318
343
  body: [
319
344
  ...base.body.slice(0, -1),
320
- { k: 'assign', name, value: (last as Extract<Stmt, { k: 'return' }>).value! } as Stmt,
321
- { k: 'return', value: { k: 'var', name } } as Stmt,
345
+ { k: 'assign', name, value: retVal },
346
+ { k: 'return', value: { k: 'var', name } },
322
347
  ],
323
348
  });
324
349
  if (deadValueVar) {
325
- tails.push(mk(deadValueVar));
350
+ tails.push({ tail: 'reuse', sfn: mk(deadValueVar) });
326
351
  }
327
- tails.push(mk(freshVar(T.s(32))));
352
+ tails.push({ tail: 'fresh', sfn: mk(freshVar(T.s(32))) });
328
353
  }
329
354
 
330
- return [base, ...tails];
355
+ return [{ tail: 'none', sfn: base }, ...tails];
331
356
  }