@asmlift/core 0.8.0 → 0.9.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 (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
@@ -27,16 +27,20 @@ import {
27
27
  poolOrderOf,
28
28
  restoreUnclaimedScales,
29
29
  } from './extscale';
30
+ import { recognizeFloatHelpers } from './floathelpers';
30
31
  import { numberPureValues } from './gvn';
31
32
  import { recognizeMagicDivision } from './magicdiv';
32
33
  import { recognizeMemberArrays } from './memberarrays';
33
34
  import { rerootNarrowReads } from './narrow';
34
35
  import { type MergeShape, mergeShapes, narrowBlockLocals } from './narrowlocal';
36
+ import { fuseParamPairs } from './pairparams';
35
37
  import { PARAM_WIDTH_GATES, narrowEntryParams } from './paramwidth';
36
38
  import { type BranchShortCircuitOptions, recognizeBranchShortCircuit, recognizeShortCircuit } from './shortcircuit';
37
39
  import { recognizeSoftDiv } from './softdiv';
38
40
  import { recognizeStructArrays } from './struct-arrays';
39
41
  import { recognizeStructs } from './structs';
42
+ import { TRUNC_LOAD_GATES, foldTruncatedLoads } from './truncload';
43
+ import { recognizeWideHelpers, refuseUnmodelledHelpers } from './widehelpers';
40
44
 
41
45
  /** Per-call options for the pass list — ONE field per pass that takes any, named for the pass, so
42
46
  * a caller reads which recognizer it is steering and a pass that takes none says so by absence.
@@ -91,8 +95,9 @@ export interface PreRecoveryPass {
91
95
 
92
96
  /** THE ordered pre-recovery pass list — the single source of truth shared by pipeline / rank / report.
93
97
  * address-numbering → const-materialize → magic-division → pow2-division → soft-division →
94
- * scaled-extension → array-legalize → struct-array → member-array → struct-pointer → short-circuit →
95
- * branch-short-circuit → narrow-reads → narrow-local → parameter-width → scaled-extension-restore.
98
+ * scaled-extension → array-legalize → truncated-load → struct-array → member-array → struct-pointer →
99
+ * short-circuit → branch-short-circuit → narrow-reads → narrow-local → parameter-width →
100
+ * scaled-extension-restore.
96
101
  * See each recognizer's file for the rationale. */
97
102
  export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
98
103
  // FIRST: collapsing duplicate address definitions removes block params every later recognizer
@@ -121,7 +126,47 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
121
126
  // ending in `br`. Verified by running it before and after each of them — same result. It sits
122
127
  // beside magicdiv so that a reader looking for division recovery finds both together.
123
128
  { id: 'divpow2', run: recognizeDivPow2, dce: true },
124
- { id: 'softdiv', run: (fn) => recognizeSoftDiv(fn), dce: false, gate: (t) => !t.capabilities.hwDivide },
129
+ {
130
+ id: 'softdiv',
131
+ run: (fn, _self, _opts, target) => recognizeSoftDiv(fn, target),
132
+ dce: false,
133
+ gate: (t) => !t.capabilities.hwDivide,
134
+ },
135
+ // NOT gated, where `softdiv` is: a float helper computes on floats, which an ISA with no FPU has no
136
+ // instruction for, and the target's runtime table is what says whether it calls one. A pass apart
137
+ // from `widehelpers` because it re-types words as floats — a helper's, a parameter a callee
138
+ // declares `double`, a literal — with its own closed-set refusal, and a single-precision helper
139
+ // is a float helper that is not wide.
140
+ {
141
+ id: 'floathelpers',
142
+ run: (fn, _self, _opts, target) => recognizeFloatHelpers(fn, target),
143
+ dce: false,
144
+ },
145
+ // NOT gated, where `softdiv` is, and the asymmetry is the argument: a soft DIVISION is
146
+ // a division the ISA has no instruction for, so a target with a divider never emits one — while
147
+ // no ISA this repo targets has 64-bit integer arithmetic at all, so a 64-bit helper is software
148
+ // on every one of them whatever `hwDivide` says.
149
+ {
150
+ id: 'widehelpers',
151
+ run: (fn, _self, _opts, target) => recognizeWideHelpers(fn, target),
152
+ dce: false,
153
+ },
154
+ // AFTER the three helper recognizers, because what it refuses is precisely what they declined: a
155
+ // call to a name the target's runtime table carries that none of `softdiv`, `floathelpers` and
156
+ // `widehelpers` folded.
157
+ // Re-emitting the compiler's own runtime call as source is the one failure that MATCHES, so the
158
+ // gap has to be written here rather than left to the backend. See raise/widehelpers.ts.
159
+ //
160
+ // `dce: false` — an `opaque` carries `effects`, so nothing it replaces becomes dead.
161
+ {
162
+ id: 'helperrefuse',
163
+ run: (fn, _self, _opts, target) => refuseUnmodelledHelpers(fn, target),
164
+ dce: false,
165
+ },
166
+ // AFTER it, because the `concat`s it reads are the ones the frontend built at the call the pass
167
+ // above has just rewritten — and because a pair the rewrite refused is a pair this must not fuse
168
+ // into a signature either.
169
+ { id: 'pairparams', run: (fn) => fuseParamPairs(fn), dce: false },
125
170
  // AFTER `const`, which folds a shift pair over a constant to the constant it computes, and BEFORE
126
171
  // the three array recognizers, whose input this pass produces: `shl(ext(x), k)` is an element
127
172
  // scale they legalize and the fused pair is not. `dce: true` — the `shl` a fold leaves readerless.
@@ -134,6 +179,31 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
134
179
  gate: foldsShiftPairCasts,
135
180
  },
136
181
  { id: 'arrays', run: recognizeArrays, dce: true },
182
+ // BEFORE the three struct synthesizers, and that is what the pass is FOR: `raise/structs.ts` is
183
+ // the pass that refuses one offset read at two widths, and it can only see one width per offset
184
+ // if the fold has already run. Below it the fold is dead — moved after `structs`,
185
+ // `pokeemerald:AnimTask_FlashHealthboxOnLevelUp_Step:agbcc` declines again on the same throw.
186
+ //
187
+ // EVERY POSITION ABOVE `structs` IS FREE, and that is measured rather than argued: two full
188
+ // `bench sweep --json` runs, this seat against the entry moved to index 0 — above `addrnum`,
189
+ // `const`, the three division recognizers, `extscale` and `arrays` — agree on all 2,430 records
190
+ // over 1,215 rows, compared by ROW IDENTITY on `src`/`len`/`diag`/`marks`/`asm`. (The 84
191
+ // `af:*:ido7.1` records whose `opts` digest churns between any two sweeps are the corpus-wide
192
+ // defect, which moves whatever anyone does; a LINE diff would report them as disagreement.)
193
+ //
194
+ // So the seat is a READING ORDER rather than a constraint: the three struct synthesizers follow
195
+ // each other here, and a reader looking for what happens to a base's access set finds them
196
+ // together. The one position that LOOKS like a prerequisite is `addrnum` — the fold groups
197
+ // accesses by base VALUE identity, so collapsing duplicate address definitions first reads as
198
+ // one. It is not, and index 0 is the run that shows it.
199
+ //
200
+ // `dce: false` — the rewrite retires nothing (the base keeps its use in the load the fold makes).
201
+ {
202
+ id: 'truncload',
203
+ run: (fn, _self, _opts, target) =>
204
+ foldTruncatedLoads(fn, target.capabilities.endianness === 'little', TRUNC_LOAD_GATES),
205
+ dce: false,
206
+ },
137
207
  // struct-arrays AFTER arrays (scalar stride==width shapes are claimed first — see the
138
208
  // discriminator note in raise/struct-arrays.ts) and BEFORE structs (an element's field
139
209
  // accesses must not be re-derived as constant-offset struct-pointer accesses).
@@ -147,7 +217,11 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
147
217
  // sits there so the three struct synthesizers read in the order of their evidence. `narrowlocal`
148
218
  // reads the carrier's extension chain rather than any address, so nothing here orders against it.
149
219
  { id: 'member-arrays', run: (fn) => recognizeMemberArrays(fn), dce: true },
150
- { id: 'structs', run: recognizeStructs, dce: false },
220
+ {
221
+ id: 'structs',
222
+ run: (fn, _self, _opts, target) => recognizeStructs(fn, target.compilerBehaviors.aggregateBoundary),
223
+ dce: false,
224
+ },
151
225
  { id: 'shortcircuit', run: recognizeShortCircuit, dce: true },
152
226
  // The control-flow sibling, value form FIRST.
153
227
  //
@@ -30,7 +30,7 @@ export function recoverTypes(fn: Fn): void {
30
30
  seedSignednessFromOpcodes(fn);
31
31
  typeDerefBases(fn);
32
32
  propagatePointers(fn);
33
- defaultUnknownsToS32(fn);
33
+ defaultUnknowns(fn);
34
34
  }
35
35
 
36
36
  /** PHASE 1 — signedness from op semantics: the operands of a signed comparison are signed integers,
@@ -115,20 +115,24 @@ function typeDerefBases(fn: Fn): void {
115
115
  // incoming `a0`) and a `p = p + stride` walk, so those values stay `unknown` and phase 4 would
116
116
  // spell them `int` — an `int→int*` assignment mwcc/agbcc REJECT (gcc warns).
117
117
 
118
- /** PHASE 4 — default every still-unknown value to s32. This is a COMPILER default (agbcc/IDO/GCC
119
- * all take plain `int` as the integer default), not a hardware fact — applied uniformly. */
120
- function defaultUnknownsToS32(fn: Fn): void {
118
+ /** PHASE 4 — default every still-unknown value to SIGNED, at its own width. Signedness is a
119
+ * COMPILER default (agbcc/IDO/GCC all take plain `int` as the integer default), not a hardware
120
+ * fact, and it is applied uniformly. The WIDTH is not a default at all: it is what the value was
121
+ * built at, and a 64-bit value retyped `s32` here would be silently truncated by the first pass
122
+ * that believed the type. `setInt` above already defaults this way. */
123
+ function defaultUnknowns(fn: Fn): void {
124
+ const settle = (v: { type: IrType }) => {
125
+ if (v.type.kind === 'unknown') {
126
+ v.type = T.int(v.type.width, true);
127
+ }
128
+ };
121
129
  for (const b of fn.blocks) {
122
130
  for (const p of b.params) {
123
- if (p.type.kind === 'unknown') {
124
- p.type = T.s(32);
125
- }
131
+ settle(p);
126
132
  }
127
133
  for (const op of b.ops) {
128
134
  for (const r of op.results) {
129
- if (r.type.kind === 'unknown') {
130
- r.type = T.s(32);
131
- }
135
+ settle(r);
132
136
  }
133
137
  }
134
138
  }
@@ -32,7 +32,7 @@
32
32
  // retired 2026-09-13).
33
33
  //
34
34
  // ONE MODEL, NOT TWO. The predicate is `narrowlocal.ts`'s exported `armIsOneSet` — no op that
35
- // `REEVAL_UNSAFE_OPS` calls unsafe, and EXACTLY one result-producing op — read here by
35
+ // `reevalUnsafe` calls unsafe, and EXACTLY one result-producing op — read here by
36
36
  // `arms-are-one-set`. It is cited there line by line to `jump.c:474/:480/:482/:483` and
37
37
  // `rtlanal.c:1770-1784`, with the three things an op count alone gets wrong, and a reach census over
38
38
  // 13733 blocks. It is SHARED rather than re-derived here because it is one optimizer guard and a
@@ -58,7 +58,8 @@ import {
58
58
  replaceAllUsesWith,
59
59
  successorsOf,
60
60
  } from '../ir/core';
61
- import { EFFECTFUL_OPS, HOIST_UNSAFE_OPS, NEGATED_ICMP, ORDER_SENSITIVE_OPS } from '../ir/opcodes';
61
+ import { effectful, forgetHelperPlacement, orderSensitive, placedAt, speculationUnsafe } from '../ir/discipline';
62
+ import { NEGATED_ICMP } from '../ir/opcodes';
62
63
  import { T } from '../ir/types';
63
64
  import { type Gate, firstRejection } from '../l3/gates';
64
65
 
@@ -226,8 +227,11 @@ export function recognizeShortCircuit(fn: Fn): boolean {
226
227
  // value ops (arith, loads, icmp) are safe: the structurer inlines them back into the `&&`/`||` RHS
227
228
  // expression, where C's own short-circuit re-guards them. Any side effect ⇒ DECLINE the fold — the
228
229
  // merge-variable spelling the fall-through leaves is correct (the side effect stays in B's block),
229
- // just possibly non-matching.
230
- if (bfeed.ops.slice(0, -1).some((op) => HOIST_UNSAFE_OPS.has(op.opcode))) {
230
+ // just possibly non-matching. A qualified read is an access, not a value, and stays in its
231
+ // arm the same way. structure/analysis.ts rests on this refusal: it names a counted op it
232
+ // finds in a connective's guarded operand at its def, above the branch, because no fold
233
+ // put one there.
234
+ if (bfeed.ops.slice(0, -1).some(speculationUnsafe)) {
231
235
  continue;
232
236
  }
233
237
  // NEGATABLE only when the orientation actually inverts the head — asked here rather than up
@@ -244,7 +248,9 @@ export function recognizeShortCircuit(fn: Fn): boolean {
244
248
  if (wantNeg && !negation) {
245
249
  continue;
246
250
  }
247
- bfeed.ops.slice(0, -1).forEach(before); // hoist B's pure body (defines Vb; harmless if a dead const)
251
+ // hoist B's pure body (defines Vb; harmless if a dead const). A helper op it moves no longer
252
+ // sits where the asm called it (ir/discipline.ts `forgetHelperPlacement`).
253
+ bfeed.ops.slice(0, -1).forEach((op) => before(forgetHelperPlacement(op)));
248
254
  foldWriteOrder(fn.writeOrder, bfeed, h); // …and its writes now follow H's (ir/core.ts)
249
255
  let condSide = cond;
250
256
  if (negation) {
@@ -577,10 +583,11 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
577
583
  continue;
578
584
  }
579
585
  // ^g's body must be pure — see the REFUSALS note.
580
- // HOIST_UNSAFE_OPS includes `opaque`: an instruction asmlift could not model, and moving it
586
+ // `speculationUnsafe` answers yes for `opaque`: an instruction asmlift could not model, and moving it
581
587
  // out of the arm that guards it is the reordering this refuses. Loud either way today — a
582
588
  // decline under `onGap: 'strict'`, an ASMLIFT_ERROR marker under `annotate`.
583
- if (g.ops.slice(0, -1).some((op) => HOIST_UNSAFE_OPS.has(op.opcode))) {
589
+ // A qualified read stays in its arm too, as in the value form above, and for the same reader.
590
+ if (g.ops.slice(0, -1).some(speculationUnsafe)) {
584
591
  continue;
585
592
  }
586
593
  // Which of ^g's edges rejoins ^h's other successor? That is the shared block. A DIRECT edge
@@ -648,7 +655,7 @@ export function recognizeBranchShortCircuit(fn: Fn, opts: BranchShortCircuitOpti
648
655
  }
649
656
  }
650
657
  rereadInArm(fn, g, otherEdge.block, reread);
651
- const body = g.ops.slice(0, -1);
658
+ const body = g.ops.slice(0, -1).map(forgetHelperPlacement);
652
659
  const second = negation ? negation.result : c2;
653
660
  const negated: Op[] = negation ? negation.ops : [];
654
661
  const res = mkValue(T.unk(32));
@@ -938,7 +945,7 @@ const readsOf = (op: Op): Value[] => [...op.operands, ...op.successors.flatMap((
938
945
  * gate in `opts.armReread` (default {@link ARM_REREAD_GATES}) refuses the site.
939
946
  *
940
947
  * The copy is exact at the arm's head because nothing runs between: `g`'s body is pure (its
941
- * caller refuses anything in HOIST_UNSAFE_OPS), so a load copied there reads what `g`'s own load
948
+ * caller refuses anything `speculationUnsafe`), so a load copied there reads what `g`'s own load
942
949
  * read, and `g` is the arm's ONLY predecessor, so the copy runs exactly when the original's
943
950
  * value would have been live there. On verified IR that also places every escaping read under
944
951
  * the arm: `g` dominates nothing else, because the shared block is reached from ^h directly.
@@ -1086,17 +1093,20 @@ function leavesALoop(fn: Fn, g: Block, arm: Block, preds: ReadonlyMap<Block, rea
1086
1093
  * - it renders nowhere single: some PURE value between it and a statement has two consumers, so
1087
1094
  * `emitPos` is null and analysis.ts materializes it whatever stands in the way
1088
1095
  * (`p[2] = p[1] & 0x80; p[3] = p[1] & 0x80;` — one `and`, two stores);
1089
- * - it has MORE THAN ONE direct reader, and an effect precedes one of their render positions —
1090
- * the multi-render rule, `isWrite = EFFECTFUL_OPS`, which exempts nothing;
1091
- * - it has ONE, and an effect precedes its render position that analysis.ts's `isBarrier`
1092
- * counts: any `astore`/`opaque`, a `call` that is not rendered inside that same statement,
1093
- * and a `store` UNLESS it is to a provably disjoint slot of the read's own base
1094
- * (`disjointConstSlots`, ir/alias.ts — the helper `isBarrier` calls).
1096
+ * - it has MORE THAN ONE direct reader, and an effect or a helper op analysis.ts names precedes
1097
+ * one of their render positions — the multi-render rule, which exempts nothing;
1098
+ * - it has ONE, and an op precedes its render position that analysis.ts's `isBarrier` counts:
1099
+ * any `astore`/`opaque`, a helper op analysis.ts names, a `call` that is not rendered inside
1100
+ * that same statement, and a `store` UNLESS it is to a provably disjoint slot of the read's own
1101
+ * base (`disjointConstSlots`, ir/alias.ts — the helper `isBarrier` calls).
1102
+ *
1103
+ * `isBarrier`'s pinned-read clause has nothing to mirror: ^g holds no pinned read, since the fold
1104
+ * refuses a ^g with anything `speculationUnsafe` in it.
1095
1105
  *
1096
1106
  * A reader in any block other than the arm answers yes — the copy then outlives the arm's first
1097
1107
  * block. What this does NOT mirror, and which way each gap errs, is ARM_REREAD_GATES' RESIDUE. */
1098
1108
  function readHeldAcrossEffect(c: ArmRereadSite): boolean {
1099
- const reads = [...c.copy].filter((op) => ORDER_SENSITIVE_OPS.has(op.opcode));
1109
+ const reads = [...c.copy].filter(orderSensitive);
1100
1110
  if (reads.length === 0) {
1101
1111
  return false;
1102
1112
  }
@@ -1127,8 +1137,17 @@ function readHeldAcrossEffect(c: ArmRereadSite): boolean {
1127
1137
  if (cs.length === 1) {
1128
1138
  return at(cs[0]);
1129
1139
  }
1130
- return ORDER_SENSITIVE_OPS.has(op.opcode) ? posOf(op) : null;
1140
+ return orderSensitive(op) ? posOf(op) : null;
1131
1141
  };
1142
+ // analysis.ts's `namedHelper`: a helper op it names where the asm ran it, which bars a read as a
1143
+ // named call does. It names one whose value renders in another block, or past an effect or
1144
+ // another named helper.
1145
+ const namedHelper = (x: Op): boolean =>
1146
+ placedAt(x) === 'helper' &&
1147
+ (escapes(x.results) ||
1148
+ consumersOf(x.results)
1149
+ .map(at)
1150
+ .some((p) => p === null || c.arm.ops.slice(posOf(x) + 1, p).some((y) => effectful(y) || namedHelper(y))));
1132
1151
  for (const l of reads) {
1133
1152
  if (escapes(l.results)) {
1134
1153
  return true;
@@ -1139,7 +1158,10 @@ function readHeldAcrossEffect(c: ArmRereadSite): boolean {
1139
1158
  }
1140
1159
  const multi = renders.length > 1;
1141
1160
  const bars = (x: Op, pos: number): boolean => {
1142
- if (!EFFECTFUL_OPS.has(x.opcode)) {
1161
+ if (namedHelper(x)) {
1162
+ return true;
1163
+ }
1164
+ if (!effectful(x)) {
1143
1165
  return false;
1144
1166
  }
1145
1167
  if (multi) {
@@ -1280,7 +1302,7 @@ export const ARM_REREAD_GATES: readonly Gate<ArmRereadSite>[] = [
1280
1302
  why: 'the target read it before the second test on both of its exits; moved, it runs on one',
1281
1303
  sound: false,
1282
1304
  guardedBy: 'branch-shortcircuit.test.ts: REFUSED: a READ only the arm consumes would move under the second test',
1283
- rejects: (c) => [...c.drop].some((op) => ORDER_SENSITIVE_OPS.has(op.opcode)),
1305
+ rejects: (c) => [...c.drop].some(orderSensitive),
1284
1306
  },
1285
1307
  {
1286
1308
  // THE BYTES HALF of the copy (see `armRereadCone`), and a claim about ONE compiler. The arm of
@@ -1323,7 +1345,7 @@ export const ARM_REREAD_GATES: readonly Gate<ArmRereadSite>[] = [
1323
1345
 
1324
1346
  /** Copy `copy` to the head of `arm`, point every read outside `g` at the copies, and delete `drop`.
1325
1347
  * A dropped original is deleted rather than left for the hoist: the arm now performs it, and a
1326
- * dead READ left in ^h is one the structurer may still spell (SPELLED_WHEN_DEAD_OPS). */
1348
+ * dead READ left in ^h is one the structurer may still spell (ir/discipline.ts `spelledWhenDead`). */
1327
1349
  function rereadInArm(fn: Fn, g: Block, arm: Block, { copy, drop }: ArmReread): void {
1328
1350
  const copyOf = new Map<Value, Value>();
1329
1351
  const copies = g.ops
@@ -15,27 +15,27 @@
15
15
  // cannot state: its match keys on a `call`'s STRING `target` attr, which the numeric `attrEquals`
16
16
  // cannot express. It is naturally inert on hardware-divide targets (which emit `div`/`divu`,
17
17
  // never `bl __divsi3`).
18
- import { Fn, mkOp } from '../ir/core';
19
- import type { Opcode } from '../ir/opcodes';
20
- import type { Prototypes } from '../proto';
18
+ import type { Fn } from '../ir/core';
19
+ import { wordsOf } from '../proto';
20
+ import { type RuntimeHelper, helperOp, isFloatHelper, isWideHelper } from '../runtime-helpers';
21
+ import type { TargetDescription } from '../target';
21
22
 
22
- // runtime helper symbol → { the division op it computes, its argument count }.
23
- const SOFT_DIV: Record<string, { op: Opcode; params: number }> = {
24
- __divsi3: { op: 'sdiv', params: 2 },
25
- __udivsi3: { op: 'udiv', params: 2 },
26
- __modsi3: { op: 'smod', params: 2 },
27
- __umodsi3: { op: 'umod', params: 2 },
28
- };
29
-
30
- /** Signatures for the soft-division runtime helpers, so a `bl __divsi3` recovers both arguments.
31
- * Consumed by the frontend's arity lookup BEHIND any caller-supplied prototype (headers win). */
32
- export const RUNTIME_HELPERS: Prototypes = Object.fromEntries(
33
- Object.entries(SOFT_DIV).map(([sym, h]) => [sym, { params: h.params }]),
34
- );
23
+ /** The 32-bit software divisions, off the target's own helper table. WHICH helpers a compiler emits
24
+ * is a compiler fact and lives there (runtime-helpers.ts); which of them THIS pass answers for is
25
+ * the question here, and it is the narrow one: a division the ISA has no instruction for. A helper
26
+ * that computes on a value wider than a register is a different question — no hardware capability
27
+ * can make one unnecessary — and `raise/widehelpers.ts` answers it, ungated. So is a float helper
28
+ * of any width, which computes on floats the ISA has no instruction for either, and
29
+ * `raise/floathelpers.ts` answers it. */
30
+ const softDivisions = (target: TargetDescription): Record<string, RuntimeHelper> =>
31
+ Object.fromEntries(
32
+ Object.entries(target.runtimeHelpers ?? {}).filter(([, h]) => h.op && !isWideHelper(h) && !isFloatHelper(h)),
33
+ );
35
34
 
36
35
  /** Rewrite each recognised soft-division helper call to its division op, in place. Returns whether
37
36
  * anything changed. Runs BEFORE type recovery so the new op's operands get signed/unsigned typing. */
38
- export function recognizeSoftDiv(fn: Fn): boolean {
37
+ export function recognizeSoftDiv(fn: Fn, target: TargetDescription): boolean {
38
+ const table = softDivisions(target);
39
39
  let changed = false;
40
40
  for (const b of fn.blocks) {
41
41
  for (let i = 0; i < b.ops.length; i++) {
@@ -43,18 +43,16 @@ export function recognizeSoftDiv(fn: Fn): boolean {
43
43
  if (op.opcode !== 'call') {
44
44
  continue;
45
45
  }
46
- const helper = SOFT_DIV[op.attrs.target as string];
47
- if (!helper) {
46
+ const helper = table[op.attrs.target as string];
47
+ if (!helper?.op) {
48
48
  continue;
49
49
  }
50
50
  // Fold only when BOTH arguments were recovered (the signature makes this the norm). A
51
51
  // mis-recovered arity leaves the call untouched rather than fabricating a wrong divide.
52
- if (op.operands.length !== helper.params || op.results.length !== 1) {
52
+ if (op.operands.length !== wordsOf(helper.params) || op.results.length !== 1) {
53
53
  continue;
54
54
  }
55
- // Reuse the SAME result Value → every existing use already points at it (no RAUW needed).
56
- const div = mkOp(helper.op, { operands: [...op.operands], results: [op.results[0]] });
57
- b.ops.splice(i, 1, div);
55
+ b.ops.splice(i, 1, helperOp(helper.op, op.attrs.target as string, op.operands, op.results[0]));
58
56
  changed = true;
59
57
  }
60
58
  }
@@ -18,6 +18,7 @@
18
18
  // UNAMBIGUOUS here (the scaled operand is the index, read from the machine code) — the unscaled
19
19
  // `add(x, y)` byte form stays out of scope (genuinely ambiguous without types).
20
20
  import { Fn, Op, Value, defOpMap, mkOp } from '../ir/core';
21
+ import { carryDiscipline } from '../ir/discipline';
21
22
  import { nextStructIndex } from '../ir/struct-names';
22
23
  import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
23
24
  import { collectStructs } from './structs';
@@ -102,7 +103,7 @@ export function recognizeStructArrays(fn: Fn): number {
102
103
  // time, because this pass runs once per function and nothing else mints the prefix, and
103
104
  // ir/struct-names.ts states why the computed 0 is kept over the assumed one.
104
105
  let name = nextStructIndex(
105
- [...collectStructs(fn)].map((s) => s.name),
106
+ [...collectStructs(fn, true)].map((s) => s.name),
106
107
  'Elem',
107
108
  );
108
109
 
@@ -254,17 +255,17 @@ export function recognizeStructArrays(fn: Fn): number {
254
255
  operands: [base, index],
255
256
  results: [op.results[0]],
256
257
  // listOrder rides along: an ldmia-expanded load keeps its stream-order caveat as an aload
257
- attrs: {
258
+ attrs: carryDiscipline(op, {
258
259
  elemSize: stride,
259
260
  signed: op.attrs.signed as boolean,
260
261
  fieldOff: op.attrs.off as number,
261
262
  ...(op.attrs.listOrder === true && { listOrder: true }),
262
- },
263
+ }),
263
264
  });
264
265
  } else if (op.opcode === 'store') {
265
266
  bb.ops[i] = mkOp('astore', {
266
267
  operands: [base, index, op.operands[1]],
267
- attrs: { elemSize: stride, fieldOff: op.attrs.off as number },
268
+ attrs: carryDiscipline(op, { elemSize: stride, fieldOff: op.attrs.off as number }),
268
269
  });
269
270
  }
270
271
  }