@asmlift/core 0.9.0 → 0.11.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 +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- 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 +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -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 +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- 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 +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- 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/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- 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 +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
package/src/ir/entry.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// asmlift — does a test placed ahead of a loop compute the loop's own test on the loop's entry values?
|
|
2
|
+
//
|
|
3
|
+
// Three questions ask exactly that, at two levels, and all read the answer from this one predicate:
|
|
4
|
+
// • structure's GUARD FUSION (`structure.ts`, through `hazards.ts`): the `if` a rotated loop is
|
|
5
|
+
// wrapped in is deleted when it is the loop's test at the first iteration, and the loop re-tests;
|
|
6
|
+
// • raise's RE-ROLL (`raise/reroll.ts`): a loop test the compiler peeled into the loop's entry is
|
|
7
|
+
// deleted when it is the latch's test at the values the entry hands the loop;
|
|
8
|
+
// • the re-roll's TOP-TEST RESTORE (`entryGuard`, same file): the guard a restored top test
|
|
9
|
+
// makes redundant is deleted when it is that test at the entry values, with the same exit args.
|
|
10
|
+
//
|
|
11
|
+
// The reading is STRUCTURAL, never semantic: two values are the same when they are one value, or
|
|
12
|
+
// when their defining ops have one opcode, one set of attributes and operands that are the same in
|
|
13
|
+
// turn. A false negative costs a loud decline, the direction to be wrong in. `entry` models the
|
|
14
|
+
// first iteration: on the loop side (`a`), a value it maps is read as the value it maps to, and
|
|
15
|
+
// below that substitution the walk is in the entry's world, where the map no longer applies — a
|
|
16
|
+
// loop-invariant back-edge argument read inside the entry's own expression is itself there, not the
|
|
17
|
+
// param it feeds.
|
|
18
|
+
//
|
|
19
|
+
// `normalize` adds the equalities the re-roll needs and guard fusion does not take: an `add` or
|
|
20
|
+
// `or` of a constant 0 on EITHER side, commutative operands in either order, and a load off
|
|
21
|
+
// `base + c` as the same load off `base` at its offset plus `c` (`str[0]` against `str[i]` at
|
|
22
|
+
// `i = 0`). Each is an identity of the integer operators, so turning it on for guard fusion is a
|
|
23
|
+
// spelling decision, not a soundness one.
|
|
24
|
+
import { Op, Value, attrsKey } from './core';
|
|
25
|
+
import { spelledWhenDead } from './discipline';
|
|
26
|
+
import { NEGATED_ICMP } from './opcodes';
|
|
27
|
+
|
|
28
|
+
export interface EntryReading {
|
|
29
|
+
/** the value each loop-side value holds on the loop's first iteration */
|
|
30
|
+
readonly entry: ReadonlyMap<Value, Value>;
|
|
31
|
+
/** values whose defining op may be read through; everything else compares by identity. Default:
|
|
32
|
+
* every value. An operand-free op that reads no memory (a constant, a symbol address) is always
|
|
33
|
+
* read through: it is its attributes wherever it sits. */
|
|
34
|
+
readonly through?: (v: Value) => boolean;
|
|
35
|
+
/** the identities above, beyond `x + 0` / `x - 0` / `x | 0` with the zero on the right */
|
|
36
|
+
readonly normalize?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Opcodes whose two operands may be read in either order, under `normalize`. */
|
|
40
|
+
const COMMUTES = new Set(['add', 'mul', 'and', 'or', 'xor', 'icmp_eq', 'icmp_ne']);
|
|
41
|
+
|
|
42
|
+
/** Is `a` (loop side), read on the loop's entry values, the same value as `b`? With `negated`, is it
|
|
43
|
+
* `b`'s logical opposite — the usual case where a guard spells the loop's test the other way round. */
|
|
44
|
+
export function sameAtEntry(
|
|
45
|
+
defs: ReadonlyMap<Value, Op>,
|
|
46
|
+
a: Value,
|
|
47
|
+
b: Value,
|
|
48
|
+
reading: EntryReading,
|
|
49
|
+
negated = false,
|
|
50
|
+
): boolean {
|
|
51
|
+
const { entry, through = () => true, normalize = false } = reading;
|
|
52
|
+
const readable = (v: Value): boolean => {
|
|
53
|
+
const d = defs.get(v);
|
|
54
|
+
return d !== undefined && (through(v) || (d.operands.length === 0 && !spelledWhenDead(d)));
|
|
55
|
+
};
|
|
56
|
+
const constOf = (v: Value): number | null => {
|
|
57
|
+
const d = defs.get(v);
|
|
58
|
+
return d?.opcode === 'const' && typeof d.attrs?.value === 'number' ? d.attrs.value : null;
|
|
59
|
+
};
|
|
60
|
+
/** A constant `v` reads as: under `normalize`, as its entry substitution reads. */
|
|
61
|
+
const constAt = (v: Value, atEntry: boolean): number | null => constOf(normalize ? view(v, atEntry).x : v);
|
|
62
|
+
/** `x0` as the comparison reads it: through the entry map while on the loop side (`!atEntry`), and
|
|
63
|
+
* with `x + 0` / `x - 0` / `x | 0` read as `x` — substituting a loop variable by its init constant
|
|
64
|
+
* turns ordinary index arithmetic into exactly these. */
|
|
65
|
+
function view(x0: Value, atEntry0: boolean): { x: Value; atEntry: boolean } {
|
|
66
|
+
let x = x0;
|
|
67
|
+
let atEntry = atEntry0;
|
|
68
|
+
for (;;) {
|
|
69
|
+
const mapped = atEntry ? undefined : entry.get(x);
|
|
70
|
+
if (mapped !== undefined) {
|
|
71
|
+
x = mapped;
|
|
72
|
+
atEntry = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const d = readable(x) ? defs.get(x) : undefined;
|
|
76
|
+
if (!d || d.operands.length !== 2 || !['add', 'sub', 'or'].includes(d.opcode)) {
|
|
77
|
+
return { x, atEntry };
|
|
78
|
+
}
|
|
79
|
+
if (constAt(d.operands[1], atEntry) === 0) {
|
|
80
|
+
x = d.operands[0];
|
|
81
|
+
} else if (normalize && d.opcode !== 'sub' && constAt(d.operands[0], atEntry) === 0) {
|
|
82
|
+
x = d.operands[1];
|
|
83
|
+
} else {
|
|
84
|
+
return { x, atEntry };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** A load's address as a base and a constant offset, the `+ c` of an `add` folded in. */
|
|
89
|
+
const address = (d: Op, atEntry0: boolean): { base: Value; off: number; atEntry: boolean } => {
|
|
90
|
+
const off = Number(d.attrs.off ?? 0);
|
|
91
|
+
const { x: base, atEntry } = view(d.operands[0], atEntry0);
|
|
92
|
+
const ad = readable(base) ? defs.get(base) : undefined;
|
|
93
|
+
if (ad?.opcode === 'add' && ad.operands.length === 2) {
|
|
94
|
+
for (const k of [0, 1]) {
|
|
95
|
+
const c = constAt(ad.operands[k], atEntry);
|
|
96
|
+
if (c !== null) {
|
|
97
|
+
return { base: ad.operands[1 - k], off: off + c, atEntry };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { base, off, atEntry };
|
|
102
|
+
};
|
|
103
|
+
// memoised per (x, y, side): a value its own consumer reads twice would otherwise double the work
|
|
104
|
+
// at every level
|
|
105
|
+
const memo = new Map<string, boolean>();
|
|
106
|
+
const ids = new Map<Value, number>();
|
|
107
|
+
const id = (v: Value) => ids.get(v) ?? (ids.set(v, ids.size), ids.size - 1);
|
|
108
|
+
/** `da` (read with `atEntry`) and `db` (the entry's own) compute the same value. */
|
|
109
|
+
const sameOp = (da: Op, db: Op, opcodeOk: boolean, atEntry: boolean): boolean => {
|
|
110
|
+
if (!opcodeOk || da.operands.length !== db.operands.length) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (normalize && da.opcode === 'load' && db.opcode === 'load') {
|
|
114
|
+
const x = address(da, atEntry);
|
|
115
|
+
const y = address(db, true);
|
|
116
|
+
return x.off === y.off && attrsKey(da, 'off') === attrsKey(db, 'off') && same(x.base, x.atEntry, y.base);
|
|
117
|
+
}
|
|
118
|
+
if (attrsKey(da) !== attrsKey(db)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (da.operands.every((o, i) => same(o, atEntry, db.operands[i]))) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return (
|
|
125
|
+
normalize &&
|
|
126
|
+
COMMUTES.has(da.opcode) &&
|
|
127
|
+
da.operands.length === 2 &&
|
|
128
|
+
same(da.operands[0], atEntry, db.operands[1]) &&
|
|
129
|
+
same(da.operands[1], atEntry, db.operands[0])
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
function same(x0: Value, atEntry0: boolean, y0: Value): boolean {
|
|
133
|
+
const { x, atEntry } = view(x0, atEntry0);
|
|
134
|
+
const { x: y } = view(y0, true);
|
|
135
|
+
if (x === y) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
const key = `${id(x)},${id(y)},${atEntry ? 1 : 0}`;
|
|
139
|
+
const hit = memo.get(key);
|
|
140
|
+
if (hit !== undefined) {
|
|
141
|
+
return hit;
|
|
142
|
+
}
|
|
143
|
+
const da = readable(x) ? defs.get(x) : undefined;
|
|
144
|
+
const db = readable(y) ? defs.get(y) : undefined;
|
|
145
|
+
const r = !!da && !!db && sameOp(da, db, da.opcode === db.opcode, atEntry);
|
|
146
|
+
memo.set(key, r);
|
|
147
|
+
return r;
|
|
148
|
+
}
|
|
149
|
+
if (!negated) {
|
|
150
|
+
return same(a, false, b);
|
|
151
|
+
}
|
|
152
|
+
const { x, atEntry } = view(a, false);
|
|
153
|
+
const { x: y } = view(b, true);
|
|
154
|
+
const da = readable(x) ? defs.get(x) : undefined;
|
|
155
|
+
const db = readable(y) ? defs.get(y) : undefined;
|
|
156
|
+
return !!da && !!db && sameOp(da, db, NEGATED_ICMP[da.opcode] === db.opcode, atEntry);
|
|
157
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
// asmlift — natural-loop discovery: the pure CFG analysis the structurer consumes
|
|
1
|
+
// asmlift — natural-loop discovery: the pure CFG analysis the structurer consumes, and the raise
|
|
2
|
+
// passes that rewrite a loop's edges before it (`raise/latch.ts`, `raise/reroll.ts`).
|
|
3
|
+
//
|
|
4
|
+
// A substrate fact like dominance, so it lives here and not under `structure/`: that keeps the
|
|
5
|
+
// dependency between the levels one way — structure imports raise, never the reverse.
|
|
2
6
|
//
|
|
3
7
|
// Produces a DEFINITE header set computed structurally from back-edges — a block is a loop header
|
|
4
8
|
// iff it is a real back-edge target (`h ∈ dom(t)` for an edge `t→h`), never inferred from `cond_br`
|
|
@@ -8,7 +12,7 @@
|
|
|
8
12
|
//
|
|
9
13
|
// Dominance itself is a substrate fact and lives in `ir/core.ts`; `analyzeLoops` takes the map as
|
|
10
14
|
// a parameter, so a caller passes `dominators(fn)` from there.
|
|
11
|
-
import { Block, Fn, predecessors, successorsOf } from '
|
|
15
|
+
import { Block, Fn, predecessors, successorsOf } from './core';
|
|
12
16
|
|
|
13
17
|
/** One natural loop, keyed by its header. */
|
|
14
18
|
export interface NaturalLoop {
|
package/src/ir/opcodes.ts
CHANGED
|
@@ -38,7 +38,14 @@ export const OPCODES = {
|
|
|
38
38
|
// Effect-free (no `effects` flag) so DCE reaps a dead one.
|
|
39
39
|
mulh: { operands: 2, results: 1 },
|
|
40
40
|
mulhu: { operands: 2, results: 1 },
|
|
41
|
-
neg: { operands: 1, results: 1 },
|
|
41
|
+
neg: { operands: 1, results: 1 }, // the wrapping negation, 0 - x as a 32-bit word
|
|
42
|
+
// The MATHEMATICAL negation: what Thumb's `cmn rA, rB` compares rA against when a signed branch
|
|
43
|
+
// reads it (frontend/thumb.ts `lowerTerminator`). It differs from `neg` at exactly one operand,
|
|
44
|
+
// 0x80000000, where its value (+2^31) is no 32-bit word at all. A separate opcode rather than an
|
|
45
|
+
// attr on `neg` because every word evaluator in core folds `neg` as the wrapping one, and an
|
|
46
|
+
// attr each of them must remember to read fails OPEN: one that does not know `neg_s` falls to
|
|
47
|
+
// its default arm and declines. Only the structurer spells it, as `-(s32)x`.
|
|
48
|
+
neg_s: { operands: 1, results: 1 },
|
|
42
49
|
not: { operands: 1, results: 1 }, // bitwise complement (`mvn`) → ~x
|
|
43
50
|
or: { operands: 2, results: 1 },
|
|
44
51
|
and: { operands: 2, results: 1 },
|
|
@@ -151,6 +158,32 @@ export const OPCODES = {
|
|
|
151
158
|
// opcode rather than a `const` over a float type for the reason the arithmetic above has its own:
|
|
152
159
|
// every pass that matches `const` is an integer fold.
|
|
153
160
|
fconst: { operands: 0, results: 1, requiredAttrs: ['bits'] },
|
|
161
|
+
// The CROSSINGS between a float and an integer, C's conversions: `fptosi`/`fptoui` truncate toward
|
|
162
|
+
// zero into a signed or unsigned integer of the result's width (32 or 64), `sitofp` converts a
|
|
163
|
+
// signed one, `fpext`/`fptrunc` move between the precisions. The only ops a float and an integer
|
|
164
|
+
// meet in (ir/verify.ts). No `uitofp`: no compiler here converts an unsigned integer in one step
|
|
165
|
+
// (agbcc open-codes it around `__floatsisf`), so it would be an opcode with no producer.
|
|
166
|
+
fptosi: { operands: 1, results: 1 },
|
|
167
|
+
fptoui: { operands: 1, results: 1 },
|
|
168
|
+
sitofp: { operands: 1, results: 1 },
|
|
169
|
+
fpext: { operands: 1, results: 1 },
|
|
170
|
+
fptrunc: { operands: 1, results: 1 },
|
|
171
|
+
// The float COMPARES, a 0/1 integer out of two floats of one type. Ten and not six, because
|
|
172
|
+
// NaN makes C's relations a partial order: `!(a > b)` is true on a NaN and `a <= b` is not, and
|
|
173
|
+
// every compiler here compiles the two differently. So each ordered relation (`o`, false on a
|
|
174
|
+
// NaN) has its logical negation as an unordered opcode (`u`, true on a NaN): `fcmp_ule` IS
|
|
175
|
+
// `!(a > b)`. C's `!=` is `fcmp_une`. They stay out of `NEGATED_ICMP`, whose integer negation
|
|
176
|
+
// (`<` ↔ `>=`) would be the wrong program here; `NEGATED_FCMP` below is theirs.
|
|
177
|
+
fcmp_oeq: { operands: 2, results: 1 },
|
|
178
|
+
fcmp_une: { operands: 2, results: 1 },
|
|
179
|
+
fcmp_olt: { operands: 2, results: 1 },
|
|
180
|
+
fcmp_uge: { operands: 2, results: 1 },
|
|
181
|
+
fcmp_ole: { operands: 2, results: 1 },
|
|
182
|
+
fcmp_ugt: { operands: 2, results: 1 },
|
|
183
|
+
fcmp_ogt: { operands: 2, results: 1 },
|
|
184
|
+
fcmp_ule: { operands: 2, results: 1 },
|
|
185
|
+
fcmp_oge: { operands: 2, results: 1 },
|
|
186
|
+
fcmp_ult: { operands: 2, results: 1 },
|
|
154
187
|
// --- memory ---
|
|
155
188
|
load: { operands: 1, results: 1, requiredAttrs: ['off', 'width', 'signed'], reads: true },
|
|
156
189
|
store: { operands: 2, results: 0, requiredAttrs: ['off', 'width'], effects: true },
|
|
@@ -171,6 +204,12 @@ export const OPCODES = {
|
|
|
171
204
|
// - a load/store through an off-0 SCALAR gaddr → a bare global `gSym` / `gSym = v`;
|
|
172
205
|
// - an indexed or non-zero-offset AGGREGATE access → the address-cast `((T *)&gSym)[i]`;
|
|
173
206
|
// - any other use (e.g. `&gSym` passed to a call) → the `{k:'addr'}` L3 node, printed `&gSym`.
|
|
207
|
+
// Optional `fromMap: true` marks a name asmlift READ OFF THE SYMBOL MAP (a promoted numeric pool
|
|
208
|
+
// word, an interior attribution, a named walk) rather than one the asm spells by relocation.
|
|
209
|
+
// With it, `mapAddr` is the map address the name was read at: a name the map places at several
|
|
210
|
+
// addresses (a file static per unit) then denotes that one, and two such reads at different
|
|
211
|
+
// addresses, one of them read off the map, are two objects under one spelling, which
|
|
212
|
+
// raise/offsetnames.ts `cellSpelledTwice` declines. Two relocations of one name are the asm's own.
|
|
174
213
|
gaddr: { operands: 0, results: 1, requiredAttrs: ['sym'] },
|
|
175
214
|
// The address of a FRAME-LOCAL object — gaddr's local twin, for the address-taken stack local
|
|
176
215
|
// (`mov rD, sp` feeding a DMA register or a callee). `off` is the byte offset inside the frame's
|
|
@@ -214,13 +253,16 @@ export const OPCODES = {
|
|
|
214
253
|
// live-in, and a live-in of the entry block is a PARAMETER — right for an argument register, a
|
|
215
254
|
// fabricated argument for anything else.
|
|
216
255
|
//
|
|
217
|
-
// Operand-free and pure like `laddr`, and out of raise/gvn.ts's NUMBERABLE set
|
|
218
|
-
// it
|
|
219
|
-
//
|
|
256
|
+
// Operand-free and pure like `laddr`, and out of raise/gvn.ts's NUMBERABLE set, which must keep
|
|
257
|
+
// it out: two undefs of one key are a storage's content and a `copied` move of it, and numbering
|
|
258
|
+
// them one value would erase the move.
|
|
220
259
|
//
|
|
221
260
|
// `key` names the storage (`sp@0`, `r4`); the structurer reads it to name the local
|
|
222
261
|
// (`uninit_sp0`) and emits NO assignment — that absence is the recovery, and an edge argument
|
|
223
|
-
// that is one emits no copy either (structure.ts undefCarriesNothing).
|
|
262
|
+
// that is one emits no copy either (structure.ts undefCarriesNothing). OPTIONAL `copied: true`:
|
|
263
|
+
// the value another storage's content arrives at a join keyed elsewhere as (`if (c) x = y;` of
|
|
264
|
+
// two uninitialised locals, frontend/ssa.ts `markCopiedUndefs`) — the machine moved it, so that
|
|
265
|
+
// edge copy IS emitted, under the name of the undef its key names.
|
|
224
266
|
undef: { operands: 0, results: 1, requiredAttrs: ['key'] },
|
|
225
267
|
// --- black-box escape hatch (keeps lifting total) ---
|
|
226
268
|
// `effects: true`: an instruction asmlift could not model may do anything — write memory, trap,
|
|
@@ -243,6 +285,11 @@ export const OPCODES = {
|
|
|
243
285
|
// Many-way switch dispatch (Regime B, jump table). The single operand is the scrutinee;
|
|
244
286
|
// successors are the N case blocks followed by the default block (the LAST successor);
|
|
245
287
|
// `cases` is the index-aligned list of the first N successors' case values.
|
|
288
|
+
// `exhaustive: true` — OPTIONAL, set by a frontend whose dispatch block is entered only past a
|
|
289
|
+
// bounds test that already sent every value outside `cases` elsewhere, so the default edge is
|
|
290
|
+
// never taken. It still names a block, because the op needs a last successor; the structurer
|
|
291
|
+
// reads the attr to emit no `default:` arm for it. A pass that rebuilds the op leaves it off and
|
|
292
|
+
// gets the default arm back, which is the conservative answer.
|
|
246
293
|
switch_br: { operands: 1, results: 0, terminator: true, successors: 'variadic', requiredAttrs: ['cases'] },
|
|
247
294
|
} as const satisfies Record<string, OpSig>;
|
|
248
295
|
|
|
@@ -272,6 +319,65 @@ export const CAST_WIDTHS: ReadonlySet<number> = new Set([8, 16]);
|
|
|
272
319
|
* an op computes ON. */
|
|
273
320
|
export const FLOAT_OPS: ReadonlySet<string> = new Set(['fadd', 'fsub', 'fmul', 'fdiv', 'fneg', 'fconst']);
|
|
274
321
|
|
|
322
|
+
/** The conversions out of a float into an integer, and into a float out of an integer. */
|
|
323
|
+
export const FLOAT_TO_INT: ReadonlySet<string> = new Set(['fptosi', 'fptoui']);
|
|
324
|
+
export const INT_TO_FLOAT: ReadonlySet<string> = new Set(['sitofp']);
|
|
325
|
+
/** The conversions between the two precisions, each named by the width it converts from and to. */
|
|
326
|
+
export const FLOAT_RESIZE: Readonly<Record<string, { from: 32 | 64; to: 32 | 64 }>> = {
|
|
327
|
+
fpext: { from: 32, to: 64 },
|
|
328
|
+
fptrunc: { from: 64, to: 32 },
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/** The logical negation of each float compare, NaN included (the registry's note on `fcmp_*`):
|
|
332
|
+
* expanded both ways from the pairs, as `NEGATED_ICMP` is. */
|
|
333
|
+
const FCMP_NEGATION_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
|
|
334
|
+
['fcmp_oeq', 'fcmp_une'],
|
|
335
|
+
['fcmp_olt', 'fcmp_uge'],
|
|
336
|
+
['fcmp_ole', 'fcmp_ugt'],
|
|
337
|
+
['fcmp_ogt', 'fcmp_ule'],
|
|
338
|
+
['fcmp_oge', 'fcmp_ult'],
|
|
339
|
+
];
|
|
340
|
+
export const NEGATED_FCMP: Readonly<Record<string, Opcode>> = Object.fromEntries(
|
|
341
|
+
FCMP_NEGATION_PAIRS.flatMap(([a, b]) => [
|
|
342
|
+
[a, b],
|
|
343
|
+
[b, a],
|
|
344
|
+
]),
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
/** What each float compare answers when its first operand is LESS than, EQUAL to or GREATER than
|
|
348
|
+
* its second, and when the two are UNORDERED (either is a NaN): the definition of the ten, from
|
|
349
|
+
* which a compare a runtime computes some other way is read (raise/floathelpers.ts). */
|
|
350
|
+
export const FCMP_TRUTH: Readonly<Record<string, readonly [lt: boolean, eq: boolean, gt: boolean, un: boolean]>> = {
|
|
351
|
+
fcmp_olt: [true, false, false, false],
|
|
352
|
+
fcmp_ole: [true, true, false, false],
|
|
353
|
+
fcmp_ogt: [false, false, true, false],
|
|
354
|
+
fcmp_oge: [false, true, true, false],
|
|
355
|
+
fcmp_oeq: [false, true, false, false],
|
|
356
|
+
fcmp_une: [true, false, true, true],
|
|
357
|
+
fcmp_uge: [false, true, true, true],
|
|
358
|
+
fcmp_ugt: [false, false, true, true],
|
|
359
|
+
fcmp_ule: [true, true, false, true],
|
|
360
|
+
fcmp_ult: [true, false, false, true],
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/** The float compare whose truth table (`FCMP_TRUTH`'s order: less, equal, greater, unordered) is
|
|
364
|
+
* `truth`, or undefined where none is — always true, say. The one reader of the table backwards,
|
|
365
|
+
* for every place that computes a compare's answers some other way (agbcc's helper tests,
|
|
366
|
+
* raise/floathelpers.ts; PowerPC's `cror` of two bits, frontend/ppc.ts). */
|
|
367
|
+
export function fcmpOfTruth(truth: readonly boolean[]): Opcode | undefined {
|
|
368
|
+
return Object.keys(FCMP_TRUTH).find((k) => FCMP_TRUTH[k].every((t, i) => t === truth[i])) as Opcode | undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Every op that computes on a float: the arithmetic, the conversions and the compares. A runtime
|
|
372
|
+
* helper whose op is one of these is a float helper (runtime-helpers.ts `isFloatHelper`). */
|
|
373
|
+
export const FLOAT_COMPUTING: ReadonlySet<string> = new Set([
|
|
374
|
+
...FLOAT_OPS,
|
|
375
|
+
...FLOAT_TO_INT,
|
|
376
|
+
...INT_TO_FLOAT,
|
|
377
|
+
...Object.keys(FLOAT_RESIZE),
|
|
378
|
+
...Object.keys(NEGATED_FCMP),
|
|
379
|
+
]);
|
|
380
|
+
|
|
275
381
|
/** Signature lookup by RUNTIME opcode string (Op.opcode is a plain string — IR consumers switch
|
|
276
382
|
* on it); undefined for an unregistered opcode. */
|
|
277
383
|
export function opSig(opcode: string): OpSig | undefined {
|
|
@@ -290,10 +396,10 @@ export function opSig(opcode: string): OpSig | undefined {
|
|
|
290
396
|
*
|
|
291
397
|
* It lives here so that every consumer that has to say "the opposite of this compare" reads THIS
|
|
292
398
|
* one, and they cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
|
|
293
|
-
* shortcircuit.ts derives its `BOOL_OPS` from these keys (asserting
|
|
294
|
-
* true today), and l3/ast.ts `NEGATE_REL` is the SAME relation
|
|
295
|
-
* vocabulary — deliberately separate, because signedness lives in the
|
|
296
|
-
* two tables are not candidates for further consolidation. */
|
|
399
|
+
* shortcircuit.ts derives its `BOOL_OPS` from these keys and `NEGATED_FCMP`'s (asserting
|
|
400
|
+
* negatable compare == boolean-op, true today), and l3/ast.ts `NEGATE_REL` is the SAME relation
|
|
401
|
+
* over the neutral L3 operator vocabulary — deliberately separate, because signedness lives in the
|
|
402
|
+
* operand types there, so the two tables are not candidates for further consolidation. */
|
|
297
403
|
const ICMP_NEGATION_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
|
|
298
404
|
['icmp_eq', 'icmp_ne'],
|
|
299
405
|
['icmp_slt', 'icmp_sge'],
|
|
@@ -308,6 +414,28 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
|
|
|
308
414
|
]),
|
|
309
415
|
);
|
|
310
416
|
|
|
417
|
+
/** The comparison each `icmp_*` is with its operands SWAPPED — `a < b` is `b > a`; `==` and `!=`
|
|
418
|
+
* are their own. Not a negation: `NEGATED_ICMP` keeps the operands and flips the answer, this
|
|
419
|
+
* keeps the answer and flips the operands. Seated here for the reason `NEGATED_ICMP` is, and
|
|
420
|
+
* expanded both ways from its pairs the same way, so `swap(swap(c)) === c` by construction; a test
|
|
421
|
+
* asserts it covers the icmp family (test/pattern.test.ts). */
|
|
422
|
+
const ICMP_SWAP_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
|
|
423
|
+
['icmp_slt', 'icmp_sgt'],
|
|
424
|
+
['icmp_sle', 'icmp_sge'],
|
|
425
|
+
['icmp_ult', 'icmp_ugt'],
|
|
426
|
+
['icmp_ule', 'icmp_uge'],
|
|
427
|
+
];
|
|
428
|
+
export const SWAPPED_ICMP: Readonly<Record<string, Opcode>> = {
|
|
429
|
+
icmp_eq: 'icmp_eq',
|
|
430
|
+
icmp_ne: 'icmp_ne',
|
|
431
|
+
...Object.fromEntries(
|
|
432
|
+
ICMP_SWAP_PAIRS.flatMap(([a, b]) => [
|
|
433
|
+
[a, b],
|
|
434
|
+
[b, a],
|
|
435
|
+
]),
|
|
436
|
+
),
|
|
437
|
+
};
|
|
438
|
+
|
|
311
439
|
/** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
|
|
312
440
|
* `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
|
|
313
441
|
* Authored rather than derived — no registry field records the operand ROLE, and the only other
|
package/src/ir/parse.ts
CHANGED
package/src/ir/simplify.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Peer of `pattern/engine.ts`'s `dce`: general, opcode-agnostic, and callable by anything that has
|
|
4
4
|
// just changed the CFG.
|
|
5
|
-
import { Block, Fn, Successor, Value, replaceAllUsesWith } from './core';
|
|
5
|
+
import { Block, Fn, Op, Successor, Value, replaceAllUsesWith } from './core';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Remove block params that are really TRIVIAL PHIS — those whose incoming args, across every
|
|
@@ -55,12 +55,7 @@ export function simplifyTrivialPhis(fn: Fn, onRemoved?: (param: Value) => void):
|
|
|
55
55
|
if (collapsed === null) {
|
|
56
56
|
continue; // a genuine join, or unreachable (no in-edges at all)
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
onRemoved?.(param);
|
|
60
|
-
b.params.splice(i, 1);
|
|
61
|
-
for (const s of incoming) {
|
|
62
|
-
s.args.splice(i, 1);
|
|
63
|
-
}
|
|
58
|
+
retireParam(fn, b, i, collapsed, incoming, onRemoved);
|
|
64
59
|
removed++;
|
|
65
60
|
changed = true;
|
|
66
61
|
}
|
|
@@ -77,14 +72,70 @@ export function simplifyTrivialPhis(fn: Fn, onRemoved?: (param: Value) => void):
|
|
|
77
72
|
* fixpoint and the non-mutating check below so the two cannot come to disagree about which params
|
|
78
73
|
* are trivial.
|
|
79
74
|
*
|
|
75
|
+
* `same` is the equality the args are held to. Identity by default, which is the trivial phi
|
|
76
|
+
* proper; `raise/unpre.ts` passes structural congruence (`ir/entry.ts` `sameAtEntry`) and gets
|
|
77
|
+
* back the FIRST arg as the class's representative, which it recomputes rather than reads. The
|
|
78
|
+
* self-reference waiver is the same for both, and so is its argument above: the first dynamic
|
|
79
|
+
* entry arrives on an edge that hands the representative's class.
|
|
80
|
+
*
|
|
80
81
|
* `=== null` IS THE ONLY SAFE TEST. A genuine collapse value is never null, but it CAN be
|
|
81
82
|
* `undefined` where an edge carries fewer args than the block has params — a malformed graph the
|
|
82
83
|
* verifier catches elsewhere, and one today's callers pass straight through rather than treat as
|
|
83
84
|
* "not trivial". A truthiness test would silently change that.
|
|
84
85
|
*/
|
|
85
|
-
function trivialPhiValue(
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
export function trivialPhiValue(
|
|
87
|
+
incoming: readonly Successor[],
|
|
88
|
+
i: number,
|
|
89
|
+
param: Value,
|
|
90
|
+
same: (a: Value, b: Value) => boolean = (a, b) => a === b,
|
|
91
|
+
): Value | null {
|
|
92
|
+
const args = incoming.map((s) => s.args[i]).filter((v) => v !== param);
|
|
93
|
+
if (args.length === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const first = args[0];
|
|
97
|
+
return args.every((v) => v === first || same(v, first)) ? first : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Retire block param `i` of `b` for `value`: every use reads `value`, and the param and each
|
|
101
|
+
* in-edge's arg for it are spliced out. `incoming` is every successor that targets `b` — the
|
|
102
|
+
* SUCCESSOR objects, not their `args`, which `replaceAllUsesWith` replaces. `onRemoved` runs after
|
|
103
|
+
* the uses move and before the splice. */
|
|
104
|
+
export function retireParam(
|
|
105
|
+
fn: Fn,
|
|
106
|
+
b: Block,
|
|
107
|
+
i: number,
|
|
108
|
+
value: Value,
|
|
109
|
+
incoming: readonly Successor[],
|
|
110
|
+
onRemoved?: (param: Value) => void,
|
|
111
|
+
): void {
|
|
112
|
+
const param = b.params[i];
|
|
113
|
+
replaceAllUsesWith(fn, param, value);
|
|
114
|
+
onRemoved?.(param);
|
|
115
|
+
b.params.splice(i, 1);
|
|
116
|
+
for (const s of incoming) {
|
|
117
|
+
s.args.splice(i, 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Every in-edge, indexed by target: the successor, the block it leaves and the terminator that
|
|
122
|
+
* carries it. ONE pass over the function, for a caller whose CFG does not change while it reads the
|
|
123
|
+
* index (params and args may: the successor objects stay). */
|
|
124
|
+
export function inEdgesOf(fn: Fn): Map<Block, { succ: Successor; from: Block; op: Op }[]> {
|
|
125
|
+
const out = new Map<Block, { succ: Successor; from: Block; op: Op }[]>();
|
|
126
|
+
for (const from of fn.blocks) {
|
|
127
|
+
for (const op of from.ops) {
|
|
128
|
+
for (const succ of op.successors) {
|
|
129
|
+
const list = out.get(succ.block);
|
|
130
|
+
if (list === undefined) {
|
|
131
|
+
out.set(succ.block, [{ succ, from, op }]);
|
|
132
|
+
} else {
|
|
133
|
+
list.push({ succ, from, op });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
88
139
|
}
|
|
89
140
|
|
|
90
141
|
/**
|
package/src/ir/types.ts
CHANGED
|
@@ -7,6 +7,11 @@ export interface StructField {
|
|
|
7
7
|
off: number;
|
|
8
8
|
type: IrType;
|
|
9
9
|
name: string;
|
|
10
|
+
/** A cell the function also reads or writes at another width or extension, declared at its
|
|
11
|
+
* widest view because the union of its views would not fit the compiler's layout
|
|
12
|
+
* (raise/structs.ts `buildUnionStruct`). An access of any other type is a cast of the base
|
|
13
|
+
* (structure.ts `memAccess`). */
|
|
14
|
+
castViews?: true;
|
|
10
15
|
}
|
|
11
16
|
|
|
12
17
|
export type IrType =
|
|
@@ -93,6 +98,31 @@ export const T = {
|
|
|
93
98
|
void: (): IrType => ({ kind: 'void' }),
|
|
94
99
|
};
|
|
95
100
|
|
|
101
|
+
/** The member name of each view a union holds, by access width. A width with no entry has no view
|
|
102
|
+
* (the deref contract admits no other scalar width either), and its overlap still declines. A
|
|
103
|
+
* narrow width loaded with both extensions has a view per extension, `u`/`s` prefixed. */
|
|
104
|
+
export const UNION_VIEW_NAMES: Readonly<Record<number, string>> = { 1: 'byte', 2: 'half', 4: 'word' };
|
|
105
|
+
|
|
106
|
+
/** The extensions a union declares a view of `width` bytes in, from the accesses of that width:
|
|
107
|
+
* THE one rule raise/structs.ts `buildUnionStruct` and the frame audit (frontend/frame-objects.ts
|
|
108
|
+
* `unionViews`) both declare by, so a struct member's union and a frame object's cannot disagree
|
|
109
|
+
* on their views. A word has one view whatever its loads say (`scalarTypeForAccess`); a narrower
|
|
110
|
+
* width a view per extension it is LOADED with, unsigned first; a width only stored the signed
|
|
111
|
+
* view where a stamped store says signed (raise/storesign.ts) and the unsigned one otherwise. */
|
|
112
|
+
export function unionViewSigns(
|
|
113
|
+
width: number,
|
|
114
|
+
accesses: readonly { readonly isLoad: boolean; readonly signed: boolean; readonly stamped: boolean }[],
|
|
115
|
+
): boolean[] {
|
|
116
|
+
if (width === 4) {
|
|
117
|
+
return [true];
|
|
118
|
+
}
|
|
119
|
+
const loaded = new Set(accesses.filter((a) => a.isLoad).map((a) => a.signed));
|
|
120
|
+
if (loaded.size === 0) {
|
|
121
|
+
return [accesses.some((a) => a.stamped && a.signed)];
|
|
122
|
+
}
|
|
123
|
+
return [false, true].filter((sg) => loaded.has(sg));
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
/** The byte width of a union view's element (an array view's element, or the scalar itself). */
|
|
97
127
|
function viewBytes(t: IrType): number {
|
|
98
128
|
const e = t.kind === 'array' ? t.elem : t;
|
|
@@ -102,6 +132,11 @@ function viewBytes(t: IrType): number {
|
|
|
102
132
|
return e.width / 8;
|
|
103
133
|
}
|
|
104
134
|
|
|
135
|
+
/** A struct as its declaration spells it (`declared`), else by its tag; any other type as printed. */
|
|
136
|
+
export function structSpelling(t: IrType): string {
|
|
137
|
+
return t.kind === 'struct' ? (t.declared ?? `struct ${t.name}`) : typeToString(t);
|
|
138
|
+
}
|
|
139
|
+
|
|
105
140
|
/** The member `name` of an aggregate — a struct's field or a union's view — or undefined when `t`
|
|
106
141
|
* is not an aggregate or declares no such member. THE one lookup the typing walk and the deref
|
|
107
142
|
* contract share, so the two cannot disagree on which member access is well-typed. */
|
|
@@ -131,8 +166,57 @@ export function unionViewAt(
|
|
|
131
166
|
if (member === undefined || member.type.kind !== 'union') {
|
|
132
167
|
return undefined;
|
|
133
168
|
}
|
|
169
|
+
return { member, ...unionView(member.type, off - member.off, width, signed, isStore) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Does a member declared at signedness `declared` read as EXACTLY the type the cast spelling this
|
|
173
|
+
* replaces would have produced — `scalarTypeForAccess(width, signed)`? That is the whole
|
|
174
|
+
* byte-exactness argument for naming a member instead of casting: same address, same read width,
|
|
175
|
+
* same C type, so every operator downstream compiles identically. A 4-byte access renders s32
|
|
176
|
+
* whatever the load said (the ISA has one word load), so only a SIGNED member may take its place;
|
|
177
|
+
* narrower accesses carry their own signedness and must match it. An undeclared signedness (the
|
|
178
|
+
* member is not a base type — a nested struct, an enum, a pointer) is never assumed to match.
|
|
179
|
+
* THE one copy of "an access names the member": structure.ts's member spellings and `isCastView`
|
|
180
|
+
* below all ask it, so they cannot drift apart. */
|
|
181
|
+
export function spellsAccessType(declared: boolean | undefined, width: number, signed: boolean): boolean {
|
|
182
|
+
if (declared === undefined) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return typeEquals(T.int(width * 8, declared), scalarTypeForAccess(width, signed));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Does an access of `width` bytes at byte `off`, inside a `castViews` field of the struct, read it
|
|
189
|
+
* at a view other than the field's own type — another width or offset, or a load whose type the
|
|
190
|
+
* field does not spell (`spellsAccessType`)? A store carries no extension (it truncates to the
|
|
191
|
+
* field's bytes whatever their type), so one of the field's width at its offset is the field. */
|
|
192
|
+
export function isCastView(
|
|
193
|
+
st: Extract<IrType, { kind: 'struct' }>,
|
|
194
|
+
off: number,
|
|
195
|
+
width: number,
|
|
196
|
+
signed: boolean,
|
|
197
|
+
isStore: boolean,
|
|
198
|
+
): boolean {
|
|
199
|
+
const f = st.fields.find(
|
|
200
|
+
(m) => m.castViews === true && m.off <= off && off + width <= m.off + (intWidth(m.type) ?? 0) / 8,
|
|
201
|
+
);
|
|
202
|
+
if (f === undefined || f.type.kind !== 'int') {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return off !== f.off || f.type.width !== width * 8 || (!isStore && !spellsAccessType(f.type.signed, width, signed));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The view of a union an access of `width` bytes at byte `at` of it reads — the rule
|
|
209
|
+
* `unionViewAt` states, asked of a union that is not a struct's member (a frame object declared
|
|
210
|
+
* as one, structure.ts `laddrType`). */
|
|
211
|
+
export function unionView(
|
|
212
|
+
u: Extract<IrType, { kind: 'union' }>,
|
|
213
|
+
at: number,
|
|
214
|
+
width: number,
|
|
215
|
+
signed: boolean,
|
|
216
|
+
isStore: boolean,
|
|
217
|
+
): { view: StructField | undefined; index: number | null } {
|
|
134
218
|
const elemOf = (t: IrType): IrType => (t.kind === 'array' ? t.elem : t);
|
|
135
|
-
const wide =
|
|
219
|
+
const wide = u.members.filter((m) => intWidth(elemOf(m.type)) === width * 8);
|
|
136
220
|
const signOf = (m: StructField): boolean | undefined => {
|
|
137
221
|
const e = elemOf(m.type);
|
|
138
222
|
return e.kind === 'int' ? e.signed : undefined;
|
|
@@ -141,7 +225,7 @@ export function unionViewAt(
|
|
|
141
225
|
width === 4 || (isStore && wide.length === 1)
|
|
142
226
|
? wide[0]
|
|
143
227
|
: wide.find((m) => signOf(m) === (isStore ? false : signed));
|
|
144
|
-
return {
|
|
228
|
+
return { view, index: view?.type.kind === 'array' ? at / width : null };
|
|
145
229
|
}
|
|
146
230
|
|
|
147
231
|
export function typeToString(t: IrType): string {
|