@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
@@ -100,12 +100,20 @@
100
100
  // the map spells two ways at either end, a width the map does not state, a width that disagrees
101
101
  // with the access, a store onto a `const` name — refuse NOTHING here and are pinned by
102
102
  // `offset-names.test.ts` alone.
103
- import { type Fn, type Op, type Value, defOpMap } from '../ir/core';
103
+ //
104
+ // AND IT IS WHERE A LIFT'S NAMES BECOME FINAL, so it is also where `cellSpelledTwice` asks whether
105
+ // any byte is now reached under two spellings — the premise `ir/alias.ts` reads two names as two
106
+ // objects on — and declines the function when one is. A refusal above is one way to get there (a
107
+ // walk left as `base + K` past the base's end spells its neighbour's byte through the base's
108
+ // name), not the only one, which is why the question is asked of the accesses and not of the gates.
109
+ import { constDisplacementOf } from '../ir/alias';
110
+ import { type Block, type Fn, type Op, type Value, defOpMap, successorsOf } from '../ir/core';
104
111
  import { verify } from '../ir/verify';
105
112
  import { type Gate, firstRejection } from '../l3/gates';
106
113
  import { dce } from '../pattern/engine';
107
114
  import { type SymbolInfo, type SymbolMap, accessSignedness, lookupSymbol } from '../symbols';
108
- import { type StampBehaviors, stampDeclaredVolatileUnder } from './declared-volatile';
115
+ import { type StampBehaviors, joinIncoming, stampDeclaredVolatileUnder } from './declared-volatile';
116
+ import { RaiseUnsupportedError } from './errors';
109
117
 
110
118
  /** One `add`/`sub` whose address the gates judge. Built only for an op that already resolves to a
111
119
  * named base plus a NON-ZERO constant — a zero offset names the base itself, which the pool path
@@ -230,8 +238,10 @@ export const OFFSET_NAME_GATES: readonly Gate<OffsetAddress>[] = [
230
238
  // The mirror of `base-address-ambiguous`, and it is not the same question asked twice: that one
231
239
  // is one NAME the map places at two addresses, this one is one ADDRESS the map gives several
232
240
  // names — aliases, or typed views of one RAM region, which `SymbolMap`'s `SymbolInfo[]` exists
233
- // to carry. `lookupSymbol` answers with the first of them, so the width the two access rules
234
- // check and the `const` the store rule checks would otherwise be read off an arbitrary pick.
241
+ // to carry. `lookupSymbol` answers with ONE of them — `objectAt`'s per-address pick, usually the
242
+ // first, the sized object behind an unsized data marker otherwise — so the width the two access
243
+ // rules check and the `const` the store rule checks would otherwise be read off whichever alias
244
+ // that pick lands on. Passing this gate means every alias agrees with the first on all of them.
235
245
  {
236
246
  id: 'target-address-ambiguous',
237
247
  why: 'the map gives the walked-to address several names that do not agree, so which one this spells is arbitrary',
@@ -317,39 +327,22 @@ function addressesByName(symbols: SymbolMap): Map<string, { addr: number; info:
317
327
  }
318
328
 
319
329
  /** The named base an address value stands on, following `add`/`sub` chains through constants —
320
- * the `adds r1,#12` walk is a chain of them, and each link is its own candidate. Stops at
321
- * anything else, block parameters included (they have no def, so a loop-carried pointer
322
- * induction never resolves). */
330
+ * the `adds r1,#12` walk is a chain of them, and each link is its own candidate. The chain is
331
+ * ir/alias.ts `constDisplacementOf`, the reading raise/structs.ts shares; it stops at anything
332
+ * else, block parameters included (they have no def, so a loop-carried pointer induction never
333
+ * resolves). */
323
334
  function namedBase(v: Value, defOf: Map<Value, Op>): { sym: string; code: boolean; offset: number } | null {
324
- const d = defOf.get(v);
325
- if (d === undefined) {
326
- return null;
327
- }
328
- if (d.opcode === 'gaddr') {
329
- return { sym: d.attrs.sym as string, code: d.attrs.code === true, offset: 0 };
330
- }
331
- if (d.opcode !== 'add' && d.opcode !== 'sub') {
332
- return null;
333
- }
334
- const [a, b] = d.operands;
335
- const ka = defOf.get(a)?.opcode === 'const' ? (defOf.get(a)!.attrs.value as number) : null;
336
- const kb = defOf.get(b)?.opcode === 'const' ? (defOf.get(b)!.attrs.value as number) : null;
337
- // `sub` is not commutative: only `base - K` is an offset off the base.
338
- if (kb !== null) {
339
- const base = namedBase(a, defOf);
340
- return base && { ...base, offset: base.offset + (d.opcode === 'sub' ? -kb : kb) };
341
- }
342
- if (ka !== null && d.opcode === 'add') {
343
- const base = namedBase(b, defOf);
344
- return base && { ...base, offset: base.offset + ka };
345
- }
346
- return null;
335
+ const disp = constDisplacementOf(defOf, v);
336
+ const d = defOf.get(disp?.root ?? v);
337
+ return d?.opcode === 'gaddr'
338
+ ? { sym: d.attrs.sym as string, code: d.attrs.code === true, offset: disp?.k ?? 0 }
339
+ : null;
347
340
  }
348
341
 
349
342
  /** Every `add`/`sub` in `fn` that resolves to a named base plus a non-zero constant, with the
350
343
  * context the gates judge. Shared by the rewrite and by `offsetNameRefusals` so the two cannot
351
344
  * disagree about which sites exist. */
352
- function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: OffsetAddress }[] {
345
+ function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: OffsetAddress; at: number }[] {
353
346
  const defOf = defOpMap(fn);
354
347
  const byName = addressesByName(symbols);
355
348
  const written = new Set<Value>();
@@ -404,7 +397,7 @@ function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: O
404
397
  }
405
398
  }
406
399
  }
407
- const out: { op: Op; sym: string; addr: OffsetAddress }[] = [];
400
+ const out: { op: Op; sym: string; addr: OffsetAddress; at: number }[] = [];
408
401
  for (const b of fn.blocks) {
409
402
  for (const op of b.ops) {
410
403
  if (op.opcode !== 'add' && op.opcode !== 'sub') {
@@ -426,6 +419,7 @@ function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: O
426
419
  out.push({
427
420
  op,
428
421
  sym: base.sym,
422
+ at: at.addr + base.offset,
429
423
  addr: {
430
424
  base: at.info,
431
425
  ambiguous: at.twice,
@@ -444,15 +438,400 @@ function offsetSites(fn: Fn, symbols: SymbolMap): { op: Op; sym: string; addr: O
444
438
  return out;
445
439
  }
446
440
 
441
+ /** Every address the map carries a name at, and whether the name is a MACRO — an address-cast
442
+ * macro expands to the numeric literal, so to the compiler it is the same spelling as a raw
443
+ * constant address, not an object of its own. Cached per map (rebuilt when its entry count moves),
444
+ * because the pass runs once per lift setting of every row. */
445
+ interface Places {
446
+ readonly addrs: number[];
447
+ macro: boolean;
448
+ }
449
+ const placesCache = new WeakMap<SymbolMap, { entries: number; places: Map<string, Places> }>();
450
+
451
+ function placesByName(symbols: SymbolMap): Map<string, Places> {
452
+ const cached = placesCache.get(symbols);
453
+ if (cached && cached.entries === symbols.size) {
454
+ return cached.places;
455
+ }
456
+ const places = new Map<string, Places>();
457
+ for (const [addr, infos] of symbols) {
458
+ for (const info of infos) {
459
+ const p = places.get(info.name) ?? places.set(info.name, { addrs: [], macro: false }).get(info.name)!;
460
+ if (!p.addrs.includes(addr)) {
461
+ p.addrs.push(addr);
462
+ }
463
+ p.macro ||= info.macroBody !== undefined;
464
+ }
465
+ }
466
+ placesCache.set(symbols, { entries: symbols.size, places });
467
+ return places;
468
+ }
469
+
470
+ /** What one access's address may stand for: the SPELLING the emitted C reaches it through (a
471
+ * global's name, or `null` for a numeric address — a raw constant or a macro), the OBJECT
472
+ * addresses that spelling may denote, and the constant displacement off them. */
473
+ interface Spelled {
474
+ readonly spelling: string | null;
475
+ readonly objects: readonly number[];
476
+ readonly k: number;
477
+ /** the name is asmlift's reading of the map (`gaddr`'s `fromMap`), not the asm's own spelling —
478
+ * read by ONE CELL PER SPELLING only; whether two spellings forward is the compiler's fact */
479
+ readonly fromMap: boolean;
480
+ }
481
+
482
+ /** One access whose bytes the map lets this pass place, under ONE of the spellings its address may
483
+ * stand for (an address that arrives through a join may stand for several, one entry each). */
484
+ interface PlacedAccess extends Spelled {
485
+ /** the access, so a pair is never one op against itself */
486
+ readonly op: Op;
487
+ readonly ranges: readonly (readonly [lo: number, hi: number])[];
488
+ readonly writes: boolean;
489
+ }
490
+
491
+ /** The 32-bit integer `v` denotes when every leaf under it is a constant, or null. Integer
492
+ * arithmetic over constants, and a join whose every in-edge brings one value, fold, because the C
493
+ * compiler folds them in the emitted C whatever this pipeline's own constant pass does: an index
494
+ * built in two steps (`movs r2, #2; adds r2, #3`), or `lsls r2, #0`, is `[5]` to agbcc.
495
+ * Memoised per call of {@link cellSpelledTwice}; a value met again on its own walk (a loop-carried
496
+ * join) does not fold. */
497
+ function constantFolder(defOf: Map<Value, Op>, incoming: Map<Value, Value[]>): (v: Value) => number | null {
498
+ const memo = new Map<Value, number | null>();
499
+ const onWalk = new Set<Value>();
500
+ const fold = (v: Value): number | null => {
501
+ const known = memo.get(v);
502
+ if (known !== undefined || memo.has(v)) {
503
+ return known ?? null;
504
+ }
505
+ if (onWalk.has(v)) {
506
+ return null;
507
+ }
508
+ onWalk.add(v);
509
+ const out = foldOnce(v);
510
+ onWalk.delete(v);
511
+ memo.set(v, out);
512
+ return out;
513
+ };
514
+ const foldOnce = (v: Value): number | null => {
515
+ const d = defOf.get(v);
516
+ if (d === undefined) {
517
+ const ins = [...new Set(incoming.get(v) ?? [])].filter((a) => a !== v);
518
+ const each = ins.map(fold);
519
+ return each.length > 0 && each.every((x) => x !== null && x === each[0]) ? each[0] : null;
520
+ }
521
+ if (d.opcode === 'const') {
522
+ return typeof d.attrs.value === 'number' ? d.attrs.value >> 0 : null;
523
+ }
524
+ const a = d.operands.length > 0 ? fold(d.operands[0]) : null;
525
+ if (a === null) {
526
+ return null;
527
+ }
528
+ if (d.operands.length === 1) {
529
+ const imm = d.attrs.imm;
530
+ const w = d.attrs.width;
531
+ switch (d.opcode) {
532
+ case 'not':
533
+ return ~a;
534
+ case 'neg':
535
+ return -a >> 0;
536
+ case 'zext':
537
+ return typeof w === 'number' && w < 32 ? (a & ((1 << w) - 1)) >>> 0 : a;
538
+ case 'sext':
539
+ return typeof w === 'number' && w < 32 ? (a << (32 - w)) >> (32 - w) : a;
540
+ }
541
+ if (typeof imm !== 'number') {
542
+ return null;
543
+ }
544
+ return shift(d.opcode, a, imm);
545
+ }
546
+ const b = d.operands.length === 2 ? fold(d.operands[1]) : null;
547
+ if (b === null) {
548
+ return null;
549
+ }
550
+ switch (d.opcode) {
551
+ case 'add':
552
+ return (a + b) >> 0;
553
+ case 'sub':
554
+ return (a - b) >> 0;
555
+ case 'mul':
556
+ return Math.imul(a, b);
557
+ case 'and':
558
+ return a & b;
559
+ case 'or':
560
+ return a | b;
561
+ case 'xor':
562
+ return a ^ b;
563
+ }
564
+ return shift(d.opcode, a, b);
565
+ };
566
+ const shift = (opcode: string, a: number, n: number): number | null => {
567
+ if (n < 0 || n > 31) {
568
+ return null;
569
+ }
570
+ switch (opcode) {
571
+ case 'shl':
572
+ return a << n;
573
+ case 'shr_u':
574
+ return (a >>> n) >> 0;
575
+ case 'shr_s':
576
+ return a >> n;
577
+ }
578
+ return null;
579
+ };
580
+ return fold;
581
+ }
582
+
583
+ /** The spellings an address stands for, through defs and joins: a literal (or a literal plus
584
+ * constants) is a numeric address, a `gaddr` plus constants its name — at the one map address
585
+ * the name was read at (`mapAddr`), or at every address the map places it at — and a block
586
+ * parameter every in-edge's value, so an address the raising tower will merge across a join (two
587
+ * pool loads of one word, a load and its `adds #0` copy) is placed as the merge spells it. An
588
+ * in-edge that passes the parameter back unchanged adds nothing; one that passes it MOVED (a
589
+ * pointer stepped round a loop) reaches bytes no constant names. Null where any reaching value
590
+ * is not placed: a parameter, a frame slot, a runtime index, a name the map does not hold. Every
591
+ * constant here is read through `fold` ({@link constantFolder}), as the C compiler will read it. */
592
+ function spellingsOf(
593
+ v: Value,
594
+ defOf: Map<Value, Op>,
595
+ incoming: Map<Value, Value[]>,
596
+ places: Map<string, Places>,
597
+ fold: (v: Value) => number | null,
598
+ ): Spelled[] | null {
599
+ // A parameter's answer is memoised per displacement once it is known not to lean on a parameter
600
+ // still being walked (`open`), so a chain of joins is walked once, not once per path through it.
601
+ const memo = new Map<Value, Map<number, Spelled[] | null>>();
602
+ const onWalk = new Map<Value, number>();
603
+ let open = false;
604
+ const walk = (x: Value, k: number): Spelled[] | null => {
605
+ // a literal however it was built, a join of one literal included
606
+ const literal = fold(x);
607
+ if (literal !== null) {
608
+ return [{ spelling: null, objects: [(literal + k) >>> 0], k: 0, fromMap: false }];
609
+ }
610
+ const disp = constDisplacementOf(defOf, x, fold);
611
+ const root = disp?.root ?? x;
612
+ const kk = k + (disp?.k ?? 0);
613
+ const d = defOf.get(root);
614
+ if (d?.opcode === 'gaddr') {
615
+ const sym = d.attrs.sym as string;
616
+ const at = places.get(sym);
617
+ if (at === undefined) {
618
+ return null;
619
+ }
620
+ const objects = typeof d.attrs.mapAddr === 'number' ? [d.attrs.mapAddr] : at.addrs;
621
+ return [{ spelling: at.macro ? null : sym, objects, k: kk, fromMap: d.attrs.fromMap === true }];
622
+ }
623
+ const ins = d === undefined ? incoming.get(root) : undefined;
624
+ if (ins === undefined) {
625
+ return null;
626
+ }
627
+ const known = memo.get(root)?.get(kk);
628
+ if (known !== undefined) {
629
+ return known;
630
+ }
631
+ const seenAt = onWalk.get(root);
632
+ if (seenAt !== undefined) {
633
+ open = true;
634
+ return seenAt === kk ? [] : null;
635
+ }
636
+ const outerOpen = open;
637
+ open = false;
638
+ onWalk.set(root, kk);
639
+ let out: Spelled[] | null = [];
640
+ for (const a of new Set(ins)) {
641
+ const each = walk(a, kk);
642
+ if (each === null) {
643
+ out = null;
644
+ break;
645
+ }
646
+ out.push(...each);
647
+ }
648
+ onWalk.delete(root);
649
+ if (!open) {
650
+ (memo.get(root) ?? memo.set(root, new Map()).get(root)!).set(kk, out);
651
+ }
652
+ open ||= outerOpen;
653
+ return out;
654
+ };
655
+ const all = walk(v, 0);
656
+ return all === null || all.length === 0 ? null : all;
657
+ }
658
+
659
+ /** Every spelling one memory access may be placed under, with the byte range at each base that
660
+ * spelling may denote; empty where its address is not placed ({@link spellingsOf}). */
661
+ function placeAccess(
662
+ op: Op,
663
+ defOf: Map<Value, Op>,
664
+ incoming: Map<Value, Value[]>,
665
+ places: Map<string, Places>,
666
+ fold: (v: Value) => number | null,
667
+ ): PlacedAccess[] {
668
+ let off: number;
669
+ let width: number;
670
+ if (op.opcode === 'load' || op.opcode === 'store') {
671
+ off = op.attrs.off as number;
672
+ width = op.attrs.width as number;
673
+ } else if (op.opcode === 'aload' || op.opcode === 'astore') {
674
+ const index = fold(op.operands[1]);
675
+ if (index === null) {
676
+ return [];
677
+ }
678
+ width = op.attrs.elemSize as number;
679
+ off = index * width;
680
+ } else {
681
+ return [];
682
+ }
683
+ const writes = op.opcode === 'store' || op.opcode === 'astore';
684
+ // A literal is compared as the UNSIGNED address the map keys by: MIPS and PPC materialize
685
+ // `0x80100000` as a negative 32-bit constant (`spellingsOf` takes `>>> 0`).
686
+ return (spellingsOf(op.operands[0], defOf, incoming, places, fold) ?? []).map((sp) => ({
687
+ ...sp,
688
+ op,
689
+ writes,
690
+ ranges: sp.objects.map((a) => [a + sp.k + off, a + sp.k + off + width] as const),
691
+ }));
692
+ }
693
+
694
+ /** The behaviours the pass reads: the declared-volatile stamp's, and whether this compiler RELOADS
695
+ * a cell stored through one spelling and read through another (`compilerBehaviors`
696
+ * `reloadsAcrossSpellings`; absent ⇒ it forwards). */
697
+ export type OffsetNameBehaviors = StampBehaviors & {
698
+ readonly reloadsAcrossSpellings?: boolean;
699
+ };
700
+
701
+ /** ONE SPELLING PER CELL, AND ONE CELL PER SPELLING — the premise `ir/alias.ts` reads names on:
702
+ * the first pair of this function's accesses that breaks it, or null when none does.
703
+ *
704
+ * ONE SPELLING PER CELL. Two different names are two objects to every C compiler, and so are a
705
+ * name and a numeric address: agbcc forwards a value stored through one past a store made
706
+ * through the other, where the machine code reads memory back (`sRandCount = 5; ((u8
707
+ * *)&sUnknown)[4] = 7; return sRandCount;` compiles to `mov r0, #5`). Several places decide a
708
+ * spelling — the pool promotion's per-address pick and its interior attribution, this pass's
709
+ * walk and the refusals that leave a walk as `base + K`, the raw constant where the map holds
710
+ * nothing — and none of them sees the others' accesses. So the question is asked once, after the
711
+ * last of them, over the BYTES each access touches rather than over the map's extents: a map that
712
+ * lays one object over another is harmless until two accesses spell one byte two ways, and an
713
+ * access walked past an object's declared end, or a raw constant beside an unsized name, reaches
714
+ * a byte twice with no overlap in the map at all. A pair declines when one of the two is a store
715
+ * (two LOADS leave no value to forward) and the compiler forwards across two spellings.
716
+ *
717
+ * Which compilers forward is a compiler fact, not a property of who chose the spelling. agbcc and
718
+ * gcc 2.7.2 forward, so an asm that went back to memory proves the source was NOT those two
719
+ * spellings, and every such pair declines. IDO 7.1 reloads (`reloadsAcrossSpellings`) across a
720
+ * name and a literal, two externs, two statics and an array element alike — but only a read that
721
+ * FOLLOWS the store: it treats two names as two objects, so a read of one placed BEFORE a byte
722
+ * store through the other is sunk past it (`v0 = D_80100004; ((u8 *)&D_80100000)[5] = 7; return
723
+ * v0;` is `sb t6, 5(at); jr ra; lw v0, 0(v1)`, the word read with byte 5 already 7). So the pair
724
+ * lifts there only in an order the compiler is measured to keep ({@link keptInOrder}): both in
725
+ * one block outside any loop, the store first — whether the asm spelled both (two relocations, a
726
+ * relocation and a literal) or asmlift chose one off the map (a promoted pool word, an interior
727
+ * attribution, a named walk: `gaddr`'s `fromMap`). Any other order declines.
728
+ *
729
+ * ONE CELL PER SPELLING. A name the map places at several addresses (one file static per unit)
730
+ * read off the map at two different ones is two objects the C would spell as one, which the
731
+ * compiler forwards across or reads once (`sFiller = 5; return sFiller;` where the asm reads
732
+ * another static's cell). It declines whenever one of the two is a map read — two relocations of
733
+ * one name are the asm's own one object — with or without a store.
734
+ *
735
+ * An address is placed through defs and joins ({@link spellingsOf}); one that reaches an access
736
+ * under several spellings (`c ? &a : &b`) is placed under each, so a pair is asked of every one.
737
+ * What this cannot place it cannot enforce: a lift with no map, or a name the map does not hold,
738
+ * has no address to compare, and an address the compiler cannot place either — a pointer
739
+ * parameter, a frame slot, a runtime index — is one it assumes may alias (agbcc reloads
740
+ * `sRandCount` after `((u8 *)&sUnknown)[a0] = 7`). */
741
+ export function cellSpelledTwice(fn: Fn, symbols: SymbolMap, reloadsAcrossSpellings = false): string | null {
742
+ const keptInOrder = reloadsAcrossSpellings ? keptInOrderOn(fn) : (): boolean => false;
743
+ const defOf = defOpMap(fn);
744
+ const incoming = joinIncoming(fn);
745
+ const places = placesByName(symbols);
746
+ const fold = constantFolder(defOf, incoming);
747
+ const placed: PlacedAccess[] = [];
748
+ for (const b of fn.blocks) {
749
+ for (const op of b.ops) {
750
+ placed.push(...placeAccess(op, defOf, incoming, places, fold));
751
+ }
752
+ }
753
+ const spell = (s: string | null): string => (s === null ? 'a numeric address' : `'${s}'`);
754
+ for (const [i, x] of placed.entries()) {
755
+ for (const y of placed.slice(i + 1)) {
756
+ if (x.op === y.op) {
757
+ continue;
758
+ }
759
+ if (x.spelling !== null && x.spelling === y.spelling) {
760
+ const oneObject = x.objects.length === 1 && y.objects.length === 1 && x.objects[0] === y.objects[0];
761
+ const several = (places.get(x.spelling)?.addrs.length ?? 0) > 1;
762
+ if (several && (x.fromMap || y.fromMap) && !oneObject) {
763
+ return `'${x.spelling}' names two of the cells the map places it at`;
764
+ }
765
+ continue;
766
+ }
767
+ if (x.spelling === y.spelling || !(x.writes || y.writes)) {
768
+ continue;
769
+ }
770
+ if (keptInOrder(x.op, y.op)) {
771
+ continue;
772
+ }
773
+ for (const [xl, xh] of x.ranges) {
774
+ for (const [yl, yh] of y.ranges) {
775
+ if (xl < yh && yl < xh) {
776
+ return `byte 0x${Math.max(xl, yl).toString(16)} is reached as ${spell(x.spelling)} and as ${spell(y.spelling)}`;
777
+ }
778
+ }
779
+ }
780
+ }
781
+ }
782
+ return null;
783
+ }
784
+
785
+ /** Whether a compiler that reloads across two spellings (`reloadsAcrossSpellings`) keeps the pair
786
+ * `a`, `b` (at least one a store) in the order the asm has, so the C reads back what the asm read.
787
+ * MEASURED on IDO 7.1 -O2: a store then a read through another name reloads, and two stores keep
788
+ * their order; a read then a store through another name does NOT — the read is sunk past the
789
+ * store. So the pair holds only when both sit in ONE block, the store first (or both stores), and
790
+ * that block is on no cycle: across blocks, or in a loop whose next iteration reads before this
791
+ * one's store, nothing measured says the read stays where the asm put it. */
792
+ function keptInOrderOn(fn: Fn): (a: Op, b: Op) => boolean {
793
+ const at = new Map<Op, { block: Block; index: number }>();
794
+ for (const block of fn.blocks) {
795
+ block.ops.forEach((op, index) => at.set(op, { block, index }));
796
+ }
797
+ const onCycle = (start: Block): boolean => {
798
+ const seen = new Set<Block>();
799
+ const work = successorsOf(start);
800
+ for (let b = work.pop(); b !== undefined; b = work.pop()) {
801
+ if (b === start) {
802
+ return true;
803
+ }
804
+ if (!seen.has(b)) {
805
+ seen.add(b);
806
+ work.push(...successorsOf(b));
807
+ }
808
+ }
809
+ return false;
810
+ };
811
+ const isStore = (op: Op): boolean => op.opcode === 'store' || op.opcode === 'astore';
812
+ return (a, b) => {
813
+ const pa = at.get(a);
814
+ const pb = at.get(b);
815
+ if (pa === undefined || pb === undefined || pa.block !== pb.block || onCycle(pa.block)) {
816
+ return false;
817
+ }
818
+ // one of the two is a store, so the pair is in order exactly when the first one is
819
+ return isStore(pa.index < pb.index ? a : b);
820
+ };
821
+ }
822
+
447
823
  /** Rewrite every admitted site into the `gaddr` the map names, in place. Returns THE NAMES it
448
824
  * spelled, in block order — what a caller has to be told, because a name this pass produced does
449
825
  * not rest on the map alone the way a pool-loaded one does: it rests on `addr(base) + K` landing
450
826
  * on the map entry it names. `DecompileResult.walkedNames` and the trace's `stage:offsetnames`
451
- * carry it out; nothing else can tell from a result that a walk was named. */
827
+ * carry it out; nothing else can tell from a result that a walk was named.
828
+ *
829
+ * It is also where the lift's names become FINAL, so it declines a function whose accesses reach
830
+ * one byte under two spellings ({@link cellSpelledTwice}), whichever pass named them. */
452
831
  export function nameOffsetAddresses(
453
832
  fn: Fn,
454
833
  symbols: SymbolMap,
455
- behaviors: StampBehaviors,
834
+ behaviors: OffsetNameBehaviors,
456
835
  gates: readonly Gate<OffsetAddress>[] = OFFSET_NAME_GATES,
457
836
  ): string[] {
458
837
  // Every site is judged against the PRE-REWRITE defs, so a chain's later links resolve through
@@ -461,7 +840,7 @@ export function nameOffsetAddresses(
461
840
  for (const s of admitted) {
462
841
  s.op.opcode = 'gaddr';
463
842
  s.op.operands = [];
464
- s.op.attrs = { sym: s.addr.target!.name };
843
+ s.op.attrs = { sym: s.addr.target!.name, fromMap: true, mapAddr: s.at >>> 0 };
465
844
  }
466
845
  if (admitted.length > 0) {
467
846
  // An access now names another object, so whether it reads one the map declares volatile is
@@ -470,6 +849,13 @@ export function nameOffsetAddresses(
470
849
  dce(fn); // the constants and the walked-off base the rewrite just orphaned
471
850
  verify(fn);
472
851
  }
852
+ const twice = cellSpelledTwice(fn, symbols, behaviors.reloadsAcrossSpellings === true);
853
+ if (twice !== null) {
854
+ throw new RaiseUnsupportedError(
855
+ `cannot lift '${fn.name}': ${twice} in one function — C the compiler would read as ` +
856
+ `${twice.startsWith('byte') ? 'two objects, forwarding a store through one past the other' : 'one object'}`,
857
+ );
858
+ }
473
859
  return admitted.map((s) => s.addr.target!.name);
474
860
  }
475
861
 
@@ -504,7 +890,7 @@ export function offsetNameRefusals(
504
890
  * pass up, which three hand-written calls do not — so `offset-names.test.ts` supplies it by
505
891
  * wrapping this record and asserting all three reach it. */
506
892
  export const OFFSET_NAME_PASS: {
507
- run: (fn: Fn, symbols: SymbolMap, behaviors: StampBehaviors) => string[];
893
+ run: (fn: Fn, symbols: SymbolMap, behaviors: OffsetNameBehaviors) => string[];
508
894
  } = {
509
895
  run: (fn, symbols, behaviors) => nameOffsetAddresses(fn, symbols, behaviors),
510
896
  };
@@ -14,6 +14,9 @@
14
14
  // 64-bit value; it is a word this function uses as a word, which is what a 64-bit argument passed
15
15
  // to a helper and separately tested would look like. Fusing it would delete a use.
16
16
  import { Fn, Op, Value } from '../ir/core';
17
+ import { intWidth } from '../ir/types';
18
+ import { type FnProto, declaredWidth } from '../proto';
19
+ import { RaiseUnsupportedError } from './errors';
17
20
 
18
21
  /** Count every operand and edge-argument occurrence of each value. Occurrences, not sites: a value
19
22
  * passed to `f(x, x)` is used twice, and a rule that said "used once" about it would be wrong. */
@@ -79,3 +82,33 @@ export function fuseParamPairs(fn: Fn): boolean {
79
82
  }
80
83
  return changed;
81
84
  }
85
+
86
+ /** A FUNCTION DECLARED WITH AN INTEGER PARAMETER WIDER THAN A REGISTER, whose lift reads that
87
+ * parameter's words apart, refuses — on every frontend, since this reads only the entry block
88
+ * `fuseParamPairs` leaves. The return's twin is pipeline.ts `refuseNarrowReturnsOfWideDeclaration`.
89
+ *
90
+ * The pair is fused only where the lift built its `concat` (a helper call, a carry chain); a body
91
+ * that tests or combines the two words itself (`return a != 0;` is `or` of the halves on every
92
+ * compiler) leaves two word parameters, and the lift prints `s32 f(s32 a0, s32 a1)` against the
93
+ * caller's own `int f(long long a)` — a candidate the compiler rejects as a redeclaration. It is
94
+ * correct C under the ABI, and the wrong function for the declaration in hand. Counting is
95
+ * enough: every fused parameter is wider than a register and only `fuseParamPairs` makes one, so
96
+ * fewer of them than the declaration names means a declared one was left in halves. A
97
+ * declaration the caller does not state (a context's own entry is left out,
98
+ * proto-context.ts `withContextPrototypes`) asks nothing here. */
99
+ export function refuseSplitWideParams(fn: Fn, self: FnProto | undefined): void {
100
+ const declared = Array.isArray(self?.params) ? self.params : [];
101
+ const wide = declared.filter((t) => (declaredWidth(t) ?? 0) > 32);
102
+ if (wide.length === 0) {
103
+ return;
104
+ }
105
+ const fused = fn.blocks[0].params.filter((p) => (intWidth(p.type) ?? 0) > 32).length;
106
+ if (fused < wide.length) {
107
+ const at = declared.findIndex((t) => (declaredWidth(t) ?? 0) > 32);
108
+ throw new RaiseUnsupportedError(
109
+ `cannot lift '${fn.name}': its declaration takes parameter ${at + 1} as '${declared[at]}', and this lift ` +
110
+ 'reads the two halves of a 64-bit value as two word parameters (a 64-bit parameter is one only where the ' +
111
+ 'lift pairs its halves)',
112
+ );
113
+ }
114
+ }