@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
package/src/ir/core.ts CHANGED
@@ -59,8 +59,57 @@ export interface Fn {
59
59
  /** L1 SIDE DATA (see {@link LocalObjects}); set by a frontend that recovered one, `undefined` on
60
60
  * every other `Fn`. REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` is. */
61
61
  localObjects: LocalObjects | undefined;
62
+ /** L1 SIDE DATA (see {@link AsmLayout}); set by a frontend, `undefined` on parsed IR.
63
+ * REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` is. */
64
+ asmLayout: AsmLayout | undefined;
62
65
  }
63
66
 
67
+ /** One block's place in the assembly a frontend lifted it from ({@link AsmLayout}). */
68
+ export interface AsmBlockLayout {
69
+ /** the block's position among the function's blocks in the asm text, first block 0 */
70
+ readonly index: number;
71
+ /** the block's instructions without its control transfer, one string per instruction as the
72
+ * asm spells it (`mnemonic op,op`), followed by `<sym>` (`<sym+0xN>` with an addend) where a
73
+ * relocation fills in a symbol the operands do not name (frontend/ssa.ts `recordAsmLayout`). A
74
+ * MIPS delay slot is part of the block it runs in. */
75
+ readonly body: readonly string[];
76
+ /** the positions in `body` of the instructions that make a call, as the frontend's ISA reads them
77
+ * (Thumb `bl`/`blx` but not a far-jump relay, MIPS `jal`/`jalr`/`bal`, PPC `bl`/`bctrl`/`blrl`) */
78
+ readonly calls: readonly number[];
79
+ /** the block ends in an unconditional jump to a label: neither a fall-through, nor a return,
80
+ * nor a conditional or indirect branch */
81
+ readonly endsInJump: boolean;
82
+ }
83
+
84
+ /** WHERE THE COMPILER PUT EACH BLOCK, and what it holds besides its branch: a measurement only the
85
+ * frontend can make. The IR keeps the CFG and drops the order, since `fn.blocks` is a list passes
86
+ * are free to rewrite (raise/reroll.ts moves a block ahead of another, and inserts one).
87
+ *
88
+ * The reader is the return-tail chooser (structure/cross-jumped-tail.ts). Where the compiler
89
+ * cross-jumps return tails (target.ts `crossJumpsReturnTails`), the layout tells a tail the source
90
+ * wrote once from one it wrote into every arm, and the CFG cannot. Keyed by Block OBJECT, so a
91
+ * clone re-keys it (cli/src/report.ts `structuredCloneFn`). A block a pass created has no entry,
92
+ * and every reader takes a missing entry as "no evidence". Recorded by the Thumb and MIPS
93
+ * frontends only: no mark is read on mwcc (it declares neither `crossJumpsReturnTails` nor an
94
+ * unfolded taken-edge constant), so frontend/ppc.ts records none until one is, and a PPC lift's
95
+ * `asmLayout` is undefined.
96
+ *
97
+ * AN ENTRY DESCRIBES THE ASM BLOCK THE IR BLOCK WAS LIFTED FROM, whatever raising later did to the
98
+ * IR block. A return sunk into it, or ops a fold absorbed (shortcircuit, tailsink, divpow2), leave
99
+ * the entry as the frontend wrote it. So `endsInJump` and `body` say what the compiler emitted,
100
+ * not what the block holds now: over the agbcc lifts of the 620 functions the join re-spells
101
+ * (2026-11-05), 786 of 10520 blocks have an entry saying they end in a jump and no longer end in a
102
+ * `br`. The return-tail chooser reads exactly that, the asm terminator. A reader of the current
103
+ * block reads its ops.
104
+ *
105
+ * BUT ON MIPS THE ASM IS THE NORMALISED ASM. frontend/mips.ts records the layout after
106
+ * `normaliseBranchLikely`, which gives a likely branch's slot a block of its own and retargets an
107
+ * unconditional `b T` whose slot copies the word at T-4 to that word, emptying the slot.
108
+ * Such a `b`'s entry has `body` ending in `nop` where the compiler emitted the copied word
109
+ * (`func_80A42DB4_jp`'s `b` at 0x80A42DCC, whose slot is `sw zero,0(a3)`), so every MIPS mark
110
+ * reads the normalised body, not the emitted one. */
111
+ export type AsmLayout = Map<Block, AsmBlockLayout>;
112
+
64
113
  /** Where a function-scope static lives, which decides how its definition is spelled: `rodata` is
65
114
  * `const` with an initializer, `data` has an initializer, `bss` has none. */
66
115
  export type LocalObjectSection = 'rodata' | 'data' | 'bss';
@@ -356,6 +405,19 @@ export function mkOp(opcode: Opcode, o: Partial<Op> = {}): Op {
356
405
  };
357
406
  }
358
407
 
408
+ /** An op's attributes as one comparable key, keys sorted (so insertion order never tells two
409
+ * equal attribute sets apart), leaving out the attribute `drop` names. */
410
+ export function attrsKey(op: Op, drop: string | null = null): string {
411
+ return JSON.stringify(
412
+ Object.entries(op.attrs ?? {})
413
+ .filter(([k]) => k !== drop)
414
+ .sort(([a], [b]) => a.localeCompare(b)),
415
+ );
416
+ }
417
+
418
+ /** One op's identity apart from its operands: opcode, result count and attributes. */
419
+ export const opShape = (op: Op): string => `${op.opcode}/${op.results.length}/${attrsKey(op)}`;
420
+
359
421
  // ── a call's callee: a NAME (`attrs.target`) or, for a call through a register, a VALUE ─────────
360
422
  //
361
423
  // An INDIRECT call carries `indirect: true` and no `target`, and calls the function whose address
@@ -438,7 +500,7 @@ export function successorsOf(b: Block): Block[] {
438
500
  * keeps a walk of its own rather than calling this one — it descends through EVERY op's successors,
439
501
  * which is the same relation on verified IR and a wider one on the hand-built IR its own note is
440
502
  * about. */
441
- export function reachableBlocks(fn: Fn): Set<Block> {
503
+ export function reachableBlocks(fn: { readonly blocks: readonly Block[] }): Set<Block> {
442
504
  const seen = new Set<Block>();
443
505
  const entry = fn.blocks[0];
444
506
  if (entry === undefined) {
@@ -484,7 +546,7 @@ export function reachForward(
484
546
  }
485
547
 
486
548
  /** Predecessor map for the whole function's CFG. */
487
- export function predecessors(fn: Fn): Map<Block, Block[]> {
549
+ export function predecessors(fn: { readonly blocks: readonly Block[] }): Map<Block, Block[]> {
488
550
  const preds = new Map<Block, Block[]>();
489
551
  for (const b of fn.blocks) {
490
552
  preds.set(b, []);
@@ -503,7 +565,7 @@ export function predecessors(fn: Fn): Map<Block, Block[]> {
503
565
  * `verify` needs it to check def-dominates-use, `ir/loops.ts` to tell a back-edge from a
504
566
  * forward one, and `raise/latch.ts` to tell a latch from a preheader. */
505
567
 
506
- export function dominators(fn: Fn): Map<Block, Set<Block>> {
568
+ export function dominators(fn: { readonly blocks: readonly Block[] }): Map<Block, Set<Block>> {
507
569
  const preds = predecessors(fn);
508
570
  const all = new Set(fn.blocks);
509
571
  const dom = new Map<Block, Set<Block>>();
@@ -536,6 +598,94 @@ export function dominators(fn: Fn): Map<Block, Set<Block>> {
536
598
  return dom;
537
599
  }
538
600
 
601
+ /** Whether `h` dominates `t`, exactly as `dominators(fn).get(t).has(h)` answers it: the
602
+ * same data-flow (dom(b) = {b} ∪ ⋂ dom(preds), the entry alone, every other block starting from all
603
+ * of them), iterated to the same greatest fixed point, over one bit per block instead of a `Set`
604
+ * per block. raise/shortcircuit.ts's `naturalLoopBodies` asks it once per scan of a function its fold
605
+ * keeps changing, where the sets cost a large function's lift most of its raising time. */
606
+ export function dominance(fn: { readonly blocks: readonly Block[] }): (h: Block, t: Block) => boolean {
607
+ const n = fn.blocks.length;
608
+ const words = Math.ceil(n / 32);
609
+ const index = new Map<Block, number>();
610
+ fn.blocks.forEach((b, i) => index.set(b, i));
611
+ const preds = predecessors(fn);
612
+ const dom = fn.blocks.map((_, i) => {
613
+ const bits = new Uint32Array(words);
614
+ if (i === 0) {
615
+ bits[0] = 1;
616
+ } else {
617
+ bits.fill(0xffffffff);
618
+ if (n % 32 !== 0) {
619
+ bits[words - 1] = 2 ** (n % 32) - 1;
620
+ }
621
+ }
622
+ return bits;
623
+ });
624
+ const next = new Uint32Array(words);
625
+ for (let changed = true; changed;) {
626
+ changed = false;
627
+ for (let i = 1; i < n; i++) {
628
+ const ps = preds.get(fn.blocks[i])!;
629
+ if (ps.length === 0) {
630
+ next.fill(0);
631
+ } else {
632
+ next.set(dom[index.get(ps[0])!]);
633
+ for (let k = 1; k < ps.length; k++) {
634
+ const dp = dom[index.get(ps[k])!];
635
+ for (let w = 0; w < words; w++) {
636
+ next[w] &= dp[w];
637
+ }
638
+ }
639
+ }
640
+ next[i >>> 5] |= 1 << (i & 31);
641
+ const prev = dom[i];
642
+ let same = true;
643
+ for (let w = 0; w < words; w++) {
644
+ if (next[w] !== prev[w]) {
645
+ same = false;
646
+ break;
647
+ }
648
+ }
649
+ if (!same) {
650
+ prev.set(next);
651
+ changed = true;
652
+ }
653
+ }
654
+ }
655
+ return (h, t) => {
656
+ const hi = index.get(h);
657
+ const ti = index.get(t);
658
+ return hi !== undefined && ti !== undefined && (dom[ti][hi >>> 5] & (1 << (hi & 31))) !== 0;
659
+ };
660
+ }
661
+
662
+ /** Each block's IMMEDIATE dominator: of its strict `dominators`, which form a chain, the nearest —
663
+ * the one every other dominates, so the one with the most dominators of its own. Read off those
664
+ * sets rather than computed again, so the tree and the sets cannot disagree; but off the sets of
665
+ * the blocks the entry reaches (`reachableBlocks`) alone, because `dominators` counts an edge
666
+ * out of an unreachable block as a way in, and one such edge into a loop's join leaves the join
667
+ * dominated by nothing but itself. The entry, and every unreachable block, have none.
668
+ * `ir/bits.ts`'s `edgeFacts` walks this tree up from a point to read what the branches above it
669
+ * prove there. */
670
+ export function immediateDominators(fn: { readonly blocks: readonly Block[] }): Map<Block, Block> {
671
+ const live = reachableBlocks(fn);
672
+ const blocks = fn.blocks.filter((b) => live.has(b));
673
+ const dom = dominators({ blocks });
674
+ const out = new Map<Block, Block>();
675
+ for (const b of blocks.slice(1)) {
676
+ let near: Block | undefined;
677
+ for (const d of dom.get(b)!) {
678
+ if (d !== b && (near === undefined || dom.get(d)!.size > dom.get(near)!.size)) {
679
+ near = d;
680
+ }
681
+ }
682
+ if (near !== undefined) {
683
+ out.set(b, near);
684
+ }
685
+ }
686
+ return out;
687
+ }
688
+
539
689
  /** A block with NO BODY OF ITS OWN: it declares no parameters and holds a single op, which is
540
690
  * therefore its terminator. It computes nothing and it binds nothing. Three sites ask this and
541
691
  * each adds its own clause (`forwardingTarget` below wants a `br` carrying no args;
@@ -605,7 +755,7 @@ export function forwardingTarget(b: Block): Block {
605
755
  * a CFG fact a parameter for one caller's extra edges — so it stays where it is, and what this
606
756
  * paragraph buys is that the next reader looking for a fourth copy knows which of the two the
607
757
  * third one is. */
608
- export function mergeClasses(fn: Pick<Fn, 'blocks'>): Map<Value, readonly Value[]> {
758
+ export function mergeClasses(fn: { readonly blocks: readonly Block[] }): Map<Value, readonly Value[]> {
609
759
  const parent = new Map<Value, Value>();
610
760
  const find = (v: Value): Value => {
611
761
  let r = v;
package/src/ir/entry.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  // `base + c` as the same load off `base` at its offset plus `c` (`str[0]` against `str[i]` at
22
22
  // `i = 0`). Each is an identity of the integer operators, so turning it on for guard fusion is a
23
23
  // spelling decision, not a soundness one.
24
- import { Op, Value } from './core';
24
+ import { Op, Value, attrsKey } from './core';
25
25
  import { spelledWhenDead } from './discipline';
26
26
  import { NEGATED_ICMP } from './opcodes';
27
27
 
@@ -100,8 +100,6 @@ export function sameAtEntry(
100
100
  }
101
101
  return { base, off, atEntry };
102
102
  };
103
- const attrsOf = (d: Op, drop: string | null) =>
104
- JSON.stringify(Object.fromEntries(Object.entries(d.attrs ?? {}).filter(([k]) => k !== drop)));
105
103
  // memoised per (x, y, side): a value its own consumer reads twice would otherwise double the work
106
104
  // at every level
107
105
  const memo = new Map<string, boolean>();
@@ -115,9 +113,9 @@ export function sameAtEntry(
115
113
  if (normalize && da.opcode === 'load' && db.opcode === 'load') {
116
114
  const x = address(da, atEntry);
117
115
  const y = address(db, true);
118
- return x.off === y.off && attrsOf(da, 'off') === attrsOf(db, 'off') && same(x.base, x.atEntry, y.base);
116
+ return x.off === y.off && attrsKey(da, 'off') === attrsKey(db, 'off') && same(x.base, x.atEntry, y.base);
119
117
  }
120
- if (attrsOf(da, null) !== attrsOf(db, null)) {
118
+ if (attrsKey(da) !== attrsKey(db)) {
121
119
  return false;
122
120
  }
123
121
  if (da.operands.every((o, i) => same(o, atEntry, db.operands[i]))) {
package/src/ir/opcodes.ts CHANGED
@@ -204,6 +204,12 @@ export const OPCODES = {
204
204
  // - a load/store through an off-0 SCALAR gaddr → a bare global `gSym` / `gSym = v`;
205
205
  // - an indexed or non-zero-offset AGGREGATE access → the address-cast `((T *)&gSym)[i]`;
206
206
  // - any other use (e.g. `&gSym` passed to a call) → the `{k:'addr'}` L3 node, printed `&gSym`.
207
+ // Optional `fromMap: true` marks a name asmlift READ OFF THE SYMBOL MAP (a promoted numeric pool
208
+ // word, an interior attribution, a named walk) rather than one the asm spells by relocation.
209
+ // With it, `mapAddr` is the map address the name was read at: a name the map places at several
210
+ // addresses (a file static per unit) then denotes that one, and two such reads at different
211
+ // addresses, one of them read off the map, are two objects under one spelling, which
212
+ // raise/offsetnames.ts `cellSpelledTwice` declines. Two relocations of one name are the asm's own.
207
213
  gaddr: { operands: 0, results: 1, requiredAttrs: ['sym'] },
208
214
  // The address of a FRAME-LOCAL object — gaddr's local twin, for the address-taken stack local
209
215
  // (`mov rD, sp` feeding a DMA register or a callee). `off` is the byte offset inside the frame's
@@ -408,6 +414,28 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
408
414
  ]),
409
415
  );
410
416
 
417
+ /** The comparison each `icmp_*` is with its operands SWAPPED — `a < b` is `b > a`; `==` and `!=`
418
+ * are their own. Not a negation: `NEGATED_ICMP` keeps the operands and flips the answer, this
419
+ * keeps the answer and flips the operands. Seated here for the reason `NEGATED_ICMP` is, and
420
+ * expanded both ways from its pairs the same way, so `swap(swap(c)) === c` by construction; a test
421
+ * asserts it covers the icmp family (test/pattern.test.ts). */
422
+ const ICMP_SWAP_PAIRS: readonly (readonly [Opcode, Opcode])[] = [
423
+ ['icmp_slt', 'icmp_sgt'],
424
+ ['icmp_sle', 'icmp_sge'],
425
+ ['icmp_ult', 'icmp_ugt'],
426
+ ['icmp_ule', 'icmp_uge'],
427
+ ];
428
+ export const SWAPPED_ICMP: Readonly<Record<string, Opcode>> = {
429
+ icmp_eq: 'icmp_eq',
430
+ icmp_ne: 'icmp_ne',
431
+ ...Object.fromEntries(
432
+ ICMP_SWAP_PAIRS.flatMap(([a, b]) => [
433
+ [a, b],
434
+ [b, a],
435
+ ]),
436
+ ),
437
+ };
438
+
411
439
  /** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
412
440
  * `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
413
441
  * Authored rather than derived — no registry field records the operand ROLE, and the only other
package/src/ir/parse.ts CHANGED
@@ -128,6 +128,7 @@ export function parse(text: string): Fn {
128
128
  slotHomes: undefined,
129
129
  paramEvidence: undefined,
130
130
  localObjects: undefined,
131
+ asmLayout: undefined,
131
132
  };
132
133
  }
133
134
 
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Peer of `pattern/engine.ts`'s `dce`: general, opcode-agnostic, and callable by anything that has
4
4
  // just changed the CFG.
5
- import { Block, Fn, Successor, Value, replaceAllUsesWith } from './core';
5
+ import { Block, Fn, Op, Successor, Value, replaceAllUsesWith } from './core';
6
6
 
7
7
  /**
8
8
  * Remove block params that are really TRIVIAL PHIS — those whose incoming args, across every
@@ -55,12 +55,7 @@ export function simplifyTrivialPhis(fn: Fn, onRemoved?: (param: Value) => void):
55
55
  if (collapsed === null) {
56
56
  continue; // a genuine join, or unreachable (no in-edges at all)
57
57
  }
58
- replaceAllUsesWith(fn, param, collapsed);
59
- onRemoved?.(param);
60
- b.params.splice(i, 1);
61
- for (const s of incoming) {
62
- s.args.splice(i, 1);
63
- }
58
+ retireParam(fn, b, i, collapsed, incoming, onRemoved);
64
59
  removed++;
65
60
  changed = true;
66
61
  }
@@ -77,14 +72,70 @@ export function simplifyTrivialPhis(fn: Fn, onRemoved?: (param: Value) => void):
77
72
  * fixpoint and the non-mutating check below so the two cannot come to disagree about which params
78
73
  * are trivial.
79
74
  *
75
+ * `same` is the equality the args are held to. Identity by default, which is the trivial phi
76
+ * proper; `raise/unpre.ts` passes structural congruence (`ir/entry.ts` `sameAtEntry`) and gets
77
+ * back the FIRST arg as the class's representative, which it recomputes rather than reads. The
78
+ * self-reference waiver is the same for both, and so is its argument above: the first dynamic
79
+ * entry arrives on an edge that hands the representative's class.
80
+ *
80
81
  * `=== null` IS THE ONLY SAFE TEST. A genuine collapse value is never null, but it CAN be
81
82
  * `undefined` where an edge carries fewer args than the block has params — a malformed graph the
82
83
  * verifier catches elsewhere, and one today's callers pass straight through rather than treat as
83
84
  * "not trivial". A truthiness test would silently change that.
84
85
  */
85
- function trivialPhiValue(incoming: readonly Successor[], i: number, param: Value): Value | null {
86
- const distinct = [...new Set(incoming.map((s) => s.args[i]).filter((v) => v !== param))];
87
- return distinct.length === 1 ? distinct[0] : null;
86
+ export function trivialPhiValue(
87
+ incoming: readonly Successor[],
88
+ i: number,
89
+ param: Value,
90
+ same: (a: Value, b: Value) => boolean = (a, b) => a === b,
91
+ ): Value | null {
92
+ const args = incoming.map((s) => s.args[i]).filter((v) => v !== param);
93
+ if (args.length === 0) {
94
+ return null;
95
+ }
96
+ const first = args[0];
97
+ return args.every((v) => v === first || same(v, first)) ? first : null;
98
+ }
99
+
100
+ /** Retire block param `i` of `b` for `value`: every use reads `value`, and the param and each
101
+ * in-edge's arg for it are spliced out. `incoming` is every successor that targets `b` — the
102
+ * SUCCESSOR objects, not their `args`, which `replaceAllUsesWith` replaces. `onRemoved` runs after
103
+ * the uses move and before the splice. */
104
+ export function retireParam(
105
+ fn: Fn,
106
+ b: Block,
107
+ i: number,
108
+ value: Value,
109
+ incoming: readonly Successor[],
110
+ onRemoved?: (param: Value) => void,
111
+ ): void {
112
+ const param = b.params[i];
113
+ replaceAllUsesWith(fn, param, value);
114
+ onRemoved?.(param);
115
+ b.params.splice(i, 1);
116
+ for (const s of incoming) {
117
+ s.args.splice(i, 1);
118
+ }
119
+ }
120
+
121
+ /** Every in-edge, indexed by target: the successor, the block it leaves and the terminator that
122
+ * carries it. ONE pass over the function, for a caller whose CFG does not change while it reads the
123
+ * index (params and args may: the successor objects stay). */
124
+ export function inEdgesOf(fn: Fn): Map<Block, { succ: Successor; from: Block; op: Op }[]> {
125
+ const out = new Map<Block, { succ: Successor; from: Block; op: Op }[]>();
126
+ for (const from of fn.blocks) {
127
+ for (const op of from.ops) {
128
+ for (const succ of op.successors) {
129
+ const list = out.get(succ.block);
130
+ if (list === undefined) {
131
+ out.set(succ.block, [{ succ, from, op }]);
132
+ } else {
133
+ list.push({ succ, from, op });
134
+ }
135
+ }
136
+ }
137
+ }
138
+ return out;
88
139
  }
89
140
 
90
141
  /**
package/src/ir/verify.ts CHANGED
@@ -211,6 +211,7 @@ export function verify(fn: Fn): void {
211
211
  // second as `u8 name[count]`, so a typed element there would be a type nothing pinned —
212
212
  // the exact answer the audit refuses to invent. (One BYTE encodes like a real `u8`
213
213
  // scalar either way; the declaration is the same one, so nothing has to tell them apart.)
214
+ // A part's element, where its own accesses pin one, rides beside them as `elem` below.
214
215
  if (op.opcode === 'laddr') {
215
216
  const count = op.attrs.count;
216
217
  if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {
@@ -229,6 +230,21 @@ export function verify(fn: Fn): void {
229
230
  if (op.attrs.part === true && count < 2) {
230
231
  throw new VerifyError(`'laddr' part must be storage of two or more bytes, got ${count}`);
231
232
  }
233
+ // …whose ELEMENT its accesses may agree on (frame-objects.ts `stampPartElements`): a
234
+ // width that tiles the part, negative for a signed narrow one, never the default `u8`
235
+ const elem = op.attrs.elem;
236
+ if (
237
+ elem !== undefined &&
238
+ (op.attrs.part !== true ||
239
+ op.attrs.views !== undefined ||
240
+ ![2, 4, -1, -2].includes(elem as number) ||
241
+ count % Math.abs(elem as number) !== 0)
242
+ ) {
243
+ throw new VerifyError(
244
+ `'laddr' elem is a part's element width (2, 4; -1, -2 signed) tiling its ${count} bytes, ` +
245
+ `got ${String(elem)}${op.attrs.part === true ? '' : ' on storage that is no part'}`,
246
+ );
247
+ }
232
248
  // …and storage accessed at two widths over one byte names the VIEWS a union of it
233
249
  // declares (frame-objects.ts `unionViews`): one per element width in bytes, negative
234
250
  // for a signed narrow one, each tiling the storage — so at least two, distinct, of a
package/src/l3/argbase.ts CHANGED
@@ -208,6 +208,8 @@ export function materializeArgBases(sfn: SFn): SFn | null {
208
208
  break;
209
209
  case 'break':
210
210
  case 'continue':
211
+ case 'goto':
212
+ case 'caseEndGoto':
211
213
  out = s;
212
214
  break;
213
215
  }