@asmlift/core 0.9.0 → 0.10.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 (101) 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 +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
package/src/l3/ast.ts CHANGED
@@ -9,7 +9,21 @@ export type Expr =
9
9
  // A `double` literal, carried as its IEEE bit pattern (ir/float-bits.ts) — a kind apart from the
10
10
  // integer `const`, so no integer fold, range test or signedness reading ever takes it for one.
11
11
  | { k: 'fconst'; bits: string }
12
- | { k: 'bin'; op: BinOp; l: Expr; r: Expr }
12
+ // `unsignedCmp` is EVIDENCE, not spelling: an ordering compare (`<` `<=` `>` `>=`) lowered from an
13
+ // `icmp_u*`, which C spells with the same token as the signed one and decides from the operand
14
+ // types. The printer never reads it; `signedOverUnsignedCompare` (l3/typing.ts) does, over the
15
+ // FINISHED tree, so a pass that substitutes into a compare cannot re-spell it signed unseen.
16
+ // `exprEquals` ignores it, as it ignores `index.operandOff`: two nodes that print alike are
17
+ // alike to a CSE. A merge that keeps one of two equal nodes keeps it through `unionEvidence`, so
18
+ // the evidence of the one it drops is not lost with it.
19
+ //
20
+ // `helperRel` is evidence too: on a float `==`/`!=`, the relation the SOURCE spelled, where the
21
+ // compiler picked a runtime helper by it (runtime-helpers.ts `RuntimeHelper.relation` — agbcc's
22
+ // `__eqsf2` and `__nesf2`). It rides through `negateCond` unchanged, so a node whose `op` is not
23
+ // its `helperRel` is one a negation moved off the helper, and `keepsHelperRel` /
24
+ // `regainsHelperRel` below are what a pass choosing between `if (c) A else B` and
25
+ // `if (!c) B else A` asks. Ignored by `exprEquals` and by the printer, like `unsignedCmp`.
26
+ | { k: 'bin'; op: BinOp; l: Expr; r: Expr; unsignedCmp?: true; helperRel?: BinOp }
13
27
  | { k: 'un'; op: '-' | '~' | '!' | 'f-'; e: Expr }
14
28
  // A post-increment `name++` (`by` 1) or post-decrement `name--` (`by` -1): the value the local
15
29
  // held BEFORE the update, with the update as a side effect. The only expression form here that
@@ -31,10 +45,10 @@ export type Expr =
31
45
  // A C-style value cast `(T)e`. Tree-level producers: a width-narrowing cast `(u8)e` (the
32
46
  // recovered form of a byte/half extend idiom — zext/sext IR ops), the STRUCT-pointer cast
33
47
  // (structure.ts memAccess/arrayAccess struct paths — see the note on `field` below), and the
34
- // integer legalization of a pointer operand under an operator C rejects (structure.ts intify,
35
- // `3 & (s32)p`). Scalar deref casts are backend-owned — the C-family printer synthesizes
36
- // them from the `index` node's width. Each backend spells the cast in its own syntax
37
- // (C: `(u8)e`; Pascal: no spelling yet → fails loud).
48
+ // integer legalization of a pointer operand under an operator C rejects (the arithmetic table,
49
+ // structure/pointer-spelling.ts: `3 & (s32)p`). Scalar deref casts are backend-owned — the
50
+ // C-family printer synthesizes them from the `index` node's width. Each backend spells the cast
51
+ // in its own syntax (C: `(u8)e`; Pascal: no spelling yet → fails loud).
38
52
  //
39
53
  // `volatile` qualifies the POINTEE of a pointer cast (`(volatile u16 *)0x4000208`) — the one
40
54
  // place asmlift can say "this access is to a volatile object" when there is no declaration to
@@ -62,13 +76,29 @@ export type Expr =
62
76
  // `sret` is the type of a struct the callee returns BY VALUE through memory. `args[0]` is then the
63
77
  // hidden pointer the frontend read from the first argument register — the address of the struct
64
78
  // the call fills — and the rest are the declared arguments. The only C spelling is a statement,
65
- // `dest = fn(rest)`, so such a call stands alone.
79
+ // `dest = fn(rest)`, so such a call stands alone. `returns` is the type of a struct the callee
80
+ // returns BY VALUE in registers, where the call is that value itself, `return mk(x);`
81
+ // (frontend/register-struct-call.ts); the declarations block prints its prototype by it.
82
+ //
83
+ // `byValue` names the arguments that are a struct passed BY VALUE — a local or a global spelled
84
+ // bare, `take(s)` — by their place in `args` and the declared parameter each is, with the struct
85
+ // the declaration passes
86
+ // (frontend/call-plan.ts `foldByValueArgs`); the declarations block defines each beside the
87
+ // callee's prototype.
66
88
  //
67
89
  // `fn` is the callee's NAME, or for an indirect call the EXPRESSION whose value is the address
68
90
  // called (ir/core.ts `calleeValue`) — a child of the node like its arguments, listed first in
69
91
  // `exprChildren`. That is no evaluation order: C leaves it unspecified, and agbcc loads the callee
70
92
  // after the arguments.
71
- | { k: 'call'; fn: string | Expr; args: Expr[]; wide64?: true; sret?: IrType }
93
+ | {
94
+ k: 'call';
95
+ fn: string | Expr;
96
+ args: Expr[];
97
+ wide64?: true;
98
+ sret?: IrType;
99
+ returns?: IrType;
100
+ byValue?: readonly { readonly at: number; readonly param: number; readonly type: IrType }[];
101
+ }
72
102
  // The ADDRESS of a named global, `&gSym` (agbcc pool `.word gSym`, frontend `gaddr` op). A
73
103
  // DEREF of it collapses to the bare global: memAccess/arrayAccess spell `*(&gSym)` as `gSym`
74
104
  // and `(&gSym)[i]` as `gSym[i]` (a global name decays to a pointer). Only a genuinely
@@ -192,6 +222,15 @@ export type Expr =
192
222
  operandOff?: number;
193
223
  baseOrdered?: true;
194
224
  baseAdvanced?: number;
225
+ /** Spell element 0 as `base[0]`, never `*base`. The two are one lvalue to C and NOT to
226
+ * agbcc's alias analysis: `u.half[0]` is an ARRAY_REF of a union member, alias set 0
227
+ * (c-common.c `c_get_alias_set`), while `*u.half` dereferences the member's decay — an
228
+ * INDIRECT_REF typed by its pointee, which a compiled `*u.half = 1; *u.word = x; return
229
+ * *u.half;` returns stale (`mov r0, #0x1`) where the subscript reloads. Set on a view of a
230
+ * frame union (structure.ts `frameUnionAccess`), the one access that rests on that set,
231
+ * and on an element of a frame array, where it only keeps the element spelling at index 0
232
+ * (structure.ts `frameArrayElement`, l3/constinit.ts `asElement`). */
233
+ subscript?: true;
195
234
  }
196
235
  // A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
197
236
  // pointer, so the byte offset resolves to a named field instead of a scaled array index).
@@ -226,9 +265,10 @@ export type Expr =
226
265
  // The divides earned it second, on pokeemerald:GetAnchorCoord — `(u32)(coord * a1) / (u32)a0`
227
266
  // standing beside two arithmetic shifts of the same values. Their only other channel is the operand
228
267
  // TYPES, reached by flipping a declaration, and there that flip is unreachable and unsound at
229
- // once: the divisor also feeds a signed compare, so the /uns-cmp reconciliation correctly refuses
230
- // it, and forcing it anyway makes agbcc delete the comparison as always-false. A per-operand pin is
231
- // the only spelling that says "this division alone is unsigned".
268
+ // once: the divisor also feeds a signed compare, so the unsigned-compare reconciliation
269
+ // (structure.ts unsignedCompareSpelling) correctly refuses it, and forcing it anyway makes agbcc
270
+ // delete the comparison as always-false. A per-operand pin is the only spelling that says "this
271
+ // division alone is unsigned".
232
272
  //
233
273
  // The COMPARISONS stay collapsed, and that asymmetry is the rule applying rather than an omission:
234
274
  // which side a compare was spelled from genuinely underdetermines — a signed spelling that
@@ -241,12 +281,20 @@ export type Expr =
241
281
  // and `+`/`-` are the pointer walks, reassociations and constant folds of a dozen L3 passes. A float
242
282
  // under any of those is a wrong program, so it gets operators none of them match — the same reason
243
283
  // the IR has `fadd` rather than a float-typed `add`. The unary minus splits the same way (`f-` on
244
- // `un`).
284
+ // `un`). The float COMPARES split for the same reason and one more: NaN makes them a partial order,
285
+ // so `negateCond` may flip `f==` and `f!=` into each other and no other — `!(a f> b)` is not
286
+ // `a f<= b`, and it prints as the `!` it is.
245
287
  export type BinOp =
246
288
  | 'f+'
247
289
  | 'f-'
248
290
  | 'f*'
249
291
  | 'f/'
292
+ | 'f<'
293
+ | 'f<='
294
+ | 'f>'
295
+ | 'f>='
296
+ | 'f=='
297
+ | 'f!='
250
298
  | '+'
251
299
  | '-'
252
300
  | '*'
@@ -296,7 +344,15 @@ export type Stmt =
296
344
  // `break;` / `continue;` — a loop early-exit / next-iteration jump. Emitted ONLY when the target is
297
345
  // the innermost enclosing loop (bare C break/continue cannot express a multi-level exit; a deeper
298
346
  // target declines). SGI/IDO Pascal has neither, so its backend loud-fails them (like `field`/`cast`).
299
- | { k: 'break' }
347
+ //
348
+ // `ofSwitch` makes the `break` the innermost enclosing SWITCH's instead: it leaves the arm it sits
349
+ // in for the code after the switch (`case 1: if (c) break; case 2: …`). Legal only inside a switch
350
+ // arm with no loop or switch between it and that arm — the producer's contract (structure.ts
351
+ // `switchBreakAt`). The C family prints both as `break;`, so its printer refuses each where C
352
+ // would bind it to the other kind — a plain one between `case` labels, this one in a loop body
353
+ // (backend/cfamily.ts `refuseMisboundBreak`). A pass that reads `break` as a loop exit must skip
354
+ // this one (l3/reindex.ts `hasLoopExit`).
355
+ | { k: 'break'; ofSwitch?: true }
300
356
  | { k: 'continue' }
301
357
  // A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
302
358
  // a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
@@ -399,6 +455,13 @@ export interface SFn {
399
455
  * one are deliberately left unstamped even where the offset is in hand — see the refusal at
400
456
  * the structurer's build site for the measurement that decided it. */
401
457
  slots?: number[];
458
+ /** The declaration's initializer, element by element, null where the source left one out
459
+ * (`SImage.init`) — present only on the copy the C-family emitter prints, where
460
+ * l3/constinit.ts folded the block copy of the compiler's anonymous constant image into it
461
+ * (`u8 sp0[16] = { … };`). Never on a tree a pass sees. */
462
+ init?: (number | InitAddress | null)[];
463
+ /** that initializer is spelled as a string literal — the image was written as one */
464
+ initString?: true;
402
465
  }[];
403
466
  /** project globals referenced with a known declaration shape (symbol map) — typed for the
404
467
  * legalization env (exprCType) but NEVER declared by a backend: the project's own headers
@@ -408,6 +471,11 @@ export interface SFn {
408
471
  * the top of its body ahead of the locals. Unlike `globals`, the backend declares these: no
409
472
  * header does, because the source defined them here. */
410
473
  statics?: SStatic[];
474
+ /** The compiler's own constant images this function copies a local's initializer from
475
+ * (structure/local-statics.ts), under labels the source never wrote. Not statics: no declaration
476
+ * may spell one, so the C-family emitter folds each into the initializer of the local its copy
477
+ * fills (l3/constinit.ts), and a backend that cannot refuses. */
478
+ images?: SImage[];
411
479
  retType: IrType;
412
480
  body: Stmt[];
413
481
  /** Struct types this function's fields reference, declared above it by the backend. Empty
@@ -426,31 +494,64 @@ export interface SFn {
426
494
  * and `l3/slotorder.ts` is the identity — never "ascending by default". */
427
495
  slotOrder?: 'ascending' | 'descending';
428
496
  /** Each declared callee's parameter types (an entry the printer cannot spell is `undefined`):
429
- * what an argument converts to. Read by the C-family printer where `dialect` says C++, which
430
- * refuses three conversions C makes implicitly (backend/cfamily.ts `argConversion`), and by the
431
- * pointer-cell conversions in every dialect (l3/ptrcell.ts). */
497
+ * what an argument converts to. Read by the C-family printer where `dialect` says C++ or
498
+ * `strictConversions` is set, either of which refuses three conversions C makes implicitly
499
+ * (backend/cfamily.ts `argConversion`), and by the pointer-cell conversions in every dialect
500
+ * (l3/ptrcell.ts). */
432
501
  declaredArgs?: Readonly<Record<string, readonly (string | undefined)[]>>;
433
502
  /** Each declared callee's return type as the project spells it, never printed: whether a call a
434
503
  * pointer cell is compared with returns a pointer (l3/ptrcell.ts). */
435
504
  declaredReturns?: Readonly<Record<string, string>>;
436
505
  /** The emitted C is compiled AS C++. An emission policy like `slotOrder`. */
437
506
  dialect?: 'c++';
507
+ /** The compiler parses a designated element in an initializer (`[8] = 2`) — a fact of its
508
+ * dialect, carried from `compilerBehaviors.designatedInitializers`. An emission policy: the
509
+ * C-family printer spells an initializer's left-out run by index where present, as zeros where
510
+ * not. */
511
+ designators?: true;
512
+ /** The compiler refuses C's implicit pointer conversions in C as well, as C++ does — carried from
513
+ * `compilerBehaviors.strictPointerConversions`. An emission policy: the C-family printer casts an
514
+ * argument to its declared parameter (`declaredArgs`), and a pointer of another pointee written
515
+ * into a pointer slot, wherever it would for C++. */
516
+ strictConversions?: true;
517
+ }
518
+
519
+ /** An initial element that is an ADDRESS CONSTANT (ir/core.ts `LocalObject.relocs`): `symbol`'s
520
+ * address plus `addend` bytes, held in the element's integer type — a global's, which a
521
+ * declaration outside the function names. */
522
+ export interface InitAddress {
523
+ symbol: string;
524
+ addend: number;
438
525
  }
439
526
 
440
527
  /** One function-scope static's definition: `static [const] T name[count] = { init };`. `init` is
441
- * the initial elements in order, absent for a static with no initializer (the target's bss).
442
- * `align` is an alignment in bytes wider than the type's own, which the definition must state. */
528
+ * the initial elements in order, absent for a static with no initializer (the target's bss); an
529
+ * element the target relocates is the {@link InitAddress} it holds. `align` is an alignment in
530
+ * bytes wider than the type's own, which the definition must state. */
443
531
  export interface SStatic {
444
532
  name: string;
445
533
  type: IrType;
446
534
  const?: true;
447
535
  align?: number;
448
- init?: number[];
536
+ init?: (number | InitAddress)[];
449
537
  /** the initializer is spelled as a string literal — a byte array the compiler aligns as a
450
538
  * string (structure/local-statics.ts) */
451
539
  string?: true;
452
540
  }
453
541
 
542
+ /** One constant image of the compiler's own (ir/core.ts `LocalObject.anonymous`), named by its
543
+ * label: an integer array typed by the directives that wrote it, and its initializer element by
544
+ * element — null where the source left one out (padding in the image, which C supplies as zero),
545
+ * the {@link InitAddress} an element the target relocates holds, and ending at the last one it
546
+ * spelled, so it may be shorter than `type`. */
547
+ export interface SImage {
548
+ name: string;
549
+ type: IrType;
550
+ init: (number | InitAddress | null)[];
551
+ /** the image was written as a string literal (`.ascii`) */
552
+ string?: true;
553
+ }
554
+
454
555
  /** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
455
556
  * type but lives in the neutral AST so a backend can print `struct N { ... };`. */
456
557
  export interface StructType {
@@ -471,6 +572,13 @@ export interface LanguageBackend {
471
572
  * cannot print it turns a whole function that used to decompile into a loud stub. The
472
573
  * structurer reads it through `StructureOptions.spellSwitchFallthrough`. */
473
574
  readonly spellsSwitchFallthrough: boolean;
575
+ /** Can this language spell a `break` that leaves a `switch` arm before its end
576
+ * (`{ k: 'break', ofSwitch: true }`)? C and C++ can; Pascal has no `break` at all, and its
577
+ * backend loud-fails the node. Asked for the same reason as `spellsSwitchFallthrough`: a
578
+ * closed arm with a mid-arm exit (`case 10: if (a) { if (b) break; g(1); } h(2); break;`) also
579
+ * spells without one, by copying the code the arm meets at into each path, which Pascal prints.
580
+ * The structurer reads it through `StructureOptions.spellSwitchBreak`. */
581
+ readonly spellsSwitchBreak: boolean;
474
582
  emit(fn: SFn): string;
475
583
  // Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
476
584
  // Used by the annotate-mode stub path to carry the failure reason + the original asm
@@ -560,6 +668,10 @@ export function exprEquals(a: Expr, b: Expr): boolean {
560
668
  (typeof a.fn === 'string' || typeof bb.fn === 'string' ? a.fn === bb.fn : exprEquals(a.fn, bb.fn)) &&
561
669
  (a.wide64 ?? false) === (bb.wide64 ?? false) &&
562
670
  (a.sret === undefined ? bb.sret === undefined : bb.sret !== undefined && typeEquals(a.sret, bb.sret)) &&
671
+ (a.returns === undefined
672
+ ? bb.returns === undefined
673
+ : bb.returns !== undefined && typeEquals(a.returns, bb.returns)) &&
674
+ (a.byValue ?? []).map((x) => x.at).join() === (bb.byValue ?? []).map((x) => x.at).join() &&
563
675
  a.args.length === bb.args.length &&
564
676
  a.args.every((x, i) => exprEquals(x, bb.args[i]))
565
677
  );
@@ -636,6 +748,60 @@ export function stmtsEqual(a: readonly Stmt[], b: readonly Stmt[]): boolean {
636
748
  return a.length === b.length && a.every((s, i) => stmtEquals(s, b[i]));
637
749
  }
638
750
 
751
+ /** The node a merge KEEPS when it found `a` and `b` equal ({@link exprEquals}) and drops one: `a`,
752
+ * with every compare carrying `unsignedCmp` if it did in EITHER. Equality ignores the evidence
753
+ * because two compares that print alike are one expression to C; but when one of them was lowered
754
+ * from an `icmp_u` and the other from a signed compare, the kept node now stands for both paths,
755
+ * and the finished-tree judgment (l3/typing.ts `signedOverUnsignedCompare`) can only see the
756
+ * unsigned one's obligation if the node still says so. Keeping `a` bare would publish a signed
757
+ * `x < y` on the path that compared unsigned (an slt arm and an sltu arm merged by l3/tailmerge.ts
758
+ * under `/signed-cmp`). Precondition: `exprEquals(a, b)`, so the two trees walk in step. */
759
+ export function unionEvidence(a: Expr, b: Expr): Expr {
760
+ // `mapExprChildren` calls `f` in `exprChildren` order (an object literal evaluates its
761
+ // properties in source order), so the i-th call pairs with b's i-th child. Not by identity: one
762
+ // child object may sit in two positions, each with its own counterpart in `b`.
763
+ const bs = exprChildren(b);
764
+ let i = 0;
765
+ const out = mapExprChildren(a, (c) => unionEvidence(c, bs[i++]));
766
+ return out.k === 'bin' && b.k === 'bin' && b.unsignedCmp === true && out.unsignedCmp !== true
767
+ ? { ...out, unsignedCmp: true }
768
+ : out;
769
+ }
770
+
771
+ /** {@link unionEvidence} over two statements {@link stmtEquals} found equal. */
772
+ export function unionStmtEvidence(a: Stmt, b: Stmt): Stmt {
773
+ switch (a.k) {
774
+ case 'assign':
775
+ return b.k === 'assign' ? { ...a, value: unionEvidence(a.value, b.value) } : a;
776
+ case 'store':
777
+ return b.k === 'store'
778
+ ? { ...a, lval: unionEvidence(a.lval, b.lval), value: unionEvidence(a.value, b.value) }
779
+ : a;
780
+ case 'exprstmt':
781
+ return b.k === 'exprstmt' ? { ...a, value: unionEvidence(a.value, b.value) } : a;
782
+ case 'return':
783
+ return b.k === 'return' && a.value !== undefined && b.value !== undefined
784
+ ? { ...a, value: unionEvidence(a.value, b.value) }
785
+ : a;
786
+ case 'if':
787
+ return b.k === 'if'
788
+ ? {
789
+ ...a,
790
+ cond: unionEvidence(a.cond, b.cond),
791
+ then: unionStmtsEvidence(a.then, b.then),
792
+ else: unionStmtsEvidence(a.else, b.else),
793
+ }
794
+ : a;
795
+ default:
796
+ return a;
797
+ }
798
+ }
799
+
800
+ /** {@link unionStmtEvidence} over two lists {@link stmtsEqual} found equal. */
801
+ export function unionStmtsEvidence(a: readonly Stmt[], b: readonly Stmt[]): Stmt[] {
802
+ return a.map((s, i) => unionStmtEvidence(s, b[i]));
803
+ }
804
+
639
805
  /** THE spelling of an `opaque`'s gap reason, in one place: `structure.ts` writes it into the
640
806
  * marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
641
807
  * classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
@@ -875,6 +1041,44 @@ function* subterms(e: Expr): Generator<Expr> {
875
1041
  }
876
1042
  }
877
1043
 
1044
+ export type Assign = Extract<Stmt, { k: 'assign' }>;
1045
+
1046
+ /** Every `assign` in the tree, at any nesting. ORDER-FREE: its callers ask what a name is ever
1047
+ * assigned, never which assignment comes first, so a switch default's position does not matter. */
1048
+ export function assignsIn(body: readonly Stmt[]): Assign[] {
1049
+ const out: Assign[] = [];
1050
+ const visit = (s: Stmt): void => {
1051
+ if (s.k === 'assign') {
1052
+ out.push(s);
1053
+ }
1054
+ stmtChildren(s).forEach(visit);
1055
+ };
1056
+ body.forEach(visit);
1057
+ return out;
1058
+ }
1059
+
1060
+ /** Grow `seed` over assignments to a fixpoint: an assignment adds its target when `propagates`
1061
+ * says its value carries what the set holds so far. A FLOW-INSENSITIVE taint, which is what each
1062
+ * caller wants — the question is what a name may EVER hold, so one assignment anywhere is
1063
+ * enough. Callers own the seed and the rule; this owns only the iteration. */
1064
+ export function taintOverAssigns(
1065
+ assigns: readonly Assign[],
1066
+ seed: Iterable<string>,
1067
+ propagates: (a: Assign, tainted: ReadonlySet<string>) => boolean,
1068
+ ): Set<string> {
1069
+ const tainted = new Set(seed);
1070
+ for (let grew = true; grew;) {
1071
+ grew = false;
1072
+ for (const a of assigns) {
1073
+ if (!tainted.has(a.name) && propagates(a, tainted)) {
1074
+ tainted.add(a.name);
1075
+ grew = true;
1076
+ }
1077
+ }
1078
+ }
1079
+ return tainted;
1080
+ }
1081
+
878
1082
  /** The statements a statement DIRECTLY contains, in the order a backend prints them — a `switch`
879
1083
  * splices its default in at `defaultAt` for that reason. NOTE for document-order walks: a `for`'s
880
1084
  * init/inc are listed here while its cond is in stmtExprs — a walker visiting exprs-then-stmts
@@ -1077,8 +1281,76 @@ export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
1077
1281
  '<=': '>',
1078
1282
  '==': '!=',
1079
1283
  '!=': '==',
1284
+ // C's float `==` is false on a NaN and `!=` true, so these two are each other's negation; the
1285
+ // ordered four have none in the operator vocabulary (the BinOp note).
1286
+ 'f==': 'f!=',
1287
+ 'f!=': 'f==',
1080
1288
  };
1081
1289
 
1290
+ /** The compares under `e`'s connectives whose runtime helper names the relation the source spelled
1291
+ * (`helperRel`), each answering whether it spells that relation now. `negateCond` flips every one
1292
+ * of them at once, through De Morgan. */
1293
+ function helperRelLeaves(e: Expr, out: boolean[] = []): boolean[] {
1294
+ if (e.k === 'bin' && (e.op === '&&' || e.op === '||')) {
1295
+ helperRelLeaves(e.l, out);
1296
+ helperRelLeaves(e.r, out);
1297
+ } else if (e.k === 'bin' && e.helperRel !== undefined) {
1298
+ out.push(e.op === e.helperRel);
1299
+ }
1300
+ return out;
1301
+ }
1302
+
1303
+ /** Whether `e` spells, at every compare of it whose runtime helper names the relation the source
1304
+ * spelled, that relation — so that negating `e` recompiles to the other helper at each of them:
1305
+ * the relation's twin and its `!` alike, since gcc folds `!(a == b)` into `a != b`. An arm swap
1306
+ * that would negate it keeps `e` and its arm order instead. */
1307
+ export function keepsHelperRel(e: Expr): boolean {
1308
+ const leaves = helperRelLeaves(e);
1309
+ return leaves.length > 0 && leaves.every((own) => own);
1310
+ }
1311
+
1312
+ /** Whether negating `e` puts every such compare back on its helper's relation: the swap a pass
1313
+ * would otherwise leave alone is the one that spells the helpers. A condition mixing the two has
1314
+ * neither answer, and no arm order spells it. */
1315
+ export function regainsHelperRel(e: Expr): boolean {
1316
+ const leaves = helperRelLeaves(e);
1317
+ return leaves.length > 0 && leaves.every((own) => !own);
1318
+ }
1319
+
1320
+ /** A loop's stay condition `P && S` whose compares under `S` are all negated off their helper's
1321
+ * relation, and whose `P` negates none: `{ head: P, guard: !S }`, which the loop spells
1322
+ * `while (P) { if (!S) break; … }` — the same tests in the same order, calling the helpers the
1323
+ * source called. `while (P && S)` has no spelling of that `S` that calls the helper (gcc folds
1324
+ * `!(a == b)` into `a != b`); the break guard is how the source wrote it before the branch fold
1325
+ * joined the two tests. Null when the condition already spells every helper's relation, or when no
1326
+ * split of its `&&` chain separates the two kinds of compare. */
1327
+ export function splitHelperBreak(cond: Expr): { head: Expr; guard: Expr } | null {
1328
+ if (keepsHelperRel(cond) || helperRelLeaves(cond).length === 0) {
1329
+ return null;
1330
+ }
1331
+ const conj: Expr[] = [];
1332
+ const flatten = (e: Expr): void => {
1333
+ if (e.k === 'bin' && e.op === '&&') {
1334
+ flatten(e.l);
1335
+ flatten(e.r);
1336
+ } else {
1337
+ conj.push(e);
1338
+ }
1339
+ };
1340
+ flatten(cond);
1341
+ const and = (xs: Expr[]): Expr => xs.reduce((l, r) => ({ k: 'bin', op: '&&', l, r }));
1342
+ // The longest prefix that negates no helper leaf; the rest must negate every one it holds.
1343
+ let k = 0;
1344
+ while (k < conj.length && helperRelLeaves(conj[k]).every((own) => own)) {
1345
+ k++;
1346
+ }
1347
+ if (k === 0 || k === conj.length) {
1348
+ return null;
1349
+ }
1350
+ const rest = and(conj.slice(k));
1351
+ return regainsHelperRel(rest) ? { head: and(conj.slice(0, k)), guard: negateCond(rest) } : null;
1352
+ }
1353
+
1082
1354
  export function negateCond(e: Expr): Expr {
1083
1355
  if (e.k === 'bin') {
1084
1356
  const flipped = NEGATE_REL[e.op];
package/src/l3/basecse.ts CHANGED
@@ -248,15 +248,35 @@ function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
248
248
  }
249
249
  }
250
250
 
251
- /** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local. */
252
- function rewrite(e: Expr, localFor: Map<string, string>): Expr {
251
+ /** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local.
252
+ *
253
+ * A `volatile` cast over a hoisted LEAF keeps its cast and takes the local under it:
254
+ * `((volatile s32 *)0x40000D4)[2]` beside a hoisted `p0 = (s32 *)0x40000D4` becomes
255
+ * `((volatile s32 *)p0)[2]`. The cast is never hoisted itself (`isHoistableBase`), since the local
256
+ * would drop its qualifier; under it the local is the same address, so the access keeps its
257
+ * qualifier and its cell, and reads the register the compiler already holds the base in. Inline, a
258
+ * qualified access at a nonzero offset makes agbcc materialize a second literal with the offset
259
+ * folded in (`.word 0x40000dc` + `ldr r0, [r0]`) where the source read `[r4, #0x8]` off the base
260
+ * it had loaded — the `dmaRegs[2];` that ends a DMA macro. `leafLocal` maps a hoisted leaf to its
261
+ * local, whichever key hoisted it. */
262
+ function rewrite(e: Expr, localFor: Map<string, string>, leafLocal: ReadonlyMap<string, string>): Expr {
253
263
  if (e.k === 'index' && isHoistableBase(e.base)) {
254
264
  const nm = localFor.get(keyOf(e.base, e.width, e.signed));
255
265
  if (nm) {
256
- return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor) };
266
+ return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor, leafLocal) };
267
+ }
268
+ }
269
+ if (e.k === 'index' && e.base.k === 'cast' && e.base.volatile === true && isHoistableLeaf(e.base.e)) {
270
+ const nm = leafLocal.get(leafId(e.base.e));
271
+ if (nm) {
272
+ return {
273
+ ...e,
274
+ base: { ...e.base, e: { k: 'var', name: nm } },
275
+ idx: rewrite(e.idx, localFor, leafLocal),
276
+ };
257
277
  }
258
278
  }
259
- return mapExprChildren(e, (c) => rewrite(c, localFor));
279
+ return mapExprChildren(e, (c) => rewrite(c, localFor, leafLocal));
260
280
  }
261
281
 
262
282
  /** One base under consideration, keyed as `(base, width, signedness)`. */
@@ -784,7 +804,15 @@ export function hoistBaseLocals(
784
804
  });
785
805
  }
786
806
 
787
- const rewritten = sfn.body.map((s) => mapStmtExprs(s, (e) => rewrite(e, localFor)));
807
+ const leafLocal = new Map<string, string>();
808
+ for (const k of hoisted) {
809
+ const b = meta.get(k)!.base;
810
+ const leaf = leafId(b.k === 'cast' ? b.e : b);
811
+ if (!leafLocal.has(leaf)) {
812
+ leafLocal.set(leaf, localFor.get(k)!);
813
+ }
814
+ }
815
+ const rewritten = sfn.body.map((s) => mapStmtExprs(s, (e) => rewrite(e, localFor, leafLocal)));
788
816
  // The new inits join the tree's LEADING run of base inits rather than being prepended above it:
789
817
  // when rank's /livebase re-runs this pass the head already carries the default run's, and
790
818
  // blindly prepending would spell the new base's pool load above locals the compiler loads first.