@asmlift/core 0.9.0 → 0.10.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 (101) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
@@ -0,0 +1,400 @@
1
+ // asmlift — un-threading a loop exit the compiler jump-threaded past the test that follows the loop
2
+ // (ISA-neutral CFG pass).
3
+ //
4
+ // `for (i = 0; i < 3; i++) if (x == t[i].a || x == t[i].b) break; if (x == t[i].a) A; else B;` —
5
+ // the code after the loop repeats the test the first `break` just made. GCC's jump threading
6
+ // (agbcc jump.c:4517 `thread_jumps`, on from -O1, toplev.c:3598) sees that the branch out of the
7
+ // loop on `x == t[i].a` lands on a block R that compares the same operands again, and retargets the
8
+ // branch straight at R's taken arm:
9
+ //
10
+ // E: ld v, t[i].a; cmp x, v; beq A ; was: beq R
11
+ // ... ; the loop's other exits reach R
12
+ // R(j): ld w, t[j].a; cmp x, w; bne B ; falls into A
13
+ //
14
+ // The loop then leaves for two blocks, A and R, and no loop spelling owns that: the function
15
+ // declines with 'unrecovered back-edge'. Pointing E back at R — the edge the source wrote — gives
16
+ // the loop one way out, and R re-tests on that path what E just tested, so it takes the edge E took.
17
+ //
18
+ // SOUND WHEN R'S BRANCH IS DECIDED BY E'S on the path E→R, and the rewrite then runs extra code that
19
+ // observes nothing. That is the compiler's own precondition read backwards (jump.c
20
+ // `rtx_equal_for_thread_p` on the operands, `comparison_dominates_p` on the codes, no register or
21
+ // memory the skipped insns depend on modified in between), and it is what the gates check:
22
+ // • R runs nothing observable: no effect, no qualified or device access, no op that traps, so
23
+ // running it once more on E's path changes nothing but which edge is taken;
24
+ // • R's condition, read with R's params bound to the args E now hands it, is E's condition or its
25
+ // negation (`sameAtEntry`, ir/entry.ts — the predicate the re-roll and guard fusion read), through
26
+ // memory reads in R and in E's block after its last effect only: both read the memory E's branch
27
+ // saw, and a read anywhere else may see other memory;
28
+ // • the edge that decides R's branch on E's sense reaches E's old target T with E's own args —
29
+ // directly (S == T, agbcc), or through one block S that runs nothing observable and branches to
30
+ // T (gcc2.7.2kmc moves the THEN arm's `li v0,1` into the threaded branch's delay slot, so E
31
+ // carries the arm's value as an arg and the arm itself is skipped).
32
+ // Then E→R→(S→)T computes what E→T did, and on every other path nothing moved. R may also be a
33
+ // block that runs nothing observable and forwards (`br`) to the block Q that re-tests: gcc2.7.2 -O1
34
+ // re-masks a `u8` operand on each exit's own block (the latch fall-through and a `beql` delay
35
+ // slot), which merge into one exit target ahead of the re-test. Q is then judged as R is above, on
36
+ // the values R's `br` hands it.
37
+ //
38
+ // SCOPE, not soundness (the heuristic gates): only the decline shape. R must be another exit target
39
+ // of the same loop and E the loop's only edge to T, so each rewrite takes one exit target away; and
40
+ // T must not merely return, because an exit that returns is an early `return` the structurer already
41
+ // spells. Every function that lifted without this pass keeps its CFG.
42
+ //
43
+ // THE ARGS E HANDS R, per param: a value, among those the loop's in-loop edges into R hand it, that
44
+ // is defined at E (dominates E's block). A bottom-tested loop's latch hands R the stepped counter,
45
+ // which E never sees, and its in-body exits hand R the counter E reads; only the second is defined
46
+ // at E. When kmc puts the step in the threaded branch's delay slot (`beq a1,v0,L` / `addiu v1,v1,4`),
47
+ // the stepped pointer is defined at E too, and R has two candidates. Each choice is only a proposal:
48
+ // the comparison above is what makes it sound, so every combination of candidates (at most
49
+ // `MAX_PROPOSALS`) is judged, and the first the gates admit is taken; a param with no such value
50
+ // refuses. Inhabitants: `synthetic:thrbreak:agbcc` (top-tested, every exit hands one counter,
51
+ // S == T), the pointer walk `synthetic:thrwalk`
52
+ // `for (p = t; p < t + 3; p++) if (x == p->a || x == p->b) break; if (x == p->a) A; else B;`
53
+ // (gcc2.7.2kmc: bottom-tested, through S; gcc2.7.2: R a forwarder, through S), and
54
+ // `synthetic:thrwalkba`, its tests swapped, where kmc's delay-slot step gives R two candidates.
55
+ // Not `synthetic:thrbreak:gcc2.7.2kmc`, whatever the counter's width: kmc indexes `t[i]` there by
56
+ // a value each exit computes on its own block (`i<<2`, re-masked `i & 0xff` for a `u8`, in the exit
57
+ // branch's delay slot), so no value R reads is defined at E. That row lifts through the break tail
58
+ // its exits share instead (structure.ts `codeBreakArm`). Nor a gcc2.7.2 walk with two separate
59
+ // break tests (`if (x == p->a) break; if (y == p->b) break;`): every in-loop edge hands R an
60
+ // `x & 0xff` its own block re-masked, none defined at E, so it declines loudly.
61
+ import { Block, Fn, Op, Value, dominators, predecessors, successorsOf } from '../ir/core';
62
+ import { effectful, placedAt, spelledWhenDead } from '../ir/discipline';
63
+ import { type EntryReading, sameAtEntry } from '../ir/entry';
64
+ import { analyzeLoops } from '../ir/loops';
65
+ import { NEGATED_ICMP, opSig } from '../ir/opcodes';
66
+ import { simplifyTrivialPhis } from '../ir/simplify';
67
+ import { type Gate, firstRejection } from '../l3/gates';
68
+ import { returnsPurely } from './latch';
69
+
70
+ /** One loop exit edge E→T and another exit target R of the same loop that re-tests E's test. */
71
+ export interface ThreadedExit {
72
+ /** E's block (in the loop) and which successor of its `cond_br` leaves for T */
73
+ from: Block;
74
+ index: number;
75
+ /** E's target, and the block R the edge would be pointed back at: the re-test, or a forwarder to it */
76
+ target: Block;
77
+ retest: Block;
78
+ /** does another exit edge of the same loop reach T? */
79
+ targetShared: boolean;
80
+ /** does every path from T reach a `ret` without an effect on the way? */
81
+ targetReturns: boolean;
82
+ /** does R (or the re-test it forwards to, or the S it reaches T through) run an op whose second
83
+ * run is observable? */
84
+ observable: boolean;
85
+ /** the args E would hand R: per param, one of the values the in-loop edges into R hand it that is
86
+ * defined at E (one proposal; each combination of such values is a site of its own) */
87
+ args: Value[];
88
+ /** once E reaches R, does every value R and the blocks it reaches read still dominate its reader? */
89
+ dominated: boolean;
90
+ /** is R's condition, on E's args, E's condition or its negation — and which one decides S? */
91
+ decided: boolean;
92
+ /** does the decided edge reach T with E's args? */
93
+ onward: boolean;
94
+ }
95
+
96
+ export const UNTHREAD_GATES: readonly Gate<ThreadedExit>[] = [
97
+ {
98
+ id: 'target-shared',
99
+ why: 'pointing one of several exit edges into a block elsewhere takes no exit target away',
100
+ sound: false,
101
+ rejects: (c) => c.targetShared,
102
+ },
103
+ {
104
+ id: 'target-returns',
105
+ why: 'an exit that only returns is an early return arm, which loop recovery spells without this',
106
+ sound: false,
107
+ rejects: (c) => c.targetReturns,
108
+ },
109
+ {
110
+ id: 'retest-observable',
111
+ why: 'the re-test now runs on the exit path as well, so a second run of it must observe nothing',
112
+ sound: true,
113
+ guardedBy: [
114
+ 'unthread.test.ts: a re-test block that stores is left alone',
115
+ 'unthread.test.ts: a re-test whose onward block stores is left alone',
116
+ 'unthread.test.ts: a forwarder that stores is left alone',
117
+ ],
118
+ rejects: (c) => c.observable,
119
+ },
120
+ {
121
+ id: 'not-dominated',
122
+ why: 'a value the re-test (or a block after it) reads from above it must exist on the exit edge too',
123
+ sound: true,
124
+ guardedBy: [
125
+ 'unthread.test.ts: a re-test reading a value from a block the exit does not pass is left alone',
126
+ 'unthread.test.ts: a forwarder handing the re-test a value from the in-body exit’s block is left alone',
127
+ ],
128
+ rejects: (c) => !c.dominated,
129
+ },
130
+ {
131
+ id: 'test-differs',
132
+ why: 'only a re-test the exit’s own test decides takes the edge the exit took',
133
+ sound: true,
134
+ guardedBy: [
135
+ 'unthread.test.ts: a re-test of another operand is left alone',
136
+ 'unthread.test.ts: a re-test reading memory the exit block writes after its own read is left alone',
137
+ ],
138
+ rejects: (c) => !c.decided,
139
+ },
140
+ {
141
+ id: 'onward-differs',
142
+ why: 'the decided edge must hand the exit target what the exit edge handed it',
143
+ sound: true,
144
+ guardedBy: [
145
+ 'unthread.test.ts: a re-test whose THEN arm hands the join another value is left alone',
146
+ 'unthread.test.ts: a re-test whose decided edge is the other one is left alone',
147
+ ],
148
+ rejects: (c) => !c.onward,
149
+ },
150
+ ];
151
+
152
+ /** May a second run of `op` be observed? An effect, a placed access (volatile, device, a helper
153
+ * call), a trap — or `undef`, whose two copies are two storages' contents and not one value. */
154
+ const observable = (op: Op): boolean =>
155
+ effectful(op) || placedAt(op) !== null || opSig(op.opcode)?.traps === true || op.opcode === 'undef';
156
+
157
+ /** Point every threaded loop exit the gates admit back at the re-test it skipped; returns how many. */
158
+ export function unthreadLoopExits(fn: Fn, gates: readonly Gate<ThreadedExit>[] = UNTHREAD_GATES): number {
159
+ let done = 0;
160
+ // one at a time: each rewrite changes the exits the next is judged against. Bounded — every rewrite
161
+ // moves an exit edge off a block it was the loop's only edge into.
162
+ for (let cap = fn.blocks.length * 4; cap > 0 && unthreadOne(fn, gates); cap--) {
163
+ done++;
164
+ }
165
+ if (done > 0) {
166
+ simplifyTrivialPhis(fn);
167
+ }
168
+ return done;
169
+ }
170
+
171
+ function unthreadOne(fn: Fn, gates: readonly Gate<ThreadedExit>[]): boolean {
172
+ const preds = predecessors(fn);
173
+ const dom = dominators(fn);
174
+ const defs = new Map<Value, Op>();
175
+ const defBlock = new Map<Value, Block>();
176
+ for (const b of fn.blocks) {
177
+ b.params.forEach((p) => defBlock.set(p, b));
178
+ for (const op of b.ops) {
179
+ op.results.forEach((r) => {
180
+ defs.set(r, op);
181
+ defBlock.set(r, b);
182
+ });
183
+ }
184
+ }
185
+ const dominatesAt = (v: Value, at: Block): boolean => {
186
+ const d = defBlock.get(v);
187
+ return d !== undefined && dom.get(at)!.has(d);
188
+ };
189
+ const forest = analyzeLoops(fn, dom);
190
+ for (const nl of forest.byHeader.values()) {
191
+ const targets = [...new Set(nl.exitEdges.map((e) => e.to))];
192
+ if (targets.length < 2) {
193
+ continue;
194
+ }
195
+ for (const E of nl.body) {
196
+ const eTerm = E.ops[E.ops.length - 1];
197
+ if (eTerm?.opcode !== 'cond_br') {
198
+ continue;
199
+ }
200
+ for (let index = 0; index < eTerm.successors.length; index++) {
201
+ const T = eTerm.successors[index].block;
202
+ if (nl.body.has(T) || eTerm.successors[1 - index].block === T) {
203
+ continue;
204
+ }
205
+ for (const R of targets) {
206
+ if (R === T || R === E || eTerm.successors[1 - index].block === R) {
207
+ continue;
208
+ }
209
+ const site = judge(nl.body, E, index, T, R, preds, dom, defs, defBlock, dominatesAt).find(
210
+ (c) => firstRejection(gates, c) === null,
211
+ );
212
+ if (site === undefined) {
213
+ continue;
214
+ }
215
+ eTerm.successors[index] = { block: R, args: site.args };
216
+ return true;
217
+ }
218
+ }
219
+ }
220
+ }
221
+ return false;
222
+ }
223
+
224
+ function judge(
225
+ body: ReadonlySet<Block>,
226
+ E: Block,
227
+ index: number,
228
+ T: Block,
229
+ R: Block,
230
+ preds: ReadonlyMap<Block, Block[]>,
231
+ dom: ReadonlyMap<Block, Set<Block>>,
232
+ defs: ReadonlyMap<Value, Op>,
233
+ defBlock: ReadonlyMap<Value, Block>,
234
+ dominatesAt: (v: Value, at: Block) => boolean,
235
+ ): ThreadedExit[] {
236
+ // THE SHAPE, untabled: R re-tests — it ends in a `cond_br` (only a branch re-takes E's decision),
237
+ // or forwards to the block Q that does (`retestOf`) — and each of its params is handed, by some
238
+ // in-loop edge into R, a value defined at E — a candidate arg. One site per combination of
239
+ // candidates, refused as a whole past `MAX_PROPOSALS`.
240
+ const Q = retestOf(R, body, T);
241
+ if (Q === null) {
242
+ return [];
243
+ }
244
+ const into = [...body].flatMap((p) => terminatorEdges(p).filter((s) => s.block === R));
245
+ let proposals: Value[][] = [[]];
246
+ for (let k = 0; k < R.params.length; k++) {
247
+ const vals = [...new Set(into.map((s) => s.args[k]).filter((v) => v !== undefined && dominatesAt(v, E)))];
248
+ proposals = proposals.flatMap((p) => vals.map((v) => [...p, v]));
249
+ if (proposals.length === 0 || proposals.length > MAX_PROPOSALS) {
250
+ return [];
251
+ }
252
+ }
253
+ return proposals.map((args) => judgeArgs(body, E, index, T, R, Q, preds, dom, defs, defBlock, args));
254
+ }
255
+
256
+ /** The block whose `cond_br` re-tests on R's path: R itself, or the one block R forwards to with a
257
+ * `br`. gcc2.7.2 -O1 re-computes a re-test operand (`andi v1,a0,0xff`) on each exit's own block —
258
+ * the latch fall-through and the `beql` delay slot, which merge into one exit target — so R is that
259
+ * block and the re-test sits behind it, entered from the loop's guard as well. */
260
+ function retestOf(R: Block, body: ReadonlySet<Block>, T: Block): Block | null {
261
+ const rTerm = R.ops[R.ops.length - 1];
262
+ if (rTerm?.opcode === 'cond_br') {
263
+ return R;
264
+ }
265
+ if (rTerm?.opcode !== 'br') {
266
+ return null;
267
+ }
268
+ const Q = rTerm.successors[0].block;
269
+ return Q !== R && Q !== T && !body.has(Q) && Q.ops[Q.ops.length - 1]?.opcode === 'cond_br' ? Q : null;
270
+ }
271
+
272
+ /** How many arg combinations one (exit, re-test) pair may propose. kmc's delay-slot step gives a
273
+ * pointer walk two; the bound keeps a re-test with many params from multiplying its comparisons. */
274
+ const MAX_PROPOSALS = 8;
275
+
276
+ function judgeArgs(
277
+ body: ReadonlySet<Block>,
278
+ E: Block,
279
+ index: number,
280
+ T: Block,
281
+ R: Block,
282
+ Q: Block,
283
+ preds: ReadonlyMap<Block, Block[]>,
284
+ dom: ReadonlyMap<Block, Set<Block>>,
285
+ defs: ReadonlyMap<Value, Op>,
286
+ defBlock: ReadonlyMap<Value, Block>,
287
+ args: Value[],
288
+ ): ThreadedExit {
289
+ const eTerm = E.ops[E.ops.length - 1];
290
+ const qTerm = Q.ops[Q.ops.length - 1];
291
+ const site: ThreadedExit = {
292
+ from: E,
293
+ index,
294
+ target: T,
295
+ retest: R,
296
+ targetShared: [...body].some((p) => p !== E && successorsOf(p).includes(T)),
297
+ targetReturns: returnsPurely(T),
298
+ observable: R.ops.some(observable) || Q.ops.some(observable),
299
+ args,
300
+ dominated: false,
301
+ decided: false,
302
+ onward: false,
303
+ };
304
+ // The edge E→R takes away, of the blocks R reaches, exactly the dominators in R's that are not in
305
+ // E's (any other dominator either dominates both or sits on every path from R). So no value
306
+ // defined in one of those may be read — as an operand or an edge arg — by R or a block it reaches.
307
+ // When R is entered from above the loop (agbcc, kmc), that set is empty; gcc2.7.2's R behind the
308
+ // in-body exit is dominated by that exit's block, whose values the re-test does not read.
309
+ const lost = new Set([...dom.get(R)!].filter((d) => d !== R && !dom.get(E)!.has(d)));
310
+ site.dominated = lost.size === 0 || !readsFrom(R, lost, defBlock);
311
+ // the reads a comparison may look through: R's and Q's own (they run nothing observable, so they
312
+ // see the memory E's branch saw), the onward block's for the same reason, and E's after its last
313
+ // effect
314
+ const lastEffect = E.ops.reduce((at, op, i) => (effectful(op) ? i : at), -1);
315
+ const eReads = new Set(E.ops.slice(lastEffect + 1));
316
+ // on E→R(→Q): R's params hold E's args, and Q's what R's `br` hands it — an R param read as the arg
317
+ // it holds, any other value as itself (one substitution: a value R computes off its own params
318
+ // compares by identity below it, which only refuses)
319
+ const entry = new Map(R.params.map((p, k) => [p, args[k]]));
320
+ if (Q !== R) {
321
+ const fwd = R.ops[R.ops.length - 1].successors[0].args;
322
+ Q.params.forEach((p, k) => entry.set(p, entry.get(fwd[k]) ?? fwd[k]));
323
+ }
324
+ const reading = (onwardBlock: Block | null): EntryReading => ({
325
+ entry,
326
+ through: (v) => {
327
+ const d = defs.get(v);
328
+ if (d === undefined || observable(d)) {
329
+ return false;
330
+ }
331
+ if (!spelledWhenDead(d)) {
332
+ return true;
333
+ }
334
+ const b = defBlock.get(v);
335
+ return b === R || b === Q || (onwardBlock !== null && b === onwardBlock) || (b === E && eReads.has(d));
336
+ },
337
+ normalize: true,
338
+ });
339
+ // which of Q's edges E's sense decides: E's edge `index` holds when E's condition is
340
+ // (index 0 ? true : false); Q's edge 0 holds when Q's is true
341
+ const rd = reading(null);
342
+ const same = sameAtEntry(defs, qTerm.operands[0], eTerm.operands[0], rd, false);
343
+ const negated = !same && sameAtEntry(defs, qTerm.operands[0], eTerm.operands[0], rd, true);
344
+ site.decided = same || negated;
345
+ // The sense, where the comparison said none, is the one the codes alone suggest: what an ablated
346
+ // `test-differs` would act on, so each later rule judges the rewrite that ablation would make.
347
+ const asNegated =
348
+ negated ||
349
+ (!same && NEGATED_ICMP[defs.get(qTerm.operands[0])?.opcode ?? ''] === defs.get(eTerm.operands[0])?.opcode);
350
+ const eHolds = index === 0;
351
+ const rHolds = asNegated ? !eHolds : eHolds;
352
+ const decided = qTerm.successors[rHolds ? 0 : 1];
353
+ const eArgs = eTerm.successors[index].args;
354
+ if (decided.block === T) {
355
+ site.onward =
356
+ decided.args.length === eArgs.length && decided.args.every((a, k) => sameAtEntry(defs, a, eArgs[k], rd));
357
+ return site;
358
+ }
359
+ // through one block S: no params, one edge on to T, entered only from Q, and (judged with R's and
360
+ // Q's ops above) nothing observable
361
+ const S = decided.block;
362
+ const sTerm = S.ops[S.ops.length - 1];
363
+ if (
364
+ S === R ||
365
+ S === Q ||
366
+ body.has(S) ||
367
+ S.params.length > 0 ||
368
+ sTerm?.opcode !== 'br' ||
369
+ sTerm.successors[0].block !== T ||
370
+ (preds.get(S) ?? []).some((p) => p !== Q)
371
+ ) {
372
+ return site;
373
+ }
374
+ site.observable ||= S.ops.some(observable);
375
+ const sd = reading(S);
376
+ const sArgs = sTerm.successors[0].args;
377
+ site.onward = sArgs.length === eArgs.length && sArgs.every((a, k) => sameAtEntry(defs, a, eArgs[k], sd));
378
+ return site;
379
+ }
380
+
381
+ const terminatorEdges = (b: Block) => b.ops[b.ops.length - 1]?.successors ?? [];
382
+
383
+ /** Does `from`, or a block it reaches, read a value defined in one of `blocks`? */
384
+ function readsFrom(from: Block, blocks: ReadonlySet<Block>, defBlock: ReadonlyMap<Value, Block>): boolean {
385
+ const seen = new Set<Block>([from]);
386
+ const work = [from];
387
+ const inside = (v: Value) => blocks.has(defBlock.get(v)!);
388
+ for (let b = work.pop(); b !== undefined; b = work.pop()) {
389
+ if (b.ops.some((op) => op.operands.some(inside) || op.successors.some((e) => e.args.some(inside)))) {
390
+ return true;
391
+ }
392
+ for (const n of successorsOf(b)) {
393
+ if (!seen.has(n)) {
394
+ seen.add(n);
395
+ work.push(n);
396
+ }
397
+ }
398
+ }
399
+ return false;
400
+ }
@@ -9,11 +9,14 @@
9
9
  // asks each emitted tree for its references. A SIBLING MODULE, never a `rank/` directory — see the
10
10
  // same note on rank-variations.ts.
11
11
  import { type Fn, type Value, defOpMap } from './ir/core';
12
+ import { qualified } from './ir/discipline';
13
+ import { MEM_BASE_OPS } from './ir/opcodes';
12
14
  import type { SFn } from './l3/ast';
13
- import { type SymbolRef, collectSymbolRefs } from './l3/symbol-refs';
14
- import type { Prototypes } from './proto';
15
+ import { type SymbolRef, collectSymbolRefs, ownReturnDefinition } from './l3/symbol-refs';
16
+ import type { FnProto, Prototypes } from './proto';
17
+ import { storeSigned, storedConstantSign } from './raise/storesign';
15
18
  import { type SymbolInfo, accessSignedness } from './symbols';
16
- import { C_TYPEDEFS, type TargetDescription } from './target';
19
+ import { C_TYPEDEFS, type TargetDescription, globalRegisterSymbol } from './target';
17
20
 
18
21
  /** Bare-global ACCESS FACTS for name-only map symbols — the width/signedness authority the
19
22
  * declaration synthesis (declare.ts) uses when the map has no shape. The map knows only the
@@ -23,15 +26,35 @@ import { C_TYPEDEFS, type TargetDescription } from './target';
23
26
  * Mirrors structure()'s scalar-global rule: a fact is recorded only for a symbol accessed
24
27
  * EXCLUSIVELY at offset 0 with ONE width and ONE load signedness — anything else (interior
25
28
  * offsets, address arithmetic, width or sign conflicts) records nothing, because those
26
- * spellings go through `&gSym` casts where every object decl is address-identical. */
29
+ * spellings go through `&gSym` casts where every object decl is address-identical.
30
+ *
31
+ * A symbol never LOADED takes its sign from its stamped stores (raise/storesign.ts); an unstamped
32
+ * store says nothing, so a cell only ever stored unstamped keeps the unsigned default. A load
33
+ * still wins, as in raise/structs.ts. Stores whose stamps disagree keep the width with the
34
+ * unsigned default: dropping the fact would declare `extern u32` under a bare `gSym = v` that
35
+ * then compiles to `sw` where the target has `sh`. The bare spelling has no cast to carry the
36
+ * stamp, so this declaration is the only place the stored constant's view can be written.
37
+ *
38
+ * An UNSTAMPED store of a constant zero-extended from its narrow sign bit vetoes a signed
39
+ * declaration. storesign.ts leaves such a store unstamped when a wider use shares the constant,
40
+ * and the structurer spells it as the literal (`gS = 65535; gW = 65535;`). Under `extern s16 gS`,
41
+ * that literal converts to -1, and the shared register splits in two. The veto only ever chooses
42
+ * the unsigned default, which is what the declaration was before stamps existed. */
27
43
  export function bareGlobalAccessFacts(fn: Fn): Map<string, { width: number; signed: boolean }> {
28
44
  const defs = defOpMap(fn);
29
45
  const symOf = (v: Value): string | null => {
30
46
  const d = defs.get(v);
31
47
  return d?.opcode === 'gaddr' && d.attrs.code !== true ? (d.attrs.sym as string) : null;
32
48
  };
33
- const acc = new Map<string, { widths: Set<number>; signs: Set<boolean>; interior: boolean }>();
34
- const get = (s: string) => acc.get(s) ?? acc.set(s, { widths: new Set(), signs: new Set(), interior: false }).get(s)!;
49
+ const acc = new Map<
50
+ string,
51
+ { widths: Set<number>; signs: Set<boolean>; stored: Set<boolean>; unsignedLiteral: boolean; interior: boolean }
52
+ >();
53
+ const get = (s: string) =>
54
+ acc.get(s) ??
55
+ acc
56
+ .set(s, { widths: new Set(), signs: new Set(), stored: new Set(), unsignedLiteral: false, interior: false })
57
+ .get(s)!;
35
58
  for (const b of fn.blocks) {
36
59
  for (const op of b.ops) {
37
60
  if (op.opcode === 'load' || op.opcode === 'store') {
@@ -44,6 +67,16 @@ export function bareGlobalAccessFacts(fn: Fn): Map<string, { width: number; sign
44
67
  a.widths.add(op.attrs.width as number);
45
68
  if (op.opcode === 'load') {
46
69
  a.signs.add(accessSignedness(op.attrs.width as number, op.attrs.signed as boolean | undefined));
70
+ } else if (op.attrs.signed !== undefined) {
71
+ a.stored.add(storeSigned(op, op.attrs.width as number));
72
+ } else {
73
+ const d = defs.get(op.operands[1]);
74
+ if (
75
+ d?.opcode === 'const' &&
76
+ storedConstantSign(d.attrs.value as number, op.attrs.width as number) === false
77
+ ) {
78
+ a.unsignedLiteral = true;
79
+ }
47
80
  }
48
81
  }
49
82
  }
@@ -65,9 +98,15 @@ export function bareGlobalAccessFacts(fn: Fn): Map<string, { width: number; sign
65
98
  }
66
99
  const out = new Map<string, { width: number; signed: boolean }>();
67
100
  for (const [s, a] of acc) {
68
- if (!a.interior && a.widths.size === 1 && a.signs.size <= 1) {
69
- out.set(s, { width: [...a.widths][0], signed: a.signs.has(true) });
101
+ if (a.interior || a.widths.size !== 1 || a.signs.size > 1) {
102
+ continue;
70
103
  }
104
+ // Loads decide when there are any. Stores whose stamps disagree pick no view, nor does a signed
105
+ // stamp beside an unstamped zero-extended literal: the declaration keeps the width and takes
106
+ // the unsigned default it had before stamps existed.
107
+ const signed =
108
+ a.signs.size === 1 ? a.signs.has(true) : a.stored.size === 1 && a.stored.has(true) && !a.unsignedLiteral;
109
+ out.set(s, { width: [...a.widths][0], signed });
71
110
  }
72
111
  return out;
73
112
  }
@@ -173,16 +212,73 @@ export type RefusedDeclarationReason =
173
212
  * module note. */
174
213
  export function bareGlobalSymbols(fn: Fn): Map<string, SymbolInfo> {
175
214
  const out = new Map<string, SymbolInfo>();
215
+ const defs = defOpMap(fn);
216
+ // A name the lift accessed QUALIFIED at its own address (ir/discipline.ts `qualified`, whichever
217
+ // placement put it there) is declared `volatile`: the structurer spells that access by the bare
218
+ // name, whose declaration owns its qualifiers (l3/volstore.ts `qualifiedMemoryAccess`), so a plain
219
+ // declaration would drop the qualifier the access carries — `gR;` under `extern u32 gR;` compiles
220
+ // to no read at all. A `declared` placement says the project's own header qualifies the name, and
221
+ // the declaration synthesized beside it says so too.
222
+ const qualifiedNames = new Set<string>();
223
+ for (const b of fn.blocks) {
224
+ for (const op of b.ops) {
225
+ const d = MEM_BASE_OPS.has(op.opcode) ? defs.get(op.operands[0]) : undefined;
226
+ if (d?.opcode === 'gaddr' && op.attrs.off === 0 && qualified(op)) {
227
+ qualifiedNames.add(d.attrs.sym as string);
228
+ }
229
+ }
230
+ }
176
231
  for (const b of fn.blocks) {
177
232
  for (const op of b.ops) {
178
233
  if (op.opcode === 'gaddr' && typeof op.attrs.sym === 'string') {
179
- out.set(op.attrs.sym, { name: op.attrs.sym, kind: 'data' });
234
+ const sym = op.attrs.sym;
235
+ out.set(sym, {
236
+ name: sym,
237
+ kind: 'data',
238
+ ...(qualifiedNames.has(sym) ? { volatile: true } : {}),
239
+ ...(typeof op.attrs.globalRegister === 'string' ? { globalRegister: op.attrs.globalRegister } : {}),
240
+ });
180
241
  }
181
242
  }
182
243
  }
183
244
  return out;
184
245
  }
185
246
 
247
+ /** The globals a function-scope static's initializer holds the address of (ir/core.ts
248
+ * `LocalObject.relocs`), as name-only symbols for the same synthesis: the candidate spells each in
249
+ * the static's definition (`(u32)&Name`), and only the static's relocations name it. Kept apart
250
+ * from {@link bareGlobalSymbols}, whose names the variation gates read as the globals the BODY
251
+ * addresses: an address in a table no access ever subscripts says nothing of how one is spelled. */
252
+ export function initAddressSymbols(fn: Fn): Map<string, SymbolInfo> {
253
+ const out = new Map<string, SymbolInfo>();
254
+ for (const obj of fn.localObjects?.values() ?? []) {
255
+ for (const r of obj.relocs ?? []) {
256
+ out.set(r.symbol, { name: r.symbol, kind: 'data' });
257
+ }
258
+ }
259
+ return out;
260
+ }
261
+
262
+ /** The global register variables a compiler that declares them only as a contiguous run
263
+ * (`globalRegisterVariables.contiguous`, mwcc 2.4: "gaps between assigned global register variables
264
+ * are not permitted") needs declared below the highest one `refs` names, and they do not. Each is a
265
+ * word nothing reads, declared only so the one the body uses is legal. */
266
+ function globalRegisterFillers(
267
+ refs: readonly SymbolRef[],
268
+ target: Pick<TargetDescription, 'globalRegisterVariables'>,
269
+ ): SymbolRef[] {
270
+ const run = target.globalRegisterVariables?.contiguous ?? [];
271
+ const named = new Set(refs.flatMap((r) => (r.info.globalRegister === undefined ? [] : [r.info.globalRegister])));
272
+ const top = Math.max(-1, ...[...named].map((r) => run.indexOf(r)));
273
+ return run
274
+ .slice(0, top + 1)
275
+ .filter((r) => !named.has(r))
276
+ .map((r) => {
277
+ const name = globalRegisterSymbol(r);
278
+ return { name, info: { name, kind: 'data', globalRegister: r }, synthesized: true };
279
+ });
280
+ }
281
+
186
282
  /** The reference collector one enumeration uses, over ONE dictionary. A factory rather than a
187
283
  * free function because all four of its inputs are per-enumeration constants that must not vary
188
284
  * per tree — naming them here is what keeps a caller from passing a different dictionary to two
@@ -206,11 +302,14 @@ export function makeRefCollector(ctx: {
206
302
  /** the project's prototype table — what lets a CALL TARGET be declared rather than refused */
207
303
  prototypes: Prototypes;
208
304
  /** the target the lift ran for, whose ABI says which declared types a printed prototype carries */
209
- target: Pick<TargetDescription, 'doubleArgWords'>;
305
+ target: Pick<TargetDescription, 'doubleArgWords' | 'globalRegisterVariables'>;
210
306
  /** reports a refusal at most once per (name, reason); the caller owns the dedup */
211
307
  refuse: (name: string, reason: RefusedDeclarationReason) => void;
308
+ /** the function's own declaration — stated, or the context's own entry — whose struct return a
309
+ * tree that returns it needs defined (`ownReturnDefinition`) */
310
+ own?: FnProto;
212
311
  }): (tree: SFn) => { symbolRefs?: SymbolRef[] } {
213
- const { declSymbols, accessFacts, mapSymbols, targetNames, prototypes, target, refuse } = ctx;
312
+ const { declSymbols, accessFacts, mapSymbols, targetNames, prototypes, target, refuse, own } = ctx;
214
313
  return (tree: SFn): { symbolRefs?: SymbolRef[] } => {
215
314
  // The names THIS tree binds. Computed per tree because the emitter mints local names per
216
315
  // spelling — but the test below is NOT `bound` alone, and the difference is a wrong answer.
@@ -218,6 +317,8 @@ export function makeRefCollector(ctx: {
218
317
  // A function-scope static is declared in the body beside the locals, so it shares their
219
318
  // namespace: one the emitter's own names can reach dies with the spelling, as below.
220
319
  const statics = new Set((tree.statics ?? []).map((st) => st.name));
320
+ // …and the compiler's anonymous images, which reach the C only as a local's initializer
321
+ const images = new Set((tree.images ?? []).map((im) => im.name));
221
322
  for (const st of statics) {
222
323
  if (bound.has(st) || EMITTER_NAME.test(st)) {
223
324
  refuse(st, 'emitter-name');
@@ -228,7 +329,7 @@ export function makeRefCollector(ctx: {
228
329
  }
229
330
  }
230
331
  const refs = collectSymbolRefs(tree, declSymbols, prototypes, target, refuse).flatMap((r) => {
231
- if (statics.has(r.name)) {
332
+ if (statics.has(r.name) || images.has(r.name)) {
232
333
  return []; // defined in the body — the map's global of the same name is another object
233
334
  }
234
335
  // THE ONE REFUSAL THAT IS NOT A REFUSAL — a name the emitted C uses for its OWN storage
@@ -284,6 +385,11 @@ export function makeRefCollector(ctx: {
284
385
  }
285
386
  return [{ ...r, ...(access ? { access } : {}), ...synthesized }];
286
387
  });
388
+ refs.push(...globalRegisterFillers(refs, target));
389
+ const returned = ownReturnDefinition(tree, own);
390
+ if (returned !== undefined) {
391
+ refs.push(returned);
392
+ }
287
393
  return refs.length ? { symbolRefs: refs } : {};
288
394
  };
289
395
  }