@asmlift/core 0.4.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 (87) 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 -164
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +341 -22
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +24 -6
  10. package/src/frontend/opaque.ts +31 -18
  11. package/src/frontend/ppc.ts +54 -7
  12. package/src/frontend/ssa.ts +632 -13
  13. package/src/frontend/thumb.ts +2786 -286
  14. package/src/ir/alias.ts +129 -0
  15. package/src/ir/bits.ts +75 -0
  16. package/src/ir/core.ts +337 -2
  17. package/src/ir/opcodes.ts +156 -27
  18. package/src/ir/parse.ts +19 -2
  19. package/src/ir/print.ts +27 -2
  20. package/src/ir/simplify.ts +190 -3
  21. package/src/ir/struct-names.ts +42 -0
  22. package/src/ir/verify.ts +43 -49
  23. package/src/l3/address.ts +62 -0
  24. package/src/l3/argbase.ts +8 -2
  25. package/src/l3/ast.ts +464 -49
  26. package/src/l3/basecse.ts +709 -88
  27. package/src/l3/coalesce.ts +521 -66
  28. package/src/l3/dce.ts +54 -19
  29. package/src/l3/gates.ts +88 -0
  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 +113 -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 +110 -85
  42. package/src/l3/reindex.ts +715 -78
  43. package/src/l3/scopebase.ts +649 -219
  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 +23 -4
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +263 -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 +236 -13
  57. package/src/pipeline.ts +206 -49
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/divpow2.ts +4 -3
  61. package/src/raise/globalshape.ts +1038 -0
  62. package/src/raise/gvn.ts +44 -19
  63. package/src/raise/latch.ts +126 -0
  64. package/src/raise/memberarrays.ts +594 -0
  65. package/src/raise/narrow.ts +124 -0
  66. package/src/raise/narrowlocal.ts +556 -0
  67. package/src/raise/paramwidth.ts +179 -0
  68. package/src/raise/pre-recovery.ts +101 -16
  69. package/src/raise/recover.ts +56 -23
  70. package/src/raise/retsink.ts +215 -14
  71. package/src/raise/shortcircuit.ts +477 -79
  72. package/src/raise/struct-arrays.ts +21 -3
  73. package/src/raise/structs.ts +61 -3
  74. package/src/rank-axes.ts +630 -0
  75. package/src/rank-declare.ts +256 -0
  76. package/src/rank.ts +1726 -251
  77. package/src/structure/analysis.ts +1516 -220
  78. package/src/structure/bitfields.ts +332 -0
  79. package/src/structure/globalaccess.ts +274 -0
  80. package/src/structure/hazards.ts +411 -20
  81. package/src/structure/loops.ts +2 -49
  82. package/src/structure/namecoalesce.ts +435 -0
  83. package/src/structure/structure.ts +2850 -533
  84. package/src/structure/switch-recover.ts +688 -147
  85. package/src/symbols.ts +62 -1
  86. package/src/target.ts +367 -24
  87. package/src/trace.ts +111 -32
@@ -0,0 +1,179 @@
1
+ // A NARROW DECLARED PARAMETER, extended once in the prologue.
2
+ //
3
+ // agbcc has no byte/half register move, so a callee whose parameter is declared `u8`/`s16` widens
4
+ // it itself, with a shift pair at the very top of the function — the caller passes a full register
5
+ // and the extension is part of the prologue. Both spellings below compiled with this benchmark's
6
+ // own agbcc, and where each one puts the pair is the whole rule:
7
+ //
8
+ // void pa(s32 *out, u8 a) { out[0]=1; out[1]=2; out[2]=a; } lsl/lsr mov str mov str str
9
+ // void pb(s32 *out, s32 a) { out[0]=1; out[1]=2; out[2]=(u8)a; } mov str mov str lsl/lsr str
10
+ //
11
+ // The idiom patterns fold that pair to one `zext`/`sext` op, so `pa`'s parameter reaches this pass
12
+ // as a value whose SOLE use is its own extension — nothing can read the raw register, which is
13
+ // exactly what a narrow declaration means. Recovered as a wide parameter instead, the same function
14
+ // has to re-spell `(u8)a` at every use; agbcc then elides an extension no use needs, gives the
15
+ // extended value no register of its own, and the whole allocation moves.
16
+ //
17
+ // WIDTH AND SIGNEDNESS ARE READ OFF, NOT GUESSED: the extension states both (agbcc's shift pair by
18
+ // its amount and its `asr`/`lsr`, PPC's by the opcode), so no width is ever enumerated here.
19
+ //
20
+ // NOT agbcc-GATED, because the shape is not agbcc's alone: mwcc's PPC prologue widens a declared
21
+ // narrow parameter with the `extsb`/`extsh` the frontend lifts to the same op, and the synthetic
22
+ // `sextb`/`tos8` rows keep matching on that toolchain through this pass.
23
+ //
24
+ // WHAT THE PROLOGUE TEST CANNOT SEE, and why the declaration settles it. The scan steps over the
25
+ // pure materializations agbcc interleaves among the extensions, so a constant the scheduler HOISTED
26
+ // above a mid-body cast leaves `pb` looking like `pa`:
27
+ //
28
+ // void pc(s32 a, s32 *out) { s32 t = 7; out[0] = (u8)a; out[1] = t; out[2] = t; }
29
+ // movs r2,#7 / lsls r0,#24 / lsrs r0,#24 / str / str / str
30
+ // void pc(u8 a, s32 *out) { s32 t = 7; out[0] = a; out[1] = t; out[2] = t; }
31
+ // lsls r0,#24 / lsrs r0,#24 / movs r2,#7 / str / str / str
32
+ //
33
+ // Those two ROM sources are DIFFERENT BYTES — the const moves across the shift pair — so the width
34
+ // is a fact here and not a spelling, while no reader of the raw register and no body code is
35
+ // present to make the gates below refuse. Nor does the ORDER decide it: sa3's `sub_802DFC8` really
36
+ // is declared `s16 direction` and agbcc emits its `movs r5, #0` before the `lsl/asr` too, so the
37
+ // hoisted-const shape arrives from both source spellings and reaches this pass as the same IR.
38
+ //
39
+ // The tiebreak is therefore not in the asm, and the SCORE cannot supply it either: asmlift
40
+ // re-materializes a small constant at each use instead of binding it to a local, so its own two
41
+ // spellings of `pc` emit the same instruction order and score alike. `proto-width` takes the
42
+ // tiebreak from the caller's declaration instead, and where none was supplied the extension stands.
43
+ // What that refusal protects is a function this pass never compiles: agbcc truncates at every
44
+ // PROTOTYPED CALL SITE of a narrow-declared callee — `lsl/asr` ahead of the `bl`, two Thumb
45
+ // instructions per site — so a wrong width here costs bytes the per-function differ cannot see.
46
+ import { type Fn, type Op, type Value, replaceAllUsesWith, successorsOf } from '../ir/core';
47
+ import { CAST_WIDTHS, MATERIALIZING_OPS } from '../ir/opcodes';
48
+ import { T } from '../ir/types';
49
+ import { type Gate, firstRejection } from '../l3/gates';
50
+ import { type FnProto, declaredWidth } from '../proto';
51
+
52
+ /** What the gates below judge: one entry parameter and the extension that reads it. */
53
+ export interface NarrowParamCandidate {
54
+ /** the parameter the extension reads */
55
+ param: Value;
56
+ /** the extension's `width` attribute */
57
+ width: number;
58
+ /** the entry block has predecessors */
59
+ entryIsJoin: boolean;
60
+ /** the extension is in the entry block's PROLOGUE — see the scan in `narrowEntryParams` */
61
+ inPrologue: boolean;
62
+ /** reads of the RAW parameter anywhere in the function */
63
+ uses: number;
64
+ /** the width the caller's own prototype declares for this parameter, if it declares one */
65
+ declared: number | undefined;
66
+ }
67
+
68
+ export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
69
+ {
70
+ id: 'entry-is-join',
71
+ why: "a joined entry's params are merge values, not the function's arguments",
72
+ sound: true,
73
+ guardedBy: 'param-width.test.ts: an entry block with a predecessor carries merge values, not arguments',
74
+ rejects: (c) => c.entryIsJoin,
75
+ },
76
+ {
77
+ id: 'param-typed',
78
+ why: 'the pointer/aggregate recovery already decided this parameter',
79
+ sound: true,
80
+ guardedBy: 'param-width.test.ts: a parameter the pointer recovery already typed is left alone',
81
+ rejects: (c) => c.param.type.kind !== 'unknown',
82
+ },
83
+ {
84
+ id: 'cast-width',
85
+ why: 'only 8 and 16 are widths a `zext`/`sext` — and so a C declaration — carries',
86
+ sound: true,
87
+ guardedBy: 'param-width.test.ts: a width no C type spells is refused',
88
+ rejects: (c) => !CAST_WIDTHS.has(c.width),
89
+ },
90
+ {
91
+ id: 'raw-reader',
92
+ why: 'a reader of the un-extended register proves the declaration was wide',
93
+ sound: true,
94
+ guardedBy: 'param-width.test.ts: a second reader of the raw parameter proves the declaration was wide',
95
+ rejects: (c) => c.uses !== 1,
96
+ },
97
+ {
98
+ id: 'proto-width',
99
+ why: "the caller's headers declare this parameter, and a declaration outranks an inference",
100
+ sound: true,
101
+ guardedBy: 'param-width.test.ts: a declared width the extension contradicts refuses the narrowing',
102
+ rejects: (c) => c.declared !== undefined && c.declared !== c.width,
103
+ },
104
+ {
105
+ id: 'not-prologue',
106
+ why: 'an extension behind body code is where the SOURCE wrote the cast',
107
+ sound: true,
108
+ guardedBy: 'param-width.test.ts: an extension behind a nullary call is body code',
109
+ rejects: (c) => !c.inPrologue,
110
+ },
111
+ ];
112
+
113
+ /** How many times `v` is read anywhere in `fn` — op operands and branch arguments alike. */
114
+ function useCount(fn: Fn, v: Value): number {
115
+ let n = 0;
116
+ for (const b of fn.blocks) {
117
+ for (const op of b.ops) {
118
+ n += op.operands.filter((o) => o === v).length;
119
+ for (const s of op.successors) {
120
+ n += s.args.filter((a) => a === v).length;
121
+ }
122
+ }
123
+ }
124
+ return n;
125
+ }
126
+
127
+ /** Type an entry parameter at the width its prologue extension proves, and drop the extension.
128
+ * `self` is the prototype the caller supplied for THIS function, if any. Returns the number of
129
+ * parameters narrowed. */
130
+ export function narrowEntryParams(
131
+ fn: Fn,
132
+ self?: FnProto,
133
+ gates: readonly Gate<NarrowParamCandidate>[] = PARAM_WIDTH_GATES,
134
+ ): number {
135
+ const entry = fn.blocks[0];
136
+ const declared = Array.isArray(self?.params) ? self.params.map(declaredWidth) : [];
137
+ const entryIsJoin = fn.blocks.some((b) => successorsOf(b).includes(entry));
138
+ const params = new Set(entry.params);
139
+ // The prologue: the entry block's leading parameter extensions, plus the `MATERIALIZING_OPS`
140
+ // agbcc interleaves among them. Scanning stops at the first op that READS a value — body code
141
+ // has run by then, and an extension behind body code is where the SOURCE wrote it.
142
+ const prologue = new Set<Op>();
143
+ for (const op of entry.ops) {
144
+ if (MATERIALIZING_OPS.has(op.opcode)) {
145
+ continue;
146
+ }
147
+ if ((op.opcode !== 'sext' && op.opcode !== 'zext') || !params.has(op.operands[0])) {
148
+ break;
149
+ }
150
+ prologue.add(op);
151
+ }
152
+ let narrowed = 0;
153
+ for (const op of [...entry.ops]) {
154
+ if (op.opcode !== 'sext' && op.opcode !== 'zext') {
155
+ continue;
156
+ }
157
+ const p = op.operands[0];
158
+ if (!params.has(p)) {
159
+ continue;
160
+ }
161
+ const width = op.attrs.width as number;
162
+ const c: NarrowParamCandidate = {
163
+ param: p,
164
+ width,
165
+ entryIsJoin,
166
+ inPrologue: prologue.has(op),
167
+ uses: useCount(fn, p),
168
+ declared: declared[entry.params.indexOf(p)],
169
+ };
170
+ if (firstRejection(gates, c) !== null) {
171
+ continue;
172
+ }
173
+ p.type = T.int(width, op.opcode === 'sext');
174
+ replaceAllUsesWith(fn, op.results[0], p);
175
+ entry.ops.splice(entry.ops.indexOf(op), 1);
176
+ narrowed++;
177
+ }
178
+ return narrowed;
179
+ }
@@ -10,25 +10,59 @@
10
10
  // rank's score-probe both verify; the report's trace entries ride pipeline's hook). The `dce` after a
11
11
  // pass that changed the IR is INTRINSIC to the pass (it declares whether it leaves dead ops) and lives
12
12
  // in the driver.
13
- import { Fn } from '../ir/core';
13
+ import { Block, Fn } from '../ir/core';
14
14
  import { simplifyTrivialPhis } from '../ir/simplify';
15
15
  import { dce } from '../pattern/engine';
16
+ import type { FnProto } from '../proto';
16
17
  import type { TargetDescription } from '../target';
17
18
  import { recognizeArrays } from './arrays';
18
19
  import { recognizeConsts } from './const';
19
20
  import { recognizeDivPow2 } from './divpow2';
20
21
  import { numberPureValues } from './gvn';
21
22
  import { recognizeMagicDivision } from './magicdiv';
22
- import { recognizeBranchShortCircuit, recognizeShortCircuit } from './shortcircuit';
23
+ import { recognizeMemberArrays } from './memberarrays';
24
+ import { rerootNarrowReads } from './narrow';
25
+ import { type MergeShape, mergeShapes, narrowBlockLocals } from './narrowlocal';
26
+ import { narrowEntryParams } from './paramwidth';
27
+ import { type BranchShortCircuitOptions, recognizeBranchShortCircuit, recognizeShortCircuit } from './shortcircuit';
23
28
  import { recognizeSoftDiv } from './softdiv';
24
29
  import { recognizeStructArrays } from './struct-arrays';
25
30
  import { recognizeStructs } from './structs';
26
31
 
32
+ /** Per-call options for the pass list — ONE field per pass that takes any, named for the pass, so
33
+ * a caller reads which recognizer it is steering and a pass that takes none says so by absence.
34
+ * A field here selects between spellings its pass can already produce; none of them relaxes a
35
+ * soundness refusal, and each recognizer documents its own. */
36
+ export interface PreRecoveryOptions {
37
+ /** raise/shortcircuit.ts `recognizeBranchShortCircuit` — the connective-vs-comparison-tree axis. */
38
+ shortCircuit?: BranchShortCircuitOptions;
39
+ }
40
+
41
+ /** CFG facts read off the function ONCE, before the first pass below rewrites it.
42
+ *
43
+ * A pass that judges the SHAPE agbcc emitted cannot read that shape off `fn` when its turn comes:
44
+ * by then divpow2 has deleted a block, both short-circuit folds have rewritten edges, and the
45
+ * eight `dce: true` passes have changed op counts. So the driver reads it here and hands it down. */
46
+ export interface PreRecoveryFacts {
47
+ /** the join shape of every block, for raise/narrowlocal.ts's `edge-extends`. Blocks a later pass
48
+ * creates are absent, and absent reads as "no diamond" — the refusing direction. */
49
+ mergeShapes: Map<Block, MergeShape>;
50
+ }
51
+
27
52
  export interface PreRecoveryPass {
28
53
  /** stable id — also the report's trace-stage key. */
29
54
  id: string;
30
- /** run the recognizer; returns a truthy value (a change count, or `true`) iff it CHANGED the IR. */
31
- run: (fn: Fn) => number | boolean;
55
+ /** run the recognizer; returns a truthy value (a change count, or `true`) iff it CHANGED the IR.
56
+ * `self` is the prototype the caller supplied for the function being raised — read only by
57
+ * parameter-width, which checks its inference against a declared width. `lifted` is the
58
+ * pre-pass CFG snapshot — read only by narrow-local. */
59
+ run: (
60
+ fn: Fn,
61
+ self: FnProto | undefined,
62
+ opts: PreRecoveryOptions,
63
+ target: TargetDescription,
64
+ lifted: PreRecoveryFacts,
65
+ ) => number | boolean;
32
66
  /** run `dce` after this pass changes the IR (the pass declares it leaves dead ops behind). */
33
67
  dce: boolean;
34
68
  /** optional target gate (soft-div only fires on a no-hardware-divide target — see raise/softdiv.ts). */
@@ -37,8 +71,8 @@ export interface PreRecoveryPass {
37
71
 
38
72
  /** THE ordered pre-recovery pass list — the single source of truth shared by pipeline / rank / report.
39
73
  * address-numbering → const-materialize → magic-division → pow2-division → soft-division → array-legalize →
40
- * struct-array → struct-pointer → short-circuit → branch-short-circuit. See each recognizer's file
41
- * for the rationale. */
74
+ * struct-array → member-array → struct-pointer → short-circuit → branch-short-circuit narrow-reads
75
+ * narrow-local → parameter-width. See each recognizer's file for the rationale. */
42
76
  export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
43
77
  // FIRST: collapsing duplicate address definitions removes block params every later recognizer
44
78
  // would otherwise have to reason around, and it can only shrink the value graph.
@@ -47,8 +81,10 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
47
81
  // Numbering alone is not enough and not safe to ship alone: collapsing the duplicates leaves a
48
82
  // block param whose edges now all carry one value, and the structurer still destroys THAT into
49
83
  // a local (it only reuses a name a carrier already has, and an inlined `gaddr` has none).
50
- // Measured: numbering without the phi cleanup made kleod:UpdateHUDCounterDisplay WORSE. So the
51
- // pair is the atomic unit, expressed as a body rather than a sum of two unrelated counts.
84
+ // Numbering alone costs kleod:UpdateHUDCounterDisplay its match, so the pair is the atomic
85
+ // unit, expressed as a body rather than a sum of two unrelated counts. It is NOT monotone,
86
+ // which is worth knowing before tuning either half: dropping the cleanup IMPROVES
87
+ // kleod:ConfigureEntityBehavior and kleod:CountCollectedGems, neither of them near matching.
52
88
  run: (fn) => {
53
89
  const n = numberPureValues(fn);
54
90
  return n + simplifyTrivialPhis(fn);
@@ -69,17 +105,63 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
69
105
  // discriminator note in raise/struct-arrays.ts) and BEFORE structs (an element's field
70
106
  // accesses must not be re-derived as constant-offset struct-pointer accesses).
71
107
  { id: 'struct-arrays', run: recognizeStructArrays, dce: true },
108
+ // member-arrays AFTER struct-arrays, and that order IS load-bearing — but the order alone does
109
+ // not deconflict them, because the two passes claim different VALUES: struct-arrays types the
110
+ // materialized `add(P, K)` and member-arrays groups on `P`, so `P->tbl[i].f` reaches both. What
111
+ // deconflicts them is member-arrays' `claimed-access` gate, reading the marks struct-arrays
112
+ // leaves. Its position before `structs` is NOT load-bearing — a base carrying both of THOSE
113
+ // shapes is refused on either side (`direct-access` here, the `unknown` check there) — and it
114
+ // sits there so the three struct synthesizers read in the order of their evidence. `narrowlocal`
115
+ // reads the carrier's extension chain rather than any address, so nothing here orders against it.
116
+ { id: 'member-arrays', run: (fn) => recognizeMemberArrays(fn), dce: true },
72
117
  { id: 'structs', run: recognizeStructs, dce: false },
73
118
  { id: 'shortcircuit', run: recognizeShortCircuit, dce: true },
74
- // The control-flow sibling, and this order IS load-bearing — value form FIRST.
119
+ // The control-flow sibling, value form FIRST.
75
120
  //
76
121
  // Their input SHAPES are disjoint (the value form's second block ends in `br` carrying a phi
77
- // argument, this one's ends in `cond_br`), so neither can eat the other's literal pattern. But
78
- // this pass REWRITES its head's condition into a `logic_or`/`logic_and`, and the value form
79
- // refuses any head whose condition is not a negatable icmpso running this one first can
80
- // permanently disqualify a value fold that was available. The reverse cannot happen: the value
81
- // form replaces its head's `cond_br` with a `br`, which this pass never matches.
82
- { id: 'branch-shortcircuit', run: recognizeBranchShortCircuit, dce: true },
122
+ // argument, this one's ends in `cond_br`), so neither can eat the other's literal pattern. The
123
+ // order is not otherwise load-bearing: this pass REWRITES its head's condition into a
124
+ // `logic_or`/`logic_and`, and the value form takes a fused headboth siblings negate a
125
+ // connective by De Morgan (`negateCondOps`, raise/shortcircuit.ts). Running this one first can
126
+ // still cost a value fold wherever that helper refuses the fused cone (a non-negatable leaf, or a
127
+ // cone over its budget), but instrumenting the value form's head gate over the benchmark corpus
128
+ // counts 0 fused heads across the 782 rows that lift map-lessly, so the hazard has no producer
129
+ // there. The reverse direction never could: the value form replaces its head's `cond_br` with a
130
+ // `br`, which this pass never matches.
131
+ {
132
+ id: 'branch-shortcircuit',
133
+ run: (fn, _self, opts) => recognizeBranchShortCircuit(fn, opts.shortCircuit),
134
+ dce: true,
135
+ },
136
+ // LAST, and the position IS load-bearing: this pass reads the CFG's edge arguments to find a
137
+ // loop variable's next value, and both short-circuit folds above rewrite the very edges it reads.
138
+ // `dce: false` — the rewrite orphans nothing, since the operand it drops keeps its other use.
139
+ { id: 'narrow', run: rerootNarrowReads, dce: false },
140
+ // The two WIDTH passes, last and in either order relative to each other: each only DELETES an
141
+ // extension and retypes the parameter that fed it, so every recognizer above sees the shape it
142
+ // was written against and neither can match a shape the other creates. They are disjoint by
143
+ // construction — `narrowlocal` refuses an entry parameter, `paramwidth` reads only entry
144
+ // parameters — and `narrowlocal` cannot take an extension `narrow` above wants either, since a
145
+ // parameter carrying BOTH a `zext` and a `sext` has two readers and is refused.
146
+ // `dce: false` on both — the extension each drops is spliced out in place, and its result has no
147
+ // other reader.
148
+ // The `target` argument is read by ONE conjunct of ONE gate — see raise/narrowlocal.ts's
149
+ // `NarrowLocalOptions`. A whole-pass `gate` would be wrong: the pass's SOUND rules are claims
150
+ // about C and run everywhere; only the join-shape evidence is a claim about gcc 2.x's optimizer.
151
+ // That same conjunct is why this is the one pass reading `lifted`: every pass above it can
152
+ // rewrite the CFG whose shape it judges.
153
+ {
154
+ id: 'narrowlocal',
155
+ run: (fn, _self, _opts, target, lifted) =>
156
+ narrowBlockLocals(
157
+ fn,
158
+ undefined,
159
+ { hoistsSingleSetArm: target.compilerBehaviors.hoistsSingleSetArm },
160
+ lifted.mergeShapes,
161
+ ),
162
+ dce: false,
163
+ },
164
+ { id: 'paramwidth', run: (fn, self) => narrowEntryParams(fn, self), dce: false },
83
165
  ];
84
166
 
85
167
  /** Run the pre-recovery passes in order. For each pass whose gate passes and that CHANGES the IR, run
@@ -89,12 +171,15 @@ export function runPreRecovery(
89
171
  fn: Fn,
90
172
  target: TargetDescription,
91
173
  afterPass?: (pass: PreRecoveryPass, result: number | boolean) => void,
174
+ self?: FnProto,
175
+ opts: PreRecoveryOptions = {},
92
176
  ): void {
177
+ const lifted: PreRecoveryFacts = { mergeShapes: mergeShapes(fn) };
93
178
  for (const pass of PRE_RECOVERY_PASSES) {
94
179
  if (pass.gate && !pass.gate(target)) {
95
180
  continue;
96
181
  }
97
- const result = pass.run(fn);
182
+ const result = pass.run(fn, self, opts, target, lifted);
98
183
  if (result) {
99
184
  if (pass.dce) {
100
185
  dce(fn);
@@ -12,13 +12,30 @@ const UNSIGNED_CMP = new Set(['icmp_ult', 'icmp_ule', 'icmp_ugt', 'icmp_uge']);
12
12
  const SIGNED_DIV = new Set(['sdiv', 'smod']);
13
13
  const UNSIGNED_DIV = new Set(['udiv', 'umod']);
14
14
 
15
+ /** Type an as-yet-untyped value as an integer of its own width. Only `unknown`s: a value some
16
+ * earlier rule already typed keeps that answer, so the phases below compose without an order
17
+ * between the rules INSIDE one of them. */
18
+ function setInt(v: Value, signed: boolean): void {
19
+ if (v.type.kind === 'unknown') {
20
+ v.type = T.int(v.type.width, signed);
21
+ }
22
+ }
23
+
24
+ /** The four phases, in the one order they are sound in: seed signedness from the opcodes that carry
25
+ * it, type the bases of memory accesses as pointers, propagate that pointer-ness across the SSA,
26
+ * then default whatever is left. Each is in place over `fn`, and each depends on the previous one
27
+ * having already refused to overwrite a type — the pointer phases must run BEFORE the s32 default
28
+ * or a loop-carried pointer is flattened to an integer. */
15
29
  export function recoverTypes(fn: Fn): void {
16
- const setInt = (v: Value, signed: boolean) => {
17
- if (v.type.kind === 'unknown') {
18
- v.type = T.int(v.type.width, signed);
19
- }
20
- };
21
- // Seed: operands of a signed comparison are signed integers.
30
+ seedSignednessFromOpcodes(fn);
31
+ typeDerefBases(fn);
32
+ propagatePointers(fn);
33
+ defaultUnknownsToS32(fn);
34
+ }
35
+
36
+ /** PHASE 1 — signedness from op semantics: the operands of a signed comparison are signed integers,
37
+ * a comparison's result is a bool, and a division carries its signedness in the OPCODE. */
38
+ function seedSignednessFromOpcodes(fn: Fn): void {
22
39
  for (const b of fn.blocks) {
23
40
  for (const op of b.ops) {
24
41
  if (SIGNED_CMP.has(op.opcode)) {
@@ -52,12 +69,14 @@ export function recoverTypes(fn: Fn): void {
52
69
  }
53
70
  }
54
71
  }
55
- // A value used as the base of a memory access is a pointer; its pointee type comes from the
56
- // access width (and, for loads, signedness). This must run before the s32 default so the
57
- // base is typed `T *` rather than being flattened to a plain integer. Both the constant-offset
58
- // forms (load/store, width) and the variable-index forms (aload/astore, elemSize) type their
59
- // base operand[0]; only the scale attribute differs.
72
+ }
60
73
 
74
+ /** PHASE 2 — a value used as the base of a memory access is a pointer; its pointee type comes from
75
+ * the access width (and, for loads, signedness). This must run before the s32 default so the base
76
+ * is typed `T *` rather than being flattened to a plain integer. Both the constant-offset forms
77
+ * (load/store, width) and the variable-index forms (aload/astore, elemSize) type their base
78
+ * operand[0]; only the scale attribute differs. */
79
+ function typeDerefBases(fn: Fn): void {
61
80
  for (const b of fn.blocks) {
62
81
  for (const op of b.ops) {
63
82
  let width: number, signed: boolean;
@@ -76,7 +95,9 @@ export function recoverTypes(fn: Fn): void {
76
95
  break;
77
96
  case 'astore':
78
97
  width = op.attrs.elemSize as number;
79
- signed = width === 4;
98
+ // an astore carries no signedness of its own unless a recovery recorded the member's;
99
+ // structure.ts reads it by the same rule
100
+ signed = (op.attrs.signed as boolean | undefined) ?? width === 4;
80
101
  break;
81
102
  default:
82
103
  continue;
@@ -87,16 +108,16 @@ export function recoverTypes(fn: Fn): void {
87
108
  }
88
109
  }
89
110
  }
90
- // Propagate pointer-ness across the SSA. The seed above types only the DIRECT base of a dereference;
91
- // a loop-carried pointer reaches its dereference through a block-arg phi (its incoming `a0`) and a
92
- // `p = p + stride` walk, so those values stay `unknown` and the s32 default below would spell them
93
- // `int` an `int→int*` assignment mwcc/agbcc REJECT (gcc warns). Flow the pointer type across the
94
- // exact same-value edges (a phi is one value; `ptr ± const` is the same pointer type). Union-find over
95
- // Value identity. SOUND: every edge connects values that provably hold the same pointer, and we only
96
- // fill `unknown`s — a class with a conflicting int member or two distinct pointees is left untouched.
97
- propagatePointers(fn);
98
- // Default every still-unknown value to s32. This is a COMPILER default (agbcc/IDO/GCC all take
99
- // plain `int` as the integer default), not a hardware fact — applied uniformly.
111
+ }
112
+
113
+ // PHASE 3 is `propagatePointers` (below, with its own note). Phase 2 types only the DIRECT base of
114
+ // a dereference; a loop-carried pointer reaches its dereference through a block-arg phi (its
115
+ // incoming `a0`) and a `p = p + stride` walk, so those values stay `unknown` and phase 4 would
116
+ // spell them `int` an `int→int*` assignment mwcc/agbcc REJECT (gcc warns).
117
+
118
+ /** PHASE 4 — default every still-unknown value to s32. This is a COMPILER default (agbcc/IDO/GCC
119
+ * all take plain `int` as the integer default), not a hardware fact applied uniformly. */
120
+ function defaultUnknownsToS32(fn: Fn): void {
100
121
  for (const b of fn.blocks) {
101
122
  for (const p of b.params) {
102
123
  if (p.type.kind === 'unknown') {
@@ -225,7 +246,19 @@ export function returnType(fn: Fn): IrType {
225
246
  const term = b.ops[b.ops.length - 1];
226
247
  if (term?.opcode === 'ret' && term.operands.length > 0) {
227
248
  const v = term.operands[0];
228
- return v.type.kind === 'unknown' ? T.s(32) : v.type;
249
+ // A NARROW value in the return register does not make the DECLARED return type narrow: the
250
+ // register is a word, and the value's width says how it was COMPUTED, not what the header
251
+ // spelled. The two readings compile alike through agbcc and do NOT through mwcc —
252
+ // `s8 f(s8 x) { return x; }` drops the `extsb` that `s32 f(s8 x) { return x; }` keeps and the
253
+ // target has (synthetic `sextb`/`tos8`).
254
+ //
255
+ // The widening lands on `s32`, the same fallback an untyped return takes, rather than on the
256
+ // value's own signedness: a `zext` states what the ARGUMENT was declared as, which is no
257
+ // evidence about the header's return type — and `u32 f(u8 x){return x;}` and its `s32` twin
258
+ // are one `lsl/lsr/bx lr` through agbcc either way. A narrowed PARAMETER
259
+ // (raise/paramwidth.ts) is the only value that reaches here narrow: no frontend types a
260
+ // load's result below a word.
261
+ return v.type.kind === 'unknown' || (v.type.kind === 'int' && v.type.width < 32) ? T.s(32) : v.type;
229
262
  }
230
263
  }
231
264
  return T.s(32);