@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
@@ -1,17 +1,20 @@
1
1
  // asmlift — the soft-float boundary (L1 recognition): where integer words become floats.
2
2
  //
3
- // Three things cross it. A runtime helper whose op is a float op (`isFloatHelper`) — the peer of
3
+ // Four things cross it. A runtime helper whose op computes on floats (`isFloatHelper`) — the peer of
4
4
  // `raise/widehelpers.ts`, gated by the target's runtime table rather than a hardware capability.
5
- // A parameter an ordinary callee declares `double` (the frontend's `doubles` on the call). And a
5
+ // A parameter an ordinary callee declares `double` (the frontend's `doubles` on the call). The
6
+ // result of an ordinary callee declared to return a floating type (its `floatReturn`). And a
6
7
  // double literal, two constant words, where the target states their order
7
8
  // (`TargetDescription.doubleArgWords`) and its compiler reads a literal back
8
- // (`compilerBehaviors.roundTripsDoubleLiterals`). What sets all three apart from the integer
9
+ // (`compilerBehaviors.roundTripsDoubleLiterals`). What sets all four apart from the integer
9
10
  // helpers is the value's KIND: the words are bits in integer registers, and the pass re-types them
10
11
  // as floats, which only data flow may justify — see the function below.
12
+ import { constWord } from '../ir/bits';
11
13
  import { Fn, Op, Value, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
12
14
  import { doubleBits, doubleOf } from '../ir/float-bits';
13
- import { T } from '../ir/types';
14
- import { arrivesAsDeclared, helperOp, isFloatHelper, lookupHelper } from '../runtime-helpers';
15
+ import { type Opcode, fcmpOfTruth } from '../ir/opcodes';
16
+ import { T, intWidth } from '../ir/types';
17
+ import { arrivesAsDeclared, floatSidesOf, helperOp, isFloatHelper, lookupHelper } from '../runtime-helpers';
15
18
  import type { TargetDescription } from '../target';
16
19
  import { RaiseUnsupportedError } from './errors';
17
20
  import { isArgumentPair } from './pairparams';
@@ -34,17 +37,36 @@ import { isArgumentPair } from './pairparams';
34
37
  * target's word order into an `fconst` (`a + 1.5` stages `0x3ff80000` in r2, its high word);
35
38
  * - an argument the function also reads on its own, as a word or as one half of a pair;
36
39
  * - a result read as a word or a half (`*(int *)&c` is a double's HIGH word, in r0), passed to
37
- * any other call, or carried across an edge.
40
+ * any other call, or carried across an edge;
41
+ * - a result NOTHING reads. Folded, it would be a dead float op, and `dce` would delete the
42
+ * arithmetic. Optimising (-O1, -O2), agbcc deletes a helper call whose result is dead
43
+ * (`s = a * b; g(); return 0;` calls no `__mulsf3`), so the result's one reader was a later call
44
+ * that took it in r0, and a guessed arity does not count a register a call wrote (frontend/ssa.ts
45
+ * `fallbackArgc`, `trimClobberedCallArgs`): `g1(a * b)` with `g1` undeclared reads as
46
+ * `__mulsf3(); return g1();`, and folded would print `return g1();`. A double's pair refuses
47
+ * that shape at the frontend, naming the half the next call reads; a single is one word, so it
48
+ * reaches here. At -O0 the rule is not asked: agbcc stores the result to the frame, which reads
49
+ * it, and nothing folds there, its arguments being read from frame homes. A frontend that reads
50
+ * those homes must re-measure this. A result some other op ALSO reads is not refused:
51
+ * `s = a * b; g(s); return s;` and `s = a * b; g(); return s;` are the same bytes on agbcc, and
52
+ * a declaration of `g` closes the trim's `g()` reading (`trimClobberedCallArgs`).
38
53
  * A call refused takes every call its result feeds or is fed by with it, so what folds is a closed
39
- * set of floats and verify's float rule holds by construction. */
54
+ * set of floats and verify's float rule holds by construction.
55
+ *
56
+ * A CONVERSION HAS AN INTEGER ON ONE SIDE (`floatSidesOf`), and only its float side is held to the
57
+ * rule above: `(float)i` converts whatever integer `i` is, a parameter, a load or a sum, and
58
+ * `(int)x` hands an integer to anything. Its result must still be read, by the dead-result rule,
59
+ * and a float the function also reads as a word is no float source for it either. */
40
60
  export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolean {
41
61
  const table = target.runtimeHelpers ?? {};
42
62
  let sites: Op[] = [];
43
63
  const consumers = new Map<Op, readonly number[]>();
44
64
  const def = new Map<Value, Op>();
45
65
  const users = new Map<Value, Array<Op | null>>(); // null: an edge argument
66
+ const returners = new Map<Op, string>(); // ordinary calls declared to return a floating type
46
67
  const use = (v: Value, by: Op | null) => users.set(v, [...(users.get(v) ?? []), by]);
47
68
  const helperOf = (op: Op) => (op.opcode === 'call' ? lookupHelper(table, String(op.attrs.target)) : undefined);
69
+ const sidesOf = (op: Op) => floatSidesOf(helperOf(op)!);
48
70
  for (const block of fn.blocks) {
49
71
  for (const op of block.ops) {
50
72
  op.results.forEach((r) => def.set(r, op));
@@ -64,9 +86,12 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
64
86
  } else if (!helper && op.opcode === 'call' && Array.isArray(op.attrs.doubles)) {
65
87
  consumers.set(op, op.attrs.doubles as number[]);
66
88
  }
89
+ if (!helper && op.opcode === 'call' && typeof op.attrs.floatReturn === 'string') {
90
+ returners.set(op, op.attrs.floatReturn);
91
+ }
67
92
  }
68
93
  }
69
- if (!sites.length && !consumers.size) {
94
+ if (!sites.length && !consumers.size && !returners.size) {
70
95
  return false;
71
96
  }
72
97
  // A projection nothing reads is not a read of a half: `dce` runs ahead of this pass only after a
@@ -78,7 +103,8 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
78
103
  // same call as a word is a word read.
79
104
  const floatUse = (u: Op | null, v: Value, calls: ReadonlySet<Op>): boolean =>
80
105
  u !== null &&
81
- (calls.has(u) || (consumers.has(u) && u.operands.every((o, i) => o !== v || consumers.get(u)!.includes(i))));
106
+ ((calls.has(u) && sidesOf(u).operands) ||
107
+ (consumers.has(u) && u.operands.every((o, i) => o !== v || consumers.get(u)!.includes(i))));
82
108
  const floatsOnly = (v: Value, calls: ReadonlySet<Op>) => (users.get(v) ?? []).every((u) => floatUse(u, v, calls));
83
109
  const entry = fn.blocks[0].params;
84
110
  // A `concat` of the same two slots in the same order as `d`: the frontend builds one afresh at each
@@ -98,24 +124,20 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
98
124
  users.get(d.operands[0])!.every((u) => samePair(u, d) && floatsOnly(u.results[0], calls))
99
125
  );
100
126
  };
127
+ // A SINGLE AN ORDINARY CALLEE IS DECLARED TO RETURN is one whole word in the return register, so
128
+ // it is a float source as a helper's result is. A double is not: the frontend reads one word of
129
+ // it (`declaredReturnWidth` sizes no floating type), and that word is not the value.
130
+ const declaredSingle = (d: Op) => returners.get(d) === 'float' && intWidth(d.results[0].type) === 32;
101
131
  const fromCall = (v: Value, calls: ReadonlySet<Op>) => {
102
132
  const d = def.get(v);
103
- return d !== undefined && calls.has(d);
133
+ return d !== undefined && ((calls.has(d) && sidesOf(d).result) || declaredSingle(d));
104
134
  };
105
135
  // A CONSTANT WORD, as a compiler may build one in two instructions: agbcc moves a word that is a
106
136
  // shifted byte as `mov`+`lsl` (thumb.md:66, `thumb_shiftable_const` at thumb.c:61) and one in
107
137
  // -255..-1 as `mov`+`neg` (thumb.md:88), so the high word of 2.0 reaches r3 as `0x80 << 23`. Its
108
- // value, or undefined where `v` is no constant.
109
- const wordOf = (v: Value): number | undefined => {
110
- const d = def.get(v);
111
- if (d?.opcode === 'const') {
112
- return Number(d.attrs.value);
113
- }
114
- const shift = d?.opcode === 'shl' ? d.attrs.imm : undefined;
115
- const w =
116
- d?.operands.length === 1 && (d.opcode === 'neg' || typeof shift === 'number') ? wordOf(d.operands[0]) : undefined;
117
- return w === undefined ? undefined : d!.opcode === 'neg' ? -w | 0 : (w << (shift as number)) | 0;
118
- };
138
+ // value, or undefined where `v` is no constant — the shared word evaluator's answer (ir/bits.ts).
139
+ const words = { defs: def };
140
+ const wordOf = (v: Value): number | undefined => constWord(words, v) ?? undefined;
119
141
  // A LITERAL IS TWO CONSTANT WORDS, HIGH WORD FIRST on a target whose double crosses a call in
120
142
  // general words (`TargetDescription.doubleArgWords`) — the order that makes the long long naming
121
143
  // of the same pair another number. Its bits, or undefined where the pair is no literal, the target
@@ -138,21 +160,78 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
138
160
  };
139
161
  const literal = (v: Value, calls: ReadonlySet<Op>) => literalBits(v) !== undefined && floatsOnly(v, calls);
140
162
  const source = (v: Value, calls: ReadonlySet<Op>) => argument(v, calls) || fromCall(v, calls) || literal(v, calls);
163
+ // A COMPARE FOLDS THROUGH THE TESTS OF ITS RESULT, which is libgcc's three-way answer: each reader
164
+ // must be a test of that integer against 0, and the compare the test then computes is read off
165
+ // the helper's contract — the sign that orders the operands, and its value on a NaN (`unordered`)
166
+ // — against `FCMP_TRUTH`. A test whose answer is no compare of the operands (always true, or true
167
+ // on both orders but not on equality), or a reader that is no test, refuses the helper.
168
+ const tests = new Map<Op, CompareTest[]>();
169
+ sites = sites.filter((op) => {
170
+ const unordered = helperOf(op)!.unordered;
171
+ if (unordered === undefined) {
172
+ return true;
173
+ }
174
+ const found = compareTests(op.results[0], users, def, wordOf, unordered);
175
+ if (found !== null) {
176
+ tests.set(op, found);
177
+ }
178
+ return found !== null;
179
+ });
141
180
  for (;;) {
142
181
  const calls = new Set(sites);
143
- const kept = sites.filter(
144
- (op) =>
145
- op.operands.every((o) => source(o, calls)) &&
146
- (users.get(op.results[0]) ?? []).every(
147
- (u) => deadHalf(u) || floatUse(u, op.results[0], calls) || u?.opcode === 'ret',
148
- ),
149
- );
182
+ const kept = sites.filter((op) => {
183
+ const sides = sidesOf(op);
184
+ const read = (users.get(op.results[0]) ?? []).filter((u) => !deadHalf(u));
185
+ return (
186
+ (!sides.operands || op.operands.every((o) => source(o, calls))) &&
187
+ read.length > 0 &&
188
+ (!sides.result || read.every((u) => floatUse(u, op.results[0], calls) || u?.opcode === 'ret'))
189
+ );
190
+ });
150
191
  if (kept.length === sites.length) {
151
192
  break;
152
193
  }
153
194
  sites = kept;
154
195
  }
155
196
  const calls = new Set(sites);
197
+ // A DECLARED FLOATING RETURN HAS NO FALLBACK EITHER. The candidate prints the call against its
198
+ // declaration, so its result stored, compared, narrowed, carried across an edge in an integer
199
+ // variable or handed to any other call is CONVERTED there — `u.f = f(); g = u.b[0];` lifted as
200
+ // `g = f();` compiles to `bl f; bl __fixsfsi` — where the bytes moved the bits. So a single may
201
+ // go only where a helper's result may (a helper the fold keeps, a `double` parameter, the
202
+ // return), and is retyped a float; a double read off its one word goes nowhere. A result nothing
203
+ // reads is a call statement, and stays one.
204
+ const returned = new Map<Op, Value>();
205
+ for (const [op, spelled] of returners) {
206
+ const r = op.results[0];
207
+ const read = (users.get(r) ?? []).filter((u) => !deadHalf(u));
208
+ if (read.length === 0) {
209
+ continue;
210
+ }
211
+ if (!declaredSingle(op) || !read.every((u) => floatUse(u, r, calls) || u?.opcode === 'ret')) {
212
+ throw new RaiseUnsupportedError(
213
+ `cannot lift '${fn.name}': '${String(op.attrs.target)}' is declared to return ${spelled}, which this ` +
214
+ 'target hands back as bits in a general register, and its result is read here as something ' +
215
+ 'other than that float — a conversion the bytes do not make',
216
+ );
217
+ }
218
+ returned.set(op, mkValue(T.f32()));
219
+ }
220
+ // A FLOAT RETURNED MAKES THE FUNCTION'S RETURN A FLOAT, so every other `ret` converts what it
221
+ // hands back: `return 5;` beside `return fget();` compiles to the bits of 5.0f where the asm
222
+ // hands back the word 5. Each value a `ret` reads must then be a float this pass makes — a kept
223
+ // helper's result or a declared single's.
224
+ const madeFloat = (v: Value) => {
225
+ const d = def.get(v);
226
+ return d !== undefined && (returned.has(d) || (calls.has(d) && sidesOf(d).result));
227
+ };
228
+ const rets = fn.blocks.flatMap((b) => b.ops.filter((op) => op.opcode === 'ret' && op.operands.length > 0));
229
+ if (rets.some((r) => r.operands.some(madeFloat)) && !rets.every((r) => r.operands.every(madeFloat))) {
230
+ throw new RaiseUnsupportedError(
231
+ `cannot lift '${fn.name}': it returns a float on one path, and another return of this function hands ` +
232
+ 'back an integer word, which C would convert to that float',
233
+ );
234
+ }
156
235
  // A DECLARED DOUBLE HAS NO FALLBACK. A helper the fold refuses stays a call for
157
236
  // `refuseUnmodelledHelpers` to gap, but an ordinary callee is no helper, and its two words handed
158
237
  // on as integers are the wrong number — so a double parameter nothing here can hand a double
@@ -170,7 +249,7 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
170
249
  }
171
250
  }
172
251
  }
173
- if (!sites.length && !consumers.size) {
252
+ if (!sites.length && !consumers.size && !returned.size) {
174
253
  return false;
175
254
  }
176
255
  const floatOf = new Map<Value, Value>();
@@ -209,23 +288,60 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
209
288
  }
210
289
  floatOf.set(o, whole);
211
290
  };
291
+ for (const [op, now] of returned) {
292
+ floatOf.set(op.results[0], now);
293
+ }
212
294
  for (const op of sites) {
213
295
  const helper = helperOf(op)!;
214
- op.operands.forEach((o, i) => retype(o, helper.params[i]));
215
- floatOf.set(op.results[0], float(helper.returns));
296
+ const sides = floatSidesOf(helper);
297
+ if (sides.operands) {
298
+ op.operands.forEach((o, i) => retype(o, helper.params[i]));
299
+ }
300
+ if (sides.result) {
301
+ floatOf.set(op.results[0], float(helper.returns));
302
+ }
216
303
  }
217
304
  for (const [op, at] of consumers) {
218
305
  at.forEach((i) => retype(op.operands[i], 64));
219
306
  }
307
+ const folded = new Map<Op, Op>();
308
+ for (const [op, found] of tests) {
309
+ if (calls.has(op)) {
310
+ for (const t of found) {
311
+ const operands = op.operands.map((o) => floatOf.get(o)!);
312
+ const fcmp = helperOp(t.opcode, String(op.attrs.target), operands, t.at.results[0]);
313
+ // The relation the source spelled, where the test leaves it open: `__eqsf2` and `__nesf2`
314
+ // each fold to `fcmp_oeq` or to `fcmp_une`, by the sense of the branch that tests them
315
+ // (runtime-helpers.ts `RuntimeHelper.relation`).
316
+ const relation = helperOf(op)!.relation;
317
+ if (relation !== undefined) {
318
+ fcmp.attrs.relation = relation;
319
+ }
320
+ folded.set(t.at, fcmp);
321
+ t.spent.forEach((x) => spent.add(x));
322
+ }
323
+ }
324
+ }
220
325
  for (const block of fn.blocks) {
221
326
  block.ops = block.ops.flatMap((op) => {
327
+ // the call stays, its result now the float it was declared
328
+ if (returned.has(op)) {
329
+ return [mkOp('call', { operands: op.operands, results: [returned.get(op)!], attrs: op.attrs })];
330
+ }
331
+ if (folded.has(op)) {
332
+ return [folded.get(op)!];
333
+ }
334
+ if (calls.has(op) && tests.has(op)) {
335
+ return [];
336
+ }
222
337
  if (calls.has(op)) {
338
+ const sides = sidesOf(op);
223
339
  return [
224
340
  helperOp(
225
341
  helperOf(op)!.op!,
226
342
  String(op.attrs.target),
227
- op.operands.map((o) => floatOf.get(o)!),
228
- floatOf.get(op.results[0])!,
343
+ op.operands.map((o) => (sides.operands ? floatOf.get(o)! : o)),
344
+ sides.result ? floatOf.get(op.results[0])! : op.results[0],
229
345
  ),
230
346
  ];
231
347
  }
@@ -241,3 +357,108 @@ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolea
241
357
  }
242
358
  return true;
243
359
  }
360
+
361
+ /** One test of a compare helper's result: the op whose 0/1 value is the test, the float compare
362
+ * it computes, and the ops between the result and it, which only it reads. */
363
+ interface CompareTest {
364
+ at: Op;
365
+ opcode: Opcode;
366
+ spent: Op[];
367
+ }
368
+
369
+ /** The integer compares a test may apply to the helper's result, each against the constant 0. */
370
+ const INT_TEST: Readonly<Record<string, (x: number, y: number) => boolean>> = {
371
+ icmp_eq: (x, y) => x === y,
372
+ icmp_ne: (x, y) => x !== y,
373
+ icmp_slt: (x, y) => x < y,
374
+ icmp_sle: (x, y) => x <= y,
375
+ icmp_sgt: (x, y) => x > y,
376
+ icmp_sge: (x, y) => x >= y,
377
+ icmp_ult: (x, y) => x >>> 0 < y >>> 0,
378
+ icmp_ule: (x, y) => x >>> 0 <= y >>> 0,
379
+ icmp_ugt: (x, y) => x >>> 0 > y >>> 0,
380
+ icmp_uge: (x, y) => x >>> 0 >= y >>> 0,
381
+ };
382
+
383
+ /** Every test of `r`, a compare helper's result, or null where some reader of it is none. The tests
384
+ * are the spellings agbcc gives a result's relation to 0: an integer compare with the constant 0
385
+ * on either side (`cmp r0, #0` before a branch or a 0/1 select), and the three sign-bit forms of a
386
+ * value — `r >> 31` (`< 0`), `~r >> 31` (`>= 0`) and `(-r | r) >> 31` (`!= 0`), each a logical
387
+ * shift by 31. Each answers by the SIGN of `r` alone, so the helper's three-way result is read at
388
+ * -1, 0 and 1 and at its NaN value, and the four answers name the compare in `FCMP_TRUTH`. */
389
+ function compareTests(
390
+ r: Value,
391
+ users: ReadonlyMap<Value, ReadonlyArray<Op | null>>,
392
+ def: ReadonlyMap<Value, Op>,
393
+ wordOf: (v: Value) => number | undefined,
394
+ unordered: 1 | -1,
395
+ ): CompareTest[] | null {
396
+ const readers = (v: Value) => users.get(v) ?? [];
397
+ const only = (v: Value): Op | null => (readers(v).length === 1 ? readers(v)[0] : null);
398
+ const signBit = (u: Op | null, of: Value): boolean =>
399
+ u?.opcode === 'shr_u' && u.operands.length === 1 && u.operands[0] === of && u.attrs.imm === 31;
400
+ const found: CompareTest[] = [];
401
+ const answer = (at: Op, spent: Op[], test: (x: number) => boolean): boolean => {
402
+ const truth = [test(-1), test(0), test(1), test(unordered)];
403
+ const opcode = fcmpOfTruth(truth);
404
+ if (opcode === undefined) {
405
+ return false;
406
+ }
407
+ found.push({ at, opcode, spent });
408
+ return true;
409
+ };
410
+ for (const u of readers(r)) {
411
+ if (u === null) {
412
+ return null;
413
+ }
414
+ const cmp = INT_TEST[u.opcode];
415
+ if (cmp !== undefined && u.operands.length === 2) {
416
+ const [x, y] = u.operands;
417
+ const ok =
418
+ x === r && y !== r && wordOf(y) === 0
419
+ ? answer(u, [], (v) => cmp(v, 0))
420
+ : y === r && x !== r && wordOf(x) === 0 && answer(u, [], (v) => cmp(0, v));
421
+ if (!ok) {
422
+ return null;
423
+ }
424
+ continue;
425
+ }
426
+ if (signBit(u, r)) {
427
+ if (!answer(u, [], (v) => v < 0)) {
428
+ return null;
429
+ }
430
+ continue;
431
+ }
432
+ if (u.opcode === 'not' && u.operands.length === 1) {
433
+ const shift = only(u.results[0]);
434
+ if (!signBit(shift, u.results[0]) || !answer(shift!, [u], (v) => v >= 0)) {
435
+ return null;
436
+ }
437
+ continue;
438
+ }
439
+ // `(-r | r) >> 31`: `r` has two readers, the `neg` and the `or`, and the test is taken at the
440
+ // `neg`; the `or` is that same test, read when it is reached.
441
+ if (u.opcode === 'neg' && u.operands.length === 1) {
442
+ const or = only(u.results[0]);
443
+ const shift = or ? only(or.results[0]) : null;
444
+ const pairs =
445
+ or?.opcode === 'or' &&
446
+ or.operands.length === 2 &&
447
+ or.operands.includes(r) &&
448
+ or.operands.includes(u.results[0]) &&
449
+ readers(r).includes(or);
450
+ if (!pairs || !signBit(shift, or!.results[0]) || !answer(shift!, [u, or!], (v) => v !== 0)) {
451
+ return null;
452
+ }
453
+ continue;
454
+ }
455
+ if (
456
+ u.opcode === 'or' &&
457
+ u.operands.some((o) => o !== r && def.get(o)?.opcode === 'neg' && def.get(o)!.operands[0] === r)
458
+ ) {
459
+ continue;
460
+ }
461
+ return null;
462
+ }
463
+ return found.length > 0 ? found : null;
464
+ }
package/src/raise/gvn.ts CHANGED
@@ -32,9 +32,9 @@
32
32
  // materializes a multi-use `const` that is live across a call into a named local, and
33
33
  // its own comment records that this exact widening ("the small-constant regression")
34
34
  // already cost matches once.
35
- // `undef` — numbering it would be VACUOUS: two `undef`s in one function always carry different
36
- // `key`s (ir/opcodes.ts says so at the opcode), so "same key, therefore same value"
37
- // never has two members to collapse.
35
+ // `undef` — numbering it would be HARMFUL: two `undef`s of one `key` are a storage's content
36
+ // and a `copied` move of it into another storage's join (ir/opcodes.ts says so at the
37
+ // opcode), and one number would erase the move the structurer spells.
38
38
  //
39
39
  // The gate is a MATCHING policy, not a property of the opcode — which is why it is not a flag on
40
40
  // the opcode table, where `const` would satisfy it.
@@ -3,7 +3,7 @@
3
3
  // A loop whose back-edge carries a register copy — `add r3, r0, #0` then `b .L6` — lifts to a block
4
4
  // holding nothing but that branch, because SSA construction turns the copy into an EDGE ARGUMENT.
5
5
  // The loop is unchanged; its latch has just become a separate empty block between the exit test and
6
- // the header. Loop DISCOVERY still finds the loop — `structure/loops.ts` reads the same back-edge —
6
+ // the header. Loop DISCOVERY still finds the loop — `ir/loops.ts` reads the same back-edge —
7
7
  // but the do-while emitter requires the latch to end in a `cond_br`, an empty one ends in `br`, and
8
8
  // the test is at the bottom so the `while` form is unavailable too. The back-edge survives to the
9
9
  // `onStack` refusal and the whole function declines with "unrecovered back-edge".
@@ -24,11 +24,13 @@
24
24
  // `if` (declining loud on the hazards). The guard therefore survives the fold either way; what the
25
25
  // gate preserves is the SHAPE — a forward trampoline is not a latch, and folding one re-casts an
26
26
  // unrelated branch as a loop guard — which is the `target-dominates` entry's own note below.
27
- import { Fn, dominators, foldWriteOrder } from '../ir/core';
27
+ import { Block, Fn, dominators, foldWriteOrder, predecessors, reachForward, successorsOf } from '../ir/core';
28
+ import { effectful } from '../ir/discipline';
29
+ import { analyzeLoops } from '../ir/loops';
28
30
  import { type Gate, firstRejection } from '../l3/gates';
29
31
 
30
32
  /** What the gates below judge: one candidate block and the block its `br` goes to. */
31
- interface LatchCandidate {
33
+ export interface LatchCandidate {
32
34
  block: Fn['blocks'][number];
33
35
  target: Fn['blocks'][number];
34
36
  dominatesBlock: boolean;
@@ -80,9 +82,9 @@ export const LATCH_GATES: readonly Gate<LatchCandidate>[] = [
80
82
  * SUCCESSOR changes, so an edge that was not a back-edge becomes one. (Dominator sets themselves
81
83
  * only ever shrink — removing a block removes it from every set it was in.)
82
84
  *
83
- * A predecessor that already branches to the target ends up with two edges into one block. That is
84
- * a block whose every edge continues the loop, so it has no exit and loop recovery declines — the
85
- * same loud decline it gave before the fold.
85
+ * A predecessor that already branches to the target ends up with two edges into one block, each
86
+ * carrying its own arguments. Where that predecessor has no other edge the loop has no exit, and
87
+ * loop recovery spells it `while (1)` with each edge's own update copy.
86
88
  */
87
89
  export function foldEmptyLatches(fn: Fn, gates: readonly Gate<LatchCandidate>[] = LATCH_GATES): number {
88
90
  let folded = 0;
@@ -124,3 +126,157 @@ export function foldEmptyLatches(fn: Fn, gates: readonly Gate<LatchCandidate>[]
124
126
  folded++;
125
127
  }
126
128
  }
129
+
130
+ // ── EMPTY EXIT TRAMPOLINES ──────────────────────────────────────────────────────────────────────
131
+ //
132
+ // The same empty block on a loop's way OUT. `for (…) { if (t[i] == k) { r = i; break; } }` keeps
133
+ // `r` in a register the break copies into (`add r2, r1, #0`), and the copy becomes an edge argument
134
+ // of a block that holds nothing but its `br` to the code after the loop — beside a second exit edge
135
+ // (the loop's own test failing) that reaches that code directly. The loop then has two exit targets
136
+ // and none of them is a `return`, so every loop form refuses it, and the back-edge declines loud at
137
+ // `onStack`. Which exit edge carries the block is the compiler's choice: agbcc puts it on the break
138
+ // (`synthetic:break_value`) or on the latch's own exit with the break going direct (kleod
139
+ // `sub_0801E664`).
140
+ //
141
+ // The second exit need not land on the target itself. ido, gcc2.7.2kmc and mwcc lay an `else` the
142
+ // loop sits beside between the loop and its join — `if (f) { for (..) if (a(i) == k) { id = i + 100;
143
+ // break; } } else id = f + 26; out(id);` (`synthetic:break_elsetail`) — so the loop's own exit is an
144
+ // empty `b`/`j` over the `else`, and the break reaches the join only through the code its arm runs.
145
+ // Folding the block there leaves the loop two exit targets all the same — the break arm's head is
146
+ // one. What it buys is that the loop's own exit becomes the join the break arm runs on into, which
147
+ // is the exit a `break` that runs code has to leave for (`codeBreakArm`'s region must reach it). So
148
+ // the scope gate asks whether another exit edge's target REACHES the target, going forward and
149
+ // outside the loop's body.
150
+ //
151
+ // Splicing it out re-points each edge into it at its target with its own arguments, and the loop
152
+ // exits to one block: a `break` that carries its copies, which the structurer already spells. The
153
+ // arguments move soundly for the reason the latch's do: the block has no params and one op, so
154
+ // every value its `br` passes dominates it, and therefore the end of each of its predecessors.
155
+
156
+ /** One candidate trampoline: the block, where its `br` goes, and the loops it may leave. */
157
+ export interface ExitTrampoline {
158
+ block: Block;
159
+ target: Block;
160
+ /** the predecessors of `block` */
161
+ preds: readonly Block[];
162
+ /** does some loop hold every predecessor of `block` but not `block` itself, and leave by another
163
+ * edge for `target` as well — landing on it, or on a block that reaches it forward without
164
+ * re-entering the loop (`reachForward`)? */
165
+ sharedExit: boolean;
166
+ /** does every path from `target` reach a `ret` without an effect on the way? */
167
+ pureReturn: boolean;
168
+ }
169
+
170
+ export const EXIT_TRAMPOLINE_GATES: readonly Gate<ExitTrampoline>[] = [
171
+ {
172
+ id: 'trampoline-has-params',
173
+ why: 'a param is a join: the br args may read it, and they would not exist at a predecessor',
174
+ // the fold then hands a predecessor a value it does not dominate, which `verify` refuses loud
175
+ sound: false,
176
+ rejects: (c) => c.block.params.length > 0,
177
+ },
178
+ {
179
+ id: 'trampoline-does-work',
180
+ why: 'the sole terminator must be an unconditional `br`: folding a `cond_br` block discards its second successor',
181
+ sound: true,
182
+ guardedBy: 'latch.test.ts: an exit block whose sole op is a cond_br is never folded',
183
+ rejects: (c) => c.block.ops[0]?.opcode !== 'br',
184
+ },
185
+ {
186
+ id: 'trampoline-self-branch',
187
+ why: 'a block branching to itself is an infinite loop rather than a trampoline',
188
+ sound: false,
189
+ rejects: (c) => c.target === c.block,
190
+ },
191
+ {
192
+ // The SCOPE of the fold, not its soundness: an exit edge's empty block is harmless to the
193
+ // structurer wherever it is the loop's one way out, and folding it there would only re-spell
194
+ // functions that already lift.
195
+ id: 'trampoline-not-a-second-exit',
196
+ why: 'only a loop that also leaves for the same target by another edge, or for a block that runs on into it, gains from the fold an exit every edge out can leave for',
197
+ sound: false,
198
+ rejects: (c) => !c.sharedExit,
199
+ },
200
+ {
201
+ // A target that only returns already lifts: the structurer takes the edge through the
202
+ // trampoline as an early `return` and copies the pure tail into it. Kept that way, so the fold
203
+ // re-spells no function that lifted before it.
204
+ id: 'trampoline-returns-already',
205
+ why: 'an exit that only returns is an early `return` arm, which the structurer spells without the fold',
206
+ sound: false,
207
+ rejects: (c) => c.pureReturn,
208
+ },
209
+ ];
210
+
211
+ /** Every path from `b` ends in a `ret` and runs no effect on the way. */
212
+ export function returnsPurely(b: Block): boolean {
213
+ const seen = new Set<Block>();
214
+ for (const stack = [b]; stack.length > 0;) {
215
+ const x = stack.pop()!;
216
+ if (seen.has(x)) {
217
+ continue; // a join two paths share, or a cycle — either way judged on its first visit
218
+ }
219
+ seen.add(x);
220
+ if (x.ops.some(effectful)) {
221
+ return false;
222
+ }
223
+ const t = x.ops[x.ops.length - 1];
224
+ if (t?.opcode === 'ret') {
225
+ continue;
226
+ }
227
+ if (t?.opcode !== 'br' && t?.opcode !== 'cond_br') {
228
+ return false;
229
+ }
230
+ stack.push(...successorsOf(x));
231
+ }
232
+ return true;
233
+ }
234
+
235
+ /** Splice out every empty EXIT TRAMPOLINE the gates above admit; returns how many were removed.
236
+ * Iterated, as the latch fold is: one splice can make another block's exit edges shared. */
237
+ export function foldExitTrampolines(fn: Fn, gates: readonly Gate<ExitTrampoline>[] = EXIT_TRAMPOLINE_GATES): number {
238
+ let folded = 0;
239
+ for (;;) {
240
+ const preds = predecessors(fn);
241
+ const dom = dominators(fn);
242
+ const loops = [...analyzeLoops(fn, dom).byHeader.values()];
243
+ const found = fn.blocks.find((b) => {
244
+ const target = b.ops[0]?.successors[0]?.block;
245
+ if (target === undefined) {
246
+ return false;
247
+ }
248
+ const into = preds.get(b) ?? [];
249
+ const sharedExit =
250
+ into.length > 0 &&
251
+ loops.some(
252
+ (nl) =>
253
+ !nl.body.has(b) &&
254
+ into.every((p) => nl.body.has(p)) &&
255
+ nl.exitEdges.some((e) => e.to !== b && reachForward(e.to, dom, nl.body).has(target)),
256
+ );
257
+ return (
258
+ firstRejection(gates, { block: b, target, preds: into, sharedExit, pureReturn: returnsPurely(target) }) === null
259
+ );
260
+ });
261
+ if (!found) {
262
+ return folded;
263
+ }
264
+ const onward = found.ops[0].successors[0];
265
+ for (const b of fn.blocks) {
266
+ let repointed = false;
267
+ for (const op of b.ops) {
268
+ op.successors.forEach((s, i) => {
269
+ if (s.block === found) {
270
+ op.successors[i] = { block: onward.block, args: [...onward.args] };
271
+ repointed = true;
272
+ }
273
+ });
274
+ }
275
+ if (repointed) {
276
+ foldWriteOrder(fn.writeOrder, found, b);
277
+ }
278
+ }
279
+ fn.blocks = fn.blocks.filter((b) => b !== found);
280
+ folded++;
281
+ }
282
+ }
@@ -71,6 +71,7 @@
71
71
  import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
72
72
  import { type IrType, type StructField, T, scalarTypeForAccess } from '../ir/types';
73
73
  import { type Gate, firstRejection } from '../l3/gates';
74
+ import { storeSigned } from './storesign';
74
75
  import { firstFreeStructIndex } from './structs';
75
76
 
76
77
  /** One variable-index access, resolved onto (base value, member offset). */
@@ -260,7 +261,9 @@ function stripExt(v: Value, defs: Map<Value, Op>): Value {
260
261
  }
261
262
  }
262
263
 
263
- /** `add(x, const)` read as (other operand, constant), or null. The add is commutative. */
264
+ /** `add(x, const)` read as (other operand, constant), or null. The add is commutative. ONE link, not
265
+ * a chain, and no `sub`: the shapes this pass reads are a single `add` of a scaled index or of a
266
+ * member offset. A chain of constant displacements is ir/alias.ts `constDisplacementOf`. */
264
267
  function constAddend(op: Op, defs: Map<Value, Op>): { of: Value; k: number } | null {
265
268
  if (op.opcode !== 'add' || op.operands.length !== 2) {
266
269
  return null;
@@ -389,7 +392,7 @@ function accessesByBase(fn: Fn, defs: Map<Value, Op>): Map<Value, MemberAccess[]
389
392
  op,
390
393
  off: add === null ? 0 : add.k,
391
394
  elemSize,
392
- signed: isLoad ? (op.attrs.signed as boolean) : elemSize === 4,
395
+ signed: isLoad ? (op.attrs.signed as boolean) : storeSigned(op, elemSize),
393
396
  isLoad,
394
397
  index: op.operands[1],
395
398
  addr: add === null ? null : op.operands[0],
@@ -583,8 +586,11 @@ export function recognizeMemberArrays(
583
586
  a.op.operands[0] = g.base;
584
587
  a.op.attrs.memberOff = a.off;
585
588
  if (!a.isLoad) {
586
- // an astore carries no signedness of its own — the member's decides how the index node
587
- // types, so the store spells through the same declaration the load does
589
+ // the member's declaration decides how the index node types, so the store spells through
590
+ // the same declaration the load does, over any stamp raise/storesign.ts left. No probed
591
+ // input reaches here with a stamp that disagrees: agbcc lifts a signed narrow constant as
592
+ // `neg`/`shl`, and the MIPS shapes (`p->t[i] = -1`, `-104` beside a u16 read) take a flat
593
+ // cast. One that does should keep its stamp, and the index node cast the cell to it.
588
594
  a.op.attrs.signed = g.members.find((m) => m.off === a.off)!.signed;
589
595
  }
590
596
  }