@asmlift/core 0.8.1 → 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 (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
@@ -0,0 +1,464 @@
1
+ // asmlift — the soft-float boundary (L1 recognition): where integer words become floats.
2
+ //
3
+ // Four things cross it. A runtime helper whose op computes on floats (`isFloatHelper`) — the peer of
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). The
6
+ // result of an ordinary callee declared to return a floating type (its `floatReturn`). And a
7
+ // double literal, two constant words, where the target states their order
8
+ // (`TargetDescription.doubleArgWords`) and its compiler reads a literal back
9
+ // (`compilerBehaviors.roundTripsDoubleLiterals`). What sets all four apart from the integer
10
+ // helpers is the value's KIND: the words are bits in integer registers, and the pass re-types them
11
+ // as floats, which only data flow may justify — see the function below.
12
+ import { constWord } from '../ir/bits';
13
+ import { Fn, Op, Value, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
14
+ import { doubleBits, doubleOf } from '../ir/float-bits';
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';
18
+ import type { TargetDescription } from '../target';
19
+ import { RaiseUnsupportedError } from './errors';
20
+ import { isArgumentPair } from './pairparams';
21
+
22
+ /** Rewrite the float-arithmetic helper calls to the float ops, in place, each value at the width its
23
+ * helper's signature states, and hand a parameter the callee declares `double` (the frontend's
24
+ * `doubles` on the call) a double. Returns whether anything changed.
25
+ *
26
+ * A SOFT-FLOAT VALUE IS BITS IN INTEGER REGISTERS, AND ONLY DATA FLOW MAY READ THEM AS A FLOAT. A
27
+ * single travels in one register; a double in the pair a long long travels in, which the frontend
28
+ * builds the way it builds a long long's, `concat(lo = r0, hi = r1)` — but agbcc stores a double
29
+ * high word first (thumb.h:335 `FLOAT_WORDS_BIG_ENDIAN`): r0 holds the sign and exponent. Moving
30
+ * the value whole keeps it right, so a float may come only from where the ABI hands one over
31
+ * whole — this function's argument slot, or two consecutive ones for a double, or another of these
32
+ * helpers' results — and may go only into another of them, the return, or a parameter a callee
33
+ * declares `double`. Anything else refuses, and the call stays for `refuseUnmodelledHelpers` to
34
+ * gap:
35
+ * - an operand built from a load or two unrelated words, whose halves the int64 naming would put
36
+ * in the wrong order. Two CONSTANT words are the exception: they are a literal, read in the
37
+ * target's word order into an `fconst` (`a + 1.5` stages `0x3ff80000` in r2, its high word);
38
+ * - an argument the function also reads on its own, as a word or as one half of a pair;
39
+ * - a result read as a word or a half (`*(int *)&c` is a double's HIGH word, in r0), passed to
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`).
53
+ * A call refused takes every call its result feeds or is fed by with it, so what folds is a closed
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. */
60
+ export function recognizeFloatHelpers(fn: Fn, target: TargetDescription): boolean {
61
+ const table = target.runtimeHelpers ?? {};
62
+ let sites: Op[] = [];
63
+ const consumers = new Map<Op, readonly number[]>();
64
+ const def = new Map<Value, Op>();
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
67
+ const use = (v: Value, by: Op | null) => users.set(v, [...(users.get(v) ?? []), by]);
68
+ const helperOf = (op: Op) => (op.opcode === 'call' ? lookupHelper(table, String(op.attrs.target)) : undefined);
69
+ const sidesOf = (op: Op) => floatSidesOf(helperOf(op)!);
70
+ for (const block of fn.blocks) {
71
+ for (const op of block.ops) {
72
+ op.results.forEach((r) => def.set(r, op));
73
+ op.operands.forEach((o) => use(o, op));
74
+ op.successors.forEach((s) => s.args.forEach((a) => use(a, null)));
75
+ const helper = helperOf(op);
76
+ if (
77
+ helper &&
78
+ isFloatHelper(helper) &&
79
+ arrivesAsDeclared(
80
+ helper,
81
+ op.operands.map((o) => o.type),
82
+ op.results.map((r) => r.type),
83
+ )
84
+ ) {
85
+ sites.push(op);
86
+ } else if (!helper && op.opcode === 'call' && Array.isArray(op.attrs.doubles)) {
87
+ consumers.set(op, op.attrs.doubles as number[]);
88
+ }
89
+ if (!helper && op.opcode === 'call' && typeof op.attrs.floatReturn === 'string') {
90
+ returners.set(op, op.attrs.floatReturn);
91
+ }
92
+ }
93
+ }
94
+ if (!sites.length && !consumers.size && !returners.size) {
95
+ return false;
96
+ }
97
+ // A projection nothing reads is not a read of a half: `dce` runs ahead of this pass only after a
98
+ // fold that changed the IR, so the frontend's split of the result back into its registers may stand.
99
+ const deadHalf = (u: Op | null) =>
100
+ u !== null && (u.opcode === 'lo32' || u.opcode === 'hi32') && !users.has(u.results[0]);
101
+ // A USE THAT TAKES THE VALUE AS A FLOAT: a helper the fold keeps, or a parameter the callee declares
102
+ // `double` (the frontend's `doubles`) — and only that parameter, so the same value handed to the
103
+ // same call as a word is a word read.
104
+ const floatUse = (u: Op | null, v: Value, calls: ReadonlySet<Op>): boolean =>
105
+ u !== null &&
106
+ ((calls.has(u) && sidesOf(u).operands) ||
107
+ (consumers.has(u) && u.operands.every((o, i) => o !== v || consumers.get(u)!.includes(i))));
108
+ const floatsOnly = (v: Value, calls: ReadonlySet<Op>) => (users.get(v) ?? []).every((u) => floatUse(u, v, calls));
109
+ const entry = fn.blocks[0].params;
110
+ // A `concat` of the same two slots in the same order as `d`: the frontend builds one afresh at each
111
+ // call it hands the pair to, so `g(x); g(x)` reads one parameter through two of them.
112
+ const samePair = (u: Op | null, d: Op): u is Op =>
113
+ u?.opcode === 'concat' && u.operands[0] === d.operands[0] && u.operands[1] === d.operands[1];
114
+ // The argument slot itself, or the pair of them `isArgumentPair` names, read by nothing but
115
+ // `concat`s of that pair, each taken as a float; `arrivesAsDeclared` has already held each operand
116
+ // to its parameter's width.
117
+ const argument = (v: Value, calls: ReadonlySet<Op>): boolean => {
118
+ const d = def.get(v);
119
+ if (d === undefined) {
120
+ return entry.includes(v) && floatsOnly(v, calls);
121
+ }
122
+ return (
123
+ isArgumentPair(entry, d, (p) => users.get(p)!.filter((u) => !samePair(u, d)).length + 1) &&
124
+ users.get(d.operands[0])!.every((u) => samePair(u, d) && floatsOnly(u.results[0], calls))
125
+ );
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;
131
+ const fromCall = (v: Value, calls: ReadonlySet<Op>) => {
132
+ const d = def.get(v);
133
+ return d !== undefined && ((calls.has(d) && sidesOf(d).result) || declaredSingle(d));
134
+ };
135
+ // A CONSTANT WORD, as a compiler may build one in two instructions: agbcc moves a word that is a
136
+ // shifted byte as `mov`+`lsl` (thumb.md:66, `thumb_shiftable_const` at thumb.c:61) and one in
137
+ // -255..-1 as `mov`+`neg` (thumb.md:88), so the high word of 2.0 reaches r3 as `0x80 << 23`. Its
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;
141
+ // A LITERAL IS TWO CONSTANT WORDS, HIGH WORD FIRST on a target whose double crosses a call in
142
+ // general words (`TargetDescription.doubleArgWords`) — the order that makes the long long naming
143
+ // of the same pair another number. Its bits, or undefined where the pair is no literal, the target
144
+ // states no such double, its compiler is not known to read the literal back
145
+ // (`compilerBehaviors.roundTripsDoubleLiterals`), or the double is not finite — which no C literal
146
+ // spells.
147
+ const literalBits = (v: Value): string | undefined => {
148
+ const d = def.get(v);
149
+ const [first, second] = (d?.opcode === 'concat' ? d.operands : []).map(wordOf);
150
+ if (
151
+ target.doubleArgWords === undefined ||
152
+ target.compilerBehaviors.roundTripsDoubleLiterals !== true ||
153
+ first === undefined ||
154
+ second === undefined
155
+ ) {
156
+ return undefined;
157
+ }
158
+ const bits = doubleBits(first, second);
159
+ return Number.isFinite(doubleOf(bits)) ? bits : undefined;
160
+ };
161
+ const literal = (v: Value, calls: ReadonlySet<Op>) => literalBits(v) !== undefined && floatsOnly(v, calls);
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
+ });
180
+ for (;;) {
181
+ const calls = new Set(sites);
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
+ });
191
+ if (kept.length === sites.length) {
192
+ break;
193
+ }
194
+ sites = kept;
195
+ }
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
+ }
235
+ // A DECLARED DOUBLE HAS NO FALLBACK. A helper the fold refuses stays a call for
236
+ // `refuseUnmodelledHelpers` to gap, but an ordinary callee is no helper, and its two words handed
237
+ // on as integers are the wrong number — so a double parameter nothing here can hand a double
238
+ // refuses the function. It counts arguments as the source does: an `sret` call's operand 0, the
239
+ // hidden pointer, is not one.
240
+ for (const [op, at] of consumers) {
241
+ for (const i of at) {
242
+ if (!source(op.operands[i], calls)) {
243
+ const arg = i + (op.attrs.sret === true ? 0 : 1);
244
+ throw new RaiseUnsupportedError(
245
+ `cannot lift '${fn.name}': argument ${arg} of the call to '${String(op.attrs.target)}' ` +
246
+ 'is a `double` its callee declares, and its two words are not a double this function was ' +
247
+ 'handed, a runtime helper returned or a finite literal staged, moved whole',
248
+ );
249
+ }
250
+ }
251
+ }
252
+ if (!sites.length && !consumers.size && !returned.size) {
253
+ return false;
254
+ }
255
+ const floatOf = new Map<Value, Value>();
256
+ const float = (bits: number) => mkValue(bits > 32 ? T.f64() : T.f32());
257
+ // the `fconst` each literal's `concat` becomes, and the ops computing the constant words only it read
258
+ const literals = new Map<Op, Op>();
259
+ const spent = new Set<Op>();
260
+ const spend = (w: Value, by: Op) => {
261
+ const d = def.get(w)!;
262
+ if (users.get(w)!.every((u) => u === by)) {
263
+ spent.add(d);
264
+ d.operands.forEach((o) => spend(o, d));
265
+ }
266
+ };
267
+ // One parameter in its first slot's place, which is where the ABI put the value.
268
+ const retype = (o: Value, bits: number) => {
269
+ if (floatOf.has(o) || fromCall(o, calls)) {
270
+ return;
271
+ }
272
+ const d = def.get(o);
273
+ const pattern = literalBits(o);
274
+ if (pattern !== undefined) {
275
+ const whole = float(64);
276
+ literals.set(d!, mkOp('fconst', { results: [whole], attrs: { bits: pattern } }));
277
+ d!.operands.forEach((w) => spend(w, d!));
278
+ floatOf.set(o, whole);
279
+ return;
280
+ }
281
+ const slots = d === undefined ? [o] : d.operands;
282
+ const whole = float(bits);
283
+ entry.splice(entry.indexOf(slots[0]), slots.length, whole);
284
+ for (const u of d === undefined ? [] : users.get(slots[0])!) {
285
+ if (samePair(u, d!)) {
286
+ floatOf.set(u.results[0], whole);
287
+ }
288
+ }
289
+ floatOf.set(o, whole);
290
+ };
291
+ for (const [op, now] of returned) {
292
+ floatOf.set(op.results[0], now);
293
+ }
294
+ for (const op of sites) {
295
+ const helper = helperOf(op)!;
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
+ }
303
+ }
304
+ for (const [op, at] of consumers) {
305
+ at.forEach((i) => retype(op.operands[i], 64));
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
+ }
325
+ for (const block of fn.blocks) {
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
+ }
337
+ if (calls.has(op)) {
338
+ const sides = sidesOf(op);
339
+ return [
340
+ helperOp(
341
+ helperOf(op)!.op!,
342
+ String(op.attrs.target),
343
+ op.operands.map((o) => (sides.operands ? floatOf.get(o)! : o)),
344
+ sides.result ? floatOf.get(op.results[0])! : op.results[0],
345
+ ),
346
+ ];
347
+ }
348
+ if (literals.has(op)) {
349
+ return [literals.get(op)!];
350
+ }
351
+ const retired = op.results.length === 1 && floatOf.has(op.results[0]);
352
+ return retired || spent.has(op) || (deadHalf(op) && floatOf.has(op.operands[0])) ? [] : [op];
353
+ });
354
+ }
355
+ for (const [was, now] of floatOf) {
356
+ replaceAllUsesWith(fn, was, now);
357
+ }
358
+ return true;
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
+ }
@@ -505,11 +505,12 @@ export const SHAPE_GATES: readonly Gate<ShapeEvidence>[] = [
505
505
  id: 'index-materialized-first',
506
506
  why: 'a scaling of the index precedes the pool load IN ITS OWN BLOCK — the pointer path, which is the cast spelling',
507
507
  sound: true,
508
- // Its SECOND reaching fixture is the compiler-shaped one: `…and the same function cast-spelled
509
- // still refuses, on the access that CAN be compared` runs the real agbcc output for a function
510
- // that subscripts the name once outside a loop and once inside it. It is not named in
511
- // `guardedBy` because that field is matched against a single test title (gate-contract.ts).
512
- guardedBy: 'global-array-shape.test.ts: one index-first access refuses a symbol the others license',
508
+ // Its SECOND reaching fixture is the compiler-shaped one: the real agbcc output for a function
509
+ // that subscripts the name once outside a loop and once inside it.
510
+ guardedBy: [
511
+ 'global-array-shape.test.ts: one index-first access refuses a symbol the others license',
512
+ 'global-array-shape.test.ts: …and the same function cast-spelled still refuses, on the access that CAN be compared',
513
+ ],
513
514
  rejects: anIndexFirstAccess,
514
515
  },
515
516
  {
@@ -955,6 +956,11 @@ export function inferGlobalArrays(
955
956
  }
956
957
  const pos = positions(fn);
957
958
  for (const [sym, accs] of accessesBySymbol(fn, target, gates.address)) {
959
+ // a function-scope static's shape is its definition's (structure/local-statics.ts), not an
960
+ // inference, and it is declared in the body rather than assumed
961
+ if (fn.localObjects?.has(sym)) {
962
+ continue;
963
+ }
958
964
  const si = Array.isArray(accs) ? shapeOf(accs, pos, gates.shape) : null;
959
965
  if (si !== null) {
960
966
  out.set(sym, si);
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.
@@ -93,11 +93,8 @@ function keyOf(op: Op): string {
93
93
  return `${op.opcode}(${attrs})`;
94
94
  }
95
95
 
96
- /**
97
- * Collapse operand-free pure definitions that share a key down to one apiece, defined in the entry
98
- * block. Returns the number of definitions removed (0 when nothing changed).
99
- */
100
- export function numberPureValues(fn: Fn): number {
96
+ /** The blocks numbering looks in: those the entry reaches through any op's successors. */
97
+ function numberedBlocks(fn: Fn): Set<Block> {
101
98
  // REACHABLE blocks only. The entry dominates everything REACHABLE — it does not dominate an
102
99
  // unreachable block, and verify()'s dominator fixpoint models that faithfully (a block with no
103
100
  // predecessors converges to dom = {itself}). Numbering a group with a member in such a block
@@ -120,6 +117,15 @@ export function numberPureValues(fn: Fn): number {
120
117
  if (fn.blocks[0]) {
121
118
  walk(fn.blocks[0]);
122
119
  }
120
+ return reachable;
121
+ }
122
+
123
+ /**
124
+ * Collapse operand-free pure definitions that share a key down to one apiece, defined in the entry
125
+ * block. Returns the number of definitions removed (0 when nothing changed).
126
+ */
127
+ export function numberPureValues(fn: Fn): number {
128
+ const reachable = numberedBlocks(fn);
123
129
  const groups = new Map<string, { op: Op; block: number }[]>();
124
130
  fn.blocks.forEach((b, bi) => {
125
131
  if (!reachable.has(b)) {
@@ -164,3 +170,18 @@ export function numberPureValues(fn: Fn): number {
164
170
  entry.ops.unshift(...hoisted);
165
171
  return removed;
166
172
  }
173
+
174
+ /** The class {@link numberPureValues} puts a value in: the key of a numberable definition in a block
175
+ * it numbers, or the value itself. Two values of one class are one value once it has run. */
176
+ export function valueNumberOf(fn: Fn): (v: Value) => Value | string {
177
+ const reachable = numberedBlocks(fn);
178
+ const key = new Map<Value, string>();
179
+ for (const b of reachable) {
180
+ for (const op of b.ops) {
181
+ if (isNumberable(op.opcode) && op.results.length === 1) {
182
+ key.set(op.results[0], keyOf(op));
183
+ }
184
+ }
185
+ }
186
+ return (v) => key.get(v) ?? v;
187
+ }