@asmlift/core 0.5.0 → 0.6.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 (86) 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 +238 -167
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +194 -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 +54 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +337 -2
  16. package/src/ir/opcodes.ts +140 -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/argbase.ts +2 -1
  24. package/src/l3/ast.ts +464 -57
  25. package/src/l3/basecse.ts +664 -76
  26. package/src/l3/coalesce.ts +429 -43
  27. package/src/l3/dce.ts +31 -9
  28. package/src/l3/gates.ts +21 -0
  29. package/src/l3/hoist.ts +293 -14
  30. package/src/l3/homesplit.ts +285 -0
  31. package/src/l3/initfirst.ts +301 -0
  32. package/src/l3/inlinebase.ts +193 -0
  33. package/src/l3/mentions.ts +113 -0
  34. package/src/l3/mulfirst.ts +42 -0
  35. package/src/l3/nearbase.ts +152 -0
  36. package/src/l3/offmember.ts +371 -0
  37. package/src/l3/parkfirst.ts +96 -0
  38. package/src/l3/pollguard.ts +154 -0
  39. package/src/l3/ptrfield.ts +227 -0
  40. package/src/l3/regspell.ts +110 -85
  41. package/src/l3/reindex.ts +715 -78
  42. package/src/l3/scopebase.ts +644 -218
  43. package/src/l3/sinkinit.ts +40 -0
  44. package/src/l3/slotorder.ts +123 -0
  45. package/src/l3/storage.ts +48 -0
  46. package/src/l3/symbol-refs.ts +41 -8
  47. package/src/l3/tailmerge.ts +15 -0
  48. package/src/l3/typing.ts +198 -9
  49. package/src/l3/unmerge.ts +263 -0
  50. package/src/l3/unreduce.ts +971 -0
  51. package/src/l3/volatileptr.ts +207 -0
  52. package/src/l3/volatileval.ts +130 -0
  53. package/src/l3/volstore.ts +229 -0
  54. package/src/l3/zerosub.ts +62 -0
  55. package/src/pattern/engine.ts +236 -13
  56. package/src/pipeline.ts +157 -56
  57. package/src/proto.ts +112 -14
  58. package/src/raise/arrays.ts +6 -1
  59. package/src/raise/divpow2.ts +2 -2
  60. package/src/raise/globalshape.ts +1038 -0
  61. package/src/raise/gvn.ts +33 -18
  62. package/src/raise/latch.ts +126 -0
  63. package/src/raise/memberarrays.ts +594 -0
  64. package/src/raise/narrow.ts +124 -0
  65. package/src/raise/narrowlocal.ts +556 -0
  66. package/src/raise/paramwidth.ts +179 -0
  67. package/src/raise/pre-recovery.ts +97 -14
  68. package/src/raise/recover.ts +56 -23
  69. package/src/raise/retsink.ts +210 -10
  70. package/src/raise/shortcircuit.ts +474 -74
  71. package/src/raise/struct-arrays.ts +19 -2
  72. package/src/raise/structs.ts +33 -3
  73. package/src/rank-axes.ts +630 -0
  74. package/src/rank-declare.ts +256 -0
  75. package/src/rank.ts +1723 -272
  76. package/src/structure/analysis.ts +1392 -141
  77. package/src/structure/bitfields.ts +332 -0
  78. package/src/structure/globalaccess.ts +274 -0
  79. package/src/structure/hazards.ts +411 -20
  80. package/src/structure/loops.ts +2 -49
  81. package/src/structure/namecoalesce.ts +435 -0
  82. package/src/structure/structure.ts +2678 -526
  83. package/src/structure/switch-recover.ts +616 -144
  84. package/src/symbols.ts +62 -1
  85. package/src/target.ts +367 -24
  86. package/src/trace.ts +111 -32
@@ -0,0 +1,154 @@
1
+ // L3 poll-shape re-spelling levers: `pollGuards` regrows an empty bottom-tested loop's guard;
2
+ // `pollReads` folds a materialized poll's re-read back into its while condition. Each carries
3
+ // its own trace argument below.
4
+ //
5
+ // do { } while (dma[2] & 0x80000000); → if (dma[2] & 0x80000000) { do { } while (…); }
6
+ //
7
+ // For an empty body the two forms compile to the SAME instructions — gcc collapses the guard
8
+ // into the bottom test late (jump optimization), AFTER flow has counted the guard's reads — so
9
+ // the choice leaves no instruction trace, only a register-allocation ripple: the extra
10
+ // source-level read raises the condition operands' ref counts, which re-orders the allocator's
11
+ // priorities for the WHOLE function (the busy-wait's base landing in a low reg vs `ip`). Which
12
+ // form the source spelled is unrecoverable from the bytes; both are emitted and the differ
13
+ // referees.
14
+ //
15
+ // SCOPE (decline over approximate): only a `dowhile` with an EMPTY body regrows a guard —
16
+ // there the two forms have IDENTICAL evaluation traces (each evaluates the condition until its
17
+ // first falsy result; the regrown guard IS the first bottom-test, not an extra one), so
18
+ // volatile reads, calls, any effect in the condition all count the same. A NON-empty body is
19
+ // where the forms genuinely differ (the body runs at least once vs at least zero times), which
20
+ // is why it never wraps. Declines (null) when no empty do-while exists.
21
+ import type { Expr, SFn, Stmt } from './ast';
22
+ import { exprChildren, exprEquals, mapExprChildren, mapStmtLists, stmtChildren, stmtExprs } from './ast';
23
+
24
+ export function pollGuards(sfn: SFn): SFn | null {
25
+ let changed = false;
26
+ const rewrite = (s: Stmt): Stmt => {
27
+ if (s.k === 'dowhile' && s.body.length === 0) {
28
+ changed = true;
29
+ return { k: 'if', cond: s.cond, then: [s], else: [] };
30
+ }
31
+ return mapStmtLists(s, (list) => list.map(rewrite));
32
+ };
33
+ const body = sfn.body.map(rewrite);
34
+ return changed ? { ...sfn, body } : null;
35
+ }
36
+
37
+ // L3 re-spelling lever: a materialized POLL re-reads in its own condition.
38
+ //
39
+ // v = dma[2]; while ((v & BUSY) != 0) { v = dma[2]; } → while ((dma[2] & BUSY) != 0) {}
40
+ //
41
+ // The structurer materializes a loop-carried load into a named temp with a pre-loop read and a
42
+ // per-iteration re-read; the source may have spelled the read INSIDE the condition of an
43
+ // empty-bodied `while`. The two forms have IDENTICAL evaluation traces — the old form reads once
44
+ // before plus once per iteration, the new form reads once per condition evaluation, and both
45
+ // count 1 + iterations — so volatile reads COUNT the same; the condition gates below (call-free,
46
+ // no volatile-rooted or raw derefs) are what make the ORDER identical too: with no other
47
+ // observable effect in the condition, there is nothing for the embedded read to reorder against.
48
+ // What differs is bytes: the pre-read + temp spelling materializes an extra register and
49
+ // instruction the in-condition spelling does not.
50
+ //
51
+ // SCOPE (decline over approximate): each admission is one named predicate below and carries its own
52
+ // refusal's reason; the temp must additionally be the function's OWN non-volatile LOCAL, because a
53
+ // bare global's assigns are stores other code observes and its declaration cannot be dropped.
54
+ // Declines (null) when no poll matches.
55
+ const countVar = (e: Expr, n: string): number =>
56
+ (e.k === 'var' && e.name === n ? 1 : 0) + exprChildren(e).reduce((a, c) => a + countVar(c, n), 0);
57
+
58
+ const countAddr = (e: Expr, n: string): number =>
59
+ (e.k === 'addr' && e.name === n ? 1 : 0) + exprChildren(e).reduce((a, c) => a + countAddr(c, n), 0);
60
+
61
+ /** call- and marker-free: no effect the fold could move or duplicate. */
62
+ const pure = (e: Expr): boolean => e.k !== 'call' && e.k !== 'marker' && exprChildren(e).every(pure);
63
+
64
+ /** The var a deref's base stands on, through casts only — null for anything else (a raw address, an
65
+ * arithmetic base), which is exactly the set `condDerefsPlain` refuses. */
66
+ const rootVar = (e: Expr): string | null => (e.k === 'var' ? e.name : e.k === 'cast' ? rootVar(e.e) : null);
67
+
68
+ /** ORDER-safety for the condition's other reads: a deref there must be rooted at a var declared
69
+ * non-volatile — a volatile-rooted or raw-address deref is (or may be) an OBSERVABLE read the
70
+ * fold would unsequence against X inside one expression, where the original sequenced them. */
71
+ const condDerefsPlain = (e: Expr, volatileLocals: ReadonlySet<string>): boolean => {
72
+ if (e.k === 'index' || e.k === 'field') {
73
+ const rv = rootVar(e.base);
74
+ if (rv === null || volatileLocals.has(rv)) {
75
+ return false;
76
+ }
77
+ }
78
+ return exprChildren(e).every((c) => condDerefsPlain(c, volatileLocals));
79
+ };
80
+
81
+ /** Every mention of `n` in a statement list — reads, `&n`, and assign targets, at any depth. */
82
+ const occurs = (list: Stmt[], n: string): number =>
83
+ list.reduce(
84
+ (a, st) =>
85
+ a +
86
+ stmtExprs(st).reduce((x, e) => x + countVar(e, n) + countAddr(e, n), 0) +
87
+ (st.k === 'assign' && st.name === n ? 1 : 0) +
88
+ occurs(stmtChildren(st), n),
89
+ 0,
90
+ );
91
+
92
+ /** The loop body must be EXACTLY the one re-read: a single assign of the same variable to the same
93
+ * expression. Any other statement there is one the fold would delete along with the loop's body. */
94
+ const bodyIsTheSoleReread = (w: Extract<Stmt, { k: 'while' }>, a: Extract<Stmt, { k: 'assign' }>): boolean =>
95
+ w.body.length === 1 && w.body[0].k === 'assign' && w.body[0].name === a.name && exprEquals(w.body[0].value, a.value);
96
+
97
+ /** The condition must read the variable EXACTLY once, as a bare var: a second read would double X's
98
+ * per-iteration evaluation, and an `&v` is not a read at all and cannot be substituted. */
99
+ const condReadsVarOnce = (cond: Expr, n: string): boolean => countVar(cond, n) === 1 && countAddr(cond, n) === 0;
100
+
101
+ /** X must not mention the variable it is assigned to — the folded form evaluates X in a condition
102
+ * where that variable no longer exists. */
103
+ const valueIsSelfFree = (value: Expr, n: string): boolean => countVar(value, n) === 0 && countAddr(value, n) === 0;
104
+
105
+ /** The condition carries no effect and no observable read of its own, which is what makes the
106
+ * embedded X unsequenceable against anything: call/marker-free, every deref `condDerefsPlain`. */
107
+ const condFoldable = (cond: Expr, volatileLocals: ReadonlySet<string>): boolean =>
108
+ pure(cond) && condDerefsPlain(cond, volatileLocals);
109
+
110
+ /** The pattern owns EVERY occurrence of the variable: both assign targets and the one condition
111
+ * read, three in all, counted over the whole function and counting `&v`. Its declaration is dropped
112
+ * with the temp, so anything else mentioning it would be left naming a variable that is gone. */
113
+ const ownsEveryOccurrence = (body: Stmt[], n: string): boolean => occurs(body, n) === 3;
114
+
115
+ export function pollReads(sfn: SFn): SFn | null {
116
+ const ownPlain = new Set(
117
+ sfn.locals.filter((l) => l.volatile !== true && l.pointeeVolatile !== true).map((l) => l.name),
118
+ );
119
+ const volatileLocals = new Set(
120
+ sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
121
+ );
122
+ const subst = (e: Expr, n: string, x: Expr): Expr =>
123
+ e.k === 'var' && e.name === n ? x : mapExprChildren(e, (c) => subst(c, n, x));
124
+ const dropped = new Set<string>();
125
+ const rewriteList = (list: Stmt[]): Stmt[] => {
126
+ const out: Stmt[] = [];
127
+ for (let i = 0; i < list.length; i++) {
128
+ const a = list[i];
129
+ const w = list[i + 1];
130
+ if (
131
+ a.k === 'assign' &&
132
+ ownPlain.has(a.name) &&
133
+ w !== undefined &&
134
+ w.k === 'while' &&
135
+ bodyIsTheSoleReread(w, a) &&
136
+ condReadsVarOnce(w.cond, a.name) &&
137
+ valueIsSelfFree(a.value, a.name) &&
138
+ pure(a.value) &&
139
+ condFoldable(w.cond, volatileLocals) &&
140
+ ownsEveryOccurrence(sfn.body, a.name)
141
+ ) {
142
+ out.push({ k: 'while', cond: subst(w.cond, a.name, a.value), body: [] });
143
+ dropped.add(a.name);
144
+ i++;
145
+ continue;
146
+ }
147
+ out.push(recurse(a));
148
+ }
149
+ return out;
150
+ };
151
+ const recurse = (s0: Stmt): Stmt => mapStmtLists(s0, rewriteList);
152
+ const body = rewriteList(sfn.body);
153
+ return dropped.size > 0 ? { ...sfn, body, locals: sfn.locals.filter((l) => !dropped.has(l.name)) } : null;
154
+ }
@@ -0,0 +1,227 @@
1
+ // L3 re-spelling lever: 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 lever 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 lever will eventually
43
+ // want. It is not built yet because nothing demands it: swept over 834 corpus trees, the lever
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
+ }
@@ -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 labels these, and a label is the instrument every census
128
+ * in this repo reads (it is 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 variant 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 variants — 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;
@@ -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[] = [];
@@ -309,23 +331,26 @@ export function registerishSpellings(sfn: SFn): SFn[] {
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
  }