@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
@@ -20,6 +20,7 @@ import { demangledName, implicitArgumentWords } from './mangle';
20
20
  import type { AggregateLayout, AggregateMember, FnProto, ParamType, Prototypes } from './proto';
21
21
  import {
22
22
  declaredCallArgs,
23
+ declaredSigned,
23
24
  declaredWidth,
24
25
  declaresAggregateReturn,
25
26
  declaresVoidReturn,
@@ -115,8 +116,39 @@ export interface ContextGlobals {
115
116
  * it. Read through `structNamed`, which is how two spellings of one struct are told to be one:
116
117
  * `typedef struct Q {…} Q;` spells the struct `Q`, `struct Q` and every other alias of it. */
117
118
  structTypes: ReadonlyMap<string, string>;
119
+ /** The storage each object of `declared` that is no aggregate and no function is declared with,
120
+ * its typedefs resolved (`declaredWidth`, `declaredSigned`): `extern s32 gN;` is a four-byte
121
+ * signed integer, `extern u8 *gP;` a four-byte one whose sign nothing reads, `extern float gF;`
122
+ * a four-byte float. A bare name reads and writes exactly this, so the structurer spells a
123
+ * global bare only where its one access agrees (structure.ts, the declaration-shape override): a
124
+ * byte read of `gN` is `*(u8 *)&gN`, never `gN`, and so is a halfword read of `gF`, which
125
+ * reinterprets the bytes where the bare name would convert them. A type this cannot read — a
126
+ * typedef carrying an attribute, an unexpanded macro, a name it never saw — is `unread`, which
127
+ * agrees with no access: the cast states the access whatever the declaration is. A pointer
128
+ * behind an attributed typedef is still read as one (`writtenDerivation`). A C++ reference is
129
+ * a `reference`, whose cell no spelling names, so the structurer declines a function that
130
+ * touches it. */
131
+ storage: ReadonlyMap<string, DeclaredStorage>;
118
132
  }
119
133
 
134
+ /** What one declared object that is no aggregate occupies ({@link ContextGlobals.storage}). */
135
+ export type DeclaredStorage =
136
+ /** an integer or a pointer of `bytes` bytes; `signed` says whether a read sign-extends it, `char`
137
+ * for a plain `char`, whose sign is the compiler's (`compilerBehaviors.plainCharSigned`), and
138
+ * undefined where nothing reads it (a pointer). A declaration with no type specifier at all
139
+ * (`extern gN;`, C89's implicit `int`) is an `int`. */
140
+ | { kind: 'integer'; bytes: number; signed: boolean | 'char' | undefined }
141
+ /** an enum, whose size is the compiler's (`compilerBehaviors.enumBytes`), unless `unsized`: one
142
+ * `packed` or holding a value an int cannot (`TypeTable.unsizedEnums`) is no size this knows */
143
+ | { kind: 'enum'; unsized: boolean }
144
+ /** a `float` (4 bytes) or a `double` (8) */
145
+ | { kind: 'float'; bytes: number }
146
+ /** a C++ reference (`extern int &gRef;`): the symbol is a cell holding the referent's address,
147
+ * which no C++ spelling names — `gRef` and `&gRef` both name the referent */
148
+ | { kind: 'reference' }
149
+ /** a type this cannot read */
150
+ | { kind: 'unread' };
151
+
120
152
  /** The struct or union `spelling` names, qualifiers aside, by the identity `structTypes` keys it
121
153
  * under: `struct Tag` as written, or what a typedef name stands for. Undefined for any other
122
154
  * spelling, and for a name the context does not resolve to one — which then names no struct this
@@ -142,10 +174,20 @@ export function globalsFromContext(src: string, language: Language): ContextGlob
142
174
  const pointeeVolatiles = new Set<string>();
143
175
  const declared = new Set<string>();
144
176
  const objectTypes = new Map<string, string>();
177
+ const storage = new Map<string, DeclaredStorage>();
145
178
  for (const d of ctx.declarations) {
146
179
  if (d.specifiers.typedef || d.body !== undefined) {
147
180
  continue;
148
181
  }
182
+ const implicit = language === 'c' ? implicitIntObject(d, table) : undefined;
183
+ if (implicit !== undefined) {
184
+ declared.add(implicit);
185
+ objectTypes.set(implicit, 'int');
186
+ storage.set(implicit, { kind: 'integer', bytes: 4, signed: true });
187
+ if (d.specifiers.qualifiers.includes('volatile')) {
188
+ volatiles.add(implicit);
189
+ }
190
+ }
149
191
  for (const x of d.declarators) {
150
192
  if (x.name === undefined || !WORD.test(x.name)) {
151
193
  continue;
@@ -159,6 +201,8 @@ export function globalsFromContext(src: string, language: Language): ContextGlob
159
201
  }
160
202
  if (isAggregate(t, table, scope.typedefs)) {
161
203
  aggregates.add(x.name);
204
+ } else if (t.derivations[0]?.kind !== 'function') {
205
+ storage.set(x.name, declaredStorage(t, table, scope.typedefs, ctx.tokens));
162
206
  }
163
207
  if (hasVolatileStorage(t, scope, 0)) {
164
208
  volatiles.add(x.name);
@@ -179,7 +223,97 @@ export function globalsFromContext(src: string, language: Language): ContextGlob
179
223
  structTypes.set(name, named);
180
224
  }
181
225
  }
182
- return { aggregates, volatiles, pointeeVolatiles, declared, objectTypes, structTypes };
226
+ return { aggregates, volatiles, pointeeVolatiles, declared, objectTypes, structTypes, storage };
227
+ }
228
+
229
+ /** The object a C declaration with a storage class and no type specifier declares, C89's implicit
230
+ * `int`: `extern gN;` reads as the type name `gN` with no declarator, since the parser cannot tell
231
+ * a name from an unknown type with nothing after it. Only a lone word that names no type the
232
+ * context declares and no base type is one. */
233
+ function implicitIntObject(d: Declaration, table: TypeTable): string | undefined {
234
+ const { storage, type, unknownWords } = d.specifiers;
235
+ if (d.declarators.length > 0 || unknownWords.length > 0 || type.kind !== 'words' || type.words.length !== 1) {
236
+ return undefined;
237
+ }
238
+ const [w] = type.words;
239
+ const named =
240
+ !WORD.test(w) ||
241
+ table.typedefs.has(w) ||
242
+ table.enums.has(w) ||
243
+ table.named.has(w) ||
244
+ declaredWidth(w) !== undefined ||
245
+ ['void', 'float', 'double', '_Bool'].includes(w);
246
+ return !named && (storage.includes('extern') || storage.includes('static')) ? w : undefined;
247
+ }
248
+
249
+ /** The storage an object of type `t`, no aggregate and no function, is declared with
250
+ * ({@link DeclaredStorage}). `written` is every typedef as written, an attributed one included
251
+ * (`typedefTypes`), which `writtenDerivation` reads past. */
252
+ function declaredStorage(
253
+ t: DeclaredType,
254
+ table: TypeTable,
255
+ written: ReadonlyMap<string, DeclaredType>,
256
+ tokens: Tokens,
257
+ ): DeclaredStorage {
258
+ const [first] = t.derivations;
259
+ if (first?.kind === 'reference') {
260
+ return { kind: 'reference' };
261
+ }
262
+ if (first !== undefined && !isPointer(first)) {
263
+ return { kind: 'unread' };
264
+ }
265
+ const r = resolve(t, table.typedefs, tokens);
266
+ // the derivation the type reaches, through a typedef the table does not read too
267
+ const behind = r.derivations[0] ?? writtenDerivation(r, written);
268
+ if (behind?.kind === 'reference') {
269
+ return { kind: 'reference' };
270
+ }
271
+ const spelled = spellType(r, tokens);
272
+ const bare = spelled
273
+ .replace(/\b(?:const|volatile)\b/g, ' ')
274
+ .trim()
275
+ .replace(/\s+/g, ' ');
276
+ const bits = declaredWidth(bare === '' ? 'int' : spelled);
277
+ if (bits !== undefined) {
278
+ const signed = bare === 'char' ? 'char' : declaredSigned(bare === '' ? 'int' : spelled);
279
+ return { kind: 'integer', bytes: bits / 8, signed };
280
+ }
281
+ if (r.derivations.length > 0) {
282
+ return { kind: 'unread' };
283
+ }
284
+ if (bare === 'float' || bare === 'double') {
285
+ return { kind: 'float', bytes: bare === 'float' ? 4 : 8 };
286
+ }
287
+ if (table.enums.has(bare) || (r.type.kind === 'tag' && r.type.keyword === 'enum')) {
288
+ return { kind: 'enum', unsized: table.unsizedEnums.has(bare) };
289
+ }
290
+ // A pointer behind a typedef that carries an attribute (`typedef u8 *P8
291
+ // __attribute__((aligned(4)));`), which the table leaves unread because the attribute may retype
292
+ // it (`realigns`). No attribute changes a pointer's width on these 32-bit targets — `aligned` and
293
+ // `__declspec(align)` move its alignment alone, and `mode` takes no pointer mode but the 4-byte
294
+ // one — so it is a 4-byte pointer, read and written bare as a word. Read `unread`, that word is
295
+ // cast to an integer, which mwcc refuses wherever it flows into a pointer (`gQ = *(s32 *)&gP;`).
296
+ return behind !== undefined && isPointer(behind)
297
+ ? { kind: 'integer', bytes: 4, signed: undefined }
298
+ : { kind: 'unread' };
299
+ }
300
+
301
+ /** The first derivation a type with none of its own reaches through the typedefs as written
302
+ * (`typedefTypes`), an attributed one and a reference the type table does not read included:
303
+ * `P8` over `typedef u8 *P8 __attribute__((aligned(4)));` is a pointer, `IR` over
304
+ * `typedef int &IR;` a reference. Undefined where the chain ends at a base type or a name it
305
+ * never saw. */
306
+ function writtenDerivation(t: DeclaredType, written: ReadonlyMap<string, DeclaredType>): Derivation | undefined {
307
+ let cur = t;
308
+ for (let hops = 0; hops < 16 && cur.derivations.length === 0; hops++) {
309
+ const name = nameOf(cur);
310
+ const next = name === undefined ? undefined : written.get(name);
311
+ if (next === undefined || nameOf(next) === name) {
312
+ return undefined;
313
+ }
314
+ cur = next;
315
+ }
316
+ return cur.derivations[0];
183
317
  }
184
318
 
185
319
  /** The struct or union the typedef `name` stands for (`ContextGlobals.structTypes`): its tag where
@@ -72,6 +72,31 @@ export function foldConstPair(opcode: string, a: number, b: number): number | nu
72
72
  return f ? f(a, b) : null;
73
73
  }
74
74
 
75
+ /** Integer opcodes whose const/const form a test can be read on, as int32: `foldConstPair`'s `add`
76
+ * and `or`, the rest of the arithmetic and the comparisons. `null` for any other opcode. Shared by
77
+ * the reroll's folded top test (`restoreTopTest`) and the `for` whose first test the compiler
78
+ * folded away (structure.ts, `forPlanAt`): which opcodes a test folds through is one decision. */
79
+ const EVAL: Readonly<Record<string, (a: number, b: number) => number>> = {
80
+ add: (a, b) => foldConstPair('add', a, b)!,
81
+ or: (a, b) => foldConstPair('or', a, b)!,
82
+ sub: (a, b) => (a - b) | 0,
83
+ and: (a, b) => a & b,
84
+ xor: (a, b) => a ^ b,
85
+ icmp_eq: (a, b) => +(a === b),
86
+ icmp_ne: (a, b) => +(a !== b),
87
+ icmp_slt: (a, b) => +(a < b),
88
+ icmp_sle: (a, b) => +(a <= b),
89
+ icmp_sgt: (a, b) => +(a > b),
90
+ icmp_sge: (a, b) => +(a >= b),
91
+ icmp_ult: (a, b) => +(a >>> 0 < b >>> 0),
92
+ icmp_ule: (a, b) => +(a >>> 0 <= b >>> 0),
93
+ icmp_ugt: (a, b) => +(a >>> 0 > b >>> 0),
94
+ icmp_uge: (a, b) => +(a >>> 0 >= b >>> 0),
95
+ };
96
+ export const evalIntPair = (opcode: string, a: number, b: number): number | null =>
97
+ opcode in EVAL ? EVAL[opcode](a, b) : null;
98
+ export const isIntPairOpcode = (opcode: string): boolean => opcode in EVAL;
99
+
75
100
  /** Does this opcode's const/const form denote a constant? The membership half of `foldConstPair`,
76
101
  * for callers that must classify an op before they have its operands' values. */
77
102
  export const isConstFoldOpcode = (opcode: string): boolean => opcode in FOLD;
@@ -84,7 +84,7 @@ function addressThroughJoins(fn: Fn): (v: Value) => Value {
84
84
  }
85
85
 
86
86
  /** Every block parameter's incoming values, one per in-edge (the entry block's excepted). */
87
- function joinIncoming(fn: Fn): Map<Value, Value[]> {
87
+ export function joinIncoming(fn: Fn): Map<Value, Value[]> {
88
88
  const incoming = new Map<Value, Value[]>();
89
89
  for (const b of fn.blocks) {
90
90
  for (const op of b.ops) {
@@ -40,6 +40,11 @@
40
40
  // `2^k - 1` for the SAME k the shift uses, the guard must test the SAME value the arms divide, and
41
41
  // the biased arm must be the negative one — the consistency check m2c's `49b5d87` also adds),
42
42
  // because every one of those is a way for a superficially similar diamond to mean something else.
43
+ //
44
+ // ir/bits.ts `integerRange` (its `remainderPair`) reads the same SUNK diamond under a remainder,
45
+ // `x - (phi >> k << k)`, as one bounded term. It cannot wait for this pass's `sdiv`: the frontends'
46
+ // frame audit asks those ranges before runPreRecovery runs; and as a bound, not a rewrite, it holds
47
+ // for any bias in [0, 2^k) on any edge, so it does not take this pass's gates.
43
48
  import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, predecessors, replaceAllUsesWith } from '../ir/core';
44
49
  import { effectful } from '../ir/discipline';
45
50
  import { T } from '../ir/types';
@@ -225,3 +230,190 @@ export function recognizeDivPow2(fn: Fn): boolean {
225
230
  }
226
231
  return changed;
227
232
  }
233
+
234
+ // ── the signed REMAINDER by a power of two, in IDO's branching form ─────────────────────────
235
+ //
236
+ // `x % 2^k` truncates toward zero, so a negative x with low bits set takes `2^k` off its masked
237
+ // value. IDO masks first and corrects on a two-test branch (modpow2, `a % 16`):
238
+ //
239
+ // bgez a0, .L ; x >= 0: the mask is the remainder
240
+ // andi v0, a0, 15 ; (delay slot: runs on both paths)
241
+ // beqz v0, .L ; low bits clear: so is it
242
+ // nop
243
+ // addiu v0, v0, -16 ; else take 2^k off
244
+ // .L:
245
+ //
246
+ // Two heads and a correction arm reach one merge, so — like the quotient above — it is a shape in
247
+ // the control flow, out of the idiom patterns' reach. Lifted as written it is spelled
248
+ // `if (x < 0 && (x & 15) != 0) v = (x & 15) + -16; else v = x & 15;`, which IDO compiles to other
249
+ // code; `x % 16` compiles back to this diamond. The identity is exact for every signed x — the
250
+ // diamond IS C's `%` — and the round trip is self-verifying for the reason the quotient's is.
251
+ //
252
+ // IDENTITY-OR-DECLINE again: the mask must be exactly `2^k - 1` of the dividend the first test
253
+ // compares against zero, the second test must compare THAT mask against zero, the correction must
254
+ // be exactly `mask - 2^k` (`correctedMask`), and the corrected value must reach the merge only from the arm
255
+ // both tests failed into. The two inner blocks are deleted, so neither may do anything else.
256
+ // agbcc, gcc and kmc COMPILE `%` as a biased floor (`x - (t >> k << k)`), which ir/bits.ts
257
+ // `remainderPair` bounds and this does not touch; mwcc's is branch-free.
258
+ //
259
+ // THE PASS IS NOT GATED BY TARGET: a HAND-WRITTEN diamond of this shape on any compiler is folded
260
+ // to `%` too (agbcc compiles `r = i & 15; if (i < 0 && r != 0) r -= 16;` to the same two-test
261
+ // diamond, which lifts `i % 16`). The identity holds whatever wrote it, so the C is the same value;
262
+ // whether `%` recompiles to the target's bytes there is the bench's question, not this pass's.
263
+
264
+ /** `and x, const(2^k - 1)` → { x, k }, else null. The and is commutative. */
265
+ function maskOf(defs: Map<Value, Op>, v: Value): { x: Value; k: number } | null {
266
+ const d = defs.get(v);
267
+ if (!d || d.opcode !== 'and' || d.operands.length !== 2) {
268
+ return null;
269
+ }
270
+ for (const [a, b] of [
271
+ [0, 1],
272
+ [1, 0],
273
+ ] as const) {
274
+ const c = defs.get(d.operands[b]);
275
+ const m = c?.opcode === 'const' ? (c.attrs.value as number) : undefined;
276
+ if (m !== undefined && m > 0 && (m & (m + 1)) === 0) {
277
+ return { x: d.operands[a], k: Math.log2(m + 1) };
278
+ }
279
+ }
280
+ return null;
281
+ }
282
+
283
+ /** The correction arm's value, `mask - 2^k` for the mask's own k, in either spelling the IR carries
284
+ * it: an `add` of the constant `-2^k` (IDO's `addiu v0, v0, -2^k`, where it fits an immediate:
285
+ * k < 16), or a `sub` of the constant `2^k` (k >= 16, where IDO materialises `2^k` as the mask's
286
+ * register plus one in the second test's delay slot, `addiu at, at, 1; subu v0, v0, at`, which
287
+ * the constant fold has made `2^k` by now). The identity is the subtraction, not its encoding. */
288
+ function correctedMask(defs: Map<Value, Op>, v: Value): { t: Value; mk: { x: Value; k: number } } | null {
289
+ const u = defs.get(v);
290
+ if ((u?.opcode !== 'add' && u?.opcode !== 'sub') || u.operands.length !== 2) {
291
+ return null;
292
+ }
293
+ // a sub's minuend is its first operand; an add's mask may stand on either side
294
+ const orders =
295
+ u.opcode === 'sub'
296
+ ? ([[0, 1]] as const)
297
+ : ([
298
+ [0, 1],
299
+ [1, 0],
300
+ ] as const);
301
+ for (const [a, b] of orders) {
302
+ const c = defs.get(u.operands[b]);
303
+ const mk = maskOf(defs, u.operands[a]);
304
+ const want = u.opcode === 'sub' ? 2 ** (mk?.k ?? 0) : -(2 ** (mk?.k ?? 0));
305
+ if (c?.opcode === 'const' && mk !== null && c.attrs.value === want) {
306
+ return { t: u.operands[a], mk };
307
+ }
308
+ }
309
+ return null;
310
+ }
311
+
312
+ /** `icmp <op> v, const 0` → the compare's opcode, else null. */
313
+ function zeroTest(defs: Map<Value, Op>, c: Value, v: Value): string | null {
314
+ const d = defs.get(c);
315
+ if (!d || d.operands.length !== 2 || d.operands[0] !== v) {
316
+ return null;
317
+ }
318
+ const z = defs.get(d.operands[1]);
319
+ return z?.opcode === 'const' && z.attrs.value === 0 ? d.opcode : null;
320
+ }
321
+
322
+ /**
323
+ * Fold IDO's branching signed-remainder-by-2^k diamond into `smod x, 2^k`. Returns true if the IR
324
+ * changed. Leaves the mask, the compares and the correction behind for the driver's DCE.
325
+ */
326
+ export function recognizeSremPow2(fn: Fn): boolean {
327
+ let changed = false;
328
+ let progress = true;
329
+ while (progress) {
330
+ progress = false;
331
+ const defs = defOpMap(fn);
332
+ const preds = predecessors(fn);
333
+ const term = (b: Block) => b.ops[b.ops.length - 1];
334
+ // a result of `blocks`' ops read anywhere outside them, which deleting them would orphan
335
+ const escapes = (blocks: Block[]): boolean => {
336
+ const inside = new Set(blocks.flatMap((b) => b.ops.flatMap((op) => op.results)));
337
+ return fn.blocks
338
+ .filter((b) => !blocks.includes(b))
339
+ .some((b) =>
340
+ b.ops.some(
341
+ (op) =>
342
+ op.operands.some((v) => inside.has(v)) || op.successors.some((s) => s.args.some((v) => inside.has(v))),
343
+ ),
344
+ );
345
+ };
346
+ outer: for (const m of fn.blocks) {
347
+ // the merge holds the remainder alone and is reached by exactly the diamond's three edges
348
+ // (the entry block has an invisible edge; see recognizeDivPow2)
349
+ const mp = preds.get(m) ?? [];
350
+ if (m.params.length !== 1 || mp.length !== 3 || new Set(mp).size !== 3 || m === fn.blocks[0]) {
351
+ continue;
352
+ }
353
+ for (const fix of mp) {
354
+ // the CORRECTION arm: `mask - 2^k` to the merge, from the second test alone, nothing else
355
+ const ft = term(fix);
356
+ if (ft.opcode !== 'br' || ft.successors[0]?.block !== m || fix === fn.blocks[0] || fix.ops.some(effectful)) {
357
+ continue;
358
+ }
359
+ const n = preds.get(fix) ?? [];
360
+ if (n.length !== 1 || !mp.includes(n[0]) || n[0] === fn.blocks[0]) {
361
+ continue;
362
+ }
363
+ const inner = n[0];
364
+ const it = term(inner);
365
+ const hp = preds.get(inner) ?? [];
366
+ if (it.opcode !== 'cond_br' || hp.length !== 1 || !mp.includes(hp[0]) || inner.ops.some(effectful)) {
367
+ continue;
368
+ }
369
+ const head = hp[0];
370
+ const ht = term(head);
371
+ if (ht.opcode !== 'cond_br' || head === m || head === fix) {
372
+ continue;
373
+ }
374
+ // the corrected value: the mask less `2^k`
375
+ const corrected = correctedMask(defs, ft.successors[0].args[0]);
376
+ if (corrected === null || corrected.mk.k < 1 || corrected.mk.k > 30) {
377
+ continue;
378
+ }
379
+ const { t, mk } = corrected;
380
+ // the first test: x >= 0 to the merge with the mask, else on to the second
381
+ const [hTaken, hFall] = ht.successors;
382
+ const hFirst = zeroTest(defs, ht.operands[0], mk.x);
383
+ const hm = hFirst === 'icmp_sge' ? hTaken : hFirst === 'icmp_slt' ? hFall : null;
384
+ const hn = hFirst === 'icmp_sge' ? hFall : hFirst === 'icmp_slt' ? hTaken : null;
385
+ if (hm?.block !== m || hn?.block !== inner || hm.args[0] !== t || hn.args.length !== 0) {
386
+ continue;
387
+ }
388
+ // the second test: the mask == 0 to the merge with the mask, else to the correction
389
+ const [iTaken, iFall] = it.successors;
390
+ const iSecond = zeroTest(defs, it.operands[0], t);
391
+ const im = iSecond === 'icmp_eq' ? iTaken : iSecond === 'icmp_ne' ? iFall : null;
392
+ const ifx = iSecond === 'icmp_eq' ? iFall : iSecond === 'icmp_ne' ? iTaken : null;
393
+ if (im?.block !== m || ifx?.block !== fix || im.args[0] !== t || ifx.args.length !== 0) {
394
+ continue;
395
+ }
396
+ if (escapes([inner, fix])) {
397
+ continue;
398
+ }
399
+ // Rewrite: the head computes the remainder and goes straight to the merge.
400
+ const by = mkValue(T.s());
401
+ const r = mkValue(T.s());
402
+ head.ops.splice(
403
+ head.ops.length - 1,
404
+ 0,
405
+ mkOp('const', { results: [by], attrs: { value: 2 ** mk.k } }),
406
+ mkOp('smod', { operands: [mk.x, by], results: [r] }),
407
+ );
408
+ head.ops[head.ops.length - 1] = mkOp('br', { successors: [{ block: m, args: [] }] });
409
+ replaceAllUsesWith(fn, m.params[0], r);
410
+ m.params = [];
411
+ fn.blocks = fn.blocks.filter((b) => b !== inner && b !== fix);
412
+ changed = true;
413
+ progress = true;
414
+ break outer;
415
+ }
416
+ }
417
+ }
418
+ return changed;
419
+ }