@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.
Files changed (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -0,0 +1,290 @@
1
+ // asmlift — A WHOLE-WORD READ OF A DECLARED STRUCT LOCAL, every use of which takes one member's
2
+ // bits, READS THOSE MEMBERS.
3
+ //
4
+ // agbcc has no sp-relative halfword or byte load, so `struct S4 { u16 a; u8 b; u8 pad; } s =
5
+ // mk(x); gOut = s.a;` reads the struct-return temp a word at a time and narrows in registers:
6
+ //
7
+ // mov r0, sp / bl mk / ldr r1, [sp] / ldr r0, =gOut / strh r1, [r0]
8
+ //
9
+ // The frame-object audit types the temp as the struct the call returns (frame-objects.ts, the
10
+ // `aggregate` stamp), so the word is bytes of a declared object, and each use of it names a byte
11
+ // range: a narrowing store or extension (the low end), or one of those applied to a right shift
12
+ // by whole bytes, or such a shift alone (the top end, whose zero fill is the member's own zero
13
+ // extension). Where every use names exactly one declared integer member, the word is those
14
+ // member reads — the same bytes, read at the same program point: each member load is placed where
15
+ // the word load was, and the word load is left to dce.
16
+ //
17
+ // REFUSALS, each leaving the word read whole (spelled through a cast, which is the same bytes):
18
+ // * the base is not a frame object the audit typed as a struct, or the read is qualified;
19
+ // * a use is anything but the shapes above (an add, a compare of the whole word, a phi
20
+ // argument, a call argument), or a shift whose uses are not all of one width;
21
+ // * a range names no declared member exactly (a straddle, padding, a bitfield, a nested
22
+ // aggregate) — the member must start at that byte and be an integer of that width.
23
+ import { type Block, type Fn, type Op, type Value, mkOp, mkValue } from '../ir/core';
24
+ import { qualified } from '../ir/discipline';
25
+ import { type IrType, T } from '../ir/types';
26
+
27
+ type StructType = Extract<IrType, { kind: 'struct' }>;
28
+
29
+ /** The struct a frame object's address points at, where the audit stamped one. */
30
+ function frameStructOf(defs: ReadonlyMap<Value, Op>, base: Value): StructType | undefined {
31
+ const d = defs.get(base);
32
+ const t = d?.results[0].type;
33
+ return d?.opcode === 'laddr' && d.attrs.aggregate === true && t?.kind === 'ptr' && t.to.kind === 'struct'
34
+ ? t.to
35
+ : undefined;
36
+ }
37
+
38
+ /** The declared integer member at exactly `[byte, byte + width)`: its signedness, or undefined. */
39
+ function memberAt(st: StructType, byte: number, width: number): boolean | undefined {
40
+ const f = st.fields.find((m) => m.off === byte);
41
+ return f !== undefined && f.type.kind === 'int' && f.type.width === width * 8 ? f.type.signed : undefined;
42
+ }
43
+
44
+ interface Use {
45
+ readonly op: Op;
46
+ readonly idx: number;
47
+ }
48
+
49
+ /** The bytes a use keeps of the value it reads, low end first: a narrowing store's or an
50
+ * extension's width; undefined for any other use. */
51
+ function narrowedTo(u: Use): number | undefined {
52
+ if (u.op.opcode === 'store' && u.idx === 1) {
53
+ return u.op.attrs.width as number;
54
+ }
55
+ return u.op.opcode === 'zext' || u.op.opcode === 'sext' ? (u.op.attrs.width as number) / 8 : undefined;
56
+ }
57
+
58
+ /** One member a word's uses take: its byte range in the word, and each op it is read for with the
59
+ * rewiring of that op onto the member's load `m` (returning an op to place after the load). */
60
+ interface MemberUse {
61
+ readonly byte: number;
62
+ readonly width: number;
63
+ readonly sites: readonly { readonly op: Op; readonly rewire: (m: Value, signed: boolean) => Op | undefined }[];
64
+ }
65
+
66
+ /** WHERE A MEMBER IS READ. The word was read once, where the compiler CSE'd the member reads of
67
+ * the source into it; the source read each member where it is used. So a member read for an op
68
+ * in another block is placed just before that op — where no write to the object lies on any path
69
+ * from the word's load to it, which is the condition under which the two read the same bytes.
70
+ * A write is any op but a load that takes the object's address (its struct-return call). */
71
+ function readableAt(
72
+ load: Op,
73
+ site: Op,
74
+ { blockOf, preds, writes }: { blockOf: Map<Op, Block>; preds: Map<Block, Block[]>; writes: (op: Op) => boolean },
75
+ ): boolean {
76
+ const seen = new Set<Block>();
77
+ const work: [Block, number][] = [[blockOf.get(site)!, blockOf.get(site)!.ops.indexOf(site)]];
78
+ while (work.length > 0) {
79
+ const [b, end] = work.pop()!;
80
+ let reached = false;
81
+ for (let i = end - 1; i >= 0; i--) {
82
+ if (b.ops[i] === load) {
83
+ reached = true;
84
+ break;
85
+ }
86
+ if (writes(b.ops[i])) {
87
+ return false;
88
+ }
89
+ }
90
+ if (reached) {
91
+ continue;
92
+ }
93
+ const ps = preds.get(b) ?? [];
94
+ if (ps.length === 0) {
95
+ return false;
96
+ }
97
+ for (const p of ps) {
98
+ if (!seen.has(p)) {
99
+ seen.add(p);
100
+ work.push([p, p.ops.length]);
101
+ }
102
+ }
103
+ }
104
+ return true;
105
+ }
106
+
107
+ export function readDeclaredMembers(fn: Fn, littleEndian: boolean): number {
108
+ const defs = new Map<Value, Op>();
109
+ const uses = new Map<Value, Use[]>();
110
+ const branchArgs = new Set<Value>();
111
+ const blockOf = new Map<Op, Block>();
112
+ const preds = new Map<Block, Block[]>();
113
+ for (const b of fn.blocks) {
114
+ for (const op of b.ops) {
115
+ blockOf.set(op, b);
116
+ op.results.forEach((r) => defs.set(r, op));
117
+ op.operands.forEach((v, idx) => (uses.get(v) ?? uses.set(v, []).get(v)!).push({ op, idx }));
118
+ op.successors.forEach((s) => {
119
+ s.args.forEach((a) => branchArgs.add(a));
120
+ (preds.get(s.block) ?? preds.set(s.block, []).get(s.block)!).push(b);
121
+ });
122
+ }
123
+ }
124
+ // every read of `from`, an edge's argument included
125
+ const replaceAll = (from: Value, to: Value): void => {
126
+ for (const { op, idx } of uses.get(from) ?? []) {
127
+ op.operands[idx] = to;
128
+ }
129
+ if (branchArgs.has(from)) {
130
+ for (const b of fn.blocks) {
131
+ for (const op of b.ops) {
132
+ op.successors.forEach((sc) => (sc.args = sc.args.map((a) => (a === from ? to : a))));
133
+ }
134
+ }
135
+ }
136
+ };
137
+ // a narrowing use of `m`: a store takes the member's low bytes, an extension of the member's
138
+ // own kind is the member read, another extension extends the member
139
+ const narrowOnto = (u: Use, m: Value, signed: boolean): undefined => {
140
+ if (u.op.opcode === 'store') {
141
+ u.op.operands[1] = m;
142
+ } else if (signed === (u.op.opcode === 'sext')) {
143
+ replaceAll(u.op.results[0], m);
144
+ } else {
145
+ u.op.operands[0] = m;
146
+ }
147
+ return undefined;
148
+ };
149
+ const narrowSite = (u: Use) => ({ op: u.op, rewire: (m: Value, signed: boolean) => narrowOnto(u, m, signed) });
150
+ // a shift that leaves exactly a member's bits, extended its own way: the member, or the member
151
+ // extended that way where it is declared the other
152
+ const extractSite = (sh: Op, width: number) => ({
153
+ op: sh,
154
+ rewire: (m: Value, signed: boolean): Op | undefined => {
155
+ const want = sh.opcode === 'shr_s';
156
+ if (signed === want) {
157
+ replaceAll(sh.results[0], m);
158
+ return undefined;
159
+ }
160
+ const x = mkOp(want ? 'sext' : 'zext', {
161
+ operands: [m],
162
+ results: [mkValue(T.unk(32))],
163
+ attrs: { width: width * 8 },
164
+ });
165
+ replaceAll(sh.results[0], x.results[0]);
166
+ return x;
167
+ },
168
+ });
169
+ const isShr = (op: Op): boolean => (op.opcode === 'shr_u' || op.opcode === 'shr_s') && op.operands.length === 1;
170
+ const frameOff = (v: Value): number | undefined => {
171
+ const d = defs.get(v);
172
+ return d?.opcode === 'laddr' ? (d.attrs.off as number) : undefined;
173
+ };
174
+ let changed = 0;
175
+ for (const blk of fn.blocks) {
176
+ for (let i = 0; i < blk.ops.length; i++) {
177
+ const load = blk.ops[i];
178
+ if (load.opcode !== 'load' || qualified(load)) {
179
+ continue;
180
+ }
181
+ const st = frameStructOf(defs, load.operands[0]);
182
+ const word = load.attrs.width as number;
183
+ const at0 = load.attrs.off as number;
184
+ const v = load.results[0];
185
+ if (st === undefined || branchArgs.has(v)) {
186
+ continue;
187
+ }
188
+ // the byte a `width`-byte range `shift` bits up the word starts at
189
+ const byteOf = (shift: number, width: number): number => (littleEndian ? shift / 8 : word - shift / 8 - width);
190
+ const plan: MemberUse[] = [];
191
+ let ok = (uses.get(v) ?? []).length > 0;
192
+ for (const u of uses.get(v) ?? []) {
193
+ const width = narrowedTo(u);
194
+ if (width !== undefined && width < word) {
195
+ plan.push({ byte: byteOf(0, width), width, sites: [narrowSite(u)] });
196
+ continue;
197
+ }
198
+ // `(x << a) >> b`: bits [b - a, 32 - a) of the word, the middle members
199
+ if (u.op.opcode === 'shl' && u.op.operands.length === 1 && !branchArgs.has(u.op.results[0])) {
200
+ const a = u.op.attrs.imm as number;
201
+ const shrs = uses.get(u.op.results[0])?.map((x) => x.op) ?? [];
202
+ const b = shrs[0]?.attrs.imm as number | undefined;
203
+ const width = b === undefined ? 0 : (32 - b) / 8;
204
+ if (
205
+ b !== undefined &&
206
+ shrs.every((x) => isShr(x) && x.attrs.imm === b && !branchArgs.has(x.results[0])) &&
207
+ b >= a &&
208
+ (b - a) % 8 === 0 &&
209
+ (width === 1 || width === 2) &&
210
+ 32 - a <= word * 8
211
+ ) {
212
+ plan.push({ byte: byteOf(b - a, width), width, sites: shrs.map((x) => extractSite(x, width)) });
213
+ continue;
214
+ }
215
+ ok = false;
216
+ break;
217
+ }
218
+ const shift = isShr(u.op) ? (u.op.attrs.imm as number) : 0;
219
+ if (shift <= 0 || shift % 8 !== 0 || shift >= word * 8 || branchArgs.has(u.op.results[0])) {
220
+ ok = false;
221
+ break;
222
+ }
223
+ const top = word - shift / 8;
224
+ const shUses = uses.get(u.op.results[0]) ?? [];
225
+ const widths = new Set(shUses.map(narrowedTo));
226
+ const narrowed = [...widths][0];
227
+ if (
228
+ u.op.opcode === 'shr_u' &&
229
+ shUses.length > 0 &&
230
+ widths.size === 1 &&
231
+ narrowed !== undefined &&
232
+ narrowed < top
233
+ ) {
234
+ plan.push({ byte: byteOf(shift, narrowed), width: narrowed, sites: shUses.map(narrowSite) });
235
+ } else if (word === 4 && (top === 1 || top === 2)) {
236
+ // the shift's fill is the top member's own extension — of a whole word, whose bits above the
237
+ // member are the shift's alone, not a narrower load's extension
238
+ plan.push({ byte: byteOf(shift, top), width: top, sites: [extractSite(u.op, top)] });
239
+ } else {
240
+ ok = false;
241
+ break;
242
+ }
243
+ }
244
+ const signs = plan.map((p) => memberAt(st, at0 + p.byte, p.width));
245
+ if (!ok || signs.some((s) => s === undefined)) {
246
+ continue;
247
+ }
248
+ const obj = frameOff(load.operands[0]);
249
+ const writes = (op: Op): boolean => op.opcode !== 'load' && op.operands.some((o) => frameOff(o) === obj);
250
+ const objectIsBranchArg = [...branchArgs].some((a) => frameOff(a) === obj);
251
+ const mkLoad = (p: MemberUse, signed: boolean): Op =>
252
+ mkOp('load', {
253
+ operands: [load.operands[0]],
254
+ results: [mkValue(T.unk(32))],
255
+ attrs: { off: at0 + p.byte, width: p.width, signed },
256
+ });
257
+ // a member read for an op of the word's own block shares one load where the word was read
258
+ const shared = new Map<string, Op>();
259
+ const after: Op[] = [];
260
+ const sunk: [Op, Op[]][] = [];
261
+ plan.forEach((p, k) => {
262
+ const signed = signs[k]!;
263
+ for (const site of p.sites) {
264
+ const elsewhere = blockOf.get(site.op) !== blk;
265
+ if (elsewhere && !objectIsBranchArg && readableAt(load, site.op, { blockOf, preds, writes })) {
266
+ const m = mkLoad(p, signed);
267
+ const extra = site.rewire(m.results[0], signed);
268
+ sunk.push([site.op, extra === undefined ? [m] : [m, extra]]);
269
+ continue;
270
+ }
271
+ const key = `${p.byte}:${p.width}`;
272
+ const m = shared.get(key) ?? shared.set(key, mkLoad(p, signed)).get(key)!;
273
+ const extra = site.rewire(m.results[0], signed);
274
+ if (extra !== undefined) {
275
+ after.push(extra);
276
+ }
277
+ }
278
+ });
279
+ for (const [site, ops] of sunk) {
280
+ const b = blockOf.get(site)!;
281
+ b.ops.splice(b.ops.indexOf(site), 0, ...ops);
282
+ ops.forEach((o) => blockOf.set(o, b));
283
+ }
284
+ blk.ops.splice(blk.ops.indexOf(load) + 1, 0, ...shared.values(), ...after);
285
+ i = blk.ops.indexOf(load);
286
+ changed++;
287
+ }
288
+ }
289
+ return changed;
290
+ }
@@ -78,7 +78,11 @@ export function recognizeArrays(fn: Fn): number {
78
78
  }
79
79
  b.ops[i] = mkOp('astore', {
80
80
  operands: [m.base, m.index, op.operands[1]],
81
- attrs: carryDiscipline(op, { elemSize: op.attrs.width as number }),
81
+ // the signedness raise/storesign.ts read off a stored constant, when it read one
82
+ attrs: carryDiscipline(op, {
83
+ elemSize: op.attrs.width as number,
84
+ ...(op.attrs.signed !== undefined && { signed: op.attrs.signed }),
85
+ }),
82
86
  });
83
87
  count++;
84
88
  }
@@ -72,6 +72,31 @@ export function foldConstPair(opcode: string, a: number, b: number): number | nu
72
72
  return f ? f(a, b) : null;
73
73
  }
74
74
 
75
+ /** Integer opcodes whose const/const form a test can be read on, as int32: `foldConstPair`'s `add`
76
+ * and `or`, the rest of the arithmetic and the comparisons. `null` for any other opcode. Shared by
77
+ * the reroll's folded top test (`restoreTopTest`) and the `for` whose first test the compiler
78
+ * folded away (structure.ts, `forPlanAt`): which opcodes a test folds through is one decision. */
79
+ const EVAL: Readonly<Record<string, (a: number, b: number) => number>> = {
80
+ add: (a, b) => foldConstPair('add', a, b)!,
81
+ or: (a, b) => foldConstPair('or', a, b)!,
82
+ sub: (a, b) => (a - b) | 0,
83
+ and: (a, b) => a & b,
84
+ xor: (a, b) => a ^ b,
85
+ icmp_eq: (a, b) => +(a === b),
86
+ icmp_ne: (a, b) => +(a !== b),
87
+ icmp_slt: (a, b) => +(a < b),
88
+ icmp_sle: (a, b) => +(a <= b),
89
+ icmp_sgt: (a, b) => +(a > b),
90
+ icmp_sge: (a, b) => +(a >= b),
91
+ icmp_ult: (a, b) => +(a >>> 0 < b >>> 0),
92
+ icmp_ule: (a, b) => +(a >>> 0 <= b >>> 0),
93
+ icmp_ugt: (a, b) => +(a >>> 0 > b >>> 0),
94
+ icmp_uge: (a, b) => +(a >>> 0 >= b >>> 0),
95
+ };
96
+ export const evalIntPair = (opcode: string, a: number, b: number): number | null =>
97
+ opcode in EVAL ? EVAL[opcode](a, b) : null;
98
+ export const isIntPairOpcode = (opcode: string): boolean => opcode in EVAL;
99
+
75
100
  /** Does this opcode's const/const form denote a constant? The membership half of `foldConstPair`,
76
101
  * for callers that must classify an op before they have its operands' values. */
77
102
  export const isConstFoldOpcode = (opcode: string): boolean => opcode in FOLD;
@@ -1,32 +1,53 @@
1
1
  // asmlift — the decider for ir/discipline.ts's `declared` placement: a memory access of an object
2
- // the project's symbol map declares volatile.
2
+ // the project's symbol map, or its declaration context, declares volatile.
3
3
  //
4
- // The stamp is a function of the IR, the map the function is lifted and structured under, and the
5
- // compiler, and nothing else: an access whose base reaches a global the map qualifies, at the byte
6
- // the access names (`declaresVolatile`) — and, for a read, one the compiler could have made of a
7
- // `volatile` (target.ts `readCouldBeVolatile`, which the device pin asks too). It is put as the
4
+ // The stamp is a function of the IR, the map the function is lifted and structured under, the
5
+ // declaration context's globals and the compiler, and nothing else: an access whose base reaches a
6
+ // global the map qualifies, at the byte the access names (`declaresVolatile`), or one the context
7
+ // declares with a volatile byte and the map does not lay out (`ContextGlobals.volatiles`, per
8
+ // object, not per byte), or one through the value of a pointer the context declares to point at a
9
+ // volatile (`ContextGlobals.pointeeVolatiles`) — and, for a read, one the compiler could have made
10
+ // of a `volatile` (target.ts `readCouldBeVolatile`, which the device pin asks too). It is put as the
8
11
  // lift is made (pipeline.ts `liftStamped`), before the first pass that asks whether a read may be
9
12
  // deleted, folded or moved, and re-derived by every pass that changes which object an access names
10
13
  // and by structuring. Structuring the same function under another map — the `/raw-globals` setting
11
14
  // has none — re-runs it, and an op the new map does not qualify loses the stamp, so a structuring
12
- // never answers for a map it was not given. The base is read as the structurer will read it: a join
13
- // whose every edge passes one address is that address (`addressThroughJoins`). A base that reaches
14
- // no name is a pointer parameter, a raw address or a genuine join: no declaration is in evidence,
15
- // and the access stays unplaced unless the lift pinned it (`device`).
15
+ // never answers for a map it was not given. The context is not the map: every candidate compiles
16
+ // against it whichever names it spells, so it is read in both settings.
17
+ //
18
+ // The base is read as the structurer will read it: a join whose every edge passes one address is
19
+ // that address (`addressThroughJoins`), and one every edge of which reaches one object — two of its
20
+ // addresses, or a pointer a loop steps through it — is an access of that object, as one every edge
21
+ // of which moves a pointer global's value is an access of its pointee (`reachThroughJoins`, one
22
+ // reader for both). A join whose edges reach different globals (`c ? gP : &gV`) is an access of any
23
+ // of them, placed only where every one is declared. A base with an edge that reaches no name — a
24
+ // pointer parameter, a raw address — has no declaration in evidence, and the access stays
25
+ // unplaced unless the lift pinned it (`device`).
16
26
  //
17
27
  // A read structuring stamps that the earlier stamps left plain was plain to every raising pass, and a
18
28
  // short-circuit fold may have lifted it out of the arm it ran in. structure.ts declines on such a
19
29
  // read in a `&&`/`||`'s guarded operand rather than place it.
20
- import { globalBaseOf, globalCellOf } from '../ir/alias';
30
+ import { globalCellOf } from '../ir/alias';
21
31
  import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
22
32
  import { DECLARED_VOLATILE } from '../ir/discipline';
23
33
  import { MEM_BASE_OPS } from '../ir/opcodes';
34
+ import type { ContextGlobals } from '../proto-context';
24
35
  import { type SymbolInfo, type SymbolMap, declaresVolatile, symbolsByName } from '../symbols';
25
36
  import { type TargetDescription, readCouldBeVolatile } from '../target';
26
37
  import { valueNumberOf } from './gvn';
27
38
 
28
- /** The compiler fact the stamp reads. */
29
- export type StampBehaviors = Pick<TargetDescription['compilerBehaviors'], 'volatileReadsExtendInRegister'>;
39
+ /** The compiler fact the stamp reads, and the declaration context's globals when there is one.
40
+ * The context is a REQUIRED key, `undefined` when there is none: the stamp clears every stamp it
41
+ * does not re-derive, so a re-stamp handed the compiler behaviors alone would unplace every read
42
+ * the context made volatile. Build one with pipeline.ts `stampBehaviorsFor`. */
43
+ export type StampBehaviors = Pick<TargetDescription['compilerBehaviors'], 'volatileReadsExtendInRegister'> & {
44
+ readonly contextGlobals: ContextGlobals | undefined;
45
+ };
46
+
47
+ /** Does the map lay out this object's volatility itself? A layout answers per member, which is
48
+ * finer than the context's per-object fact; an entry with none (a name and a size off the symbol
49
+ * table) leaves the context's answer standing. */
50
+ const laysOut = (info: SymbolInfo | undefined): boolean => Array.isArray(info?.layout) && info.layout.length > 0;
30
51
 
31
52
  /** May this memory access be stamped: a store, or a read the compiler could have made of a
32
53
  * `volatile`? */
@@ -41,6 +62,29 @@ const stampable = (op: Op, behaviors: StampBehaviors): boolean =>
41
62
  * is such a parameter, and the stamp is put before that pass, so it reads the base the structurer
42
63
  * will. */
43
64
  function addressThroughJoins(fn: Fn): (v: Value) => Value {
65
+ const incoming = joinIncoming(fn);
66
+ const numberOf = valueNumberOf(fn);
67
+ const through = (v: Value, onWalk: Set<Value>): Value | null => {
68
+ const ins = incoming.get(v);
69
+ if (ins === undefined) {
70
+ return v;
71
+ }
72
+ if (onWalk.has(v)) {
73
+ return null;
74
+ }
75
+ onWalk.add(v);
76
+ const each = ins.filter((a) => a !== v).map((a) => through(a, onWalk));
77
+ onWalk.delete(v);
78
+ const first = each[0];
79
+ return first !== undefined && first !== null && each.every((a) => a !== null && numberOf(a) === numberOf(first))
80
+ ? first
81
+ : null;
82
+ };
83
+ return (v) => through(v, new Set()) ?? v;
84
+ }
85
+
86
+ /** Every block parameter's incoming values, one per in-edge (the entry block's excepted). */
87
+ export function joinIncoming(fn: Fn): Map<Value, Value[]> {
44
88
  const incoming = new Map<Value, Value[]>();
45
89
  for (const b of fn.blocks) {
46
90
  for (const op of b.ops) {
@@ -56,48 +100,180 @@ function addressThroughJoins(fn: Fn): (v: Value) => Value {
56
100
  }
57
101
  }
58
102
  }
59
- const numberOf = valueNumberOf(fn);
60
- const through = (v: Value, onWalk: Set<Value>): Value | null => {
61
- const ins = incoming.get(v);
103
+ return incoming;
104
+ }
105
+
106
+ /** What an address reaches: an access of a global OBJECT (`gW.b`, `gVA[i]`), or of what the pointer a
107
+ * global holds points at (`*gP`, `gQ->b`) — the POINTEE of that global. */
108
+ type Reach = { readonly kind: 'object' | 'pointee'; readonly sym: string };
109
+
110
+ /** The globals an address reaches, one per edge that reaches a different one, each as an object or
111
+ * as a pointer's pointee, read through address arithmetic and joins alike: a `gaddr` is its object;
112
+ * a word read from a global's own first byte is that global's value, so an address moved from it
113
+ * reaches the global's pointee (asked only of a global in `pointers`, the ones the context declares
114
+ * to point at a volatile: any other word read is an index, not a base) — and so is a word read from
115
+ * a join every edge of which is such a global's address; an `add` with one reaching operand and a
116
+ * `sub` from one reach what they do; and a block parameter reaches what each in-edge's value
117
+ * reaches, an edge that passes the parameter itself moved by an offset (a strength-reduced loop
118
+ * pointer `v0 = v0 + 2`) aside. So `c ? &gW.a : &gW.b` and a pointer seeded with `&gVA` and stepped
119
+ * through it are accesses of that one object, `c ? gP + 1 : gP + 2` and a pointer seeded with `gP`
120
+ * and stepped are accesses of gP's pointee, and `c ? gP : gQ` or `c ? gP : &gV` is an access of
121
+ * either, which the caller places only where every one is declared. ONE reader answers all of
122
+ * them, so a shape the object path reads the pointee path reads too. Null where an edge reaches
123
+ * none. `addressThroughJoins` — one value on every edge — is narrower. */
124
+ function reachThroughJoins(
125
+ fn: Fn,
126
+ defs: Map<Value, Op>,
127
+ pointers: ReadonlySet<string> | undefined,
128
+ ): (v: Value) => readonly Reach[] | null {
129
+ const incoming = joinIncoming(fn);
130
+ const SELF = Symbol('self');
131
+ type Step = readonly Reach[] | typeof SELF | null;
132
+ const union = (steps: readonly (readonly Reach[])[]): Reach[] => {
133
+ const out: Reach[] = [];
134
+ for (const r of steps.flat()) {
135
+ if (!out.some((o) => o.kind === r.kind && o.sym === r.sym)) {
136
+ out.push(r);
137
+ }
138
+ }
139
+ return out;
140
+ };
141
+ /** The globals whose own address every edge of `v` passes, unmoved; null where one passes anything else. */
142
+ // A value's answer is the same from every walk that never cut a cycle below it, so those are
143
+ // remembered: without that, a chain of diamonds whose arms both step one pointer is walked once
144
+ // per path, 2^n times, on every stamp. An answer that cut a cycle depends on the walk and is not.
145
+ let cuts = 0;
146
+ const addressMemo = new Map<Value, string[] | null>();
147
+ const reachMemo = new Map<Value, Step>();
148
+ const addresses = (v: Value, onWalk: Set<Value>): string[] | null => {
149
+ const d = defs.get(v);
150
+ if (d?.opcode === 'gaddr') {
151
+ return [d.attrs.sym as string];
152
+ }
153
+ const ins = d === undefined ? incoming.get(v) : undefined;
62
154
  if (ins === undefined) {
63
- return v;
155
+ return null;
64
156
  }
65
157
  if (onWalk.has(v)) {
158
+ cuts++;
66
159
  return null;
67
160
  }
161
+ if (addressMemo.has(v)) {
162
+ return addressMemo.get(v)!;
163
+ }
164
+ const before = cuts;
68
165
  onWalk.add(v);
69
- const each = ins.filter((a) => a !== v).map((a) => through(a, onWalk));
166
+ const each = ins.filter((a) => a !== v).map((a) => addresses(a, onWalk));
70
167
  onWalk.delete(v);
71
- const first = each[0];
72
- return first !== undefined && first !== null && each.every((a) => a !== null && numberOf(a) === numberOf(first))
73
- ? first
74
- : null;
168
+ const out = each.length > 0 && each.every((a) => a !== null) ? [...new Set(each.flat())] : null;
169
+ if (cuts === before) {
170
+ addressMemo.set(v, out);
171
+ }
172
+ return out;
173
+ };
174
+ const reach = (v: Value, onWalk: Set<Value>): Step => {
175
+ if (reachMemo.has(v)) {
176
+ return reachMemo.get(v)!;
177
+ }
178
+ const before = cuts;
179
+ const out = reachOnce(v, onWalk);
180
+ if (cuts === before) {
181
+ reachMemo.set(v, out);
182
+ }
183
+ return out;
184
+ };
185
+ const reachOnce = (v: Value, onWalk: Set<Value>): Step => {
186
+ const d = defs.get(v);
187
+ if (d?.opcode === 'gaddr') {
188
+ return [{ kind: 'object', sym: d.attrs.sym as string }];
189
+ }
190
+ if (d?.opcode === 'load' && ((d.attrs.off as number | undefined) ?? 0) === 0) {
191
+ const syms = addresses(d.operands[0], new Set());
192
+ return syms !== null && syms.every((sym) => pointers?.has(sym) === true)
193
+ ? syms.map((sym) => ({ kind: 'pointee', sym }))
194
+ : null;
195
+ }
196
+ if (d?.opcode === 'add' && d.operands.length === 2) {
197
+ const a = reach(d.operands[0], onWalk);
198
+ const b = reach(d.operands[1], onWalk);
199
+ if (a !== null && b !== null) {
200
+ // An object's address plus a pointer's value is an index into the object (a byte array
201
+ // subscripted by a global the context also declares a pointer, or by a join of two such
202
+ // values); two of a kind name none, and neither does a side that mixes the kinds.
203
+ if (a === SELF || b === SELF) {
204
+ return null;
205
+ }
206
+ const all = (r: readonly Reach[], kind: Reach['kind']): boolean => r.every((x) => x.kind === kind);
207
+ if (all(a, 'object') && all(b, 'pointee')) {
208
+ return a;
209
+ }
210
+ return all(b, 'object') && all(a, 'pointee') ? b : null;
211
+ }
212
+ return a ?? b;
213
+ }
214
+ if (d?.opcode === 'sub' && d.operands.length === 2) {
215
+ return reach(d.operands[0], onWalk);
216
+ }
217
+ const ins = d === undefined ? incoming.get(v) : undefined;
218
+ if (ins === undefined) {
219
+ return null;
220
+ }
221
+ if (onWalk.has(v)) {
222
+ cuts++;
223
+ return SELF;
224
+ }
225
+ onWalk.add(v);
226
+ const each = ins.map((a) => reach(a, onWalk)).filter((r) => r !== SELF);
227
+ onWalk.delete(v);
228
+ return each.length > 0 && each.every((r) => r !== null) ? union(each as (readonly Reach[])[]) : null;
229
+ };
230
+ return (v) => {
231
+ const r = reach(v, new Set());
232
+ return r === null || r === SELF ? null : r;
75
233
  };
76
- return (v) => through(v, new Set()) ?? v;
77
234
  }
78
235
 
79
- /** Stamp every memory access of `fn` to an object `symbols` declares volatile — a read only where a
80
- * compiler behaving as `behaviors` says could have made it of one — and clear the stamp from every
81
- * other op. Returns the ops it stamped that were not stamped before. */
236
+ /** Stamp every memory access of `fn` to an object `symbols` or `behaviors.contextGlobals` declares
237
+ * volatile — a read only where a compiler behaving as `behaviors` says could have made it of one —
238
+ * and clear the stamp from every other op. Returns the ops it stamped that were not stamped before. */
82
239
  export function stampDeclaredVolatile(
83
240
  fn: Fn,
84
241
  symbols: ReadonlyMap<string, SymbolInfo> | undefined,
85
242
  behaviors: StampBehaviors,
86
243
  ): Op[] {
87
244
  const added: Op[] = [];
88
- const defs = symbols ? defOpMap(fn) : undefined;
89
- const addressOf = symbols ? addressThroughJoins(fn) : undefined;
245
+ const contextVolatiles = behaviors.contextGlobals?.volatiles;
246
+ const pointees = behaviors.contextGlobals?.pointeeVolatiles;
247
+ const reads =
248
+ symbols !== undefined ||
249
+ (contextVolatiles !== undefined && contextVolatiles.size > 0) ||
250
+ (pointees !== undefined && pointees.size > 0);
251
+ const defs = reads ? defOpMap(fn) : undefined;
252
+ const addressOf = reads ? addressThroughJoins(fn) : undefined;
253
+ const reachOf = reads ? reachThroughJoins(fn, defs!, pointees) : undefined;
90
254
  for (const b of fn.blocks) {
91
255
  for (const op of b.ops) {
92
256
  const addr = defs && stampable(op, behaviors) ? addressOf!(op.operands[0]) : undefined;
93
- const base = addr !== undefined ? globalBaseOf(defs!, addr) : null;
257
+ const reached = addr !== undefined ? reachOf!(addr) : null;
94
258
  // A subscript reaches the object while naming no cell, so only a `load`/`store` asks by byte,
95
- // and an access that names no byte is placed only where every byte it could reach is qualified.
259
+ // and an access that names no byte is placed only where every byte it could reach is
260
+ // qualified. An access that may reach any of several globals is placed only where each one
261
+ // declares it.
96
262
  const cell =
97
- base !== null && (op.opcode === 'load' || op.opcode === 'store')
263
+ reached?.length === 1 && reached[0].kind === 'object' && (op.opcode === 'load' || op.opcode === 'store')
98
264
  ? globalCellOf(defs!, addr!, op.attrs.off as number)
99
265
  : null;
100
- const declared = base !== null && declaresVolatile(symbols!.get(base), cell === null ? null : cell.byte);
266
+ const declares = (r: Reach): boolean => {
267
+ if (r.kind === 'pointee') {
268
+ return pointees?.has(r.sym) === true;
269
+ }
270
+ const info = symbols?.get(r.sym);
271
+ return (
272
+ declaresVolatile(info, cell === null ? null : cell.byte) ||
273
+ (contextVolatiles?.has(r.sym) === true && !laysOut(info))
274
+ );
275
+ };
276
+ const declared = reached !== null && reached.every(declares);
101
277
  // A fresh record either way: a rebuilder may hand two ops one `attrs` object.
102
278
  if (declared && op.attrs[DECLARED_VOLATILE] !== true) {
103
279
  op.attrs = { ...op.attrs, [DECLARED_VOLATILE]: true };