@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.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- 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
|
-
//
|
|
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).
|
|
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
|
|
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 {
|
|
14
|
-
import {
|
|
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)
|
|
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
|
|
110
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
|
36
|
-
// `
|
|
37
|
-
//
|
|
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.
|
package/src/raise/latch.ts
CHANGED
|
@@ -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 — `
|
|
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
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
|
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
|
-
//
|
|
587
|
-
//
|
|
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
|
}
|