@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.
Files changed (101) 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 +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
package/src/ir/core.ts CHANGED
@@ -86,6 +86,28 @@ export interface LocalObject {
86
86
  /** What an object FILE records of where the object sits, absent for an assembler listing, whose
87
87
  * `.align` is among its directives. */
88
88
  placement?: LocalObjectPlacement;
89
+ /** Every address the initializer holds, in offset order — the words a relocation fills in at
90
+ * link time, which the bytes alone do not carry. Absent for an object holding none. The bytes
91
+ * under one say nothing: the relocation's `addend` is the whole of it. */
92
+ relocs?: readonly LocalObjectReloc[];
93
+ /** The object is the compiler's own, under a label the source never wrote (agbcc's `.LC0`): the
94
+ * constant image a local aggregate's initializer is copied from. `name` is then the label, no
95
+ * declaration spells it, and it reaches the C only as that local's initializer
96
+ * (l3/constinit.ts). */
97
+ anonymous?: true;
98
+ }
99
+
100
+ /** The bytes of every address an initializer holds ({@link LocalObjectReloc}): a pointer's, on every
101
+ * target whose initializers the frontends relocate (agbcc's `.word sym`, mwcc's `R_PPC_ADDR32`). */
102
+ export const LOCAL_OBJECT_ADDRESS_BYTES = 4;
103
+
104
+ /** One address an initializer holds: the pointer-width word at byte `offset` of the object is
105
+ * `symbol`'s address plus `addend` — an ordinary global's, the only kind a declaration outside the
106
+ * function can name (frontend/reloc-symbol.ts). */
107
+ export interface LocalObjectReloc {
108
+ offset: number;
109
+ symbol: string;
110
+ addend: number;
89
111
  }
90
112
 
91
113
  /** What an object file records of where a static sits. */
@@ -112,6 +134,12 @@ export interface LocalObjectDirectives {
112
134
  /** every value directive writes a string (`.ascii`), as the compiler writes a string literal —
113
135
  * whole, or as the elements of an array or struct of strings */
114
136
  string: boolean;
137
+ /** every run of padding among the value directives, as [byte offset, byte length], when there
138
+ * is one: in an array, the elements its brace initializer left out — the compiler writes every
139
+ * element the source spelled, zero or not, as a value, and each run it left out as a `.space`
140
+ * (varasm.c `output_constructor`), wherever the run falls: after the last value, ahead of the
141
+ * first, or between two (a designator, a nested brace's short row). In a struct, alignment too. */
142
+ gaps?: [number, number][];
115
143
  }
116
144
 
117
145
  /** The function-scope statics a function DEFINES, keyed by {@link LocalObject.symbol} — the object a
@@ -135,14 +163,16 @@ export type LocalObjects = ReadonlyMap<string, LocalObject>;
135
163
  * `deadHome` — THE STORE DOES NOT REACH THE IR. Every slot-modelling frontend spells a word
136
164
  * sp-relative store as a write to the SSA key `sp@k` instead of a `store` op (`stackSlotKey`), so
137
165
  * a slot nothing reloads has no reader, no op and no value: it simply is not there by L1. MIPS and
138
- * Thumb lift such a store. PowerPC refuses it, because an unread store may be a call's outgoing
139
- * stack argument (frontend/ppc.ts), so a PowerPC lift never reports a dead home. A
166
+ * Thumb lift such a store. PowerPC refuses it, because an unread slot store may be a call's
167
+ * outgoing stack argument; in a function that makes no call, mwcc's local area takes such a store
168
+ * as a store to a frame object instead of a slot (frontend/ppc.ts `measurePpcFrame`). Either way a
169
+ * PowerPC lift never reports a dead home. A
140
170
  * parameter stored to two slots, one of them reloaded, still counts — the dead store happened —
141
171
  * and the reader's own gates decide what a parameter with that much traffic may become.
142
172
  *
143
173
  * It asks NO FRAME PARTITION, and that is the difference from {@link SlotHomes}. A slot home is a
144
174
  * DECLARATION RANK, so `sp@40` had to be classified as this function's local before it could be
145
- * stamped, and MIPS declares no partition and so stamps none. "Stored here and never read back"
175
+ * stamped, and MIPS declares no `declaredLocals` and so stamps none. "Stored here and never read back"
146
176
  * needs no such classification: it is a statement about the store, true at any offset.
147
177
  *
148
178
  * `selfRedefined` — THE REGISTER IDENTITY DOES NOT REACH THE IR EITHER. SSA renames, so nothing
@@ -178,7 +208,7 @@ export type ParamEvidence = ReadonlyMap<Value, ParamObservation>;
178
208
  * A KEY IS NOT A FRAME COORDINATE, and the frontends differ on exactly that. `sp@40` is a local
179
209
  * on one ABI and the caller's fifth argument on another, so the shared stamp asks the frontend's
180
210
  * own `LiveInModel.declaredLocals` and refuses an offset outside it — Thumb declares a range,
181
- * MIPS and PPC declare no partition and so stamp nothing. An entry here therefore means "storage
211
+ * MIPS and PPC declare none and so stamp nothing. An entry here therefore means "storage
182
212
  * this function DECLARES, homed at k", not "some sp-relative key".
183
213
  *
184
214
  * DECLARES, NOT OWNS, AND THE DIFFERENCE IS AN AGBCC FACT WITH A LIVE DEPENDENCY: `ownedLocals`,
@@ -283,6 +313,35 @@ export function foldWriteOrder(order: WriteOrder | undefined, from: Block, into:
283
313
  order.writes.set(into, base + (order.writes.get(from) ?? 0));
284
314
  }
285
315
 
316
+ /** The `laddr` attribute that marks a function's OWN struct return object: the local its hidden
317
+ * struct-return pointer became, or the one its struct returned in a register is built in
318
+ * (frontend/own-struct-return.ts). Neither lives in this frame, so no frame offset names it and
319
+ * its `off` means nothing. */
320
+ export const OWN_RETURN_OBJECT = 'returned';
321
+
322
+ /** The `call` attribute of L1 that says its result is the IMAGE of a struct its callee is declared
323
+ * to return in the return register (frontend/call-plan.ts `registerStructLocal`): a word the
324
+ * frontend reads as any call's, which `liftRegisterStructCalls` (frontend/register-struct-call.ts)
325
+ * turns into the struct assigned to a local and re-read off it. No call carries it past that.
326
+ * `true`, or {@link STRUCT_HOMED} where the frontend saw the image stored to a frame home. */
327
+ export const REGISTER_STRUCT = 'registerStruct';
328
+
329
+ /** The value of {@link REGISTER_STRUCT} on a call whose image the function stores to a frame word
330
+ * (frontend/ppc.ts): mwcc gives a struct the source names a home, read or not, and one the source
331
+ * only returns none — `struct T s = mk(1); return s;` is `bl mk; stw r3,8(r1)`, `return mk(1);` is
332
+ * `bl mk` alone. */
333
+ export const STRUCT_HOMED = 'homed';
334
+
335
+ /** The `laddr` attribute that marks the local a struct returned in a register is assigned to
336
+ * (frontend/register-struct-call.ts), holding the callee's name, which names the local. The
337
+ * struct lives in the return register, not in this frame, so no frame offset names it and its
338
+ * `off` means nothing, as for {@link OWN_RETURN_OBJECT}. */
339
+ export const CALL_RETURN_OBJECT = 'callReturned';
340
+
341
+ /** The `ret` attribute that says it hands back the {@link OWN_RETURN_OBJECT} whole, by value: its
342
+ * operand is that object's address, and the C returns the object. */
343
+ export const RETURNS_OBJECT = 'returnsObject';
344
+
286
345
  export function mkValue(type: IrType): Value {
287
346
  return { type };
288
347
  }
@@ -314,6 +373,15 @@ export function calleeName(op: Op): string | undefined {
314
373
  return op.opcode === 'call' && typeof op.attrs.target === 'string' ? op.attrs.target : undefined;
315
374
  }
316
375
 
376
+ /** The operands a call passes a struct BY VALUE (frontend/call-plan.ts `foldByValueArgs`): each
377
+ * one's place in the operands and the declared parameter it is. The two differ by more than a
378
+ * hidden return pointer wherever a parameter before it takes two operands, so the parameter is
379
+ * carried rather than re-derived. */
380
+ export function byValueOperands(op: Op): { at: number; param: number }[] {
381
+ const pairs = op.opcode === 'call' ? ((op.attrs.byValue as number[] | undefined) ?? []) : [];
382
+ return Array.from({ length: pairs.length / 2 }, (_, i) => ({ at: pairs[2 * i], param: pairs[2 * i + 1] }));
383
+ }
384
+
317
385
  /** A call's arguments, the callee value of an indirect call aside. */
318
386
  export function callArgs(op: Op): Value[] {
319
387
  return calleeValue(op) === undefined ? op.operands : op.operands.slice(0, -1);
@@ -388,6 +456,33 @@ export function reachableBlocks(fn: Fn): Set<Block> {
388
456
  return seen;
389
457
  }
390
458
 
459
+ /** The blocks `from` reaches along FORWARD edges — never a back-edge, whose target dominates its
460
+ * source — `from` itself included. A block of `stop` is reached but not walked past.
461
+ *
462
+ * Forward only, so an enclosing loop's next iteration does not count: the exit of a loop nested in
463
+ * another reaches every block of the inner body round the outer back-edge, and a full walk would
464
+ * say it runs the inner loop's own arms. Two passes ask "does a loop's exit run this block":
465
+ * `raise/latch.ts`'s exit-trampoline scope gate and `structure.ts`'s `offExitPathOf` (the
466
+ * duplication policy `codeBreakArm` and `earlyReturnArm` share). */
467
+ export function reachForward(
468
+ from: Block,
469
+ dom: ReadonlyMap<Block, ReadonlySet<Block>>,
470
+ stop: ReadonlySet<Block> = new Set(),
471
+ ): Set<Block> {
472
+ const seen = new Set<Block>();
473
+ for (const stack = [from]; stack.length > 0;) {
474
+ const x = stack.pop()!;
475
+ if (seen.has(x)) {
476
+ continue;
477
+ }
478
+ seen.add(x);
479
+ if (!stop.has(x)) {
480
+ stack.push(...successorsOf(x).filter((s) => !dom.get(x)!.has(s)));
481
+ }
482
+ }
483
+ return seen;
484
+ }
485
+
391
486
  /** Predecessor map for the whole function's CFG. */
392
487
  export function predecessors(fn: Fn): Map<Block, Block[]> {
393
488
  const preds = new Map<Block, Block[]>();
@@ -405,7 +500,7 @@ export function predecessors(fn: Fn): Map<Block, Block[]> {
405
500
  /** Forward dominators (iterative data-flow). dom(b) = {b} ∪ ⋂ dom(preds).
406
501
  *
407
502
  * A CFG fact, so it lives beside `predecessors` it is built on rather than with either consumer:
408
- * `verify` needs it to check def-dominates-use, `structure/loops.ts` to tell a back-edge from a
503
+ * `verify` needs it to check def-dominates-use, `ir/loops.ts` to tell a back-edge from a
409
504
  * forward one, and `raise/latch.ts` to tell a latch from a preheader. */
410
505
 
411
506
  export function dominators(fn: Fn): Map<Block, Set<Block>> {
@@ -4,8 +4,10 @@
4
4
  // Two kinds of fact meet here. The registry's flags (ir/opcodes.ts `OpSig`: `effects`, `reads`,
5
5
  // `traps`) are facts about an OPCODE. A PLACEMENT is a fact about one OP, decided once by the pass
6
6
  // that knew it: `call` is its own opcode; `helper` is the stamp `runtime-helpers.ts` `helperOp` puts
7
- // on a value op the asm computed by calling a runtime routine; `device` is the `volatile` the
8
- // frontend's device-pin pass (frontend/device-pins.ts) puts on a memory access; `declared` is the
7
+ // on a value op the asm computed by calling a runtime routine; `device` is the `volatile` a frontend
8
+ // puts on a memory access, decided by two pins — the device pin (frontend/device-pins.ts), for an
9
+ // access of a device register, and the dead-read pin (frontend/dead-reads.ts), for a read at ANY
10
+ // address whose value the lifted function never uses; `declared` is the
9
11
  // stamp raise/declared-volatile.ts puts on an access of an object the symbol map declares volatile —
10
12
  // a read only where the compiler could have made it of a `volatile` — as the lift is made and again
11
13
  // when structuring starts. Every question below takes the op, so a pass
@@ -50,7 +52,7 @@
50
52
  // a read the stamp cannot place by byte — a runtime index, an `aload` — of an object the map
51
53
  // qualifies only some members of, which may reach a plain member (symbols.ts
52
54
  // `declaresVolatile`).
53
- import type { Op } from './core';
55
+ import type { Block, Op, Value } from './core';
54
56
  import { MEM_BASE_OPS, type OpSig, opSig } from './opcodes';
55
57
 
56
58
  /** Why an op must run where the asm ran it. */
@@ -107,23 +109,37 @@ const PLACEMENT_ANSWERS: Readonly<Record<Placement, PlacementAnswers>> = {
107
109
  },
108
110
  };
109
111
 
110
- /** The placement an op carries, or null for an op that may render wherever its value is used. */
111
- export function placedAt(op: Op): Placement | null {
112
+ /** Every placement stamp an op carries, the one that places it first. An op can carry two: a read
113
+ * of an object the map declares volatile is `declared` and, kept by the lift's own pin (frontend/
114
+ * dead-reads.ts, frontend/device-pins.ts), `device` too.
115
+ *
116
+ * A declaration that qualifies the object places the access before a lift's own stamp does: both
117
+ * say the access is `volatile`, and the declaration is the spelling the source used, so the
118
+ * access counts and spells as the object's (raise/declared-volatile.ts re-derives it per map, and
119
+ * under a map that declares nothing the lift's stamp places it again — which is why a rebuild
120
+ * carries both, `carryDiscipline`). */
121
+ function stampsOf(op: Op): Placement[] {
112
122
  if (op.opcode === 'call') {
113
- return 'call';
114
- }
115
- if (MEM_BASE_OPS.has(op.opcode) && op.attrs.volatile === true) {
116
- return 'device';
123
+ return ['call'];
117
124
  }
125
+ const stamps: Placement[] = [];
118
126
  if (MEM_BASE_OPS.has(op.opcode) && op.attrs[DECLARED_VOLATILE] === true) {
119
- return 'declared';
127
+ stamps.push('declared');
128
+ }
129
+ if (MEM_BASE_OPS.has(op.opcode) && op.attrs.volatile === true) {
130
+ stamps.push('device');
120
131
  }
121
132
  // An `opaque` carries `helper` too, naming the call nothing could fold (`refuseUnmodelledHelpers`);
122
133
  // it is no value op, and its `effects` flag already places it.
123
134
  if (op.opcode !== 'opaque' && typeof op.attrs.helper === 'string') {
124
- return 'helper';
135
+ stamps.push('helper');
125
136
  }
126
- return null;
137
+ return stamps;
138
+ }
139
+
140
+ /** The placement an op carries, or null for an op that may render wherever its value is used. */
141
+ export function placedAt(op: Op): Placement | null {
142
+ return stampsOf(op)[0] ?? null;
127
143
  }
128
144
 
129
145
  const answered = (op: Op, q: 'keptWhenDead' | 'speculationUnsafe' | 'counted'): boolean => {
@@ -161,12 +177,59 @@ export function effectful(op: Op): boolean {
161
177
  }
162
178
 
163
179
  /** May a dead result be deleted? Registered, no effect, not control flow, and not an op whose
164
- * placement keeps it. */
180
+ * placement keeps it. A memory read whose value the RAW lift never uses is never asked: the
181
+ * frontend stamps it `volatile` first (frontend/dead-reads.ts), because a compiler deletes every
182
+ * plain read nobody uses, so one still in the target was qualified. What reaches this question
183
+ * plain is a read only asmlift's own rewrites orphaned, or one that pass's gates refused. */
165
184
  export function deletableWhenDead(op: Op): boolean {
166
185
  const sig = sigOf(op);
167
186
  return !!sig && !sig.effects && !sig.terminator && !answered(op, 'keptWhenDead');
168
187
  }
169
188
 
189
+ /** THE DEAD OPS of these blocks: each op `deletableWhenDead` answers yes for whose single result no
190
+ * op outside the set reads, as an operand or as a successor's argument, to a fixed point. The ONE
191
+ * definition of dead: pattern/engine.ts `dce` deletes exactly this set, and the dead-read pin
192
+ * (frontend/dead-reads.ts) asks it of the raw lift before anything is deleted, so the pin's "dead"
193
+ * cannot drift from what the pass would delete.
194
+ *
195
+ * `usesOperands` says whether an op's operands are uses at all. Every op's are, for `dce`; the pin
196
+ * passes the one exception a declaration makes: the `ret` of a function its own prototype declares
197
+ * void, whose operand the structurer suppresses (structure/analysis.ts `indexUses`). */
198
+ export function deadOps(blocks: readonly Block[], usesOperands: (op: Op) => boolean = () => true): Set<Op> {
199
+ const dead = new Set<Op>();
200
+ let changed = true;
201
+ while (changed) {
202
+ changed = false;
203
+ const used = new Set<Value>();
204
+ for (const b of blocks) {
205
+ for (const op of b.ops) {
206
+ if (dead.has(op)) {
207
+ continue;
208
+ }
209
+ if (usesOperands(op)) {
210
+ for (const o of op.operands) {
211
+ used.add(o);
212
+ }
213
+ }
214
+ for (const s of op.successors) {
215
+ for (const a of s.args) {
216
+ used.add(a);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ for (const b of blocks) {
222
+ for (const op of b.ops) {
223
+ if (!dead.has(op) && deletableWhenDead(op) && op.results.length === 1 && !used.has(op.results[0])) {
224
+ dead.add(op);
225
+ changed = true;
226
+ }
227
+ }
228
+ }
229
+ }
230
+ return dead;
231
+ }
232
+
170
233
  /** Must the structurer still SPELL this op when nothing consumes its result — an effect, or a
171
234
  * memory read?
172
235
  *
@@ -174,8 +237,9 @@ export function deletableWhenDead(op: Op): boolean {
174
237
  * the C claim: nothing observes a load nobody reads. This one is the COMPILER claim: an optimizing
175
238
  * compiler deletes every dead read it is allowed to delete, so one still in the target is evidence
176
239
  * the source's access was `volatile`. A yes here only says the op may not be dropped silently;
177
- * whether a `volatile` can reach the access is an ADDRESS-level question the caller asks
178
- * (structure.ts `volatileQualifiable`), and a read it answers no to is dropped.
240
+ * whether the read was `volatile` is decided once, by the dead-read pin (frontend/dead-reads.ts),
241
+ * and the caller spells a read only where it is `qualified` (structure.ts `volatileQualifiable`),
242
+ * dropping one the pin left plain.
179
243
  *
180
244
  * Its own function and not `orderSensitive`, though the two agree on every registered opcode: they
181
245
  * ask different questions, and a registry change that splits them is a decision to make at each. */
@@ -238,11 +302,12 @@ export function forgetHelperPlacement(op: Op): Op {
238
302
  return op;
239
303
  }
240
304
 
241
- /** `attrs`, plus the placement of `from`, for an op a pass builds to stand for `from`: a memory
242
- * access keeps its `volatile` or its `declared` stamp, a value op its `helper`. Every op rebuilder
243
- * carries the stamps through here, so a placement decided once is not lost to a rebuild that
244
- * forgot to copy it. */
305
+ /** `attrs`, plus every placement stamp of `from`, for an op a pass builds to stand for `from`: a
306
+ * memory access keeps its `volatile` and its `declared` stamp, a value op its `helper`. Every op
307
+ * rebuilder carries the stamps through here, so a placement decided once is not lost to a rebuild
308
+ * that forgot to copy it. EVERY stamp, not only the one that places the op: a read carrying both
309
+ * `declared` and `device` that kept only `declared` would be left plain — and deleted as dead — by
310
+ * a later stamp that no longer finds the declaration. */
245
311
  export function carryDiscipline(from: Op, attrs: Op['attrs']): Op['attrs'] {
246
- const p = placedAt(from);
247
- return p === null ? attrs : { ...attrs, ...PLACEMENT_ANSWERS[p].carried(from) };
312
+ return stampsOf(from).reduce((acc, p) => ({ ...acc, ...PLACEMENT_ANSWERS[p].carried(from) }), attrs);
248
313
  }
@@ -0,0 +1,159 @@
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 } 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
+ const attrsOf = (d: Op, drop: string | null) =>
104
+ JSON.stringify(Object.fromEntries(Object.entries(d.attrs ?? {}).filter(([k]) => k !== drop)));
105
+ // memoised per (x, y, side): a value its own consumer reads twice would otherwise double the work
106
+ // at every level
107
+ const memo = new Map<string, boolean>();
108
+ const ids = new Map<Value, number>();
109
+ const id = (v: Value) => ids.get(v) ?? (ids.set(v, ids.size), ids.size - 1);
110
+ /** `da` (read with `atEntry`) and `db` (the entry's own) compute the same value. */
111
+ const sameOp = (da: Op, db: Op, opcodeOk: boolean, atEntry: boolean): boolean => {
112
+ if (!opcodeOk || da.operands.length !== db.operands.length) {
113
+ return false;
114
+ }
115
+ if (normalize && da.opcode === 'load' && db.opcode === 'load') {
116
+ const x = address(da, atEntry);
117
+ const y = address(db, true);
118
+ return x.off === y.off && attrsOf(da, 'off') === attrsOf(db, 'off') && same(x.base, x.atEntry, y.base);
119
+ }
120
+ if (attrsOf(da, null) !== attrsOf(db, null)) {
121
+ return false;
122
+ }
123
+ if (da.operands.every((o, i) => same(o, atEntry, db.operands[i]))) {
124
+ return true;
125
+ }
126
+ return (
127
+ normalize &&
128
+ COMMUTES.has(da.opcode) &&
129
+ da.operands.length === 2 &&
130
+ same(da.operands[0], atEntry, db.operands[1]) &&
131
+ same(da.operands[1], atEntry, db.operands[0])
132
+ );
133
+ };
134
+ function same(x0: Value, atEntry0: boolean, y0: Value): boolean {
135
+ const { x, atEntry } = view(x0, atEntry0);
136
+ const { x: y } = view(y0, true);
137
+ if (x === y) {
138
+ return true;
139
+ }
140
+ const key = `${id(x)},${id(y)},${atEntry ? 1 : 0}`;
141
+ const hit = memo.get(key);
142
+ if (hit !== undefined) {
143
+ return hit;
144
+ }
145
+ const da = readable(x) ? defs.get(x) : undefined;
146
+ const db = readable(y) ? defs.get(y) : undefined;
147
+ const r = !!da && !!db && sameOp(da, db, da.opcode === db.opcode, atEntry);
148
+ memo.set(key, r);
149
+ return r;
150
+ }
151
+ if (!negated) {
152
+ return same(a, false, b);
153
+ }
154
+ const { x, atEntry } = view(a, false);
155
+ const { x: y } = view(b, true);
156
+ const da = readable(x) ? defs.get(x) : undefined;
157
+ const db = readable(y) ? defs.get(y) : undefined;
158
+ return !!da && !!db && sameOp(da, db, NEGATED_ICMP[da.opcode] === db.opcode, atEntry);
159
+ }
@@ -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 '../ir/core';
15
+ import { Block, Fn, predecessors, successorsOf } from './core';
12
16
 
13
17
  /** One natural loop, keyed by its header. */
14
18
  export interface NaturalLoop {