@asmlift/core 0.9.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 (117) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +2 -0
  5. package/src/backend/cfamily.ts +197 -61
  6. package/src/backend/cpp.ts +3 -1
  7. package/src/backend/lines.ts +10 -0
  8. package/src/backend/pascal.ts +47 -16
  9. package/src/cdecl/parse.ts +21 -0
  10. package/src/contracts.ts +246 -36
  11. package/src/declare.ts +69 -20
  12. package/src/frontend/arm-body.ts +155 -0
  13. package/src/frontend/asmdata.ts +9 -2
  14. package/src/frontend/call-plan.ts +1117 -61
  15. package/src/frontend/callee-body.ts +370 -0
  16. package/src/frontend/dead-reads.ts +628 -0
  17. package/src/frontend/device-pins.ts +33 -4
  18. package/src/frontend/emit.ts +4 -3
  19. package/src/frontend/fpu.ts +224 -17
  20. package/src/frontend/frame-objects.ts +1689 -206
  21. package/src/frontend/frontend.ts +84 -3
  22. package/src/frontend/high-half.ts +53 -4
  23. package/src/frontend/jump-table.ts +545 -0
  24. package/src/frontend/local-object.ts +158 -32
  25. package/src/frontend/mips.ts +2296 -217
  26. package/src/frontend/own-struct-return.ts +500 -0
  27. package/src/frontend/ppc.ts +2748 -245
  28. package/src/frontend/register-struct-call.ts +399 -0
  29. package/src/frontend/reloc-symbol.ts +22 -7
  30. package/src/frontend/ssa.ts +1721 -43
  31. package/src/frontend/stackargs.ts +199 -72
  32. package/src/frontend/thumb-entry.ts +340 -0
  33. package/src/frontend/thumb.ts +2400 -325
  34. package/src/ir/alias.ts +65 -2
  35. package/src/ir/bits.ts +1699 -4
  36. package/src/ir/core.ts +254 -9
  37. package/src/ir/discipline.ts +86 -21
  38. package/src/ir/entry.ts +157 -0
  39. package/src/{structure → ir}/loops.ts +6 -2
  40. package/src/ir/opcodes.ts +137 -9
  41. package/src/ir/parse.ts +1 -0
  42. package/src/ir/simplify.ts +61 -10
  43. package/src/ir/types.ts +86 -2
  44. package/src/ir/verify.ts +96 -11
  45. package/src/l3/argbase.ts +2 -0
  46. package/src/l3/ast.ts +582 -18
  47. package/src/l3/basecse.ts +68 -15
  48. package/src/l3/caseendgoto.ts +223 -0
  49. package/src/l3/coalesce.ts +42 -14
  50. package/src/l3/constinit.ts +410 -0
  51. package/src/l3/dce.ts +53 -9
  52. package/src/l3/initfirst.ts +42 -11
  53. package/src/l3/intcell.ts +2 -1
  54. package/src/l3/narrowdecl.ts +77 -36
  55. package/src/l3/pollguard.ts +8 -2
  56. package/src/l3/ptrcell.ts +33 -34
  57. package/src/l3/ptrfield.ts +123 -8
  58. package/src/l3/regspell.ts +11 -4
  59. package/src/l3/reindex.ts +295 -99
  60. package/src/l3/scopebase.ts +2 -0
  61. package/src/l3/storage.ts +98 -1
  62. package/src/l3/symbol-refs.ts +84 -6
  63. package/src/l3/tailmerge.ts +6 -2
  64. package/src/l3/typing.ts +108 -15
  65. package/src/l3/unreduce.ts +6 -5
  66. package/src/l3/volatileptr.ts +17 -54
  67. package/src/mangle.ts +30 -0
  68. package/src/pattern/engine.ts +361 -73
  69. package/src/pipeline.ts +1039 -38
  70. package/src/proto-context.ts +1039 -45
  71. package/src/proto.ts +555 -40
  72. package/src/raise/aggmembers.ts +290 -0
  73. package/src/raise/arrays.ts +5 -1
  74. package/src/raise/const.ts +25 -0
  75. package/src/raise/declared-volatile.ts +207 -31
  76. package/src/raise/divpow2.ts +192 -0
  77. package/src/raise/floathelpers.ts +254 -33
  78. package/src/raise/gvn.ts +3 -3
  79. package/src/raise/latch.ts +162 -6
  80. package/src/raise/memberarrays.ts +10 -4
  81. package/src/raise/offsetnames.ts +422 -36
  82. package/src/raise/pairparams.ts +33 -0
  83. package/src/raise/paramwidth.ts +196 -6
  84. package/src/raise/pre-recovery.ts +107 -21
  85. package/src/raise/pre-structure.ts +73 -0
  86. package/src/raise/recover.ts +12 -5
  87. package/src/raise/reroll.ts +674 -0
  88. package/src/raise/shortcircuit.ts +217 -34
  89. package/src/raise/softdiv.ts +1 -1
  90. package/src/raise/storesign.ts +153 -0
  91. package/src/raise/struct-arrays.ts +30 -11
  92. package/src/raise/structs.ts +396 -33
  93. package/src/raise/truncload.ts +4 -3
  94. package/src/raise/unpre.ts +594 -0
  95. package/src/raise/unthread.ts +400 -0
  96. package/src/rank-declare.ts +118 -12
  97. package/src/rank-variations.ts +186 -35
  98. package/src/rank.ts +536 -68
  99. package/src/runtime-helpers.ts +140 -22
  100. package/src/structure/analysis.ts +217 -8
  101. package/src/structure/arith-ops.ts +23 -0
  102. package/src/structure/cross-jumped-tail.ts +216 -0
  103. package/src/structure/globalaccess.ts +157 -10
  104. package/src/structure/hazards.ts +16 -64
  105. package/src/structure/local-statics.ts +145 -9
  106. package/src/structure/namecoalesce.ts +1 -1
  107. package/src/structure/pointer-spelling.ts +1106 -0
  108. package/src/structure/redundant-test.ts +1 -1
  109. package/src/structure/structure.ts +6192 -1213
  110. package/src/structure/switch-recover.ts +583 -120
  111. package/src/structure/unfolded-branch-constant.ts +90 -0
  112. package/src/symbols.ts +58 -15
  113. package/src/target.ts +1028 -54
  114. package/src/trace.ts +49 -17
  115. package/src/variation-definitions.ts +333 -20
  116. package/src/variation-gates.ts +4 -0
  117. package/src/variation-tokens.ts +68 -3
@@ -122,11 +122,46 @@
122
122
  // pool load in the benchmark's agbcc references (extscale.ts's header has the census). Only the
123
123
  // fold's own extensions are judged this way; a plain cast's extension sits at its right shift,
124
124
  // where its position says nothing about its `lsl`'s.
125
- import { type Fn, type Op, type Value, replaceAllUsesWith, successorsOf } from '../ir/core';
125
+ //
126
+ // EVERY READER AN EXTENSION, the `/narrow-param` lift variation (`narrowExtendedParams`). agbcc does
127
+ // not always widen a narrow declared parameter in the prologue: where every use of it is itself an
128
+ // extension it widens at each use instead, and then the object carries no declaration to read.
129
+ // `pokeemerald:LoadMonInfo` declares `s16 partyId` and reads it as `lsl #16` behind a pool load, an
130
+ // `asr #14` for the scaled index and an `lsr #16` for a `u16` argument, so `raw-reader` refuses it
131
+ // (two readers), and with that ablated `not-prologue` (behind the pool load) and `fused-behind-pool`.
132
+ // Declared `s16` and read `a << 2` and `(u16)a`, or declared `s32` and read `(s16)a << 2` and
133
+ // `(u16)a`, the two are one object in isolation and differ only through the register allocation
134
+ // around them (a sibling `u8` parameter's prologue copy), so nothing in the asm decides it and the
135
+ // differ referees. Declaring the parameter at width w changes no reader's value when every reader is
136
+ // an extension to w' ≤ w: ext_w'(trunc_w x) = ext_w'(x). The width is the widest reader's, its
137
+ // signedness the caller's declaration where that spells one and the first reader of that width's
138
+ // otherwise; each reader of exactly that type becomes the parameter itself. Where no prototype
139
+ // declares the width, the candidate loses every score tie (rank.ts `guessesParamWidth`): without
140
+ // a sibling to separate them the two declarations are often one object, and the narrow one costs
141
+ // each prototyped caller the truncation `proto-width` is about.
142
+ //
143
+ // A LOW MASK IS AN EXTENSION READER. mwcc reads a `u16` parameter as `clrlwi rD,rS,16`, which the
144
+ // PPC frontend lifts as `and rS, 0xffff` — as it must, since the same instruction is every other
145
+ // mask too — so a parameter mwcc reads only through it reaches this pass with no `zext` reader at
146
+ // all. `and v, 0xffff` is `zext v {width=16}` value for value, so it counts as one (`extensionOf`),
147
+ // unsigned, and becomes the parameter itself when the parameter is declared `u16`. The two
148
+ // declarations are not one object there: over a `long *a`, mwcc_242_81 compiles
149
+ // `*a = *a & ~b; *a = *a | c;` to `not; and` when `b` and `c` are `unsigned short`, and to `andc` when
150
+ // they are `unsigned long` and the body masks them, at `-O4,p` and at Mario Party 4's unit flags.
151
+ //
152
+ // The rule is about the IR, not the ISA, so it reaches every frontend that lifts a mask as `and`.
153
+ // IDO reads a declared `u16`/`u8` parameter as `sw a0,0(sp); andi a0,a0,0xffff`, and frontend/mips.ts
154
+ // lifts the `andi` as that mask too: `s32 f(u16 a, s32 b){ return a + b; }` scores 3 wide and 0
155
+ // narrowed. The witness gates still decide whether IDO may narrow: a mask applied in place with no
156
+ // home store (`a = a & 0xffff`) is refused by `unhomed-param`. agbcc spells a mask as `and` with a
157
+ // pooled `0xffff` or a `mov #0xff`, so the variation is offered there too, and loses, because agbcc
158
+ // extends a declared narrow parameter with a shift pair: `s32 f(u32 a, s32 b){ return (a & 0xffff)
159
+ // + b; }` scores 0 wide and 6 narrowed.
160
+ import { type Fn, type Op, type Value, defOpMap, replaceAllUsesWith, successorsOf } from '../ir/core';
126
161
  import { CAST_WIDTHS, MATERIALIZING_OPS } from '../ir/opcodes';
127
162
  import { T } from '../ir/types';
128
163
  import { type Gate, firstRejection } from '../l3/gates';
129
- import { type FnProto, declaredWidth } from '../proto';
164
+ import { type FnProto, declaredSigned, declaredWidth } from '../proto';
130
165
  import type { NarrowParamWitness } from '../target';
131
166
 
132
167
  /** What the gates below judge: one entry parameter and the extension that reads it. */
@@ -141,6 +176,8 @@ export interface NarrowParamCandidate {
141
176
  inPrologue: boolean;
142
177
  /** reads of the RAW parameter anywhere in the function */
143
178
  uses: number;
179
+ /** reads of it that are not an extension to `width` or narrower */
180
+ wideReaders: number;
144
181
  /** the width the caller's own prototype declares for this parameter, if it declares one */
145
182
  declared: number | undefined;
146
183
  /** the extension is one raise/extscale.ts re-split from a fused pair whose `shl` the machine ran
@@ -192,14 +229,14 @@ export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
192
229
  },
193
230
  {
194
231
  id: 'not-prologue',
195
- why: 'an extension behind body code is where the SOURCE wrote the cast — and on a schedule that interleaves them, the refusing answer',
232
+ why: 'an extension behind body code may be the source writing the cast, and nothing in the fan referees a narrow signature against the wide one',
196
233
  sound: true,
197
234
  guardedBy: 'param-width.test.ts: an extension behind a nullary call is body code',
198
235
  rejects: (c) => !c.inPrologue,
199
236
  },
200
237
  {
201
238
  id: 'no-declaration-witness',
202
- why: 'this compiler spells a narrow declaration and a body cast the same way, so the asm decides nothing',
239
+ why: 'the compiler spells a narrow declaration and a body cast the same way, so the assembly decides nothing',
203
240
  sound: true,
204
241
  guardedBy:
205
242
  'param-width.test.ts: a compiler whose two spellings are one object refuses the narrowing, measured or not',
@@ -207,14 +244,14 @@ export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
207
244
  },
208
245
  {
209
246
  id: 'unhomed-param',
210
- why: 'this compiler homes a narrow DECLARED parameter, so the absent home store proves the declaration was wide',
247
+ why: 'the compiler stores a narrow declared parameter to the stack on entry, so one it did not store was declared wide',
211
248
  sound: true,
212
249
  guardedBy: 'param-width.test.ts: a homing compiler refuses the parameter it did not home',
213
250
  rejects: (c) => c.witness === 'home-store-and-in-place' && !c.homed,
214
251
  },
215
252
  {
216
253
  id: 'widened-elsewhere',
217
- why: 'this compiler widens a narrow DECLARED parameter in the argument register itself, so a widening that lands in a scratch register is not a declaration',
254
+ why: 'the compiler widens a narrow declared parameter in its own argument register, so a widening into another register is no declaration',
218
255
  sound: true,
219
256
  guardedBy:
220
257
  'param-width.test.ts: \u2026and refuses one it homed but widened SOMEWHERE ELSE \u2014 that is a 64-bit half',
@@ -229,6 +266,96 @@ export const PARAM_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
229
266
  },
230
267
  ];
231
268
 
269
+ const byId = (id: string): Gate<NarrowParamCandidate> => PARAM_WIDTH_GATES.find((g) => g.id === id)!;
270
+
271
+ /** `/narrow-param`'s table (`narrowExtendedParams`): a second CONSUMER of the default's rules, not an
272
+ * ablation of its table, so it is built by id rather than through `ablateHeuristic`. It leaves out
273
+ * the three rules that read the prologue's evidence and adds the one that reads the readers.
274
+ * `raw-reader` gives way to `wide-reader`, which is sound for the same reason it is: every reader
275
+ * is an extension the pass rewrites. `not-prologue` and `fused-behind-pool` read fields this pass
276
+ * does not observe (it fills `inPrologue` and `fusedBehindPool` false). Their `sound` is a claim
277
+ * about the DEFAULT, whose narrowing reaches every candidate with no wide sibling, so a wrong width
278
+ * there is a signature nothing referees and every prototyped caller pays for. A /narrow-param
279
+ * candidate keeps the wide default beside it and loses every tie unless a prototype declares the
280
+ * width (rank.ts `guessesParamWidth`), so for it the same question is refereed: a rule sound for a
281
+ * declaration is a heuristic for a generated candidate (l3/gates.ts, `ORDER_SHAPE_GATES`).
282
+ * param-width.test.ts holds the set left out to exactly these three, so a sound rule added to the
283
+ * default is either added here or argued out by name. */
284
+ export const PARAM_READER_WIDTH_GATES: readonly Gate<NarrowParamCandidate>[] = [
285
+ byId('entry-is-join'),
286
+ byId('param-typed'),
287
+ byId('cast-width'),
288
+ {
289
+ id: 'wide-reader',
290
+ why: 'a reader that is not an extension to this width or narrower reads bits a narrow declaration drops',
291
+ sound: true,
292
+ guardedBy: 'param-width.test.ts: a reader that is no narrow extension keeps the parameter wide',
293
+ rejects: (c) => c.wideReaders !== 0,
294
+ },
295
+ byId('proto-width'),
296
+ byId('no-declaration-witness'),
297
+ byId('unhomed-param'),
298
+ byId('widened-elsewhere'),
299
+ ];
300
+
301
+ const isExtension = (op: Op): boolean => op.opcode === 'sext' || op.opcode === 'zext';
302
+
303
+ /** The unsigned extension a MASK spells: `and v, 0xff` is `zext v {width=8}` and `and v, 0xffff` is
304
+ * `zext v {width=16}`, value for value. */
305
+ const MASK_WIDTHS: ReadonlyMap<number, number> = new Map([
306
+ [0xff, 8],
307
+ [0xffff, 16],
308
+ ]);
309
+
310
+ /** What `op` reads `v` as, when it reads it ONLY through an extension: its width and signedness.
311
+ * An `and` with a low mask counts, because that is the extension's spelling where the machine
312
+ * has no extension of its own to lift: PPC's `clrlwi rD,rS,16` and MIPS's `andi rD,rS,0xffff`
313
+ * (each frontend lifts it as the mask, and must, because every other mask is the same instruction).
314
+ * Null for any other reader, including an extension or a mask of something besides `v` alone. */
315
+ function extensionOf(op: Op, v: Value, defs: ReadonlyMap<Value, Op>): { width: number; signed: boolean } | null {
316
+ if (isExtension(op)) {
317
+ return op.operands.length === 1 && op.operands[0] === v
318
+ ? { width: op.attrs.width as number, signed: op.opcode === 'sext' }
319
+ : null;
320
+ }
321
+ if (op.opcode !== 'and' || op.operands.length !== 2 || !op.operands.includes(v)) {
322
+ return null;
323
+ }
324
+ const mask = defs.get(op.operands[op.operands[0] === v ? 1 : 0]);
325
+ const width = mask?.opcode === 'const' ? MASK_WIDTHS.get(mask.attrs.value as number) : undefined;
326
+ return width === undefined ? null : { width, signed: false };
327
+ }
328
+
329
+ /** The ops that read `v` and the number of branch arguments that pass it, anywhere in `fn`. */
330
+ function readersOf(fn: Fn, v: Value): { ops: Op[]; edges: number } {
331
+ const ops: Op[] = [];
332
+ let edges = 0;
333
+ for (const b of fn.blocks) {
334
+ for (const op of b.ops) {
335
+ if (op.operands.includes(v)) {
336
+ ops.push(op);
337
+ }
338
+ for (const s of op.successors) {
339
+ edges += s.args.filter((a) => a === v).length;
340
+ }
341
+ }
342
+ }
343
+ return { ops, edges };
344
+ }
345
+
346
+ /** How many reads of `v` are not an extension of `v` alone (`extensionOf`) to `width` or narrower. */
347
+ function wideReaderCount(fn: Fn, v: Value, width: number, defs: ReadonlyMap<Value, Op>): number {
348
+ const { ops, edges } = readersOf(fn, v);
349
+ return (
350
+ edges +
351
+ ops.reduce(
352
+ (n, op) =>
353
+ n + ((extensionOf(op, v, defs)?.width ?? Infinity) <= width ? 0 : op.operands.filter((o) => o === v).length),
354
+ 0,
355
+ )
356
+ );
357
+ }
358
+
232
359
  /** How many times `v` is read anywhere in `fn` — op operands and branch arguments alike. */
233
360
  function useCount(fn: Fn, v: Value): number {
234
361
  let n = 0;
@@ -282,6 +409,7 @@ export function narrowEntryParams(
282
409
  }
283
410
  prologue.add(op);
284
411
  }
412
+ const defs = defOpMap(fn);
285
413
  let narrowed = 0;
286
414
  for (const op of [...entry.ops]) {
287
415
  if (op.opcode !== 'sext' && op.opcode !== 'zext') {
@@ -298,6 +426,7 @@ export function narrowEntryParams(
298
426
  entryIsJoin,
299
427
  inPrologue: prologue.has(op),
300
428
  uses: useCount(fn, p),
429
+ wideReaders: wideReaderCount(fn, p, width, defs),
301
430
  declared: declared[entry.params.indexOf(p)],
302
431
  fusedBehindPool: fusedBehindPool.has(op),
303
432
  witness,
@@ -314,3 +443,64 @@ export function narrowEntryParams(
314
443
  }
315
444
  return narrowed;
316
445
  }
446
+
447
+ /** `/narrow-param`: type each entry parameter every reader of which is an extension of it at the
448
+ * widest reader's width, and drop the readers of exactly that type (EVERY READER AN EXTENSION).
449
+ * `apply: false` changes nothing and reports what it would narrow. Returns each parameter it
450
+ * narrowed. */
451
+ export function narrowExtendedParams(
452
+ fn: Fn,
453
+ witness: NarrowParamWitness,
454
+ self?: FnProto,
455
+ gates: readonly Gate<NarrowParamCandidate>[] = PARAM_READER_WIDTH_GATES,
456
+ apply = true,
457
+ ): NarrowParamCandidate[] {
458
+ const evidence = fn.paramEvidence;
459
+ const entry = fn.blocks[0];
460
+ const declaredTypes = Array.isArray(self?.params) ? self.params : [];
461
+ const entryIsJoin = fn.blocks.some((b) => successorsOf(b).includes(entry));
462
+ const narrowed: NarrowParamCandidate[] = [];
463
+ const defs = defOpMap(fn);
464
+ for (const [i, p] of entry.params.entries()) {
465
+ const extensions = readersOf(fn, p).ops.flatMap((op) => {
466
+ const ext = extensionOf(op, p, defs);
467
+ return ext === null ? [] : [{ op, ...ext }];
468
+ });
469
+ if (extensions.length === 0) {
470
+ continue;
471
+ }
472
+ const width = Math.max(...extensions.map((e) => e.width));
473
+ const first = extensions.find((e) => e.width === width)!;
474
+ const declared = declaredTypes[i];
475
+ const c: NarrowParamCandidate = {
476
+ param: p,
477
+ width,
478
+ entryIsJoin,
479
+ inPrologue: false,
480
+ uses: useCount(fn, p),
481
+ wideReaders: wideReaderCount(fn, p, width, defs),
482
+ declared: declared === undefined ? undefined : declaredWidth(declared),
483
+ fusedBehindPool: false,
484
+ witness,
485
+ homed: evidence?.get(p)?.deadHome ?? false,
486
+ selfRedefined: evidence?.get(p)?.selfRedefined ?? false,
487
+ };
488
+ if (firstRejection(gates, c) !== null) {
489
+ continue;
490
+ }
491
+ narrowed.push(c);
492
+ if (!apply) {
493
+ continue;
494
+ }
495
+ const signed = (declared === undefined ? undefined : declaredSigned(declared)) ?? first.signed;
496
+ p.type = T.int(width, signed);
497
+ for (const { op, ...ext } of extensions) {
498
+ if (ext.width === width && ext.signed === signed) {
499
+ replaceAllUsesWith(fn, op.results[0], p);
500
+ const home = fn.blocks.find((b) => b.ops.includes(op))!;
501
+ home.ops.splice(home.ops.indexOf(op), 1);
502
+ }
503
+ }
504
+ }
505
+ return narrowed;
506
+ }
@@ -12,12 +12,13 @@
12
12
  // in the driver.
13
13
  import { Block, Fn } from '../ir/core';
14
14
  import { simplifyTrivialPhis } from '../ir/simplify';
15
- import { dce } from '../pattern/engine';
16
- import type { FnProto } from '../proto';
15
+ import { NOT_FCMP_PATTERNS, applyPattern, dce } from '../pattern/engine';
16
+ import { type FnProto, declaresVoidReturn } from '../proto';
17
17
  import type { TargetDescription } from '../target';
18
+ import { readDeclaredMembers } from './aggmembers';
18
19
  import { recognizeArrays } from './arrays';
19
20
  import { recognizeConsts } from './const';
20
- import { recognizeDivPow2 } from './divpow2';
21
+ import { recognizeDivPow2, recognizeSremPow2 } from './divpow2';
21
22
  import {
22
23
  type PoolOrder,
23
24
  type ScaleRecord,
@@ -33,12 +34,13 @@ import { recognizeMagicDivision } from './magicdiv';
33
34
  import { recognizeMemberArrays } from './memberarrays';
34
35
  import { rerootNarrowReads } from './narrow';
35
36
  import { type MergeShape, mergeShapes, narrowBlockLocals } from './narrowlocal';
36
- import { fuseParamPairs } from './pairparams';
37
- import { PARAM_WIDTH_GATES, narrowEntryParams } from './paramwidth';
37
+ import { fuseParamPairs, refuseSplitWideParams } from './pairparams';
38
+ import { PARAM_READER_WIDTH_GATES, PARAM_WIDTH_GATES, narrowEntryParams, narrowExtendedParams } from './paramwidth';
38
39
  import { type BranchShortCircuitOptions, recognizeBranchShortCircuit, recognizeShortCircuit } from './shortcircuit';
39
40
  import { recognizeSoftDiv } from './softdiv';
41
+ import { seedNarrowStoreSigns } from './storesign';
40
42
  import { recognizeStructArrays } from './struct-arrays';
41
- import { recognizeStructs } from './structs';
43
+ import { type DisplacedOptions, foldDisplacedAccesses, recognizeStructs } from './structs';
42
44
  import { TRUNC_LOAD_GATES, foldTruncatedLoads } from './truncload';
43
45
  import { recognizeWideHelpers, refuseUnmodelledHelpers } from './widehelpers';
44
46
 
@@ -49,6 +51,19 @@ import { recognizeWideHelpers, refuseUnmodelledHelpers } from './widehelpers';
49
51
  export interface PreRecoveryOptions {
50
52
  /** raise/shortcircuit.ts `recognizeBranchShortCircuit` — the connective-vs-comparison-tree question. */
51
53
  shortCircuit?: BranchShortCircuitOptions;
54
+ /** raise/paramwidth.ts `narrowExtendedParams` — the `/narrow-param` lift. */
55
+ paramWidth?: ParamWidthOptions;
56
+ /** raise/structs.ts `foldDisplacedAccesses` — the `/far-cast` lift. */
57
+ displaced?: DisplacedOptions;
58
+ }
59
+
60
+ export interface ParamWidthOptions {
61
+ /** also narrow a parameter every reader of which is an extension (`/narrow-param`) */
62
+ extendedReaders?: boolean;
63
+ /** called when a lift without `extendedReaders` has a parameter that would narrow with it */
64
+ onExtendedReaders?: () => void;
65
+ /** called when the `extendedReaders` lift narrows a parameter whose width no prototype declares */
66
+ onUndeclaredWidth?: () => void;
52
67
  }
53
68
 
54
69
  /** CFG facts read off the function ONCE, before the first pass below rewrites it.
@@ -94,10 +109,10 @@ export interface PreRecoveryPass {
94
109
  }
95
110
 
96
111
  /** THE ordered pre-recovery pass list — the single source of truth shared by pipeline / rank / report.
97
- * address-numbering → const-materialize → magic-division → pow2-division → soft-division →
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.
112
+ * address-numbering → const-materialize → magic-division → pow2-division → pow2-remainder →
113
+ * soft-division → scaled-extension → store-sign → array-legalize → truncated-load → struct-array →
114
+ * member-array → struct-pointer → short-circuit → branch-short-circuit → narrow-reads →
115
+ * narrow-local → parameter-width → scaled-extension-restore.
101
116
  * See each recognizer's file for the rationale. */
102
117
  export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
103
118
  // FIRST: collapsing duplicate address definitions removes block params every later recognizer
@@ -126,6 +141,9 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
126
141
  // ending in `br`. Verified by running it before and after each of them — same result. It sits
127
142
  // beside magicdiv so that a reader looking for division recovery finds both together.
128
143
  { id: 'divpow2', run: recognizeDivPow2, dce: true },
144
+ // …and its remainder sibling, IDO's two-test diamond (raise/divpow2.ts `recognizeSremPow2`): no
145
+ // other pass reads a three-edge merge whose last arm takes a power of two off the mask.
146
+ { id: 'srempow2', run: recognizeSremPow2, dce: true },
129
147
  {
130
148
  id: 'softdiv',
131
149
  run: (fn, _self, _opts, target) => recognizeSoftDiv(fn, target),
@@ -137,9 +155,19 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
137
155
  // from `widehelpers` because it re-types words as floats — a helper's, a parameter a callee
138
156
  // declares `double`, a literal — with its own closed-set refusal, and a single-precision helper
139
157
  // is a float helper that is not wide.
158
+ // A fold re-runs the float compares' negation idioms (pattern/engine.ts `NOT_FCMP_PATTERNS`),
159
+ // which ran before there was a float compare to negate.
140
160
  {
141
161
  id: 'floathelpers',
142
- run: (fn, _self, _opts, target) => recognizeFloatHelpers(fn, target),
162
+ run: (fn, _self, _opts, target) => {
163
+ if (!recognizeFloatHelpers(fn, target)) {
164
+ return false;
165
+ }
166
+ if (NOT_FCMP_PATTERNS.reduce((n, p) => n + applyPattern(fn, p, target), 0) > 0) {
167
+ dce(fn);
168
+ }
169
+ return true;
170
+ },
143
171
  dce: false,
144
172
  },
145
173
  // NOT gated, where `softdiv` is, and the asymmetry is the argument: a soft DIVISION is
@@ -166,7 +194,17 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
166
194
  // AFTER it, because the `concat`s it reads are the ones the frontend built at the call the pass
167
195
  // above has just rewritten — and because a pair the rewrite refused is a pair this must not fuse
168
196
  // into a signature either.
169
- { id: 'pairparams', run: (fn) => fuseParamPairs(fn), dce: false },
197
+ // It also refuses a declared 64-bit parameter the lift left in halves, which is the one place the
198
+ // fusion's answer is final (`refuseSplitWideParams`).
199
+ {
200
+ id: 'pairparams',
201
+ run: (fn, self) => {
202
+ const fused = fuseParamPairs(fn);
203
+ refuseSplitWideParams(fn, self);
204
+ return fused;
205
+ },
206
+ dce: false,
207
+ },
170
208
  // AFTER `const`, which folds a shift pair over a constant to the constant it computes, and BEFORE
171
209
  // the three array recognizers, whose input this pass produces: `shl(ext(x), k)` is an element
172
210
  // scale they legalize and the fused pair is not. `dce: true` — the `shl` a fold leaves readerless.
@@ -178,6 +216,11 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
178
216
  dce: true,
179
217
  gate: foldsShiftPairCasts,
180
218
  },
219
+ // BEFORE `arrays` and `struct-arrays`, which carry the stamp into the `astore` they make (and
220
+ // struct-arrays types a field only ever stored from it), and before `structs`, which does the
221
+ // same for a constant-offset field. `member-arrays` overwrites an astore's `signed` with its
222
+ // member's (raise/memberarrays.ts says why). `dce: false` — it changes no op.
223
+ { id: 'storesign', run: (fn, _self, _opts, target) => seedNarrowStoreSigns(fn, target), dce: false },
181
224
  { id: 'arrays', run: recognizeArrays, dce: true },
182
225
  // BEFORE the three struct synthesizers, and that is what the pass is FOR: `raise/structs.ts` is
183
226
  // the pass that refuses one offset read at two widths, and it can only see one width per offset
@@ -207,7 +250,11 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
207
250
  // struct-arrays AFTER arrays (scalar stride==width shapes are claimed first — see the
208
251
  // discriminator note in raise/struct-arrays.ts) and BEFORE structs (an element's field
209
252
  // accesses must not be re-derived as constant-offset struct-pointer accesses).
210
- { id: 'struct-arrays', run: recognizeStructArrays, dce: true },
253
+ {
254
+ id: 'struct-arrays',
255
+ run: (fn, _self, _opts, target) => recognizeStructArrays(fn, target.compilerBehaviors.aggregateBoundary),
256
+ dce: true,
257
+ },
211
258
  // member-arrays AFTER struct-arrays, and that order IS load-bearing — but the order alone does
212
259
  // not deconflict them, because the two passes claim different VALUES: struct-arrays types the
213
260
  // materialized `add(P, K)` and member-arrays groups on `P`, so `P->tbl[i].f` reaches both. What
@@ -217,11 +264,28 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
217
264
  // sits there so the three struct synthesizers read in the order of their evidence. `narrowlocal`
218
265
  // reads the carrier's extension chain rather than any address, so nothing here orders against it.
219
266
  { id: 'member-arrays', run: (fn) => recognizeMemberArrays(fn), dce: true },
267
+ // RIGHT BEFORE `structs`, whose input it widens: an access through a materialised `add(p, K)`
268
+ // joins `p`'s access set at `off + K`. After `truncload`, `struct-arrays` and `member-arrays`, so
269
+ // none of them sees a folded access, and a displacement either of the last two typed is refused.
270
+ // `dce: true` — the `add` chain a fold leaves readerless. `opts.displaced` is `/far-cast`.
271
+ {
272
+ id: 'displaced',
273
+ run: (fn, self, opts, target) =>
274
+ foldDisplacedAccesses(fn, opts.displaced, target.compilerBehaviors, declaresVoidReturn(self)),
275
+ dce: true,
276
+ },
220
277
  {
221
278
  id: 'structs',
222
279
  run: (fn, _self, _opts, target) => recognizeStructs(fn, target.compilerBehaviors.aggregateBoundary),
223
280
  dce: false,
224
281
  },
282
+ // AFTER `structs`, which synthesizes layouts for bases it can type and never for a frame object
283
+ // the audit already typed as a declared struct — the only base this reads.
284
+ {
285
+ id: 'aggmembers',
286
+ run: (fn, _self, _opts, target) => readDeclaredMembers(fn, target.capabilities.endianness === 'little'),
287
+ dce: true,
288
+ },
225
289
  { id: 'shortcircuit', run: recognizeShortCircuit, dce: true },
226
290
  // The control-flow sibling, value form FIRST.
227
291
  //
@@ -278,16 +342,13 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
278
342
  // The `target` argument is read by TWO of this pass's gates, and unlike `narrowlocal`'s single
279
343
  // conjunct it is not a tuning knob: `narrowParamWitness` says WHICH fact in the object settles a
280
344
  // parameter's declared width on this compiler, and a target that names none refuses outright.
345
+ //
346
+ // `opts.paramWidth` adds the `/narrow-param` lift after it, over the parameters it left wide
347
+ // (raise/paramwidth.ts, EVERY READER AN EXTENSION), or asks whether that lift would narrow any.
281
348
  {
282
349
  id: 'paramwidth',
283
- run: (fn, self, _opts, target, lifted) =>
284
- narrowEntryParams(
285
- fn,
286
- target.compilerBehaviors.narrowParamWitness ?? 'none',
287
- self,
288
- PARAM_WIDTH_GATES,
289
- lifted.scales.behindPool,
290
- ),
350
+ run: (fn, self, opts, target, lifted) =>
351
+ narrowParamWidths(fn, self, opts.paramWidth, target, lifted, PARAM_WIDTH_GATES, PARAM_READER_WIDTH_GATES),
291
352
  dce: false,
292
353
  },
293
354
  // LAST, after every pass that can CLAIM what `extscale` exposed — the two width passes above take
@@ -302,6 +363,31 @@ export const PRE_RECOVERY_PASSES: PreRecoveryPass[] = [
302
363
  },
303
364
  ];
304
365
 
366
+ /** The `paramwidth` entry's body, taking both of its tables, so a census can pass wrapped ones. */
367
+ export function narrowParamWidths(
368
+ fn: Fn,
369
+ self: FnProto | undefined,
370
+ opts: ParamWidthOptions | undefined,
371
+ target: TargetDescription,
372
+ lifted: PreRecoveryFacts,
373
+ prologueGates: typeof PARAM_WIDTH_GATES,
374
+ readerGates: typeof PARAM_READER_WIDTH_GATES,
375
+ ): number {
376
+ const witness = target.compilerBehaviors.narrowParamWitness ?? 'none';
377
+ const n = narrowEntryParams(fn, witness, self, prologueGates, lifted.scales.behindPool);
378
+ if (opts?.extendedReaders) {
379
+ const lifted = narrowExtendedParams(fn, witness, self, readerGates);
380
+ if (lifted.some((c) => c.declared === undefined)) {
381
+ opts.onUndeclaredWidth?.();
382
+ }
383
+ return n + lifted.length;
384
+ }
385
+ if (opts?.onExtendedReaders && narrowExtendedParams(fn, witness, self, readerGates, false).length > 0) {
386
+ opts.onExtendedReaders();
387
+ }
388
+ return n;
389
+ }
390
+
305
391
  /** Run the pre-recovery passes in order. For each pass whose gate passes and that CHANGES the IR, run
306
392
  * `dce` when the pass declares it, then invoke `afterPass(pass, result)` (the caller's verify/trace
307
393
  * hook), if given.
@@ -0,0 +1,73 @@
1
+ // asmlift — the CFG folds that run last before structuring, after return-sinking: the loop folds
2
+ // rewrite a loop's edges into a shape a loop spelling owns, and the last one takes a parameter off a
3
+ // switch's join (a case fallen into, or the switch's exit). Each is its own stage in the trace, so a
4
+ // re-spelled function names the fold that moved it.
5
+ //
6
+ // A MUTABLE RECORD, not a sum of calls, for the reason `PRE_RECOVERY_PASSES` is one: it is the
7
+ // caller-side seam a process outside core swaps to census a fold's gate table (`pnpm bench gates`,
8
+ // apps/benchmark/src/run/gate-census.ts). A static import binding cannot be swapped.
9
+ import { Fn } from '../ir/core';
10
+ import { foldEmptyLatches, foldExitTrampolines } from './latch';
11
+ import { rerollPeeledTests } from './reroll';
12
+ import { recomputeDerivedParams, recomputeRedundantParams } from './unpre';
13
+ import { unthreadLoopExits } from './unthread';
14
+
15
+ export interface PreStructureFold {
16
+ /** stable; the trace stage is `stage:<id>` */
17
+ readonly id: string;
18
+ /** the trace stage's title */
19
+ readonly title: string;
20
+ /** how many rewrites the fold made; 0 leaves the function as it was */
21
+ run: (fn: Fn) => number;
22
+ }
23
+
24
+ /** THE ORDER CARRIES LOAD, and each fold runs ONCE: there is no fixpoint across folds, so a fold
25
+ * that exposes another's shape must run before it. The re-roll needs the latch's way out and the
26
+ * entry's to reach ONE block, and on `synthetic:break_value:agbcc` they reach it only after the
27
+ * trampoline fold has spliced out the empty block the break leaves through; ahead of it, the re-roll
28
+ * declines and the loop with it ('unrecovered back-edge'). `reroll.test.ts` pins it by lifting that
29
+ * row's pre-fold IR through this record. The latch fold comes first for the same reason in kind:
30
+ * the folds after it read the back edge it leaves. The un-threading reads loop exits, which the
31
+ * trampoline fold reshapes, and sits ahead of the re-roll so a loop it gives one way out is one
32
+ * the re-roll may then read. That place is a precaution: `synthetic:thrbreak:agbcc` and the
33
+ * pointer walk unthread.ts names lift the same C wherever the fold sits, so `unthread.test.ts`
34
+ * pins its position, not a lift that needs it. The two parameter recomputes run last, the
35
+ * derived one (`derivedparam`) just ahead of `unpre`, because that is the one place each was
36
+ * measured, and `unpre.test.ts` pins the places, not a lift that needs them. Both MOVE EDGES: a
37
+ * block either one's orphan sweep leaves holding only its `br` is threaded out of the CFG
38
+ * (`threadEmptied`, raise/unpre.ts), its in-edges retargeted and the block removed, so `unpre`
39
+ * reads the CFG `derivedparam` left, and a loop fold moved after either would read a reshaped one
40
+ * it was never measured on. */
41
+ export const PRE_STRUCTURE_FOLDS: PreStructureFold[] = [
42
+ {
43
+ id: 'latchfold',
44
+ title: 'Empty-latch folding (a latch that only jumps back becomes its back edge)',
45
+ run: (fn) => foldEmptyLatches(fn),
46
+ },
47
+ {
48
+ id: 'trampoline',
49
+ title: 'Exit-trampoline folding (an empty block on a loop exit becomes that edge)',
50
+ run: (fn) => foldExitTrampolines(fn),
51
+ },
52
+ {
53
+ id: 'unthread',
54
+ title: 'Loop-exit un-threading (an exit jump-threaded past the re-test after the loop goes back to it)',
55
+ run: (fn) => unthreadLoopExits(fn),
56
+ },
57
+ {
58
+ id: 'reroll',
59
+ title: 'Peeled-test re-rolling (a loop test peeled into the entry becomes the first iteration)',
60
+ run: (fn) => rerollPeeledTests(fn),
61
+ },
62
+ {
63
+ id: 'derivedparam',
64
+ title:
65
+ "Derived-parameter recomputing (a parameter every edge hands one pure op of a sibling's argument becomes that op)",
66
+ run: (fn) => recomputeDerivedParams(fn),
67
+ },
68
+ {
69
+ id: 'unpre',
70
+ title: 'Redundant-parameter recomputing (a parameter every edge hands one pure expression becomes it)',
71
+ run: (fn) => recomputeRedundantParams(fn),
72
+ },
73
+ ];
@@ -2,8 +2,9 @@
2
2
  // (signed comparisons/divisions ⇒ signed operands), types memory-access bases as pointers,
3
3
  // propagates pointer-ness across the SSA, then defaults the rest to s32. Emits recovered
4
4
  // types onto the SSA values in place.
5
- import { Fn, Value, defOpMap } from '../ir/core';
5
+ import { Fn, RETURNS_OBJECT, Value, defOpMap } from '../ir/core';
6
6
  import { IrType, T, scalarTypeForAccess, typeEquals } from '../ir/types';
7
+ import { storeSigned } from './storesign';
7
8
 
8
9
  const SIGNED_CMP = new Set(['icmp_slt', 'icmp_sle', 'icmp_sgt', 'icmp_sge']);
9
10
  const UNSIGNED_CMP = new Set(['icmp_ult', 'icmp_ule', 'icmp_ugt', 'icmp_uge']);
@@ -87,7 +88,7 @@ function typeDerefBases(fn: Fn): void {
87
88
  break;
88
89
  case 'store':
89
90
  width = op.attrs.width as number;
90
- signed = width === 4;
91
+ signed = storeSigned(op, width);
91
92
  break;
92
93
  case 'aload':
93
94
  width = op.attrs.elemSize as number;
@@ -95,9 +96,7 @@ function typeDerefBases(fn: Fn): void {
95
96
  break;
96
97
  case 'astore':
97
98
  width = op.attrs.elemSize as number;
98
- // an astore carries no signedness of its own unless a recovery recorded the member's;
99
- // structure.ts reads it by the same rule
100
- signed = (op.attrs.signed as boolean | undefined) ?? width === 4;
99
+ signed = storeSigned(op, width);
101
100
  break;
102
101
  default:
103
102
  continue;
@@ -250,6 +249,14 @@ export function returnType(fn: Fn): IrType {
250
249
  const term = b.ops[b.ops.length - 1];
251
250
  if (term?.opcode === 'ret' && term.operands.length > 0) {
252
251
  const v = term.operands[0];
252
+ // the function's own struct return object, handed back whole (ir/core.ts `RETURNS_OBJECT`)
253
+ if (term.attrs[RETURNS_OBJECT] === true && v.type.kind === 'ptr') {
254
+ return v.type.to;
255
+ }
256
+ // a struct a call returned by value, handed back whole (frontend/register-struct-call.ts)
257
+ if (v.type.kind === 'struct') {
258
+ return v.type;
259
+ }
253
260
  // A NARROW value in the return register does not make the DECLARED return type narrow: the
254
261
  // register is a word, and the value's width says how it was COMPUTED, not what the header
255
262
  // spelled. The two readings compile alike through agbcc and do NOT through mwcc —