@asmlift/core 0.10.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 (68) hide show
  1. package/package.json +1 -1
  2. package/src/backend/c.ts +1 -0
  3. package/src/backend/cfamily.ts +33 -12
  4. package/src/backend/cpp.ts +1 -0
  5. package/src/backend/lines.ts +10 -0
  6. package/src/backend/pascal.ts +17 -5
  7. package/src/cdecl/parse.ts +21 -0
  8. package/src/contracts.ts +73 -20
  9. package/src/frontend/arm-body.ts +155 -0
  10. package/src/frontend/asmdata.ts +2 -1
  11. package/src/frontend/call-plan.ts +197 -45
  12. package/src/frontend/callee-body.ts +370 -0
  13. package/src/frontend/device-pins.ts +18 -8
  14. package/src/frontend/frame-objects.ts +504 -61
  15. package/src/frontend/frontend.ts +20 -0
  16. package/src/frontend/jump-table.ts +35 -20
  17. package/src/frontend/mips.ts +226 -8
  18. package/src/frontend/ppc.ts +26 -2
  19. package/src/frontend/register-struct-call.ts +1 -86
  20. package/src/frontend/ssa.ts +263 -1
  21. package/src/frontend/stackargs.ts +95 -17
  22. package/src/frontend/thumb-entry.ts +340 -0
  23. package/src/frontend/thumb.ts +410 -61
  24. package/src/ir/alias.ts +29 -8
  25. package/src/ir/bits.ts +1081 -66
  26. package/src/ir/core.ts +154 -4
  27. package/src/ir/entry.ts +3 -5
  28. package/src/ir/opcodes.ts +28 -0
  29. package/src/ir/parse.ts +1 -0
  30. package/src/ir/simplify.ts +61 -10
  31. package/src/ir/verify.ts +16 -0
  32. package/src/l3/argbase.ts +2 -0
  33. package/src/l3/ast.ts +294 -2
  34. package/src/l3/basecse.ts +35 -10
  35. package/src/l3/caseendgoto.ts +223 -0
  36. package/src/l3/coalesce.ts +42 -14
  37. package/src/l3/constinit.ts +34 -6
  38. package/src/l3/dce.ts +52 -8
  39. package/src/l3/ptrcell.ts +2 -0
  40. package/src/l3/regspell.ts +2 -0
  41. package/src/l3/reindex.ts +13 -4
  42. package/src/l3/scopebase.ts +2 -0
  43. package/src/l3/tailmerge.ts +2 -0
  44. package/src/pipeline.ts +507 -23
  45. package/src/proto-context.ts +135 -1
  46. package/src/raise/const.ts +25 -0
  47. package/src/raise/declared-volatile.ts +1 -1
  48. package/src/raise/divpow2.ts +192 -0
  49. package/src/raise/offsetnames.ts +412 -10
  50. package/src/raise/pre-recovery.ts +8 -5
  51. package/src/raise/pre-structure.ts +24 -5
  52. package/src/raise/reroll.ts +4 -24
  53. package/src/raise/shortcircuit.ts +192 -23
  54. package/src/raise/unpre.ts +594 -0
  55. package/src/rank-variations.ts +97 -4
  56. package/src/rank.ts +258 -43
  57. package/src/structure/analysis.ts +56 -10
  58. package/src/structure/cross-jumped-tail.ts +216 -0
  59. package/src/structure/globalaccess.ts +46 -1
  60. package/src/structure/structure.ts +4221 -368
  61. package/src/structure/switch-recover.ts +345 -26
  62. package/src/structure/unfolded-branch-constant.ts +90 -0
  63. package/src/symbols.ts +52 -15
  64. package/src/target.ts +269 -8
  65. package/src/trace.ts +28 -14
  66. package/src/variation-definitions.ts +207 -6
  67. package/src/variation-gates.ts +2 -0
  68. package/src/variation-tokens.ts +63 -2
@@ -17,10 +17,10 @@
17
17
  * measured against; IDO, gcc 2.7.2 and mwcc reload past every store of another type (strict
18
18
  * aliasing does not reorder there), so the cast and union refusals below are only more cautious on
19
19
  * MIPS and PPC. */
20
- import { linearFactor } from '../ir/bits';
20
+ import { type StridedRange, linearFactor } from '../ir/bits';
21
21
  import { type Block, type Op, type Value, byValueOperands, calleeName, mkOp, mkValue, successorsOf } from '../ir/core';
22
22
  import { type IrType, T, structSpelling, typeEquals, typeToString, unionViewSigns } from '../ir/types';
23
- import type { Gate } from '../l3/gates';
23
+ import { type Gate, firstRejection } from '../l3/gates';
24
24
  import { blockCopyRoles } from '../proto';
25
25
  import type { SymbolMap } from '../symbols';
26
26
  import { type TargetDescription, blockTransferRead, sourceControlRead, sourceReach } from '../target';
@@ -53,6 +53,11 @@ export interface FrameObjectAudit {
53
53
  escapesReliedOn?: ReadonlySet<number>;
54
54
  /** every capture the add arm re-minted at a constant offset from it */
55
55
  movedCaptures: ReadonlySet<Value>;
56
+ /** the `laddr`s the frontend minted for a DIRECT frame access (`off(sp)` inside its local area)
57
+ * rather than for a captured address: a word reached only through them is one its slot model
58
+ * lifts again outside a relift's bytes (`reliftRange`). Thumb, which lifts a direct word access
59
+ * as a slot, has none. */
60
+ directAccesses?: ReadonlySet<Value>;
56
61
  /** whether a callee's declaration rules out a struct returned through a hidden pointer at
57
62
  * argument 0 (aggregate.ts `returnsWithoutHiddenPointer`), asked of the frontend's call
58
63
  * declarations */
@@ -62,6 +67,17 @@ export interface FrameObjectAudit {
62
67
  /** the retraction rules an escape is judged by; `FRAME_ESCAPE_GATES` when absent, and a
63
68
  * census or an ablation hands in its own (`FRAME_OBJECT_AUDIT`) */
64
69
  gates?: readonly Gate<FrameEscape>[];
70
+ /** the rules that keep a frame's end word in the one object (`reliftRange`);
71
+ * `RELIFT_HAND_BACK_GATES` when absent, and a census or an ablation hands in its own */
72
+ reliftGates?: readonly Gate<HandBack>[];
73
+ /** THE ARGUMENT HOMES THE FRONTEND KEPT FROM THE SLOT MODEL FOR THE ONE-OBJECT ANSWER ALONE: frame
74
+ * words an incoming argument register is stored to on entry, whose narrow reloads the frontend
75
+ * left as captures rather than reading them as the word narrowed, because a by-value struct
76
+ * parameter's home recompiles as that one object (Thumb `argumentHomes`). Where that answer is
77
+ * withheld and the audit refuses, it asks for a relift with them released
78
+ * (`FrameObjectRelift.homesReleased`) rather than refusing, and a relift that declines reports
79
+ * this refusal first. Absent or empty where none was kept. */
80
+ releasableHomes?: ReadonlySet<number>;
65
81
  /** the bytes a first audit asked to have kept as ONE object in memory (`FrameObjectRelift`):
66
82
  * the frontend routes every access inside them through an `laddr`, and the audit declares
67
83
  * them one object rather than judging each offset on its own */
@@ -100,31 +116,83 @@ export function areaAboveOutgoing({
100
116
  export const landsOnArea = (area: FrameRange, at: number, width: number): boolean =>
101
117
  at < area.to && at + width > area.from;
102
118
 
103
- /** What an audit answers instead of a refusal when a device may read the frame, or a callee write
104
- * it, without bound: lift again with `oneObject` set to these bytes. */
119
+ /** Whether a `width`-byte access at `at` inside a frontend's local area is MEMORY in a relift that
120
+ * keeps `kept` as one object: inside it, yes; wholly outside, no — a word the first audit handed
121
+ * back to the slot model (`reliftRange`); across its edge, refused, half memory and half slot. */
122
+ export function keptInRelift(kept: FrameRange, at: number, width: number, what: () => string, name: string): boolean {
123
+ if (at >= kept.from && at + width <= kept.to) {
124
+ return true;
125
+ }
126
+ if (at >= kept.to || at + width <= kept.from) {
127
+ return false;
128
+ }
129
+ throw new FrontendUnsupportedError(
130
+ `cannot lift '${name}': address-taken stack local — ${what()} reaches across an edge of the bytes ` +
131
+ `[sp,#${kept.from}) to [sp,#${kept.to}) kept as one object — part memory, part slot`,
132
+ );
133
+ }
134
+
135
+ /** What an audit answers instead of a refusal. Where a device may read the frame, or a callee write
136
+ * it, without bound: lift again with `oneObject` set to these bytes. Where the one-object answer is
137
+ * withheld and the audit refuses a frame with an argument home the frontend kept out of the slot
138
+ * model for that answer alone (`FrameObjectAudit.releasableHomes`): lift again with `oneObject`
139
+ * unset and `homesReleased`, so the frontend reads those homes as the slots they are. */
105
140
  export interface FrameObjectRelift {
106
- readonly oneObject: FrameRange;
141
+ readonly oneObject: FrameRange | undefined;
142
+ /** with `homesReleased`, the first audit's refusal, which a relift that declines again reports
143
+ * first: the frame as the first lift read it, followed by the second lift's own refusal where it
144
+ * differs (`bothRefusals`) */
145
+ readonly homesReleased?: { readonly refusal: FrontendUnsupportedError };
107
146
  }
108
147
 
109
148
  /** THE RELIFT PROTOCOL, AT MOST TWICE, the one copy every frontend that runs the audit calls: lift,
110
- * and where the audit answers with bytes to keep as one object (`FrameObjectRelift`), lift once
111
- * more with them routed through an `laddr`. The second audit judges them as one object and refuses
112
- * rather than asking again, so a second request is an internal error, not a third lift. */
149
+ * and where the audit answers with a relift (`FrameObjectRelift`), lift once more as it asks —
150
+ * bytes to keep as one object routed through an `laddr`, or argument homes released to the slot
151
+ * model. The second audit judges the frame as asked and refuses rather than asking again, so a
152
+ * second request is an internal error, not a third lift. A second lift with homes released that
153
+ * declines reports the first audit's refusal (`homesReleased.refusal`), then its own where that
154
+ * differs. A frontend that hands the audit no `releasableHomes` is never asked to release one. */
113
155
  export function liftWithRelift<F>(
114
156
  name: string,
115
- liftOnce: (oneObject: FrameRange | undefined) => F | FrameObjectRelift,
157
+ liftOnce: (oneObject: FrameRange | undefined, homesReleased: boolean) => F | FrameObjectRelift,
116
158
  ): F {
117
- const first = liftOnce(undefined);
159
+ const first = liftOnce(undefined, false);
118
160
  if (!isRelift(first)) {
119
161
  return first;
120
162
  }
121
- const again = liftOnce(first.oneObject);
163
+ const released = first.homesReleased;
164
+ let again: F | FrameObjectRelift;
165
+ try {
166
+ again = liftOnce(first.oneObject, released !== undefined);
167
+ } catch (e) {
168
+ if (released !== undefined && e instanceof FrontendUnsupportedError) {
169
+ throw bothRefusals(name, released.refusal, e);
170
+ }
171
+ throw e;
172
+ }
122
173
  if (isRelift(again)) {
123
174
  throw new Error(`internal: the frame-object audit of '${name}' asked for a second relift`);
124
175
  }
125
176
  return again;
126
177
  }
127
178
 
179
+ /** A released relift's decline, worded as both lifts read the frame: the first audit's refusal of
180
+ * the frame with the homes held, then the second's with them released, which may name another
181
+ * obstacle (an escape the first refusal stopped short of). The first leads, so a message that
182
+ * only the first lift's refusal explains reads as it would with no release; the second is
183
+ * appended only where it says something else. */
184
+ function bothRefusals(
185
+ name: string,
186
+ first: FrontendUnsupportedError,
187
+ second: FrontendUnsupportedError,
188
+ ): FrontendUnsupportedError {
189
+ const lead = `cannot lift '${name}': `;
190
+ const reason = second.message.startsWith(lead) ? second.message.slice(lead.length) : second.message;
191
+ return first.message.endsWith(reason)
192
+ ? first
193
+ : new FrontendUnsupportedError(`${first.message}; with the argument home released: ${reason}`);
194
+ }
195
+
128
196
  const isRelift = <F>(x: F | FrameObjectRelift): x is FrameObjectRelift =>
129
197
  typeof x === 'object' && x !== null && 'oneObject' in x;
130
198
 
@@ -348,7 +416,30 @@ export const FRAME_ESCAPE_GATES: readonly Gate<FrameEscape>[] = [
348
416
  * the fold, the split, the one-object answer and the stamp; `foldedHere`, which the split adds to
349
417
  * and the slot-overlap refusal reads; and `ModelChoice.refused`, which the model choice and the
350
418
  * shape stage set and the relift test reads. */
351
- export function auditFrameObjects({
419
+ export function auditFrameObjects(audit: FrameObjectAudit): FrameObjectVerdict {
420
+ // AN ARGUMENT HOME HELD FOR AN ANSWER THAT DID NOT COME. The frontend keeps a by-value struct
421
+ // parameter's home out of the slot model (`releasableHomes`) so the one-object answer can take it
422
+ // whole. Where this audit declines instead, that answer withheld (an address escapes to a writer
423
+ // it cannot hold, such as a struct-return temp), the decline becomes a relift with the homes
424
+ // released. The hold is for byte-exactness, not soundness: a home's narrow reload read as the
425
+ // word narrowed is the value the entry stored, and the second audit judges the frame whole, so
426
+ // the release is value-sound whichever refusal fired. Asked of the first lift alone: a relift
427
+ // that keeps one object has already had the answer the homes were held for.
428
+ const { oneObject, releasableHomes } = audit;
429
+ if (oneObject !== undefined || releasableHomes === undefined || releasableHomes.size === 0) {
430
+ return auditFrame(audit);
431
+ }
432
+ try {
433
+ return auditFrame(audit);
434
+ } catch (e) {
435
+ if (e instanceof FrontendUnsupportedError) {
436
+ return { oneObject: undefined, homesReleased: { refusal: e } };
437
+ }
438
+ throw e;
439
+ }
440
+ }
441
+
442
+ function auditFrame({
352
443
  name,
353
444
  irBlocks,
354
445
  ownedLocals: owned,
@@ -358,10 +449,12 @@ export function auditFrameObjects({
358
449
  capturedObjectIsTheWholeFrame,
359
450
  escapesReliedOn = new Set(),
360
451
  movedCaptures,
452
+ directAccesses = new Set(),
361
453
  returnsWithoutHiddenPointer,
362
454
  symbols,
363
455
  target,
364
456
  gates = FRAME_ESCAPE_GATES,
457
+ reliftGates = RELIFT_HAND_BACK_GATES,
365
458
  oneObject,
366
459
  }: FrameObjectAudit): FrameObjectVerdict {
367
460
  const { laddrs: captures, foldedHere } = foldMovedCaptures(irBlocks, movedCaptures);
@@ -437,7 +530,17 @@ export function auditFrameObjects({
437
530
  model.unboundedAt.has(e.off) &&
438
531
  (e.objectReached !== undefined || e.slotReached !== undefined || e.unaccountedWord !== undefined);
439
532
  if (model.onOffer && (model.refused || shapes.overSlot.length > 0 || escapes.some(answered))) {
440
- return { oneObject: { from: declared.from, to: declared.to } };
533
+ return {
534
+ oneObject: reliftRange({
535
+ declared,
536
+ objects,
537
+ uses,
538
+ usedSlotOffsets,
539
+ directAccesses,
540
+ splits: splitsOneObject(target),
541
+ gates: reliftGates,
542
+ }),
543
+ };
441
544
  }
442
545
  const failWithheld: Refuse = (why) => fail(model.withheld(why));
443
546
  for (const [off, width] of shapes.overSlot) {
@@ -455,6 +558,147 @@ export function auditFrameObjects({
455
558
  return { policy: 'per-object', sinks };
456
559
  }
457
560
 
561
+ /** THE BYTES THE ONE-OBJECT RELIFT KEEPS: the declared local area, less the words at either end
562
+ * that are a scalar's and nothing else's — a word the slot model keys and no access names, or one
563
+ * the frontend reached only by direct whole-word accesses (`directAccesses`), which escapes
564
+ * nowhere — and, past such a word, words nothing touches at all. The frontend lifts a word
565
+ * outside the range with its slot model (Thumb `isFrameObjectAccess`, MIPS `keptInRelift`; PPC
566
+ * is never handed a narrowed range, `no-split-follows`), so the one object is the bytes its
567
+ * escapes and accesses reach, and `provenParts` can split it where they tile: agbcc's reload puts
568
+ * spill slots ABOVE the arrays (the Thumb frame grows up, thumb.h:569/573, reload1.c:2274),
569
+ * ido7.1 puts them below, and gcc 2.7.2 leaves a pad word between (each array starts at a
570
+ * multiple of 8); kept in the one object, such a word joins the part beside it.
571
+ *
572
+ * A word is handed back only where no rule of `RELIFT_HAND_BACK_GATES` keeps it: a reach through
573
+ * the object, or no split to follow. What makes a word a scalar's (`scalarWord`) is not tabled,
574
+ * and its clauses are not each measured for soundness: a word it gets wrong meets the second
575
+ * audit's refusals where an object or an access lands outside the kept bytes (`keepAsOneObject`).
576
+ *
577
+ * The copies do NOT tell a spill from a struct member only stored (`struct { u8 a[16], b[16];
578
+ * u32 y; }` with `s.y = x` and nothing reading it): that member is handed back too, and its store
579
+ * drops. No value changes, since nothing reads the word through the object (an index or an
580
+ * escape reaching it keeps it); what is lost is one store the target made. */
581
+ function reliftRange({
582
+ declared,
583
+ objects,
584
+ uses,
585
+ usedSlotOffsets,
586
+ directAccesses,
587
+ splits,
588
+ gates,
589
+ }: {
590
+ declared: FrameRange;
591
+ objects: ReadonlyMap<number, readonly Op[]>;
592
+ uses: FrameUses;
593
+ usedSlotOffsets: ReadonlySet<number>;
594
+ directAccesses: ReadonlySet<Value>;
595
+ splits: boolean;
596
+ gates: readonly Gate<HandBack>[];
597
+ }): FrameRange {
598
+ const whole = { from: declared.from, to: declared.to };
599
+ const reaches = frameReaches(uses);
600
+ const fills = copyStarts(reaches);
601
+ const kept = (word: number): boolean => firstRejection(gates, { word, reaches, fills, splits }) !== null;
602
+ const scalarWord = (w: number, pastScalar: boolean): boolean => {
603
+ const touching = uses.fixed.filter((f) => overlaps(f.at, f.width, w, 4));
604
+ const owners = [...objects.keys()].filter((o) => overlaps(o, 1, w, 4));
605
+ if ([...usedSlotOffsets].some((s) => s !== w && overlaps(s, 4, w, 4))) {
606
+ return false;
607
+ }
608
+ if (usedSlotOffsets.has(w) || touching.length === 0) {
609
+ // …and past one, a word nothing touches: no value lives in it either way
610
+ return owners.length === 0 && touching.length === 0 && (usedSlotOffsets.has(w) || pastScalar);
611
+ }
612
+ return (
613
+ owners.length === 1 &&
614
+ owners[0] === w &&
615
+ !uses.escaped.has(w) &&
616
+ uses.useCount.get(w) === (uses.accesses.get(w)?.length ?? 0) &&
617
+ touching.every((f) => {
618
+ const by = uses.accessOps.get(f.op);
619
+ return f.at === w && f.width === 4 && by?.off === w && directAccesses.has(by.through);
620
+ })
621
+ );
622
+ };
623
+ const handedBack = (w: number, pastScalar: boolean): boolean => !kept(w) && scalarWord(w, pastScalar);
624
+ let { from, to } = whole;
625
+ while (to - from > 4 && handedBack(to - 4, to < whole.to)) {
626
+ to -= 4;
627
+ }
628
+ while (to - from > 4 && handedBack(from, from > whole.from)) {
629
+ from += 4;
630
+ }
631
+ return { from, to };
632
+ }
633
+
634
+ /** Whether `keepAsOneObject` may split the one object on this target at all: where it places
635
+ * address-taken arrays in a known order and at a known alignment. */
636
+ function splitsOneObject(target: TargetDescription): boolean {
637
+ return (
638
+ target.compilerBehaviors.addressTakenArrayOrder !== undefined &&
639
+ target.compilerBehaviors.addressTakenArrayAlignment !== undefined
640
+ );
641
+ }
642
+
643
+ /** What `RELIFT_HAND_BACK_GATES` asks of one end word of the local area: the word, and every
644
+ * escape and runtime index of the frame as the bytes each may reach (`frameReaches`). */
645
+ export interface HandBack {
646
+ readonly word: number;
647
+ /** every escape and runtime index, with the bytes each may reach */
648
+ readonly reaches: readonly OwnedReach[];
649
+ /** where a bounded copy writes into the frame */
650
+ readonly fills: ReadonlySet<number>;
651
+ /** the target may split the one object (`splitsOneObject`) */
652
+ readonly splits: boolean;
653
+ }
654
+
655
+ /** WHAT KEEPS AN END WORD IN THE ONE OBJECT (`reliftRange`): a reach through the object that may
656
+ * land on it, or no split to gain by handing it back. A word handed back is a local whose address
657
+ * nothing takes, so the recompile keeps it in a register or drops its store, and whatever reached
658
+ * it through the object in the source reads or writes another byte. */
659
+ export const RELIFT_HAND_BACK_GATES: readonly Gate<HandBack>[] = [
660
+ // `struct S { u8 a[16], b[16]; u32 y; } s; … s.y = x; h(s.a, 16); g(s.b); return s.y + s.a[3];`
661
+ // on agbcc: `g` may write `s.y` through the address it was handed, and the target reads it back
662
+ {
663
+ id: 'escape-without-bound',
664
+ why: 'a callee handed an address nothing bounds may reach any byte of the frame from it',
665
+ sound: true,
666
+ guardedBy: 'frame-audit-stages.test.ts: keeps every word where an escape no literal count bounds may reach it',
667
+ rejects: (c) => c.reaches.some((r) => r.by === 'escape' && r.reach === null),
668
+ },
669
+ // `memcpy(s + 4, src, 8)` over a slot keyed at 8 writes the word the slot model would keep
670
+ {
671
+ id: 'copy-reaches-word',
672
+ why: 'a block copy into or out of the frame names the bytes it moves, and this word is one of them',
673
+ sound: true,
674
+ guardedBy: 'frame-audit-stages.test.ts: keeps a word a bounded copy reaches',
675
+ rejects: (c) => c.reaches.some((r) => r.by === 'escape' && r.reach !== null && mayTouch(r.reach, c.word, 4)),
676
+ },
677
+ // An index nothing bounds reaches every byte of the object it is based in, so no split follows
678
+ // it (`provenParts` keeps the one object whole), and a word handed back would only leave the
679
+ // bytes the index may reach. `*(u32 *)(s + 32) = x; return s[16 + i];` on agbcc is spelled
680
+ // exactly as a spill slot beside an index is. An index whose range is proven (its `reach`:
681
+ // `s[16 + (i & 7) * 2]`) reaches only the bytes it names, so a word outside every such reach is
682
+ // read through no index, and handing it back loses nothing an index could read.
683
+ {
684
+ id: 'runtime-index',
685
+ why: 'a runtime index may reach this word: nothing bounds it, or its bounded reach covers it',
686
+ sound: true,
687
+ guardedBy: 'thumb-frontend.test.ts: a word a runtime index may reach is not handed back to the slot model',
688
+ rejects: (c) => c.reaches.some((r) => r.by === 'index' && mayTouch(r.reach, c.word, 4)),
689
+ },
690
+ // Handing back buys a split and nothing else: two or more copies with a literal count must fill
691
+ // the area, on a target that splits at all (PPC_MWCC places address-taken arrays in no order
692
+ // `target.ts` records). Elsewhere it would only cost a stored-only member's store.
693
+ {
694
+ id: 'no-split-follows',
695
+ why: 'a word is handed back only so that the copies can split the one object',
696
+ sound: false,
697
+ guardedBy: 'frame-audit-stages.test.ts: keeps every word where no split could follow',
698
+ rejects: (c) => c.fills.size < 2 || !c.splits,
699
+ },
700
+ ];
701
+
458
702
  /** Refuses the whole function, naming the address-taken local as what it cannot lift. */
459
703
  type Refuse = (why: string) => never;
460
704
 
@@ -981,10 +1225,70 @@ interface FrameAccess {
981
1225
  }
982
1226
 
983
1227
  /** A load or store judged an access to the object at `off`, and the address value it reaches the
984
- * object through: a capture, or a phi that carries one — for a runtime index, the indexed one. */
1228
+ * object through: a capture, or a phi that carries one — for a runtime index, the indexed one,
1229
+ * and then `index` is the access as `FrameUses.indexed` records it. Without `index` the access
1230
+ * is at its own displacement from `through`. */
985
1231
  interface FrameAccessOp {
986
1232
  readonly off: number;
987
1233
  readonly through: Value;
1234
+ readonly index?: IndexedAccess;
1235
+ }
1236
+
1237
+ /** THE FRAME BYTES `[lo, hi)` ONE USE MAY TOUCH, or `null` where nothing bounds it, which is every
1238
+ * byte: a runtime index's reach (`FrameUses.indexed`) and an escape's extent
1239
+ * (`FrameUses.escapeExtents`) are each one. */
1240
+ type FrameReach = { readonly lo: number; readonly hi: number } | null;
1241
+
1242
+ /** Whether a use reaching `r` may touch any of the `width` bytes at `lo`: every byte where
1243
+ * nothing bounds it. */
1244
+ function mayTouch(r: FrameReach, lo: number, width: number): boolean {
1245
+ return r === null || overlaps(lo, width, r.lo, r.hi - r.lo);
1246
+ }
1247
+
1248
+ /** Whether two reaches may share a byte. */
1249
+ function mayShare(a: FrameReach, b: FrameReach): boolean {
1250
+ return b === null || mayTouch(a, b.lo, b.hi - b.lo);
1251
+ }
1252
+
1253
+ /** One reach through the frame, with the object whose address it starts from (`owner`), what
1254
+ * makes it (`by`: an escape of that address, or a runtime index into it), and `into` where it is
1255
+ * a bounded block copy that WRITES the bytes (the address is its destination). */
1256
+ interface OwnedReach {
1257
+ readonly owner: number;
1258
+ readonly by: 'escape' | 'index';
1259
+ readonly reach: FrameReach;
1260
+ readonly into: boolean;
1261
+ }
1262
+
1263
+ /** EVERY REACH THROUGH THE FRAME, the one list the split of the one object (`provenParts`) and
1264
+ * the words handed back to the slot model (`reliftRange`) read: each escape's extent, and each
1265
+ * runtime-indexed access's reach. */
1266
+ function frameReaches(uses: FrameUses): OwnedReach[] {
1267
+ return [
1268
+ ...[...uses.escapeExtents].flatMap(([owner, extents]) =>
1269
+ extents.map((e): OwnedReach => ({
1270
+ owner,
1271
+ by: 'escape',
1272
+ reach: e === null ? null : { lo: e.lo, hi: e.hi },
1273
+ into: e?.into === true,
1274
+ })),
1275
+ ),
1276
+ ...[...uses.indexed].flatMap(([owner, accesses]) =>
1277
+ accesses.map((x): OwnedReach => ({ owner, by: 'index', reach: x.reach, into: false })),
1278
+ ),
1279
+ ];
1280
+ }
1281
+
1282
+ /** Where a bounded block copy writes into the frame: the start of each copy's destination. */
1283
+ function copyStarts(reaches: readonly OwnedReach[]): Set<number> {
1284
+ return new Set(reaches.flatMap((r) => (r.into && r.reach !== null ? [r.reach.lo] : [])));
1285
+ }
1286
+
1287
+ /** One access through a runtime index: its width and signedness, and the bytes it may touch. */
1288
+ interface IndexedAccess {
1289
+ readonly width: number;
1290
+ readonly signed: boolean;
1291
+ readonly reach: FrameReach;
988
1292
  }
989
1293
 
990
1294
  /** Every use of every frame object, by offset — what each later stage reads. Each field is
@@ -1006,7 +1310,9 @@ interface FrameUses {
1006
1310
  /** the calls each object is passed to BY VALUE (`foldByValueArgs`), with the struct each passes */
1007
1311
  readonly byValueReads: ReadonlyMap<number, readonly { readonly op: Op; readonly type: IrType }[]>;
1008
1312
  readonly useCount: ReadonlyMap<number, number>;
1009
- readonly indexed: ReadonlyMap<number, readonly { readonly width: number; readonly signed: boolean }[]>;
1313
+ /** every runtime-indexed access of each object, with the frame bytes `[lo, hi)` it may touch
1314
+ * (`reach`, `indexedAccess`), or `null` where nothing bounds the index */
1315
+ readonly indexed: ReadonlyMap<number, readonly IndexedAccess[]>;
1010
1316
  readonly members: readonly { readonly at: number; readonly width: number }[];
1011
1317
  readonly memberRefusal: string | undefined;
1012
1318
  /** every escape of each object's address, as the frame bytes it may reach: `[lo, hi)` where a
@@ -1127,7 +1433,7 @@ function classifyFrameUses({
1127
1433
  // The accesses through a runtime-indexed address, per object — kept apart from `accesses`,
1128
1434
  // which types the object at its own offset: `a[i]` says what one ELEMENT is, not what sits at
1129
1435
  // `a`.
1130
- const indexed = new Map<number, { width: number; signed: boolean }[]>();
1436
+ const indexed = new Map<number, IndexedAccess[]>();
1131
1437
  for (const off of objects.keys()) {
1132
1438
  accesses.set(off, []);
1133
1439
  }
@@ -1256,7 +1562,14 @@ function classifyFrameUses({
1256
1562
  // judged with the rest (`objectShapes`).
1257
1563
  const other = op.opcode === 'add' && op.operands.length === 2 ? op.operands[1 - idx] : undefined;
1258
1564
  if (other !== undefined && taint.get(other) === undefined && defOf.get(other)?.opcode !== 'const') {
1259
- indexedAccess(irBlocks, { indexed, accessOps }, { off, through: v }, op.results[0], facts.constOf, fail);
1565
+ indexedAccess(
1566
+ irBlocks,
1567
+ { indexed, accessOps },
1568
+ { off, through: v },
1569
+ { sum: op.results[0], range: facts.addressRangeOf(other, blk, 'narrowest') },
1570
+ facts.constOf,
1571
+ fail,
1572
+ );
1260
1573
  continue;
1261
1574
  }
1262
1575
  // A CONSTANT move left after the fold (`foldMovedCaptures`) moves a capture a phi carried.
@@ -1371,19 +1684,30 @@ function transferRead(
1371
1684
  // compiles to `add r4, r4, sp` and the source's `mov r1, sp / add r0, r1, r4` does not: the `u16`
1372
1685
  // twin `((u16 *)sp0)[a0]` misses the same way. Left raw, the access compiles to the source's
1373
1686
  // bytes with or without the `+ 0`.
1687
+ //
1688
+ // WHAT IT MAY REACH is the object's offset plus every value the index may hold (`range`, ir/bits.ts
1689
+ // `stridedRange`, asked at the sum's block): a mask, a zero-fill shift, an unsigned narrow load or a
1690
+ // `zext` — by its width, or by its operand's range where that is narrower (`(u8)k` of a counter in
1691
+ // [0, 7]); a compare on the way to the access (`if (i < 8) a[i]`); a loop counter its latch bounds,
1692
+ // by induction where the latch bounds one side; and linear forms of them. The range is widened by
1693
+ // the access's width — the stride is dropped, so the bytes are a superset of the ones reached.
1694
+ // Every element is at displacement 0 from the sum (above), so the address is exactly the capture
1695
+ // plus the index. An index nothing bounds, or one whose sum could wrap past the top of the address
1696
+ // space back below the object, reaches every byte (`null`).
1374
1697
  function indexedAccess(
1375
1698
  irBlocks: readonly Block[],
1376
- {
1377
- indexed,
1378
- accessOps,
1379
- }: { indexed: Map<number, { width: number; signed: boolean }[]>; accessOps: Map<Op, FrameAccessOp> },
1699
+ { indexed, accessOps }: { indexed: Map<number, IndexedAccess[]>; accessOps: Map<Op, FrameAccessOp> },
1380
1700
  object: FrameAccessOp,
1381
- sum: Value,
1701
+ { sum, range }: { sum: Value; range: StridedRange | undefined },
1382
1702
  constOf: (v: Value) => number | undefined,
1383
1703
  fail: Refuse,
1384
1704
  ): void {
1385
1705
  const { off } = object;
1386
1706
  const got = indexed.get(off) ?? indexed.set(off, []).get(off)!;
1707
+ const reach = (width: number): FrameReach => {
1708
+ const starts = range === undefined ? null : displaced(range, off, width);
1709
+ return starts === null ? null : { lo: starts.lo, hi: starts.hi + width };
1710
+ };
1387
1711
  const judge = (at: Value, by: number): void => {
1388
1712
  for (const blk of irBlocks) {
1389
1713
  for (const op of blk.ops) {
@@ -1414,8 +1738,10 @@ function indexedAccess(
1414
1738
  'only a load or store at the indexed address is modelled',
1415
1739
  );
1416
1740
  }
1417
- got.push({ width: op.attrs.width as number, signed: op.opcode === 'load' && op.attrs.signed === true });
1418
- accessOps.set(op, object);
1741
+ const width = op.attrs.width as number;
1742
+ const index = { width, signed: op.opcode === 'load' && op.attrs.signed === true, reach: reach(width) };
1743
+ got.push(index);
1744
+ accessOps.set(op, { ...object, index });
1419
1745
  }
1420
1746
  }
1421
1747
  }
@@ -1570,7 +1896,7 @@ function readWindow(
1570
1896
  const base = facts.literalAddrOf(op.operands[0]);
1571
1897
  if (base === undefined) {
1572
1898
  if (!flow.frameOnEveryPath.has(op.operands[0])) {
1573
- if (!misses(op, facts, cnt)) {
1899
+ if (!misses(op, facts, cnt, blk)) {
1574
1900
  return unbounded('a later store through an unresolved pointer may re-arm');
1575
1901
  }
1576
1902
  spared = true;
@@ -1617,6 +1943,16 @@ function readWindow(
1617
1943
  return { lo, hi, why: 'that reads through it', ...(spared ? { sparedByMiss: true as const } : {}) };
1618
1944
  }
1619
1945
 
1946
+ /** The addresses a strided range moved by `off` may hold, where an access `width` bytes wide at
1947
+ * any of them stays inside one address space — or null where a start could fall below 0 or an
1948
+ * access wrap past 2^32, so no interval names the bytes it touches. What a runtime index into the
1949
+ * frame may reach (`indexedAccess`) and where a computed store may land (`misses`) both read it. */
1950
+ function displaced(r: StridedRange, off: number, width: number): StridedRange | null {
1951
+ const lo = r.lo + off;
1952
+ const hi = r.hi + off;
1953
+ return lo < 0 || hi + width > 2 ** 32 ? null : { lo, hi, stride: r.stride };
1954
+ }
1955
+
1620
1956
  /** A store through a computed pointer that provably misses the control halfword at `cnt`: no
1621
1957
  * address its base may hold (`addressRangeOf`, a strided interval), displaced by the store's own
1622
1958
  * offset, starts a store whose bytes overlap `[cnt, cnt + 2)`. agbcc's `gEnts[i].f = 1` after a
@@ -1627,18 +1963,17 @@ function readWindow(
1627
1963
  * over": an immediate transfer is, but a later store that sets the enable bit again reloads the
1628
1964
  * source register and reads the frame anew (GBATEK, DMA control bit 15), which is what this
1629
1965
  * guards against. A store this cannot bound on every value stays an unresolved pointer. */
1630
- function misses(op: Op, facts: FrameAddressFacts, cnt: number): boolean {
1631
- const r = facts.addressRangeOf(op.operands[0]);
1966
+ function misses(op: Op, facts: FrameAddressFacts, cnt: number, blk: Block): boolean {
1967
+ const r = facts.addressRangeOf(op.operands[0], blk);
1632
1968
  if (r === undefined) {
1633
1969
  return false;
1634
1970
  }
1635
- const off = op.attrs.off as number;
1636
1971
  const width = op.attrs.width as number;
1637
- const lo = r.lo + off;
1638
- const hi = r.hi + off;
1639
- if (lo < 0 || hi + width > 2 ** 32) {
1972
+ const starts = displaced(r, op.attrs.off as number, width);
1973
+ if (starts === null) {
1640
1974
  return false;
1641
1975
  }
1976
+ const { lo, hi } = starts;
1642
1977
  // the first start at or above the lowest one that overlaps, against the highest that does
1643
1978
  const from = cnt - width + 1;
1644
1979
  const first = from <= lo || r.stride === 0 ? lo : lo + Math.ceil((from - lo) / r.stride) * r.stride;
@@ -2166,7 +2501,7 @@ function failIfSlotKeysIt(
2166
2501
  * holds (`writerNotKept` says which writers it can hold) — or one per part, where the accesses
2167
2502
  * prove the bytes are several objects no use reads across (`provenParts`) — and every access
2168
2503
  * inside it is a cast-spelled access to that array (an element of a part, by its index:
2169
- * structure.ts `frameArrayElement`), or a view of the union it is declared as where two widths
2504
+ * structure.ts `framePartDecls`), or a view of the union it is declared as where two widths
2170
2505
  * meet at one byte — so each store the machine made there is a store the
2171
2506
  * recompile makes too. That is what agbcc does for an object whose address escaped: it keeps
2172
2507
  * every store to it, in order (flow.c deletes a memory store only when an identical later
@@ -2210,13 +2545,21 @@ function keepAsOneObject({
2210
2545
  fail(`${kept} cannot hold every writer — ${writer}`);
2211
2546
  }
2212
2547
  // A runtime index names no byte, so the rule below is asked of it at EVERY byte it may reach:
2213
- // all of them. Two indexed widths refuse here; an index beside a member of another width
2214
- // refuses after the members are checked against each other, so a member pair keeps naming its
2215
- // own byte (`indexedWidth` says why one width is enough).
2216
- const index = indexedWidth([...uses.indexed.values()].flat());
2217
- if (index.refusal !== null) {
2218
- fail(`a runtime index into ${kept} ${index.refusal}`);
2219
- }
2548
+ // all of them where nothing bounds it, the bytes its `reach` names where one does — a byte no
2549
+ // index reaches is read at one type whatever the index's is, and accesses that share no byte
2550
+ // cannot serve each other a stale value. Two indexed widths that may share a byte refuse here;
2551
+ // an index beside a member of another width refuses after the members are checked against each
2552
+ // other, so a member pair keeps naming its own byte (`indexedWidth` says why one width is
2553
+ // enough).
2554
+ const indexes = [...uses.indexed.values()].flat();
2555
+ indexes.forEach((x, i) => {
2556
+ for (const y of indexes.slice(i + 1)) {
2557
+ const { refusal } = indexedWidth([x, y]);
2558
+ if (refusal !== null && mayShare(x.reach, y.reach)) {
2559
+ fail(`a runtime index into ${kept} ${refusal}`);
2560
+ }
2561
+ }
2562
+ });
2220
2563
  // ONE TYPE PER BYTE, OR ONE UNION. agbcc at -O2 turns on type-based alias analysis
2221
2564
  // (toplev.c:3616), and compiled, a `u32` read through a cast is served the earlier `u32` store
2222
2565
  // straight past a `u16` store to the same bytes — a stale value. So a byte two accesses of
@@ -2252,8 +2595,9 @@ function keepAsOneObject({
2252
2595
  }
2253
2596
  }
2254
2597
  if (twoWidths !== undefined) {
2255
- // any runtime index, a byte one too: `indexedWidth` leaves width 1 out because a char access
2256
- // agrees with every width, but a union view is still spelled for fixed offsets alone
2598
+ // any runtime index, a byte one too, and wherever its reach lies: `indexedWidth` leaves width
2599
+ // 1 out because a char access agrees with every width, but a union view is spelled for fixed
2600
+ // offsets on the object alone, and an indexed access is not one (the rewrite below refuses it)
2257
2601
  const why = unionRefusal({ from, to, uses, indexed: uses.indexed.size > 0 });
2258
2602
  if (why !== null) {
2259
2603
  fail(
@@ -2263,9 +2607,10 @@ function keepAsOneObject({
2263
2607
  }
2264
2608
  }
2265
2609
  for (const [b, had] of widthAt) {
2266
- if (index.width !== null && had !== index.width) {
2610
+ const by = indexes.find((x) => x.width !== 1 && x.width !== had && mayTouch(x.reach, b, 1));
2611
+ if (by !== undefined) {
2267
2612
  fail(
2268
- `the byte at [sp,#${b}] of ${kept} is accessed ${had} bytes wide and by a runtime index ${index.width} ` +
2613
+ `the byte at [sp,#${b}] of ${kept} is accessed ${had} bytes wide and by a runtime index ${by.width} ` +
2269
2614
  `bytes wide, ${STALE_ACROSS_TYPES}`,
2270
2615
  );
2271
2616
  }
@@ -2296,7 +2641,8 @@ function keepAsOneObject({
2296
2641
  : [{ from, to }];
2297
2642
  // …stamped `part` where there are several: storage a copy proved one object of the source's, so
2298
2643
  // its bytes are that object's elements rather than the one object's mix of members and spills,
2299
- // and the emitter may name them as elements (structure.ts `frameArrayElement`)
2644
+ // and the emitter may name them as elements (structure.ts `framePartDecls`), of the type their
2645
+ // accesses agree on (`stampPartElements`, below)
2300
2646
  const laddrs = parts.map(({ from: lo, to: hi }) =>
2301
2647
  mkOp('laddr', {
2302
2648
  results: [mkValue(T.unk(32))],
@@ -2369,6 +2715,9 @@ function keepAsOneObject({
2369
2715
  }
2370
2716
  object.attrs = { ...object.attrs, views: unionViews(irBlocks, base) };
2371
2717
  }
2718
+ if (laddrs.length > 1) {
2719
+ stampPartElements(irBlocks, laddrs, uses.accessOps);
2720
+ }
2372
2721
  irBlocks[0].ops.unshift(...(arrayOrder === 'descending' ? [...laddrs].reverse() : laddrs));
2373
2722
  }
2374
2723
 
@@ -2382,10 +2731,11 @@ function keepAsOneObject({
2382
2731
  * folded into its own local (l3/constinit.ts).
2383
2732
  *
2384
2733
  * ONE PART, never a refusal, whenever a byte could be read through two parts: a runtime index
2385
- * (it names no byte, so it may reach every one), an escape nothing bounds (a callee or a holder
2386
- * that may reach any byte from the address it was given), a byte no use names (nothing says
2387
- * which side it belongs to), or a use through a member that lands outside the part its own
2388
- * address lies in (`&a[16]` read at [-1]: one pointer walking two parts is one object). The
2734
+ * nothing bounds (it names no byte, so it may reach every one) or one whose `reach` (the range
2735
+ * ir/bits.ts proves of the index: `a[(i & 7) * 2]`) crosses a part, an escape nothing bounds (a
2736
+ * callee or a holder that may reach any byte from the address it was given), a byte no use names
2737
+ * (nothing says which side it belongs to), or a use through a member that lands outside the part
2738
+ * its own address lies in (`&a[16]` read at [-1]: one pointer walking two parts is one object). The
2389
2739
  * union views of two widths at one byte are not split either (the caller asks only without
2390
2740
  * them). An access straddling two runs chains them into one part by construction. */
2391
2741
  function provenParts(
@@ -2395,27 +2745,31 @@ function provenParts(
2395
2745
  alignment: number,
2396
2746
  ): FrameRange[] {
2397
2747
  const whole = [{ from, to }];
2398
- if (uses.indexed.size > 0) {
2399
- return whole;
2400
- }
2401
2748
  const touched: { owner: number; lo: number; hi: number }[] = [];
2402
- for (const [op, { off }] of uses.accessOps) {
2749
+ for (const [op, { off, index }] of uses.accessOps) {
2750
+ // …an access through a runtime index is not at its own displacement: it is its reach (below)
2751
+ if (index !== undefined) {
2752
+ continue;
2753
+ }
2403
2754
  const at = off + (op.attrs.off as number);
2404
2755
  touched.push({ owner: off, lo: at, hi: at + (op.attrs.width as number) });
2405
2756
  }
2757
+ // An escape and a runtime index each touch every byte they may reach (`frameReaches`), so each
2758
+ // chains the runs it spans as one access as wide would — a reach across a copy's start makes
2759
+ // the two copies one part — and a reach that starts in a part other than its object's fails the
2760
+ // owner test below
2761
+ const reaches = frameReaches(uses);
2762
+ for (const { owner, reach } of reaches) {
2763
+ if (reach === null) {
2764
+ return whole;
2765
+ }
2766
+ touched.push({ owner, ...reach });
2767
+ }
2406
2768
  for (const [off, reads] of uses.byValueReads) {
2407
2769
  reads.forEach(({ type }) =>
2408
2770
  touched.push({ owner: off, lo: off, hi: off + (type.kind === 'struct' ? (type.size ?? Infinity) : Infinity) }),
2409
2771
  );
2410
2772
  }
2411
- for (const [off, extents] of uses.escapeExtents) {
2412
- for (const e of extents) {
2413
- if (e === null) {
2414
- return whole;
2415
- }
2416
- touched.push({ owner: off, ...e });
2417
- }
2418
- }
2419
2773
  // …and a boundary only where a bounded copy starts: an address handed to `memcpy` is where
2420
2774
  // the source put an object it fills. A run starting anywhere else is bytes reached from below,
2421
2775
  // which says nothing of where one object ends — the one-object relift reads every slot through
@@ -2426,7 +2780,7 @@ function provenParts(
2426
2780
  // reads `buf[6]` through a base it holds where the target re-formed sp+6. Only a copy's
2427
2781
  // DESTINATION counts: a copy reading OUT of the frame says where the source took bytes from,
2428
2782
  // which is as likely `memcpy(g, &w[2], 8)` from the middle of one array as a second object.
2429
- const starts = new Set([...uses.escapeExtents.values()].flat().flatMap((e) => (e?.into ? [e.lo] : [])));
2783
+ const starts = copyStarts(reaches);
2430
2784
  const parts: { from: number; to: number }[] = [];
2431
2785
  for (const { lo, hi } of [...touched].sort((a, b) => a.lo - b.lo)) {
2432
2786
  const last = parts.at(-1);
@@ -2525,6 +2879,95 @@ function unionViews(irBlocks: readonly Block[], base: Value): number[] {
2525
2879
  .flatMap(([w, accesses]) => unionViewSigns(w, accesses).map((sg) => (sg && w !== 4 ? -w : w)));
2526
2880
  }
2527
2881
 
2882
+ /** THE ELEMENT EACH PROVEN PART IS DECLARED WITH, from the census of its own accesses — the way
2883
+ * `unionViews` types a union's views from theirs — stamped `elem` on the part's `laddr` (a width in
2884
+ * bytes, negative for a signed narrow one; absent is the storage default, `u8`).
2885
+ *
2886
+ * A part is an object of the source's (`provenParts`), and its bare element `sp64[i]` is not the
2887
+ * same compile as the cast `((u16 *)sp64)[i]` beside a second array whose base agbcc holds (see
2888
+ * structure.ts `framePartDecls`) — but the bare spelling exists only for an access of the declared
2889
+ * element's width and extension, so a part of `u16` or `s8` elements declared `u8` reaches no
2890
+ * access bare. Every access agreeing on one element is the evidence a declaration of it needs. An access that disagrees with the declaration keeps its cast, which is
2891
+ * one value under any declaration (structure.ts `bareArrayElement`), so the stamp never changes
2892
+ * what an access reads; nor what it aliases, since a bare element of a `u16` array is a `u16`
2893
+ * lvalue, the type the cast it replaces names.
2894
+ *
2895
+ * - One width over every access (a block copy or any other escape has none), aligned to it — the
2896
+ * part's start, its size and every fixed offset — else the storage default.
2897
+ * - Its extension from the LOADS: one extension is that one, both is no element (no evidence picks
2898
+ * between them). An unsigned byte read whose every reader sign-extends it is either: agbcc has
2899
+ * no `ldrsb` with an immediate offset and mwcc no signed byte load at all, so `s8 a[64]`'s `a[3]`
2900
+ * is that read (structure.ts `declaredExtendedRead` spells it bare once the part says `s8`). With
2901
+ * no load committing, such a read makes the element signed, and else a store raise/storesign.ts
2902
+ * stamped signed does, as `unionViewSigns` reads a view's.
2903
+ * - A word is one element whatever its loads say (`scalarTypeForAccess`).
2904
+ * An access whose part is not known — one through a pointer a phi carries, an index nothing bounds
2905
+ * — leaves every part at the default: its width is not counted, so no census is whole. */
2906
+ function stampPartElements(
2907
+ irBlocks: readonly Block[],
2908
+ laddrs: readonly Op[],
2909
+ accessOps: ReadonlyMap<Op, FrameAccessOp>,
2910
+ ): void {
2911
+ const bases = laddrs.map((d) => d.results[0]);
2912
+ const start = laddrs.map((d) => d.attrs.off as number);
2913
+ const end = laddrs.map((d) => (d.attrs.off as number) + (d.attrs.count as number));
2914
+ const users = new Map<Value, Op[]>();
2915
+ for (const op of irBlocks.flatMap((b) => b.ops)) {
2916
+ op.operands.forEach((v) => (users.get(v) ?? users.set(v, []).get(v)!).push(op));
2917
+ }
2918
+ type Access = { width: number; at: number | null; load: boolean; signed: boolean | null; stamped: boolean };
2919
+ const census = laddrs.map((): Access[] => []);
2920
+ for (const [op, { index }] of accessOps) {
2921
+ const fixed = bases.indexOf(op.operands[0]);
2922
+ const reach = index?.reach ?? null;
2923
+ const part =
2924
+ fixed >= 0 ? fixed : reach === null ? -1 : start.findIndex((lo, i) => reach.lo >= lo && reach.hi <= end[i]);
2925
+ if (part < 0) {
2926
+ return;
2927
+ }
2928
+ const width = op.attrs.width as number;
2929
+ const load = op.opcode === 'load';
2930
+ // …a sign extension as the frontend lifts it: mwcc's `extsb`, or the `lsl #24; asr #24` pair
2931
+ // (the idiom stage folds it to a `sext` later, pattern/engine.ts `sextPat`)
2932
+ const extends8 = (u: Op): boolean =>
2933
+ (u.opcode === 'sext' && u.attrs.width === 8) ||
2934
+ (u.opcode === 'shl' &&
2935
+ u.attrs.imm === 24 &&
2936
+ (users.get(u.results[0]) ?? []).length > 0 &&
2937
+ users.get(u.results[0])!.every((s) => s.opcode === 'shr_s' && s.attrs.imm === 24));
2938
+ const readers = load ? (users.get(op.results[0]) ?? []) : [];
2939
+ const either = width === 1 && op.attrs.signed !== true && readers.length > 0 && readers.every(extends8);
2940
+ census[part].push({
2941
+ width,
2942
+ at: fixed >= 0 ? (op.attrs.off as number) : null,
2943
+ load,
2944
+ signed: !load || either ? null : op.attrs.signed === true,
2945
+ stamped: !load && op.attrs.signed === true,
2946
+ });
2947
+ }
2948
+ laddrs.forEach((d, i) => {
2949
+ const accesses = census[i];
2950
+ const widths = new Set(accesses.map((a) => a.width));
2951
+ if (widths.size !== 1) {
2952
+ return;
2953
+ }
2954
+ const [w] = widths;
2955
+ const count = d.attrs.count as number;
2956
+ if (start[i] % w !== 0 || count % w !== 0 || accesses.some((a) => a.at !== null && a.at % w !== 0)) {
2957
+ return;
2958
+ }
2959
+ const signs = new Set(accesses.flatMap((a) => (a.signed === null ? [] : [a.signed])));
2960
+ if (signs.size > 1) {
2961
+ return;
2962
+ }
2963
+ const signed = signs.size === 1 ? [...signs][0] : accesses.some((a) => a.load || a.stamped);
2964
+ const elem = signed && w !== 4 ? -w : w;
2965
+ if (elem !== 1) {
2966
+ d.attrs = { ...d.attrs, elem };
2967
+ }
2968
+ });
2969
+ }
2970
+
2528
2971
  /** One object's shape: `count` elements of `width` bytes. */
2529
2972
  interface ObjectShape {
2530
2973
  readonly width: number;