@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
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 `bareArrayIndex`, 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,8 +344,44 @@ 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
+ //
356
+ // `label` names a switch-scoped `break` a case-end `goto` lands on (`L: break;`, see `goto`).
357
+ | { k: 'break'; ofSwitch?: true; label?: string }
300
358
  | { k: 'continue' }
359
+ // `goto <label>;` — L3 HOLDS TWO GOTOS, both narrow (docs/asmlift-101.md §2.7).
360
+ //
361
+ // A CASE-LABEL goto jumps to a case label of the switch it sits in, written where two or more arms
362
+ // enter one sibling arm's body and only one of them can reach it by falling through (`case 1: if
363
+ // (c) continue; goto L; case 2: … case 3: L: …`). `label` names the target arm's
364
+ // `SwitchCase.label`. It is the LAST statement of a case or default body, at that body's top
365
+ // level, and its label is a case of THAT switch — so no loop or nested switch stands between the
366
+ // jump and its target, and the jump runs exactly what a fall-through into that arm would run.
367
+ //
368
+ // A CASE-END goto is the next kind, `caseEndGoto`. Both are checked by `caseGotoViolation`
369
+ // (contracts.ts `assertCaseGotosNarrow`, which every structured and respelled tree passes). Any
370
+ // other goto is not representable. A goto ends its path like `break` and `continue`. SGI/IDO
371
+ // Pascal has no goto into a `case-of` arm, so its backend loud-fails it.
372
+ | { k: 'goto'; label: string }
373
+ // `goto <label>;` — THE CASE-END GOTO, the second goto L3 holds, a jump to the end of the arm it
374
+ // sits in: `label` names a labelled switch-scoped `break` (`L: break;`) that is an element of a
375
+ // statement list enclosing the goto, later in that list than the statement holding it, with no
376
+ // switch between the two and no loop around the label inside the arm — so the jump leaves every
377
+ // loop it sits in and then the switch, and runs nothing on the way. It is the source's `for (j…)
378
+ // { if (hit) { …; goto L; } } k(i); L: break;`, which agbcc threads past the loop around the
379
+ // switch (jump.c `follow_jumps`). Its own kind, not a flag on `goto`, so every pass that reads a
380
+ // `goto` as the jump into a case arm (`switchArmSuccessors`, `switchJumpsBack`) never meets one,
381
+ // and every exhaustive switch over `Stmt` decides it. It ends its path, and leaves the innermost
382
+ // switch around it. Written only by l3/caseendgoto.ts. SGI/IDO Pascal has no `break`, so its
383
+ // backend loud-fails it.
384
+ | { k: 'caseEndGoto'; label: string }
301
385
  // A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
302
386
  // a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
303
387
  // is emitted after `defaultAt` of them, or after all of them when that is absent.
@@ -342,6 +426,10 @@ export interface SwitchCase {
342
426
  values: number[];
343
427
  body: Stmt[];
344
428
  fallsThrough: boolean;
429
+ /** The C label printed after this arm's `case` labels, present only where a `goto` of this same
430
+ * switch names it (see the `goto` statement). A pass that merges, drops or reorders arms keeps it
431
+ * on the arm it labels. */
432
+ label?: string;
345
433
  }
346
434
 
347
435
  export interface SFn {
@@ -399,6 +487,13 @@ export interface SFn {
399
487
  * one are deliberately left unstamped even where the offset is in hand — see the refusal at
400
488
  * the structurer's build site for the measurement that decided it. */
401
489
  slots?: number[];
490
+ /** The declaration's initializer, element by element, null where the source left one out
491
+ * (`SImage.init`) — present only on the copy the C-family emitter prints, where
492
+ * l3/constinit.ts folded the block copy of the compiler's anonymous constant image into it
493
+ * (`u8 sp0[16] = { … };`). Never on a tree a pass sees. */
494
+ init?: (number | InitAddress | null)[];
495
+ /** that initializer is spelled as a string literal — the image was written as one */
496
+ initString?: true;
402
497
  }[];
403
498
  /** project globals referenced with a known declaration shape (symbol map) — typed for the
404
499
  * legalization env (exprCType) but NEVER declared by a backend: the project's own headers
@@ -408,6 +503,11 @@ export interface SFn {
408
503
  * the top of its body ahead of the locals. Unlike `globals`, the backend declares these: no
409
504
  * header does, because the source defined them here. */
410
505
  statics?: SStatic[];
506
+ /** The compiler's own constant images this function copies a local's initializer from
507
+ * (structure/local-statics.ts), under labels the source never wrote. Not statics: no declaration
508
+ * may spell one, so the C-family emitter folds each into the initializer of the local its copy
509
+ * fills (l3/constinit.ts), and a backend that cannot refuses. */
510
+ images?: SImage[];
411
511
  retType: IrType;
412
512
  body: Stmt[];
413
513
  /** Struct types this function's fields reference, declared above it by the backend. Empty
@@ -426,31 +526,64 @@ export interface SFn {
426
526
  * and `l3/slotorder.ts` is the identity — never "ascending by default". */
427
527
  slotOrder?: 'ascending' | 'descending';
428
528
  /** 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). */
529
+ * what an argument converts to. Read by the C-family printer where `dialect` says C++ or
530
+ * `strictConversions` is set, either of which refuses three conversions C makes implicitly
531
+ * (backend/cfamily.ts `argConversion`), and by the pointer-cell conversions in every dialect
532
+ * (l3/ptrcell.ts). */
432
533
  declaredArgs?: Readonly<Record<string, readonly (string | undefined)[]>>;
433
534
  /** Each declared callee's return type as the project spells it, never printed: whether a call a
434
535
  * pointer cell is compared with returns a pointer (l3/ptrcell.ts). */
435
536
  declaredReturns?: Readonly<Record<string, string>>;
436
537
  /** The emitted C is compiled AS C++. An emission policy like `slotOrder`. */
437
538
  dialect?: 'c++';
539
+ /** The compiler parses a designated element in an initializer (`[8] = 2`) — a fact of its
540
+ * dialect, carried from `compilerBehaviors.designatedInitializers`. An emission policy: the
541
+ * C-family printer spells an initializer's left-out run by index where present, as zeros where
542
+ * not. */
543
+ designators?: true;
544
+ /** The compiler refuses C's implicit pointer conversions in C as well, as C++ does — carried from
545
+ * `compilerBehaviors.strictPointerConversions`. An emission policy: the C-family printer casts an
546
+ * argument to its declared parameter (`declaredArgs`), and a pointer of another pointee written
547
+ * into a pointer slot, wherever it would for C++. */
548
+ strictConversions?: true;
549
+ }
550
+
551
+ /** An initial element that is an ADDRESS CONSTANT (ir/core.ts `LocalObject.relocs`): `symbol`'s
552
+ * address plus `addend` bytes, held in the element's integer type — a global's, which a
553
+ * declaration outside the function names. */
554
+ export interface InitAddress {
555
+ symbol: string;
556
+ addend: number;
438
557
  }
439
558
 
440
559
  /** 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. */
560
+ * the initial elements in order, absent for a static with no initializer (the target's bss); an
561
+ * element the target relocates is the {@link InitAddress} it holds. `align` is an alignment in
562
+ * bytes wider than the type's own, which the definition must state. */
443
563
  export interface SStatic {
444
564
  name: string;
445
565
  type: IrType;
446
566
  const?: true;
447
567
  align?: number;
448
- init?: number[];
568
+ init?: (number | InitAddress)[];
449
569
  /** the initializer is spelled as a string literal — a byte array the compiler aligns as a
450
570
  * string (structure/local-statics.ts) */
451
571
  string?: true;
452
572
  }
453
573
 
574
+ /** One constant image of the compiler's own (ir/core.ts `LocalObject.anonymous`), named by its
575
+ * label: an integer array typed by the directives that wrote it, and its initializer element by
576
+ * element — null where the source left one out (padding in the image, which C supplies as zero),
577
+ * the {@link InitAddress} an element the target relocates holds, and ending at the last one it
578
+ * spelled, so it may be shorter than `type`. */
579
+ export interface SImage {
580
+ name: string;
581
+ type: IrType;
582
+ init: (number | InitAddress | null)[];
583
+ /** the image was written as a string literal (`.ascii`) */
584
+ string?: true;
585
+ }
586
+
454
587
  /** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
455
588
  * type but lives in the neutral AST so a backend can print `struct N { ... };`. */
456
589
  export interface StructType {
@@ -471,6 +604,19 @@ export interface LanguageBackend {
471
604
  * cannot print it turns a whole function that used to decompile into a loud stub. The
472
605
  * structurer reads it through `StructureOptions.spellSwitchFallthrough`. */
473
606
  readonly spellsSwitchFallthrough: boolean;
607
+ /** Can this language spell a `break` that leaves a `switch` arm before its end
608
+ * (`{ k: 'break', ofSwitch: true }`)? C and C++ can; Pascal has no `break` at all, and its
609
+ * backend loud-fails the node. Asked for the same reason as `spellsSwitchFallthrough`: a
610
+ * closed arm with a mid-arm exit (`case 10: if (a) { if (b) break; g(1); } h(2); break;`) also
611
+ * spells without one, by copying the code the arm meets at into each path, which Pascal prints.
612
+ * The structurer reads it through `StructureOptions.spellSwitchBreak`. */
613
+ readonly spellsSwitchBreak: boolean;
614
+ /** Can this language spell a `return` before the end of the function? C and C++ can; Pascal
615
+ * cannot, and its backend loud-fails one. Asked for the same reason again: the join past early
616
+ * returns ends each arm that leaves early in a `return`, and the same branch also spells by
617
+ * copying the code below it into each arm (the return-tail copies), which Pascal prints. The
618
+ * structurer reads it through `StructureOptions.spellEarlyReturns`. */
619
+ readonly spellsEarlyReturn: boolean;
474
620
  emit(fn: SFn): string;
475
621
  // Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
476
622
  // Used by the annotate-mode stub path to carry the failure reason + the original asm
@@ -560,6 +706,10 @@ export function exprEquals(a: Expr, b: Expr): boolean {
560
706
  (typeof a.fn === 'string' || typeof bb.fn === 'string' ? a.fn === bb.fn : exprEquals(a.fn, bb.fn)) &&
561
707
  (a.wide64 ?? false) === (bb.wide64 ?? false) &&
562
708
  (a.sret === undefined ? bb.sret === undefined : bb.sret !== undefined && typeEquals(a.sret, bb.sret)) &&
709
+ (a.returns === undefined
710
+ ? bb.returns === undefined
711
+ : bb.returns !== undefined && typeEquals(a.returns, bb.returns)) &&
712
+ (a.byValue ?? []).map((x) => x.at).join() === (bb.byValue ?? []).map((x) => x.at).join() &&
563
713
  a.args.length === bb.args.length &&
564
714
  a.args.every((x, i) => exprEquals(x, bb.args[i]))
565
715
  );
@@ -636,6 +786,60 @@ export function stmtsEqual(a: readonly Stmt[], b: readonly Stmt[]): boolean {
636
786
  return a.length === b.length && a.every((s, i) => stmtEquals(s, b[i]));
637
787
  }
638
788
 
789
+ /** The node a merge KEEPS when it found `a` and `b` equal ({@link exprEquals}) and drops one: `a`,
790
+ * with every compare carrying `unsignedCmp` if it did in EITHER. Equality ignores the evidence
791
+ * because two compares that print alike are one expression to C; but when one of them was lowered
792
+ * from an `icmp_u` and the other from a signed compare, the kept node now stands for both paths,
793
+ * and the finished-tree judgment (l3/typing.ts `signedOverUnsignedCompare`) can only see the
794
+ * unsigned one's obligation if the node still says so. Keeping `a` bare would publish a signed
795
+ * `x < y` on the path that compared unsigned (an slt arm and an sltu arm merged by l3/tailmerge.ts
796
+ * under `/signed-cmp`). Precondition: `exprEquals(a, b)`, so the two trees walk in step. */
797
+ export function unionEvidence(a: Expr, b: Expr): Expr {
798
+ // `mapExprChildren` calls `f` in `exprChildren` order (an object literal evaluates its
799
+ // properties in source order), so the i-th call pairs with b's i-th child. Not by identity: one
800
+ // child object may sit in two positions, each with its own counterpart in `b`.
801
+ const bs = exprChildren(b);
802
+ let i = 0;
803
+ const out = mapExprChildren(a, (c) => unionEvidence(c, bs[i++]));
804
+ return out.k === 'bin' && b.k === 'bin' && b.unsignedCmp === true && out.unsignedCmp !== true
805
+ ? { ...out, unsignedCmp: true }
806
+ : out;
807
+ }
808
+
809
+ /** {@link unionEvidence} over two statements {@link stmtEquals} found equal. */
810
+ export function unionStmtEvidence(a: Stmt, b: Stmt): Stmt {
811
+ switch (a.k) {
812
+ case 'assign':
813
+ return b.k === 'assign' ? { ...a, value: unionEvidence(a.value, b.value) } : a;
814
+ case 'store':
815
+ return b.k === 'store'
816
+ ? { ...a, lval: unionEvidence(a.lval, b.lval), value: unionEvidence(a.value, b.value) }
817
+ : a;
818
+ case 'exprstmt':
819
+ return b.k === 'exprstmt' ? { ...a, value: unionEvidence(a.value, b.value) } : a;
820
+ case 'return':
821
+ return b.k === 'return' && a.value !== undefined && b.value !== undefined
822
+ ? { ...a, value: unionEvidence(a.value, b.value) }
823
+ : a;
824
+ case 'if':
825
+ return b.k === 'if'
826
+ ? {
827
+ ...a,
828
+ cond: unionEvidence(a.cond, b.cond),
829
+ then: unionStmtsEvidence(a.then, b.then),
830
+ else: unionStmtsEvidence(a.else, b.else),
831
+ }
832
+ : a;
833
+ default:
834
+ return a;
835
+ }
836
+ }
837
+
838
+ /** {@link unionStmtEvidence} over two lists {@link stmtsEqual} found equal. */
839
+ export function unionStmtsEvidence(a: readonly Stmt[], b: readonly Stmt[]): Stmt[] {
840
+ return a.map((s, i) => unionStmtEvidence(s, b[i]));
841
+ }
842
+
639
843
  /** THE spelling of an `opaque`'s gap reason, in one place: `structure.ts` writes it into the
640
844
  * marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
641
845
  * classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
@@ -730,6 +934,8 @@ export function stmtExprs(s: Stmt): Expr[] {
730
934
  return [s.scrutinee];
731
935
  case 'break':
732
936
  case 'continue':
937
+ case 'goto':
938
+ case 'caseEndGoto':
733
939
  return [];
734
940
  }
735
941
  }
@@ -769,6 +975,8 @@ export function mapStmtExprs(s: Stmt, f: (e: Expr) => Expr): Stmt {
769
975
  };
770
976
  case 'break':
771
977
  case 'continue':
978
+ case 'goto':
979
+ case 'caseEndGoto':
772
980
  return s;
773
981
  }
774
982
  }
@@ -875,6 +1083,44 @@ function* subterms(e: Expr): Generator<Expr> {
875
1083
  }
876
1084
  }
877
1085
 
1086
+ export type Assign = Extract<Stmt, { k: 'assign' }>;
1087
+
1088
+ /** Every `assign` in the tree, at any nesting. ORDER-FREE: its callers ask what a name is ever
1089
+ * assigned, never which assignment comes first, so a switch default's position does not matter. */
1090
+ export function assignsIn(body: readonly Stmt[]): Assign[] {
1091
+ const out: Assign[] = [];
1092
+ const visit = (s: Stmt): void => {
1093
+ if (s.k === 'assign') {
1094
+ out.push(s);
1095
+ }
1096
+ stmtChildren(s).forEach(visit);
1097
+ };
1098
+ body.forEach(visit);
1099
+ return out;
1100
+ }
1101
+
1102
+ /** Grow `seed` over assignments to a fixpoint: an assignment adds its target when `propagates`
1103
+ * says its value carries what the set holds so far. A FLOW-INSENSITIVE taint, which is what each
1104
+ * caller wants — the question is what a name may EVER hold, so one assignment anywhere is
1105
+ * enough. Callers own the seed and the rule; this owns only the iteration. */
1106
+ export function taintOverAssigns(
1107
+ assigns: readonly Assign[],
1108
+ seed: Iterable<string>,
1109
+ propagates: (a: Assign, tainted: ReadonlySet<string>) => boolean,
1110
+ ): Set<string> {
1111
+ const tainted = new Set(seed);
1112
+ for (let grew = true; grew;) {
1113
+ grew = false;
1114
+ for (const a of assigns) {
1115
+ if (!tainted.has(a.name) && propagates(a, tainted)) {
1116
+ tainted.add(a.name);
1117
+ grew = true;
1118
+ }
1119
+ }
1120
+ }
1121
+ return tainted;
1122
+ }
1123
+
878
1124
  /** The statements a statement DIRECTLY contains, in the order a backend prints them — a `switch`
879
1125
  * splices its default in at `defaultAt` for that reason. NOTE for document-order walks: a `for`'s
880
1126
  * init/inc are listed here while its cond is in stmtExprs — a walker visiting exprs-then-stmts
@@ -887,6 +1133,8 @@ export function stmtChildren(s: Stmt): Stmt[] {
887
1133
  case 'return':
888
1134
  case 'break':
889
1135
  case 'continue':
1136
+ case 'goto':
1137
+ case 'caseEndGoto':
890
1138
  return [];
891
1139
  case 'if':
892
1140
  return [...s.then, ...s.else];
@@ -937,10 +1185,254 @@ export function isLoop(s: Stmt): s is Loop {
937
1185
  case 'return':
938
1186
  case 'break':
939
1187
  case 'continue':
1188
+ case 'goto':
1189
+ case 'caseEndGoto':
940
1190
  return false;
941
1191
  }
942
1192
  }
943
1193
 
1194
+ /** Why `body` breaks the two forms a `goto` may take in L3 (the `goto` statement's contract), or
1195
+ * null when every `goto` in it keeps one. A case-label goto is the LAST statement of a case or
1196
+ * default body, at that body's top level, naming a label one case of THAT switch carries. A
1197
+ * case-end goto (`caseEndGoto`) names a labelled switch-scoped `break` later in a statement list that
1198
+ * encloses it, inside the same switch arm, with no switch between them; ifs and loops may stand
1199
+ * between, but no loop may stand between the labelled `break` and its arm. Labels are unique
1200
+ * across the function, case labels and `break` labels together. Any other placement is refused:
1201
+ * a case-label goto under an `if`, in a loop body, in a nested switch's arm aimed at the outer
1202
+ * switch or before another statement; a case-end goto in a nested switch, in another arm, outside
1203
+ * the switch, or after its label; a labelled `break` that is not the switch's or sits in a loop.
1204
+ *
1205
+ * Shared by contracts.ts `assertCaseGotosNarrow`, which every structured and respelled tree
1206
+ * passes, and the C-family printer, which asks it of every function it prints: a pass that moves
1207
+ * a statement across an arm boundary is caught at the one place the program is spelled. */
1208
+ export function caseGotoViolation(body: readonly Stmt[]): string | null {
1209
+ const seen = new Set<string>();
1210
+ const claim = (label: string): string | null => {
1211
+ if (seen.has(label)) {
1212
+ return `the label \`${label}\` is carried by two switch arms`;
1213
+ }
1214
+ seen.add(label);
1215
+ return null;
1216
+ };
1217
+ /** `labels`: the case labels a case-label goto here may name (null outside an arm's top level's
1218
+ * reach); `ends`: the `break` labels a case-end goto here may name; `inArm`: inside a switch arm
1219
+ * with no loop entered since it began, where a labelled `break` may sit. */
1220
+ type Scope = { labels: ReadonlySet<string> | null; armTop: boolean; ends: ReadonlySet<string>; inArm: boolean };
1221
+ const walk = (list: readonly Stmt[], scope: Scope): string | null => {
1222
+ // `after[i]`: the `break` labels later in this list than `i`, beside the enclosing ones — built
1223
+ // only for a list holding one, so the walk allocates nothing per statement on a goto-free tree
1224
+ const after = endsAfter(list, scope.ends);
1225
+ for (const [i, s] of list.entries()) {
1226
+ if (s.k === 'caseEndGoto') {
1227
+ if (!scope.ends.has(s.label)) {
1228
+ return `\`goto ${s.label};\` names no labelled \`break;\` that ends the switch arm it sits in`;
1229
+ }
1230
+ continue;
1231
+ }
1232
+ if (s.k === 'goto') {
1233
+ if (scope.labels === null || !scope.armTop || i !== list.length - 1) {
1234
+ return `\`goto ${s.label};\` is not the last statement of a switch arm's own body`;
1235
+ }
1236
+ if (!scope.labels.has(s.label)) {
1237
+ return `\`goto ${s.label};\` names no case label of the switch it sits in`;
1238
+ }
1239
+ continue;
1240
+ }
1241
+ if (s.k === 'break' && s.label !== undefined) {
1242
+ if (s.ofSwitch !== true || !scope.inArm) {
1243
+ return `the label \`${s.label}\` is not on a \`break;\` that leaves a switch arm`;
1244
+ }
1245
+ const why = claim(s.label);
1246
+ if (why !== null) {
1247
+ return why;
1248
+ }
1249
+ continue;
1250
+ }
1251
+ if (s.k === 'switch') {
1252
+ const own = new Set<string>();
1253
+ for (const c of s.cases) {
1254
+ if (c.label !== undefined) {
1255
+ const why = claim(c.label);
1256
+ if (why !== null) {
1257
+ return why;
1258
+ }
1259
+ own.add(c.label);
1260
+ }
1261
+ }
1262
+ for (const arm of [...s.cases.map((c) => c.body), s.default ?? []]) {
1263
+ const why = walk(arm, { labels: own, armTop: true, ends: NO_ENDS, inArm: true });
1264
+ if (why !== null) {
1265
+ return why;
1266
+ }
1267
+ }
1268
+ continue;
1269
+ }
1270
+ // The `break` labels later in this list are the ends a goto inside `s` may reach. Inside a
1271
+ // loop the switch's case labels are out of reach (a case-label goto there would leave the
1272
+ // loop) and no `break;` leaves the arm.
1273
+ const ends = after?.[i] ?? scope.ends;
1274
+ const loop = isLoop(s);
1275
+ for (const inner of stmtLists(s)) {
1276
+ const why = walk(inner, {
1277
+ labels: loop ? null : scope.labels,
1278
+ armTop: false,
1279
+ ends,
1280
+ inArm: scope.inArm && !loop,
1281
+ });
1282
+ if (why !== null) {
1283
+ return why;
1284
+ }
1285
+ }
1286
+ if (s.k === 'for') {
1287
+ const why = walk([s.init, s.inc], { labels: null, armTop: false, ends: NO_ENDS, inArm: false });
1288
+ if (why !== null) {
1289
+ return why;
1290
+ }
1291
+ }
1292
+ }
1293
+ return null;
1294
+ };
1295
+ return walk(body, { labels: null, armTop: false, ends: NO_ENDS, inArm: false });
1296
+ }
1297
+
1298
+ const NO_ENDS: ReadonlySet<string> = new Set();
1299
+
1300
+ /** For each index of `list`, `outer` and the labels of the labelled `break`s that are elements of
1301
+ * `list` after it; undefined where `list` holds none, so every index reads `outer`. */
1302
+ function endsAfter(list: readonly Stmt[], outer: ReadonlySet<string>): ReadonlySet<string>[] | undefined {
1303
+ if (!list.some((s) => s.k === 'break' && s.label !== undefined)) {
1304
+ return undefined;
1305
+ }
1306
+ const out: ReadonlySet<string>[] = new Array(list.length);
1307
+ let later: ReadonlySet<string> = outer;
1308
+ for (let i = list.length - 1; i >= 0; i--) {
1309
+ out[i] = later;
1310
+ const s = list[i];
1311
+ if (s.k === 'break' && s.label !== undefined) {
1312
+ later = new Set([...later, s.label]);
1313
+ }
1314
+ }
1315
+ return out;
1316
+ }
1317
+
1318
+ /** Does a case-end `goto` in `list`, at any depth, name a labelled `break` that `list` does not
1319
+ * hold — so the jump leaves `list`, and every loop of it the goto sits in? A pass asking whether a
1320
+ * loop body leaves its loop asks this beside its `break`s (l3/reindex.ts `hasLoopExit`). */
1321
+ export function caseEndGotoLeaves(list: readonly Stmt[]): boolean {
1322
+ const held = new Set<string>();
1323
+ const named: string[] = [];
1324
+ const visit = (s: Stmt): void => {
1325
+ if (s.k === 'break' && s.label !== undefined) {
1326
+ held.add(s.label);
1327
+ } else if (s.k === 'caseEndGoto') {
1328
+ named.push(s.label);
1329
+ }
1330
+ stmtChildren(s).forEach(visit);
1331
+ };
1332
+ list.forEach(visit);
1333
+ return named.some((l) => !held.has(l));
1334
+ }
1335
+
1336
+ /** Can control fall off the END of `list`? False only where every path through it is provably
1337
+ * left first — by `return`, `continue`, a loop's `break`, a `goto`, a switch-scoped `break` or a
1338
+ * case-end `goto` (which leave the arm they sit in), or an `if` or `switch` none of whose paths
1339
+ * reaches its own end. A loop is read as reaching its end (a decided exit is not looked for), so
1340
+ * the answer errs toward "reaches", which is the side a caller appending a jump after the list
1341
+ * can afford: the jump it then writes is one no path runs, dead code and never a path cut. */
1342
+ export function reachesEnd(list: readonly Stmt[]): boolean {
1343
+ // `end`: some path falls off the list's last statement; `leaves`: some path takes a
1344
+ // switch-scoped `break` out of it, which ends the innermost switch around it.
1345
+ const flow = (ss: readonly Stmt[]): { end: boolean; leaves: boolean } => {
1346
+ let leaves = false;
1347
+ for (const s of ss) {
1348
+ switch (s.k) {
1349
+ case 'return':
1350
+ case 'continue':
1351
+ case 'goto':
1352
+ return { end: false, leaves };
1353
+ // a case-end goto leaves the innermost switch around it, as its labelled `break;` does
1354
+ case 'caseEndGoto':
1355
+ return { end: false, leaves: true };
1356
+ case 'break':
1357
+ return { end: false, leaves: leaves || s.ofSwitch === true };
1358
+ case 'if': {
1359
+ const t = flow(s.then);
1360
+ const e = flow(s.else);
1361
+ leaves ||= t.leaves || e.leaves;
1362
+ if (!t.end && !e.end) {
1363
+ return { end: false, leaves };
1364
+ }
1365
+ break;
1366
+ }
1367
+ case 'switch': {
1368
+ // Its end is reached by a `break` out of an arm, by an arm that neither falls nor jumps
1369
+ // on and reaches its own end, or — with no `default` — by a value no case names.
1370
+ const next = switchArmSuccessors(s);
1371
+ const ends =
1372
+ s.default === undefined ||
1373
+ [...s.cases.map((c) => c.body), s.default].some((body, i) => {
1374
+ const f = flow(body);
1375
+ return f.leaves || (f.end && next[i] === undefined);
1376
+ });
1377
+ if (!ends) {
1378
+ return { end: false, leaves };
1379
+ }
1380
+ break;
1381
+ }
1382
+ case 'while':
1383
+ case 'dowhile':
1384
+ case 'for':
1385
+ case 'assign':
1386
+ case 'store':
1387
+ case 'exprstmt':
1388
+ break;
1389
+ }
1390
+ }
1391
+ return { end: true, leaves };
1392
+ };
1393
+ return flow(list).end;
1394
+ }
1395
+
1396
+ /** For each arm of a `switch` — its cases in order, then its `default` at index `cases.length` —
1397
+ * the arm control runs on into from that arm's end, or undefined where it leaves the switch: the
1398
+ * arm a trailing `goto` names (its last top-level statement, `caseGotoViolation`), else the next
1399
+ * arm where the case falls through (the last case into the `default`, or out where there is
1400
+ * none). The one reading of "arm `i` runs on into arm `j`" that contracts.ts's effect count and
1401
+ * l3/coalesce.ts's arm-disjointness both follow, transitively. */
1402
+ export function switchArmSuccessors(sw: Extract<Stmt, { k: 'switch' }>): (number | undefined)[] {
1403
+ const labelled = new Map(sw.cases.flatMap((c, i) => (c.label === undefined ? [] : [[c.label, i] as const])));
1404
+ const jumpOf = (body: readonly Stmt[]): number | undefined => {
1405
+ const last = body[body.length - 1];
1406
+ return last?.k === 'goto' ? labelled.get(last.label) : undefined;
1407
+ };
1408
+ const last = sw.cases.length;
1409
+ return [
1410
+ ...sw.cases.map(
1411
+ (c, i) => jumpOf(c.body) ?? (c.fallsThrough && (i + 1 < last || sw.default !== undefined) ? i + 1 : undefined),
1412
+ ),
1413
+ sw.default === undefined ? undefined : jumpOf(sw.default),
1414
+ ];
1415
+ }
1416
+
1417
+ /** Does a case-label `goto` of this switch land on an arm LAID OUT AT OR ABOVE the arm it ends —
1418
+ * a BACK EDGE with no loop statement around it? Then the arm it lands on runs AFTER the jumping
1419
+ * arm, so preorder stops being execution order inside the switch's own range, exactly as it does
1420
+ * inside a loop body; a pass that reads preorder as liveness must count such a switch with the
1421
+ * loops (l3/coalesce.ts `spans`). Positions are the layout {@link stmtChildren} walks: the
1422
+ * `default` sits at `defaultAt`. A fall-through only ever runs on DOWNWARD, so only the jumps are
1423
+ * asked. */
1424
+ export function switchJumpsBack(sw: Extract<Stmt, { k: 'switch' }>): boolean {
1425
+ const at = sw.defaultAt ?? sw.cases.length;
1426
+ const laidOut = (arm: number): number => (arm === sw.cases.length ? at : arm < at ? arm : arm + 1);
1427
+ const labelled = new Map(sw.cases.flatMap((c, i) => (c.label === undefined ? [] : [[c.label, i] as const])));
1428
+ const bodies = [...sw.cases.map((c) => c.body), sw.default ?? []];
1429
+ return bodies.some((body, i) => {
1430
+ const last = body[body.length - 1];
1431
+ const to = last?.k === 'goto' ? labelled.get(last.label) : undefined;
1432
+ return to !== undefined && laidOut(to) <= laidOut(i);
1433
+ });
1434
+ }
1435
+
944
1436
  /** The nested statement LISTS of a statement — the SCOPES it opens.
945
1437
  *
946
1438
  * Deliberately not `stmtChildren`, which flattens a `for`'s `init`/`inc` in with its body: those
@@ -968,6 +1460,8 @@ export function stmtLists(s: Stmt): Stmt[][] {
968
1460
  case 'return':
969
1461
  case 'break':
970
1462
  case 'continue':
1463
+ case 'goto':
1464
+ case 'caseEndGoto':
971
1465
  return [];
972
1466
  }
973
1467
  }
@@ -997,6 +1491,8 @@ export function mapStmtLists(s: Stmt, f: (list: Stmt[]) => Stmt[]): Stmt {
997
1491
  case 'return':
998
1492
  case 'break':
999
1493
  case 'continue':
1494
+ case 'goto':
1495
+ case 'caseEndGoto':
1000
1496
  return s;
1001
1497
  }
1002
1498
  }
@@ -1077,8 +1573,76 @@ export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
1077
1573
  '<=': '>',
1078
1574
  '==': '!=',
1079
1575
  '!=': '==',
1576
+ // C's float `==` is false on a NaN and `!=` true, so these two are each other's negation; the
1577
+ // ordered four have none in the operator vocabulary (the BinOp note).
1578
+ 'f==': 'f!=',
1579
+ 'f!=': 'f==',
1080
1580
  };
1081
1581
 
1582
+ /** The compares under `e`'s connectives whose runtime helper names the relation the source spelled
1583
+ * (`helperRel`), each answering whether it spells that relation now. `negateCond` flips every one
1584
+ * of them at once, through De Morgan. */
1585
+ function helperRelLeaves(e: Expr, out: boolean[] = []): boolean[] {
1586
+ if (e.k === 'bin' && (e.op === '&&' || e.op === '||')) {
1587
+ helperRelLeaves(e.l, out);
1588
+ helperRelLeaves(e.r, out);
1589
+ } else if (e.k === 'bin' && e.helperRel !== undefined) {
1590
+ out.push(e.op === e.helperRel);
1591
+ }
1592
+ return out;
1593
+ }
1594
+
1595
+ /** Whether `e` spells, at every compare of it whose runtime helper names the relation the source
1596
+ * spelled, that relation — so that negating `e` recompiles to the other helper at each of them:
1597
+ * the relation's twin and its `!` alike, since gcc folds `!(a == b)` into `a != b`. An arm swap
1598
+ * that would negate it keeps `e` and its arm order instead. */
1599
+ export function keepsHelperRel(e: Expr): boolean {
1600
+ const leaves = helperRelLeaves(e);
1601
+ return leaves.length > 0 && leaves.every((own) => own);
1602
+ }
1603
+
1604
+ /** Whether negating `e` puts every such compare back on its helper's relation: the swap a pass
1605
+ * would otherwise leave alone is the one that spells the helpers. A condition mixing the two has
1606
+ * neither answer, and no arm order spells it. */
1607
+ export function regainsHelperRel(e: Expr): boolean {
1608
+ const leaves = helperRelLeaves(e);
1609
+ return leaves.length > 0 && leaves.every((own) => !own);
1610
+ }
1611
+
1612
+ /** A loop's stay condition `P && S` whose compares under `S` are all negated off their helper's
1613
+ * relation, and whose `P` negates none: `{ head: P, guard: !S }`, which the loop spells
1614
+ * `while (P) { if (!S) break; … }` — the same tests in the same order, calling the helpers the
1615
+ * source called. `while (P && S)` has no spelling of that `S` that calls the helper (gcc folds
1616
+ * `!(a == b)` into `a != b`); the break guard is how the source wrote it before the branch fold
1617
+ * joined the two tests. Null when the condition already spells every helper's relation, or when no
1618
+ * split of its `&&` chain separates the two kinds of compare. */
1619
+ export function splitHelperBreak(cond: Expr): { head: Expr; guard: Expr } | null {
1620
+ if (keepsHelperRel(cond) || helperRelLeaves(cond).length === 0) {
1621
+ return null;
1622
+ }
1623
+ const conj: Expr[] = [];
1624
+ const flatten = (e: Expr): void => {
1625
+ if (e.k === 'bin' && e.op === '&&') {
1626
+ flatten(e.l);
1627
+ flatten(e.r);
1628
+ } else {
1629
+ conj.push(e);
1630
+ }
1631
+ };
1632
+ flatten(cond);
1633
+ const and = (xs: Expr[]): Expr => xs.reduce((l, r) => ({ k: 'bin', op: '&&', l, r }));
1634
+ // The longest prefix that negates no helper leaf; the rest must negate every one it holds.
1635
+ let k = 0;
1636
+ while (k < conj.length && helperRelLeaves(conj[k]).every((own) => own)) {
1637
+ k++;
1638
+ }
1639
+ if (k === 0 || k === conj.length) {
1640
+ return null;
1641
+ }
1642
+ const rest = and(conj.slice(k));
1643
+ return regainsHelperRel(rest) ? { head: and(conj.slice(0, k)), guard: negateCond(rest) } : null;
1644
+ }
1645
+
1082
1646
  export function negateCond(e: Expr): Expr {
1083
1647
  if (e.k === 'bin') {
1084
1648
  const flipped = NEGATE_REL[e.op];