@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
@@ -15,7 +15,7 @@
15
15
  * that matter — a sub-word or register-offset access is not a slot, and one anywhere turns the
16
16
  * whole word-slot model off at the same gate this analysis's `blocker` feeds, so no access that
17
17
  * could alias a licensed word is ever silently missing from the events. */
18
- export type StackArgsEvent<C> = StackArgsSlot | StackArgsCall<C>;
18
+ export type StackArgsEvent<C> = StackArgsSlot | StackArgsCall<C> | StackArgsEscape;
19
19
 
20
20
  /** A whole-word access to a reserved frame slot at `off`, which must lie inside `[0, localArea)`. */
21
21
  export interface StackArgsSlot {
@@ -23,11 +23,24 @@ export interface StackArgsSlot {
23
23
  readonly off: number;
24
24
  }
25
25
 
26
+ /** A frame address `sp + off` LEAVING this function — in an argument register at a call, or stored
27
+ * to memory a device or another function reads. Its receiver may read the frame from `off` up, so
28
+ * a word there that this function stored and never reloads has been read, by the receiver.
29
+ *
30
+ * Emitted only under `compilerBehaviors.escapedFrameAddressNamesALocal`: that premise is what
31
+ * makes the words from `off` up a local's rather than an argument area's. It ends only what is
32
+ * pending WHERE it fires, never a word staged after it, and the field says why that is the safe
33
+ * reading of a whole-function layout fact (a block copy's base escapes too). */
34
+ export interface StackArgsEscape {
35
+ readonly kind: 'escape';
36
+ readonly off: number;
37
+ }
38
+
26
39
  /** A call, and what its callee's DECLARATION says it takes on the stack. `declared` is the block
27
40
  * `[0, 4*(n - argRegs))` as an ascending offset list, or null when nothing declares this call —
28
41
  * an indirect call, a callee with no prototype, or one whose arity fits in registers. The code
29
42
  * alone cannot say where a null declaration's block ENDS, so such a call is never handed a stack
30
- * word; it lifts only when `survivorBound` clears every word that could open one. */
43
+ * word; it lifts only when `loadedWordBound` clears every word that could open one. */
31
44
  export interface StackArgsCall<C> {
32
45
  readonly kind: 'call';
33
46
  /** The frontend's own handle for this call — the key of `OutgoingArgs.blocks`. */
@@ -60,10 +73,12 @@ export interface StackArgsInput<C> {
60
73
  readonly argRegs: number;
61
74
  /** The frontend's verdict that the whole frame is one addressable object handed to a callee. */
62
75
  readonly capturedWholeFrame: boolean;
63
- /** The compiler keeps ONE outgoing area at the bottom of the frame, sized for its widest call and
64
- * below every local, and never reads a word of it back after a call. Only then does a word
65
- * loaded after a call prove itself a local — see `survivorBound`. */
66
- readonly localsAboveOutgoingArea: boolean;
76
+ /** The lowest frame word this function loads where its compiler loads no word of the outgoing
77
+ * area, before a call or after (`target.ts` `outgoingAreaNeverLoadedFrom`, the strong form of
78
+ * `localsAboveOutgoingArea`, 0 on agbcc): `loadedWordBound` over these blocks, which the frontend
79
+ * computes once and hands the frame-object audit too. Every word at or above it is a local's.
80
+ * `Infinity` where the compiler claims no such offset. */
81
+ readonly loadedWordBound: number;
67
82
  }
68
83
 
69
84
  /** What this function's calls do with the BOTTOM of its frame — the outgoing stack-argument area
@@ -82,45 +97,35 @@ export interface OutgoingArgs<C> {
82
97
  }
83
98
 
84
99
  const isCallEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsCall<C> => ev.kind === 'call';
100
+ const isSlotEvent = <C>(ev: StackArgsEvent<C>): ev is StackArgsSlot => ev.kind === 'store' || ev.kind === 'load';
85
101
 
86
- /** THE LOWEST FRAME WORD THAT SURVIVES A CALL, or `Infinity` when none does. A word LOADED on some
87
- * path out of a live call before anything re-stores it held a value across that call. Under
88
- * `localsAboveOutgoingArea` the caller never reads its outgoing area back after a call, so that
89
- * word is a local; and since the area is one region at the frame bottom that every call shares,
90
- * every word at or above the bound is proven not to be an argument.
102
+ /** THE LOWEST FRAME WORD THIS FUNCTION LOADS AT OR ABOVE `from`, or `Infinity` when it loads none
103
+ * or nothing states `from`. The Thumb frontend calls it once per lift and hands the number to both
104
+ * readers: `analyzeOutgoingArgs` and the frame-object audit (`FrameObjectAudit.loadedWords`).
105
+ * Under `outgoingAreaNeverLoadedFrom` the caller never reads a word of its outgoing area from that
106
+ * offset up — not after a call, where the callee may have assigned to it, and not before one
107
+ * either: the area holds only what the caller staged for a callee, every argument that contains a
108
+ * call is computed before any word is staged, and nothing is saved there around a nested call
109
+ * (agbcc-restage.s pins all three). So a word it loads is a local; and since the area is one
110
+ * region at the frame bottom that every call shares, every word at or above the bound is proven
111
+ * not to be an argument.
91
112
  *
92
- * A backward liveness over the live blocks: a load makes its offset live, a store kills it, and a
93
- * call is transparent — a word read after two calls survived both. Dead blocks contribute nothing:
94
- * a load that cannot run proves nothing, and a call that cannot run is not a call it survived. */
95
- function survivorBound<C>(
113
+ * Only a load in a live block counts: a load that cannot run proves nothing. An ESCAPE does not
114
+ * count: its receiver MAY read the words, and a bound is a proof, which a read that may not happen
115
+ * cannot supply. */
116
+ export function loadedWordBound<C>(
96
117
  blocks: readonly StackArgsBlock<C>[],
97
- succs: readonly (readonly number[])[],
98
118
  live: ReadonlySet<number>,
119
+ from: number | undefined,
99
120
  ): number {
100
- const liveIn = blocks.map(() => new Set<number>());
101
- const liveOut = (b: number): Set<number> => new Set(succs[b].flatMap((s) => [...liveIn[s]]));
121
+ if (from === undefined) {
122
+ return Infinity;
123
+ }
102
124
  let bound = Infinity;
103
- for (let changed = true; changed;) {
104
- changed = false;
105
- for (let b = blocks.length - 1; b >= 0; b--) {
106
- if (!live.has(b)) {
107
- continue;
108
- }
109
- const cur = liveOut(b);
110
- for (const ev of [...blocks[b].events].reverse()) {
111
- if (isCallEvent(ev)) {
112
- for (const o of cur) {
113
- bound = Math.min(bound, o);
114
- }
115
- } else if (ev.kind === 'load') {
116
- cur.add(ev.off);
117
- } else {
118
- cur.delete(ev.off);
119
- }
120
- }
121
- if ([...cur].some((o) => !liveIn[b].has(o))) {
122
- liveIn[b] = cur;
123
- changed = true;
125
+ for (const b of live) {
126
+ for (const ev of blocks[b].events) {
127
+ if (ev.kind === 'load' && ev.off >= from) {
128
+ bound = Math.min(bound, ev.off);
124
129
  }
125
130
  }
126
131
  }
@@ -164,14 +169,14 @@ function survivorBound<C>(
164
169
  // and (b) below, kept for every call no declaration covers).
165
170
  //
166
171
  // A THIRD WITNESS SAYS WHERE THE AREA ENDS, and it only ever clears words. Under a compiler that
167
- // keeps one outgoing area below every local and never reads it back after a call
168
- // (`localsAboveOutgoingArea`), a word LOADED after a call before any re-store is a local, and so
169
- // is every word above it (`survivorBound`). Every refusal that reads the code — NOTHING EXTRA,
170
- // (a) and (b) — then weighs only the words beneath the bound, so an undeclared call lifts once no
171
- // word there could open an argument block, and takes no stack word. Hand-written asm that reads
172
- // its own outgoing argument back after a call defeats it, and lifts with that argument dropped — a
173
- // producer assumption of the same kind as CONTIGUITY below, which is why the premise is declared
174
- // per compiler (`target.ts` `localsAboveOutgoingArea`) and never inferred.
172
+ // keeps one outgoing area below every local and never loads a word of it
173
+ // (`outgoingAreaNeverLoadedFrom`), a word this function LOADS is a local, and so is every word above it
174
+ // (`loadedWordBound`). Every refusal that reads the code — NOTHING EXTRA, (a) and (b) — then
175
+ // weighs only the words beneath the bound, so an undeclared call lifts once no word there could
176
+ // open an argument block, and takes no stack word. Hand-written asm that reads its own outgoing
177
+ // argument defeats it, and lifts with that argument dropped — a producer assumption of the same
178
+ // kind as CONTIGUITY below, which is why the premise is declared per compiler (`target.ts`
179
+ // `outgoingAreaNeverLoadedFrom`) and never inferred.
175
180
  //
176
181
  // THE TWO SIDES ARE CHECKED AGAINST DIFFERENT SETS, and the asymmetry is the point.
177
182
  // * NOTHING EXTRA is checked against the MAY set (stored and unreloaded on SOME path): the
@@ -183,12 +188,12 @@ function survivorBound<C>(
183
188
  // must never render as a plausible value.
184
189
  //
185
190
  // A SPILL LIVE ACROSS A LICENSED CALL sits in the may set and is not in the declared block — agbcc's
186
- // commonest frame with an outgoing area. The declaration cannot excuse it; `survivorBound` can: a
187
- // word loaded after a call before any re-store is a local, so NOTHING EXTRA counts only the pending
188
- // words beneath the bound. A declared block that reaches the bound contradicts it, and needs no
189
- // check of its own: the block is contiguous words from zero, so it holds the bound's word, which
190
- // this function loads — "this function also LOADS it" below refuses it if nothing earlier does.
191
- // A spill with no survivor at or beneath it still refuses, and its decline names a STORE ("[sp,#k]
191
+ // commonest frame with an outgoing area. The declaration cannot excuse it; `loadedWordBound` can: a
192
+ // word this function loads is a local, so NOTHING EXTRA counts only the pending words beneath the
193
+ // bound. A declared block that reaches the bound contradicts it, and needs no check of its own:
194
+ // the block is contiguous words from zero, so it holds the bound's word, which this function loads
195
+ // — "this function also LOADS it" below refuses it if nothing earlier does.
196
+ // A spill with no load at or beneath it still refuses, and its decline names a STORE ("[sp,#k]
192
197
  // also reaches the call unread") rather than the capability, so a gap histogram groups it under
193
198
  // that message.
194
199
  // The must set is an intersection over predecessors, which is exactly what a TAIL-MERGED call
@@ -213,7 +218,7 @@ export function analyzeOutgoingArgs<C>({
213
218
  localArea,
214
219
  argRegs,
215
220
  capturedWholeFrame,
216
- localsAboveOutgoingArea,
221
+ loadedWordBound: bound,
217
222
  }: StackArgsInput<C>): OutgoingArgs<C> {
218
223
  const refuse = (blocker: string): OutgoingArgs<C> => ({ blocker, blocks: new Map(), area: 0 });
219
224
  // EVERY block, not the entry-reachable ones: a call in dead code stages nothing, so the
@@ -242,6 +247,9 @@ export function analyzeOutgoingArgs<C>({
242
247
  // that computes it owns which escapes qualify. A declared fifth argument says [sp,#0] is a
243
248
  // DIFFERENT callee's argument slot. Two contradictory claims about the same word, and nothing
244
249
  // here can decide which to believe, so the honest answer is the decline it has always been.
250
+ // This is the flow-INSENSITIVE reading of an escape, sound only at one word; the `escape` event
251
+ // is the per-program-point reading of the same escapes at any frame size, and its contradiction
252
+ // with a declared block is the "ESCAPED ADDRESS AT OR BELOW A LICENSED WORD" refusal below.
245
253
  if (capturedWholeFrame) {
246
254
  for (const ev of calls) {
247
255
  const offs = ev.declared;
@@ -272,7 +280,6 @@ export function analyzeOutgoingArgs<C>({
272
280
  // code-reading refusals below — the licence's NOTHING EXTRA, (a) and (b) — each hunt for a word
273
281
  // that could be one, so the words they weigh are cut to those beneath the bound once, where
274
282
  // they are collected (`mayAt`, `storedAnywhere`), and none of them repeats the test.
275
- const bound = localsAboveOutgoingArea ? survivorBound(asmBlocks, succs, live) : Infinity;
276
283
  const belowBound = (s: Iterable<number>): Set<number> => new Set([...s].filter((o) => o < bound));
277
284
 
278
285
  const everySlot: number[] = [];
@@ -332,9 +339,18 @@ export function analyzeOutgoingArgs<C>({
332
339
  may.add(ev.off);
333
340
  must.add(ev.off);
334
341
  stored.add(ev.off);
335
- } else {
342
+ } else if (ev.kind === 'load') {
336
343
  may.delete(ev.off);
337
344
  must.delete(ev.off);
345
+ } else {
346
+ // the receiver of the escaped address reads what it was handed, so those words stop
347
+ // being pending exactly as a reload would end them
348
+ for (const o of everySlot) {
349
+ if (o >= ev.off) {
350
+ may.delete(o);
351
+ must.delete(o);
352
+ }
353
+ }
338
354
  }
339
355
  }
340
356
  const grow = (out: Array<Set<number>>, cur: Set<number>): void => {
@@ -400,6 +416,24 @@ export function analyzeOutgoingArgs<C>({
400
416
  licensed.add(o);
401
417
  }
402
418
  }
419
+ // An ESCAPED ADDRESS AT OR BELOW A LICENSED WORD — the one-word captured frame's contradiction,
420
+ // at any frame size. The escape names the words from its offset up as an object's, a licensed
421
+ // block names one of them as a callee's argument, and nothing here can decide which to believe.
422
+ // Under agbcc the two never meet (an address-taken local sits ABOVE the outgoing area, and at
423
+ // [sp,#0] only when no call takes a stack word), so a function that has both is not agbcc's
424
+ // layout and declines.
425
+ const escapes = [...live].flatMap((b) => asmBlocks[b].events.flatMap((ev) => (ev.kind === 'escape' ? [ev.off] : [])));
426
+ const lowestEscape = Math.min(Infinity, ...escapes);
427
+ for (const ev of calls) {
428
+ const offs = blocks.get(ev.call) ?? [];
429
+ const clash = offs.find((o) => o >= lowestEscape);
430
+ if (clash !== undefined) {
431
+ return refuse(
432
+ `callee \`${ev.callee}\` is declared with ${declaredAs(ev, offs)}, so [sp,#${clash}] is its outgoing stack argument — ` +
433
+ `but the frame address [sp,#${lowestEscape}] escapes this function, so the words from there up are an object's, and the two name the same word`,
434
+ );
435
+ }
436
+ }
403
437
  // The two whole-function facts the next two refusals read: every offset live code stores, and
404
438
  // every offset live code loads back. A reload in dead code is not evidence that anything reads
405
439
  // the slot back, so it does not count.
@@ -407,11 +441,26 @@ export function analyzeOutgoingArgs<C>({
407
441
  const stores = new Set<number>();
408
442
  for (const b of live) {
409
443
  for (const ev of asmBlocks[b].events) {
410
- if (ev.kind !== 'call') {
444
+ if (isSlotEvent(ev)) {
411
445
  (ev.kind === 'store' ? stores : reloaded).add(ev.off);
412
446
  }
413
447
  }
414
448
  }
449
+ // …and a word an escaped address hands over is read by its receiver, which is a reload for (a):
450
+ // its never being read back HERE is explained by who reads it THERE.
451
+ //
452
+ // WHOLE-FUNCTION, unlike the pending sets above: a store after the last escape, or on a path
453
+ // that never escapes, is excused here too. That is sound because (a) is not what clears a word
454
+ // for a call. A word that is some call's argument reaches that call pending, and the licence's
455
+ // equality (a declared callee) or (b) (an undeclared one) refuses it there, per program point:
456
+ // `qh5`'s fifth `h5` word, staged after its memcpy base escapes, is refused by (b). What this
457
+ // widening lets through is a store that reaches no call pending with its lower slots supplied,
458
+ // which by CONTIGUITY (above) no call takes as an argument.
459
+ for (const o of everySlot) {
460
+ if (o >= lowestEscape) {
461
+ reloaded.add(o);
462
+ }
463
+ }
415
464
  const storedAnywhere = belowBound(stores);
416
465
  // A LICENSED WORD THIS FUNCTION ALSO LOADS. The area belongs to the CALLEE — which may assign
417
466
  // to a stack parameter — so after the `bl` the word holds whatever the callee left, and an