@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
package/src/runtime-helpers.ts
CHANGED
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
// REGISTERS the list occupies, and `irWidthOf` gives the IR width one parameter ARRIVES at once a
|
|
30
30
|
// frontend has paired those registers up.
|
|
31
31
|
import { type Op, type Value, mkOp } from './ir/core';
|
|
32
|
-
import {
|
|
32
|
+
import { FLOAT_COMPUTING, FLOAT_TO_INT, INT_TO_FLOAT, NEGATED_FCMP, type Opcode, WIDE_BITS } from './ir/opcodes';
|
|
33
33
|
import { type IrType, intWidth } from './ir/types';
|
|
34
34
|
import { type Prototypes, wordsOf } from './proto';
|
|
35
|
+
import type { TargetDescription } from './target';
|
|
35
36
|
|
|
36
37
|
export interface RuntimeHelper {
|
|
37
38
|
/** the op this helper computes, where asmlift has one. Absent for a helper it can only SIGN. */
|
|
@@ -40,6 +41,19 @@ export interface RuntimeHelper {
|
|
|
40
41
|
params: readonly number[];
|
|
41
42
|
/** the returned width: 0 (void), 32, or 64 (a register PAIR). */
|
|
42
43
|
returns: 0 | 32 | 64;
|
|
44
|
+
/** A THREE-WAY FLOAT COMPARE: its result's SIGN orders its two operands (negative, zero, positive
|
|
45
|
+
* for less, equal, greater), and this is the value it returns where either is a NaN. A compare
|
|
46
|
+
* folds through the integer test its caller applies to the result (raise/floathelpers.ts), so it
|
|
47
|
+
* carries this in place of an `op`. */
|
|
48
|
+
unordered?: 1 | -1;
|
|
49
|
+
/** THE RELATION THE SOURCE SPELLED, for a compare helper whose relation and its negation are both
|
|
50
|
+
* C operators — `__eqsf2` for `==`, `__nesf2` for `!=`. agbcc's gcc folds `!(a == b)` into
|
|
51
|
+
* `a != b` before it picks a helper, so the helper names the relation the source wrote, and a
|
|
52
|
+
* lift that spells its negation (or the negation's `!`) recompiles to the other helper. The fold
|
|
53
|
+
* stamps it on the compare as `attrs.relation` (raise/floathelpers.ts), and the structurer keeps
|
|
54
|
+
* that relation's spelling by choosing the arm order instead (l3/ast.ts `Expr.helperRel`). The
|
|
55
|
+
* ordered four need none: `!(a > b)` keeps `__gtsf2`, and no other helper computes it. */
|
|
56
|
+
relation?: 'fcmp_oeq' | 'fcmp_une';
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
/** The entry a target's table holds for `callee`, or undefined. THE ONE READER, because the table
|
|
@@ -118,19 +132,34 @@ export function helperOp(opcode: Opcode, helper: string, operands: readonly Valu
|
|
|
118
132
|
return mkOp(opcode, { operands: [...operands], results: [result], attrs: { helper } });
|
|
119
133
|
}
|
|
120
134
|
|
|
121
|
-
/** Whether a helper computes on floats: its op is a float op,
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* values re-typed as floats
|
|
135
|
+
/** Whether a helper computes on floats: its op is a float op, a conversion or a compare, so the
|
|
136
|
+
* operands and the result that op takes as floats are floats of the widths its signature states,
|
|
137
|
+
* carried in integer registers by a soft-float runtime. Every reader of the table asks this and
|
|
138
|
+
* not the width, because the two families fold into different IR: an integer helper into its op
|
|
139
|
+
* over the same values, a float helper into its op over values re-typed as floats where the op
|
|
140
|
+
* takes one (`raise/floathelpers.ts`). */
|
|
126
141
|
export function isFloatHelper(h: RuntimeHelper): boolean {
|
|
127
|
-
return h.op !== undefined &&
|
|
142
|
+
return h.unordered !== undefined || (h.op !== undefined && FLOAT_COMPUTING.has(h.op));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Which sides of a float helper are floats: its operands (one kind for all of them, in every op
|
|
146
|
+
* here) and its result. A conversion has an integer on one side; a compare returns one. */
|
|
147
|
+
export function floatSidesOf(h: RuntimeHelper): { operands: boolean; result: boolean } {
|
|
148
|
+
const op = h.op ?? '';
|
|
149
|
+
return {
|
|
150
|
+
operands: !INT_TO_FLOAT.has(op),
|
|
151
|
+
result: h.unordered === undefined && !FLOAT_TO_INT.has(op) && NEGATED_FCMP[op] === undefined,
|
|
152
|
+
};
|
|
128
153
|
}
|
|
129
154
|
|
|
130
155
|
/** Signatures for a target's helpers, in the WORD arity the frontend's prototype lookup speaks.
|
|
131
156
|
* Consumed behind any caller-supplied prototype — the project's own headers win. */
|
|
132
|
-
export function helperPrototypes(
|
|
133
|
-
|
|
157
|
+
export function helperPrototypes(
|
|
158
|
+
target: Pick<TargetDescription, 'runtimeHelpers' | 'evenWordAlignedArgs'>,
|
|
159
|
+
): Prototypes {
|
|
160
|
+
return Object.fromEntries(
|
|
161
|
+
Object.entries(target.runtimeHelpers ?? {}).map(([sym, h]) => [sym, { params: wordsOf(h.params, target) }]),
|
|
162
|
+
);
|
|
134
163
|
}
|
|
135
164
|
|
|
136
165
|
/** agbcc's runtime, which is GCC 2.9's libgcc for ARM.
|
|
@@ -142,20 +171,32 @@ export function helperPrototypes(table: Readonly<Record<string, RuntimeHelper>>
|
|
|
142
171
|
* operand SET-UP instead — `asr rN,rM,#31` per half for signed, `mov rN,#0` for unsigned. The
|
|
143
172
|
* DIVISIONS do split, and their entries say so.
|
|
144
173
|
*
|
|
145
|
-
* THE
|
|
146
|
-
* `init_floating_libfuncs` names
|
|
147
|
-
* `thumb.md` has no DF pattern but the move, so every one of them is a call;
|
|
148
|
-
* long's (thumb.h:632, 655).
|
|
174
|
+
* THE FLOAT ARITHMETIC OF BOTH PRECISIONS IS HERE, AND FOLDS. `optabs.c:4022`
|
|
175
|
+
* `init_floating_libfuncs` names each mode's add, sub, mul, div (4129-4140) and neg (4155), and
|
|
176
|
+
* `thumb.md` has no SF or DF pattern but the move, so every one of them is a call; a single
|
|
177
|
+
* travels as an int's word and a double as a long long's pair (thumb.h:632, 655).
|
|
178
|
+
* `raise/floathelpers.ts` folds them to the float ops over a `float` or a `double`.
|
|
149
179
|
*
|
|
150
|
-
* NAMING THEM IS A TRADE, and these
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
180
|
+
* NAMING THEM IS A TRADE, and these ten make it: a call the fold refuses declines where an unnamed
|
|
181
|
+
* helper would publish as a pass-through. So a function whose float arithmetic feeds a compare, a
|
|
182
|
+
* conversion or an ordinary call declines, where unnamed it would print `__muldf3(); g();`. For
|
|
183
|
+
* the singles that pass-through is wrong C: unnamed, `__mulsf3`'s arity is GUESSED (frontend/ssa.ts
|
|
184
|
+
* `fallbackArgc`), and a guess drops what the function passes on unchanged — `a * b + c` as
|
|
185
|
+
* `__addsf3(__mulsf3(), a2)`, `a + b` as `__addsf3()`.
|
|
186
|
+
*
|
|
187
|
+
* THE CONVERSIONS FOLD TOO, to the cast each computes (`fptosi`, `sitofp`, `fpext` …), with an
|
|
188
|
+
* integer on one side that `raise/floathelpers.ts` takes as any integer. THE COMPARES fold through
|
|
189
|
+
* the test their caller applies to the integer they return, by libgcc's contract for it
|
|
190
|
+
* (`unordered`), and one whose result is read any other way refuses (`refuseUnmodelledHelpers`).
|
|
191
|
+
* Unnamed, each would be the same guess, dropping the argument passed on unchanged and matching
|
|
192
|
+
* byte for byte: `(int)x` as `__fixsfsi()`, `(float)i` as `__floatsisf()`, `a > b` as
|
|
193
|
+
* `__gtsf2() > 0`. The names are the ones agbcc
|
|
194
|
+
* emits for every compare, every conversion between `int`, `unsigned`, `long long` and either
|
|
195
|
+
* precision, and every widening and narrowing between the two precisions (compiled evidence: one
|
|
196
|
+
* function per operation at the canonical flags). There is no conversion of an unsigned integer
|
|
197
|
+
* INTO a float: agbcc open-codes `(float)u` around `__floatsisf` and `__addsf3`. `__cmpdi2`, which
|
|
198
|
+
* it also calls converting an `unsigned long long`, is an integer compare and outside this
|
|
199
|
+
* family. */
|
|
159
200
|
export const AGBCC_RUNTIME_HELPERS: Readonly<Record<string, RuntimeHelper>> = {
|
|
160
201
|
// 32-bit software division — the ops `raise/softdiv.ts` rewrites, gated on the target having no
|
|
161
202
|
// hardware divider, which is what those four are about.
|
|
@@ -180,6 +221,45 @@ export const AGBCC_RUNTIME_HELPERS: Readonly<Record<string, RuntimeHelper>> = {
|
|
|
180
221
|
__muldf3: { op: 'fmul', params: [64, 64], returns: 64 },
|
|
181
222
|
__divdf3: { op: 'fdiv', params: [64, 64], returns: 64 },
|
|
182
223
|
__negdf2: { op: 'fneg', params: [64], returns: 64 },
|
|
224
|
+
// Single arithmetic, one float in one register each way (thumb.h:632's word).
|
|
225
|
+
__addsf3: { op: 'fadd', params: [32, 32], returns: 32 },
|
|
226
|
+
__subsf3: { op: 'fsub', params: [32, 32], returns: 32 },
|
|
227
|
+
__mulsf3: { op: 'fmul', params: [32, 32], returns: 32 },
|
|
228
|
+
__divsf3: { op: 'fdiv', params: [32, 32], returns: 32 },
|
|
229
|
+
__negsf2: { op: 'fneg', params: [32], returns: 32 },
|
|
230
|
+
// The compares, one int out of two floats of either precision, each libgcc's three-way
|
|
231
|
+
// `__fpcmp_parts` when both are ordered (fp-bit.c:1117-1188). On a NaN each returns the value its
|
|
232
|
+
// own relation tests FALSE on — `== 0`, `< 0`, `<= 0` are false on 1, `> 0`, `>= 0` on -1 — except
|
|
233
|
+
// `__nesf2`, whose 1 makes `!=` true (fp-bit.c:1214-1309). agbcc tests the result against 0 with
|
|
234
|
+
// the relation the helper is named for.
|
|
235
|
+
__eqsf2: { params: [32, 32], returns: 32, unordered: 1, relation: 'fcmp_oeq' },
|
|
236
|
+
__nesf2: { params: [32, 32], returns: 32, unordered: 1, relation: 'fcmp_une' },
|
|
237
|
+
__gtsf2: { params: [32, 32], returns: 32, unordered: -1 },
|
|
238
|
+
__gesf2: { params: [32, 32], returns: 32, unordered: -1 },
|
|
239
|
+
__ltsf2: { params: [32, 32], returns: 32, unordered: 1 },
|
|
240
|
+
__lesf2: { params: [32, 32], returns: 32, unordered: 1 },
|
|
241
|
+
__eqdf2: { params: [64, 64], returns: 32, unordered: 1, relation: 'fcmp_oeq' },
|
|
242
|
+
__nedf2: { params: [64, 64], returns: 32, unordered: 1, relation: 'fcmp_une' },
|
|
243
|
+
__gtdf2: { params: [64, 64], returns: 32, unordered: -1 },
|
|
244
|
+
__gedf2: { params: [64, 64], returns: 32, unordered: -1 },
|
|
245
|
+
__ltdf2: { params: [64, 64], returns: 32, unordered: 1 },
|
|
246
|
+
__ledf2: { params: [64, 64], returns: 32, unordered: 1 },
|
|
247
|
+
// The conversions: to and from a word, to and from a long long, and between the precisions.
|
|
248
|
+
// `__fix*` truncates toward zero, as C's conversion out of a float does.
|
|
249
|
+
__fixsfsi: { op: 'fptosi', params: [32], returns: 32 },
|
|
250
|
+
__fixunssfsi: { op: 'fptoui', params: [32], returns: 32 },
|
|
251
|
+
__floatsisf: { op: 'sitofp', params: [32], returns: 32 },
|
|
252
|
+
__fixdfsi: { op: 'fptosi', params: [64], returns: 32 },
|
|
253
|
+
__fixunsdfsi: { op: 'fptoui', params: [64], returns: 32 },
|
|
254
|
+
__floatsidf: { op: 'sitofp', params: [32], returns: 64 },
|
|
255
|
+
__fixsfdi: { op: 'fptosi', params: [32], returns: 64 },
|
|
256
|
+
__fixunssfdi: { op: 'fptoui', params: [32], returns: 64 },
|
|
257
|
+
__floatdisf: { op: 'sitofp', params: [64], returns: 32 },
|
|
258
|
+
__fixdfdi: { op: 'fptosi', params: [64], returns: 64 },
|
|
259
|
+
__fixunsdfdi: { op: 'fptoui', params: [64], returns: 64 },
|
|
260
|
+
__floatdidf: { op: 'sitofp', params: [64], returns: 64 },
|
|
261
|
+
__extendsfdf2: { op: 'fpext', params: [32], returns: 64 },
|
|
262
|
+
__truncdfsf2: { op: 'fptrunc', params: [64], returns: 32 },
|
|
183
263
|
};
|
|
184
264
|
|
|
185
265
|
/** CodeWarrior's PowerPC runtime (`Runtime.PPCEABI.H`), as the GameCube projects vendor it.
|
|
@@ -220,6 +300,44 @@ export const PPC_MWCC_RUNTIME_HELPERS: Readonly<Record<string, RuntimeHelper>> =
|
|
|
220
300
|
__shr2u: { op: 'shr_u', params: [64, 32], returns: 64 },
|
|
221
301
|
};
|
|
222
302
|
|
|
303
|
+
/** A compiler runtime's OUT-OF-LINE REGISTER SAVE AND RESTORE ROUTINES: a prologue that saves a run
|
|
304
|
+
* of callee-saved registers by calling into the runtime instead of storing each one inline. A
|
|
305
|
+
* compiler fact, like the table above, and it hangs off `TargetDescription` for the same reason:
|
|
306
|
+
* which compiler emits the family, at which level, is measured per build, and a frontend that
|
|
307
|
+
* hard-coded the names would recognise them on a target whose runtime has no such routine.
|
|
308
|
+
*
|
|
309
|
+
* A call to one is FRAME BOOKKEEPING, not a call: it writes no register but the link register, and
|
|
310
|
+
* the frame address in `frameReg` is the only thing it reads (frontend/ppc.ts `saveRoutine`). */
|
|
311
|
+
export interface RegisterSaveRoutines {
|
|
312
|
+
/** the register a routine takes its frame address in. The save area is the words just BELOW it. */
|
|
313
|
+
frameReg: string;
|
|
314
|
+
/** every routine name, with three groups: `save` or `rest`, the register file (`gpr`/`fpr`), and
|
|
315
|
+
* the first register the routine moves — it moves that one up to the file's last, 31. */
|
|
316
|
+
name: RegExp;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The routine a callee symbol names under `routines`, or null when it names none. */
|
|
320
|
+
export function registerSaveRoutineOf(
|
|
321
|
+
routines: RegisterSaveRoutines | undefined,
|
|
322
|
+
sym: string,
|
|
323
|
+
): { sym: string; restore: boolean; fpr: boolean; first: number } | null {
|
|
324
|
+
const m = routines?.name.exec(sym);
|
|
325
|
+
return m ? { sym: m[0], restore: m[1] === 'rest', fpr: m[2] === 'fpr', first: Number(m[3]) } : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** CodeWarrior's `_savegpr_NN` / `_restgpr_NN` and `_savefpr_NN` / `_restfpr_NN`, called after
|
|
329
|
+
* `addi r11,r1,N`. `_savegpr_NN` stores rNN..r31 into the 4*(32-NN) bytes just below r11, and
|
|
330
|
+
* `_savefpr_NN` stores fNN..f31 into the 8*(32-NN) bytes below it.
|
|
331
|
+
*
|
|
332
|
+
* COMPILED, per build: 2.4.2 and 2.4.7 emit the family at every level (`-O4,p`, `-O4,s`, `-O0,p`).
|
|
333
|
+
* 2.3.3 never does at Pikmin's `-O4,p`, where it saves with `stmw` and inline `stfd`, but does at
|
|
334
|
+
* `-O0,p` and `-O4,s`; all three builds share `PPC_MWCC`, so the description carries it. The bench's
|
|
335
|
+
* feature tagger reads this same pattern (apps/benchmark `features.ts`). */
|
|
336
|
+
export const PPC_MWCC_SAVE_ROUTINES: RegisterSaveRoutines = {
|
|
337
|
+
frameReg: 'r11',
|
|
338
|
+
name: /^_(save|rest)(gpr|fpr)_(\d+)$/,
|
|
339
|
+
};
|
|
340
|
+
|
|
223
341
|
/** IDO 7.1's runtime for the 64-bit operations MIPS II has no instruction for, a family of its own
|
|
224
342
|
* (`__ll_*` for signed, `__ull_*` for unsigned): a scan for `__*di3` reports zero on it.
|
|
225
343
|
*
|
|
@@ -892,7 +892,7 @@ export interface AnalyzeOptions {
|
|
|
892
892
|
* A compiler behavior (TargetDescription.compilerBehaviors `readsStayWhereWritten`), not
|
|
893
893
|
* a differ-refereed variation, and that distinction is the argument: a variation exists where the asm
|
|
894
894
|
* UNDERDETERMINES the source, some pass having collapsed two spellings onto one output
|
|
895
|
-
* (`/
|
|
895
|
+
* (`/signed-cmp`'s non-negativity proof is the type case). Where the compiler emits a read in the
|
|
896
896
|
* block the source spelled it in, re-spelling it at the block the asm read in reproduces that
|
|
897
897
|
* asm while the sunk spelling is one it emits only for a source that read per arm — nothing for
|
|
898
898
|
* the differ to referee, and the extra candidate is pure cost. Which compilers may declare
|
|
@@ -1521,6 +1521,9 @@ interface LoopRules {
|
|
|
1521
1521
|
/** The def's value enters some loop's header live and every consumer sits outside that loop, as
|
|
1522
1522
|
* does the def: the value is carried ACROSS the loop, not into it. */
|
|
1523
1523
|
liveAcrossLoop(def: Op, r: Value, consumers: Op[]): boolean;
|
|
1524
|
+
/** `liveAcrossLoop`, and neither the def nor any consumer sits in any loop, enclosing ones
|
|
1525
|
+
* included: never a value the compiler could have hoisted out of one. */
|
|
1526
|
+
carriedPastLoops(def: Op, r: Value, consumers: Op[]): boolean;
|
|
1524
1527
|
/** Does `r`, computed by `op` in `L`'s body, read a loop variable the update overwrites? */
|
|
1525
1528
|
readsPreUpdate(L: BottomTestedLoop, op: Op, r: Value): boolean;
|
|
1526
1529
|
escapesAheadOfUpdate(op: Op, r: Value, consumers: Op[]): boolean;
|
|
@@ -1551,6 +1554,9 @@ function makeLoopRules(a: {
|
|
|
1551
1554
|
!L.body.has(opBlock.get(def)!) &&
|
|
1552
1555
|
consumers.every((c) => !L.body.has(opBlock.get(c)!)),
|
|
1553
1556
|
);
|
|
1557
|
+
const carriedPastLoops = (def: Op, r: Value, consumers: Op[]): boolean =>
|
|
1558
|
+
liveAcrossLoop(def, r, consumers) &&
|
|
1559
|
+
loopBodies.every((L) => !L.body.has(opBlock.get(def)!) && consumers.every((c) => !L.body.has(opBlock.get(c)!)));
|
|
1554
1560
|
/** THE PRE-UPDATE ESCAPE. A value a bottom-tested loop computes and the code after it reads,
|
|
1555
1561
|
* whose expression reads a loop variable — `do { s = &f->v; f = f->next; } while (--n); *out =
|
|
1556
1562
|
* s;`. Inlined at the post-loop read it re-reads that variable's NAME, which by then holds the
|
|
@@ -1710,6 +1716,7 @@ function makeLoopRules(a: {
|
|
|
1710
1716
|
multiBlockHeaders,
|
|
1711
1717
|
bottomTested,
|
|
1712
1718
|
liveAcrossLoop,
|
|
1719
|
+
carriedPastLoops,
|
|
1713
1720
|
readsPreUpdate,
|
|
1714
1721
|
escapesAheadOfUpdate,
|
|
1715
1722
|
rebuiltPast,
|
|
@@ -2042,6 +2049,20 @@ function namePureOp(rules: MaterializeRules, state: MaterializeState, op: Op, b:
|
|
|
2042
2049
|
} else if (op.opcode !== 'const' && pr && copyInterdependent.has(pr) && !scopes.addressCone(op)) {
|
|
2043
2050
|
materialize.add(op);
|
|
2044
2051
|
}
|
|
2052
|
+
// A PURE VALUE CARRIED ACROSS A LOOP, computed and read outside every loop, as `nameAccess` names
|
|
2053
|
+
// a read (`carriedPastLoops`; a value read inside a loop is `/expr-home`'s question, where loop.c's
|
|
2054
|
+
// invariant motion may have put it). `j = n * 3; for (…) { if (t[i] == 7) { gHit = j; break; } }`
|
|
2055
|
+
// computes `j` ahead of the loop and holds it in a register to the break arm, which sits outside
|
|
2056
|
+
// the natural loop. Inlined, `gHit = n * 3` moves the computation into the arm, and every profile
|
|
2057
|
+
// compiles it there (IDO scores the two alike). A `const` is re-materialized where it is used, and
|
|
2058
|
+
// an address loses its inline byte-stride cast standing alone and is what a compiler CSEs and
|
|
2059
|
+
// moves freely, so neither is named.
|
|
2060
|
+
if (op.opcode !== 'const' && pr && useSitesOf.has(pr) && !scopes.addressCone(op) && !rendersAsAddress(op)) {
|
|
2061
|
+
const cons = [...new Set((useSitesOf.get(pr) ?? []).map((s) => s.op))];
|
|
2062
|
+
if (loops.carriedPastLoops(op, pr, cons)) {
|
|
2063
|
+
materialize.add(op);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2045
2066
|
// A float PRODUCT read by a float add or subtract, directly or through a negation, is named
|
|
2046
2067
|
// on a compiler that contracts (target.ts `contractsFloatProducts`): it fuses only WITHIN
|
|
2047
2068
|
// one expression, so the inline `a * b + c` recompiles to one `fmadds` — one rounding, a
|
|
@@ -2248,7 +2269,7 @@ function isBarrier(rules: MaterializeRules, materialize: ReadonlySet<Op>, access
|
|
|
2248
2269
|
// order is the compiler's to choose (`gY = VCOUNT - TM0CNT_L` reads VCOUNT first); a
|
|
2249
2270
|
// qualified read spelled as a bare statement because nothing uses it is an access too.
|
|
2250
2271
|
if (once && !isCall && (x.opcode === 'load' || x.opcode === 'aload') && counted(x)) {
|
|
2251
|
-
return
|
|
2272
|
+
return !interchangeableReads(rules, x, op);
|
|
2252
2273
|
}
|
|
2253
2274
|
if (!isCall) {
|
|
2254
2275
|
return false;
|
|
@@ -2261,6 +2282,148 @@ function isBarrier(rules: MaterializeRules, materialize: ReadonlySet<Op>, access
|
|
|
2261
2282
|
return false;
|
|
2262
2283
|
}
|
|
2263
2284
|
|
|
2285
|
+
const COMMUTATIVE_OPS: ReadonlySet<string> = new Set(['add', 'mul', 'and', 'or', 'xor']);
|
|
2286
|
+
|
|
2287
|
+
/** One op's identity apart from its operands: opcode, result count and attributes, keys sorted. */
|
|
2288
|
+
const opShape = (op: Op): string =>
|
|
2289
|
+
`${op.opcode}/${op.results.length}/${JSON.stringify(Object.entries(op.attrs).sort(([a], [b]) => a.localeCompare(b)))}`;
|
|
2290
|
+
|
|
2291
|
+
/** Do two values name the same address: one value, or two `gaddr`s of one symbol? */
|
|
2292
|
+
function sameAddress(defOf: Map<Value, Op>, a: Value, b: Value): boolean {
|
|
2293
|
+
if (a === b) {
|
|
2294
|
+
return true;
|
|
2295
|
+
}
|
|
2296
|
+
const da = defOf.get(a);
|
|
2297
|
+
const db = defOf.get(b);
|
|
2298
|
+
return (
|
|
2299
|
+
da !== undefined &&
|
|
2300
|
+
db !== undefined &&
|
|
2301
|
+
da.opcode === 'gaddr' &&
|
|
2302
|
+
db.opcode === 'gaddr' &&
|
|
2303
|
+
opShape(da) === opShape(db)
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
/** Are two qualified reads of ONE cell interchangeable in the one expression they feed: the same
|
|
2308
|
+
* access of the same address, each the sole use of its value up a chain of ops alike but for it,
|
|
2309
|
+
* the two chains meeting as the two operands of one commutative op? Then the expression's value is
|
|
2310
|
+
* the same whichever read the compiler makes first, and its order is the compiler's to choose:
|
|
2311
|
+
* `gR + gR` over `extern volatile u32 gR;` and `*gPV + *gPV` over `u16 *volatile gPV` read the
|
|
2312
|
+
* object twice, in an order the source never stated, so naming the first read into a temp spells
|
|
2313
|
+
* an order the source did not have (ido 7.1 loads `gR + gR` in the other order). The meeting op
|
|
2314
|
+
* may also be a sum both reads enter with one sign, past a reassociation (`sameSignInOneSum`).
|
|
2315
|
+
* `gR - gR` is not symmetric — the order decides which read is the minuend — and stays sequenced.
|
|
2316
|
+
* `a` is the later read, `b` the earlier one placed past it. */
|
|
2317
|
+
function interchangeableReads(rules: MaterializeRules, a: Op, b: Op): boolean {
|
|
2318
|
+
const { defOf } = rules;
|
|
2319
|
+
if (
|
|
2320
|
+
a === b ||
|
|
2321
|
+
opShape(a) !== opShape(b) ||
|
|
2322
|
+
a.operands.length !== b.operands.length ||
|
|
2323
|
+
!a.operands.every((v, i) => sameAddress(defOf, v, b.operands[i]))
|
|
2324
|
+
) {
|
|
2325
|
+
return false;
|
|
2326
|
+
}
|
|
2327
|
+
return mirroredChains(rules, a, b) || sameSignInOneSum(rules, b, a);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
/** The two reads' sole-use chains are alike but for them and meet as the two operands of one
|
|
2331
|
+
* commutative op: `(gR << 1) * (gR << 1)`. */
|
|
2332
|
+
function mirroredChains(rules: MaterializeRules, a: Op, b: Op): boolean {
|
|
2333
|
+
const { useSitesOf } = rules.uses;
|
|
2334
|
+
let ca = a;
|
|
2335
|
+
let cb = b;
|
|
2336
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
2337
|
+
const va = ca.results[0];
|
|
2338
|
+
const vb = cb.results[0];
|
|
2339
|
+
const ua = va === undefined ? undefined : useSitesOf.get(va);
|
|
2340
|
+
const ub = vb === undefined ? undefined : useSitesOf.get(vb);
|
|
2341
|
+
if (ua?.length !== 1 || ub?.length !== 1) {
|
|
2342
|
+
return false;
|
|
2343
|
+
}
|
|
2344
|
+
const na = ua[0].op;
|
|
2345
|
+
const nb = ub[0].op;
|
|
2346
|
+
if (na === nb) {
|
|
2347
|
+
return (
|
|
2348
|
+
COMMUTATIVE_OPS.has(na.opcode) &&
|
|
2349
|
+
na.successors.length === 0 &&
|
|
2350
|
+
na.operands.length === 2 &&
|
|
2351
|
+
((na.operands[0] === va && na.operands[1] === vb) || (na.operands[0] === vb && na.operands[1] === va))
|
|
2352
|
+
);
|
|
2353
|
+
}
|
|
2354
|
+
if (
|
|
2355
|
+
opShape(na) !== opShape(nb) ||
|
|
2356
|
+
na.successors.length > 0 ||
|
|
2357
|
+
nb.successors.length > 0 ||
|
|
2358
|
+
(MEM_BASE_OPS.has(na.opcode) && counted(na)) ||
|
|
2359
|
+
na.operands.length !== nb.operands.length ||
|
|
2360
|
+
!na.operands.every((v, i) => (v === va ? nb.operands[i] === vb : v === nb.operands[i] && v !== vb))
|
|
2361
|
+
) {
|
|
2362
|
+
return false;
|
|
2363
|
+
}
|
|
2364
|
+
ca = na;
|
|
2365
|
+
cb = nb;
|
|
2366
|
+
}
|
|
2367
|
+
return false;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
/** Each read's sole-use chain up through `add`/`sub` ops, as the ops it passes and the read's sign in
|
|
2371
|
+
* each: entering an `add`, or a `sub` as its minuend, keeps the sign; entering a `sub` as its
|
|
2372
|
+
* subtrahend flips it. The value of every op on the chain is then that sign times the read, plus
|
|
2373
|
+
* terms the read is not in. */
|
|
2374
|
+
function additiveChain(rules: MaterializeRules, read: Op): Map<Op, { sign: number; operand: number }> {
|
|
2375
|
+
const { useSitesOf } = rules.uses;
|
|
2376
|
+
const chain = new Map<Op, { sign: number; operand: number }>();
|
|
2377
|
+
let v = read.results[0];
|
|
2378
|
+
let sign = 1;
|
|
2379
|
+
for (let depth = 0; depth < 8 && v !== undefined; depth++) {
|
|
2380
|
+
const uses = useSitesOf.get(v);
|
|
2381
|
+
if (uses?.length !== 1) {
|
|
2382
|
+
break;
|
|
2383
|
+
}
|
|
2384
|
+
const n = uses[0].op;
|
|
2385
|
+
const operand = n.operands.indexOf(v);
|
|
2386
|
+
if (
|
|
2387
|
+
(n.opcode !== 'add' && n.opcode !== 'sub') ||
|
|
2388
|
+
n.operands.length !== 2 ||
|
|
2389
|
+
n.results.length !== 1 ||
|
|
2390
|
+
n.successors.length > 0 ||
|
|
2391
|
+
n.operands.lastIndexOf(v) !== operand
|
|
2392
|
+
) {
|
|
2393
|
+
break;
|
|
2394
|
+
}
|
|
2395
|
+
sign = n.opcode === 'sub' && operand === 1 ? -sign : sign;
|
|
2396
|
+
chain.set(n, { sign, operand });
|
|
2397
|
+
v = n.results[0];
|
|
2398
|
+
}
|
|
2399
|
+
return chain;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
/** The two reads meet in one sum with the same sign: their sole-use chains run through `add`/`sub`
|
|
2403
|
+
* ops only, to an op one reaches through each operand, where each counts with the same sign. The
|
|
2404
|
+
* sum is then the same whichever read is made first, however the compiler reassociated it:
|
|
2405
|
+
* ido 7.1 compiles `(gR - x) + (gR - x)` as `((gR - x) + gR) - x`, where one read passes a `sub`
|
|
2406
|
+
* before the `add` they meet in and the other does not, so the chains are not mirrored. `gR - gR`
|
|
2407
|
+
* meets with opposite signs and stays sequenced. Wrapping arithmetic keeps the sum associative.
|
|
2408
|
+
* The value is then right in either order, but the match is not: the earlier read must also be
|
|
2409
|
+
* the one the expression spells first, entering the meeting op as its left operand, or a compiler
|
|
2410
|
+
* that reads operands left to right reverses the asm's two loads. mwcc 2.4.2 reads
|
|
2411
|
+
* `gMS.f + ((t + gMS.f) + t)`'s right-hand `gMS.f` first, so spelling both in one expression
|
|
2412
|
+
* there puts the asm's second load first. */
|
|
2413
|
+
function sameSignInOneSum(rules: MaterializeRules, earlier: Op, later: Op): boolean {
|
|
2414
|
+
const ofEarlier = additiveChain(rules, earlier);
|
|
2415
|
+
if (ofEarlier.size === 0) {
|
|
2416
|
+
return false;
|
|
2417
|
+
}
|
|
2418
|
+
for (const [n, inLater] of additiveChain(rules, later)) {
|
|
2419
|
+
const inEarlier = ofEarlier.get(n);
|
|
2420
|
+
if (inEarlier !== undefined) {
|
|
2421
|
+
return inEarlier.operand === 0 && inLater.operand === 1 && inEarlier.sign === inLater.sign;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
return false;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2264
2427
|
/** The rules for a memory access — a `call`, `load` or `aload` — in order; the first that names
|
|
2265
2428
|
* the op ends the walk. */
|
|
2266
2429
|
function nameAccess(rules: MaterializeRules, state: MaterializeState, op: Op, b: Block): void {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// asmlift structurer — the IR's integer arithmetic opcodes and the L3 operator each one renders
|
|
2
|
+
// as. The float ops are FLOAT_TO_BIN in structure.ts, which says why they are not here.
|
|
3
|
+
import type { BinOp } from '../l3/ast';
|
|
4
|
+
|
|
5
|
+
export const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
6
|
+
add: '+',
|
|
7
|
+
sub: '-',
|
|
8
|
+
mul: '*',
|
|
9
|
+
sdiv: '/',
|
|
10
|
+
// the UNSIGNED quotient/remainder — the C backend spells them `/`/`%` over an operand it casts
|
|
11
|
+
// unsigned (l3/ast.ts BinOp, backend/cfamily.ts C_SPELLING)
|
|
12
|
+
udiv: '/u',
|
|
13
|
+
smod: '%',
|
|
14
|
+
umod: '%u',
|
|
15
|
+
or: '|',
|
|
16
|
+
and: '&',
|
|
17
|
+
xor: '^',
|
|
18
|
+
shl: '<<',
|
|
19
|
+
shr_u: '>>>', // the LOGICAL right shift; the C backend spells it `>>` over an unsigned operand
|
|
20
|
+
shr_s: '>>',
|
|
21
|
+
logic_and: '&&',
|
|
22
|
+
logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
|
|
23
|
+
};
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
// THE GLOBAL-ACCESS ADDRESS DECOMPOSITION — the pure half of the symbol-map access spellings.
|
|
2
2
|
//
|
|
3
|
-
// Everything here is a function of an `Expr`,
|
|
4
|
-
// naming walk, no loop context. It answers three questions the access spellings in
|
|
5
|
-
// ask over and over — is this address `&gSym` plus something; does that something
|
|
6
|
-
// whole elements; and do the terms of it name the DECLARED subscripts of a
|
|
7
|
-
// — and it answers them the same way for every caller, which is the point
|
|
8
|
-
// rank-pinning fallback and the declared-subscript recovery share
|
|
9
|
-
// cannot disagree about what a bare element spelling is.
|
|
3
|
+
// Everything here is a function of an `Expr`, the map's declarations and a width: no structurer
|
|
4
|
+
// state, no naming walk, no loop context. It answers three questions the access spellings in
|
|
5
|
+
// structure.ts ask over and over — is this address `&gSym` plus something; does that something
|
|
6
|
+
// divide into whole elements; and do the terms of it name the DECLARED subscripts of a
|
|
7
|
+
// multidimensional array — and it answers them the same way for every caller, which is the point
|
|
8
|
+
// of the split: the rank-pinning fallback and the declared-subscript recovery share
|
|
9
|
+
// `bareArrayElement`, so the two cannot disagree about what a bare element spelling is. The same
|
|
10
|
+
// holds for what the map declares a named member to be (`ptrMemberDecl`), which the access folds
|
|
11
|
+
// and the pointer spelling (pointer-spelling.ts) both read.
|
|
10
12
|
import type { Expr } from '../l3/ast';
|
|
11
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type DeclaredField,
|
|
15
|
+
type SymbolInfo,
|
|
16
|
+
type SymbolStructField,
|
|
17
|
+
arrayInnerExtents,
|
|
18
|
+
declaredFields,
|
|
19
|
+
isArrayField,
|
|
20
|
+
isBitfieldField,
|
|
21
|
+
isPtrField,
|
|
22
|
+
pointeeFields,
|
|
23
|
+
} from '../symbols';
|
|
12
24
|
|
|
13
25
|
// `&gSym`, possibly wearing the value-context integer cast the additive lowering adds
|
|
14
|
-
// (`(u32)&gSym` — see `
|
|
26
|
+
// (`(u32)&gSym` — see `ARITH_ROWS` in pointer-spelling.ts): both spell the same link-time
|
|
15
27
|
// constant, so the fold rules match through the cast and every access that CAN spell a named
|
|
16
28
|
// element still does.
|
|
17
29
|
// WIDTH 32 ONLY — a NARROWING cast (`(u8)&gSym`, from a zext/sext lowering) is a different
|
|
@@ -298,3 +310,93 @@ export function addOffset(idx: Expr, n: number): Expr {
|
|
|
298
310
|
? { k: 'const', value: idx.value + n }
|
|
299
311
|
: { k: 'bin', op: '+', l: idx, r: { k: 'const', value: n } };
|
|
300
312
|
}
|
|
313
|
+
|
|
314
|
+
/** May a member be NAMED by an access of this direction, given the qualifiers on its declaration?
|
|
315
|
+
* The named spelling REPLACES a cast through `(u8 *)`, which carries no qualifier at all, so a
|
|
316
|
+
* qualifier the name reintroduces changes what the compiler emits:
|
|
317
|
+
* • `volatile` makes the access observable — the load may no longer be folded or reordered,
|
|
318
|
+
* which is a different instruction sequence (measured: 6 insns where the cast form was 5);
|
|
319
|
+
* • `const` under a STORE is a hard error, where the cast form merely cast the qualifier away.
|
|
320
|
+
* Either way the honest spelling is the cast form, so the member simply is not nameable here. */
|
|
321
|
+
export function memberQualsAllow(f: SymbolStructField, containerConst: boolean | undefined, isStore: boolean): boolean {
|
|
322
|
+
if (f.volatile) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
return !(isStore && (f.const || containerConst));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** The member a `width`-byte access at byte `byte` of a struct global names, `gSym.member`: one at
|
|
329
|
+
* that offset and of that size, no array or bitfield, whose qualifiers allow the access. An ARRAY
|
|
330
|
+
* member would match a byte access by (offset, size) and spell `.x`, which is not an lvalue of
|
|
331
|
+
* that width; a plain read of a BITFIELD's bytes is not a read of its bits. */
|
|
332
|
+
export function structMemberAt(
|
|
333
|
+
si: SymbolInfo,
|
|
334
|
+
byte: number,
|
|
335
|
+
width: number,
|
|
336
|
+
isStore: boolean,
|
|
337
|
+
): DeclaredField | undefined {
|
|
338
|
+
if (si.shape !== 'struct') {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
const f = declaredFields(si.layout)?.find(
|
|
342
|
+
(x) => x.offset === byte && x.size === width && !isArrayField(x) && !isBitfieldField(x),
|
|
343
|
+
);
|
|
344
|
+
return f !== undefined && memberQualsAllow(f, si.const, isStore) ? f : undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The members a symbol's declaration seats: a struct global's own, or a pointer global's
|
|
348
|
+
* pointee's. */
|
|
349
|
+
export interface MemberLookup {
|
|
350
|
+
fieldsOf(name: string): DeclaredField[] | null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** {@link MemberLookup.fieldsOf} over `info`, MEMOIZED per symbol. `declaredFields` validates every
|
|
354
|
+
* member and returns a fresh sorted copy on every call, and pointer-spelling.ts's `roleOf`
|
|
355
|
+
* asks it for every member operand of every arithmetic op lowered, so an uncached lookup is an
|
|
356
|
+
* O(n log n) allocation on a hot path — inside a `structure()` a ranked run repeats once per
|
|
357
|
+
* candidate, 17,856 times on the largest fan. */
|
|
358
|
+
export function memoFieldsOf(info: (name: string) => SymbolInfo | undefined): MemberLookup['fieldsOf'] {
|
|
359
|
+
const cache = new Map<string, DeclaredField[] | null>();
|
|
360
|
+
return (name) => {
|
|
361
|
+
const hit = cache.get(name);
|
|
362
|
+
if (hit !== undefined) {
|
|
363
|
+
return hit;
|
|
364
|
+
}
|
|
365
|
+
const si = info(name);
|
|
366
|
+
const fields =
|
|
367
|
+
si?.shape === 'struct' ? declaredFields(si.layout) : si?.shape === 'pointer' ? pointeeFields(si.pointee) : null;
|
|
368
|
+
cache.set(name, fields);
|
|
369
|
+
return fields;
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** The map member a `field` node NAMES, or null when it names none — THE one resolver for "what
|
|
374
|
+
* does the declaration say about this member", for rules that must reason about a member's type
|
|
375
|
+
* after the access rules have already spelled it. Both named spellings resolve, through the same
|
|
376
|
+
* shared gate their spelling passed: `gSym.member` off a struct global's {@link declaredFields},
|
|
377
|
+
* `gPtr->member` off the pointee's ({@link pointeeFields}). A synthesized `field_K` — the
|
|
378
|
+
* recovered-struct spelling, which no map declares — resolves to null, and so does any base that
|
|
379
|
+
* is not a map-shaped global, which is what makes every caller refuse rather than guess.
|
|
380
|
+
*
|
|
381
|
+
* The pointee arm reaches only what the MAP lets it: `pointeeAccess` gates every `gPtr->member`
|
|
382
|
+
* spelling on `spellsAccessType(f.signed, …)`, so a pointer field declaring no signedness — which
|
|
383
|
+
* is every one in the corpus's vendored maps — is never named, and a pointer read one indirection
|
|
384
|
+
* down spells `((s32 *)gQ)[1]`. That is a fact about those maps, not about this code: `SymbolMap`
|
|
385
|
+
* is a caller-supplied input, and one field flips it (`signed: true` on a 4-byte pointer member
|
|
386
|
+
* of a pointee yields `(u8 *)gQ->pInner`, cast and all, pinned in pointer-members.test.ts). So
|
|
387
|
+
* the arm is live and tested, and the byte-arithmetic rule that reads this answer is correct for
|
|
388
|
+
* it — where resolving a pointee member to null would reopen the double-scaling hole silently. */
|
|
389
|
+
export function declaredMemberOf(x: Expr, sym: MemberLookup | undefined): DeclaredField | null {
|
|
390
|
+
if (x.k !== 'field' || x.base.k !== 'var' || sym === undefined) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
return sym.fieldsOf(x.base.name)?.find((f) => f.name === x.name) ?? null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** The map member a `field` node names when the declaration makes it a POINTER — {@link
|
|
397
|
+
* isPtrField} being the shared two-fact test, so this and the synthesized declaration cannot
|
|
398
|
+
* disagree about what a member is. */
|
|
399
|
+
export function ptrMemberDecl(x: Expr, sym: MemberLookup | undefined): DeclaredField | null {
|
|
400
|
+
const f = declaredMemberOf(x, sym);
|
|
401
|
+
return f !== null && isPtrField(f) ? f : null;
|
|
402
|
+
}
|