@asmlift/core 0.8.1 → 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 (130) hide show
  1. package/README.md +36 -31
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +397 -0
  4. package/src/backend/c.ts +6 -2
  5. package/src/backend/cfamily.ts +370 -56
  6. package/src/backend/cpp.ts +116 -10
  7. package/src/backend/pascal.ts +69 -11
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +777 -84
  15. package/src/declare.ts +132 -13
  16. package/src/frontend/asmdata.ts +26 -6
  17. package/src/frontend/call-plan.ts +1590 -0
  18. package/src/frontend/dead-reads.ts +628 -0
  19. package/src/frontend/device-pins.ts +296 -0
  20. package/src/frontend/emit.ts +4 -3
  21. package/src/frontend/errors.ts +5 -0
  22. package/src/frontend/flags-edge.ts +101 -0
  23. package/src/frontend/fpu.ts +374 -0
  24. package/src/frontend/frame-objects.ts +3171 -0
  25. package/src/frontend/frontend.ts +64 -3
  26. package/src/frontend/high-half.ts +55 -5
  27. package/src/frontend/jump-table.ts +530 -0
  28. package/src/frontend/local-object.ts +616 -0
  29. package/src/frontend/mips.ts +2392 -208
  30. package/src/frontend/opaque.ts +97 -18
  31. package/src/frontend/own-struct-return.ts +500 -0
  32. package/src/frontend/ppc.ts +3137 -267
  33. package/src/frontend/register-struct-call.ts +484 -0
  34. package/src/frontend/reloc-symbol.ts +109 -13
  35. package/src/frontend/splat.ts +108 -5
  36. package/src/frontend/ssa.ts +2098 -89
  37. package/src/frontend/stackargs.ts +172 -42
  38. package/src/frontend/thumb.ts +4563 -2091
  39. package/src/ir/alias.ts +71 -0
  40. package/src/ir/bits.ts +681 -1
  41. package/src/ir/core.ts +199 -6
  42. package/src/ir/discipline.ts +313 -0
  43. package/src/ir/entry.ts +159 -0
  44. package/src/ir/float-bits.ts +47 -0
  45. package/src/{structure → ir}/loops.ts +6 -2
  46. package/src/ir/opcodes.ts +193 -121
  47. package/src/ir/parse.ts +1 -0
  48. package/src/ir/types.ts +202 -2
  49. package/src/ir/verify.ts +219 -3
  50. package/src/l3/address.ts +5 -0
  51. package/src/l3/argcopy.ts +38 -6
  52. package/src/l3/ast.ts +467 -23
  53. package/src/l3/basecse.ts +33 -5
  54. package/src/l3/coalesce.ts +21 -13
  55. package/src/l3/constinit.ts +382 -0
  56. package/src/l3/dce.ts +34 -12
  57. package/src/l3/gates.ts +9 -3
  58. package/src/l3/hoist.ts +8 -7
  59. package/src/l3/initfirst.ts +68 -19
  60. package/src/l3/intcell.ts +98 -0
  61. package/src/l3/mentions.ts +10 -4
  62. package/src/l3/narrowdecl.ts +259 -0
  63. package/src/l3/nearbase.ts +10 -5
  64. package/src/l3/parkfirst.ts +8 -5
  65. package/src/l3/pollguard.ts +11 -4
  66. package/src/l3/ptrcell.ts +354 -0
  67. package/src/l3/ptrfield.ts +123 -8
  68. package/src/l3/regspell.ts +26 -7
  69. package/src/l3/reindex.ts +293 -103
  70. package/src/l3/storage.ts +98 -1
  71. package/src/l3/symbol-refs.ts +182 -23
  72. package/src/l3/tailmerge.ts +9 -20
  73. package/src/l3/typing.ts +237 -32
  74. package/src/l3/unreduce.ts +10 -9
  75. package/src/l3/volatileptr.ts +17 -54
  76. package/src/l3/volstore.ts +96 -7
  77. package/src/mangle.ts +41 -1
  78. package/src/pattern/engine.ts +373 -82
  79. package/src/pipeline.ts +589 -27
  80. package/src/proto-context.ts +1648 -0
  81. package/src/proto.ts +1203 -49
  82. package/src/raise/aggmembers.ts +290 -0
  83. package/src/raise/arrays.ts +8 -3
  84. package/src/raise/declared-volatile.ts +307 -0
  85. package/src/raise/divpow2.ts +2 -2
  86. package/src/raise/errors.ts +17 -0
  87. package/src/raise/floathelpers.ts +464 -0
  88. package/src/raise/globalshape.ts +11 -5
  89. package/src/raise/gvn.ts +29 -8
  90. package/src/raise/latch.ts +162 -6
  91. package/src/raise/memberarrays.ts +10 -4
  92. package/src/raise/narrow.ts +29 -2
  93. package/src/raise/narrowlocal.ts +11 -6
  94. package/src/raise/offsetnames.ts +494 -0
  95. package/src/raise/pairparams.ts +114 -0
  96. package/src/raise/paramwidth.ts +203 -6
  97. package/src/raise/pre-recovery.ts +174 -17
  98. package/src/raise/pre-structure.ts +54 -0
  99. package/src/raise/recover.ts +26 -15
  100. package/src/raise/reroll.ts +694 -0
  101. package/src/raise/retsink.ts +1 -1
  102. package/src/raise/shortcircuit.ts +66 -30
  103. package/src/raise/softdiv.ts +21 -23
  104. package/src/raise/storesign.ts +153 -0
  105. package/src/raise/struct-arrays.ts +34 -14
  106. package/src/raise/structs.ts +674 -98
  107. package/src/raise/truncload.ts +367 -0
  108. package/src/raise/unthread.ts +400 -0
  109. package/src/raise/widehelpers.ts +106 -0
  110. package/src/rank-declare.ts +143 -12
  111. package/src/rank-variations.ts +117 -31
  112. package/src/rank.ts +563 -122
  113. package/src/runtime-helpers.ts +405 -0
  114. package/src/stillborn.ts +289 -0
  115. package/src/structure/analysis.ts +1449 -449
  116. package/src/structure/arith-ops.ts +23 -0
  117. package/src/structure/globalaccess.ts +111 -9
  118. package/src/structure/hazards.ts +641 -114
  119. package/src/structure/local-statics.ts +517 -0
  120. package/src/structure/namecoalesce.ts +2 -2
  121. package/src/structure/pointer-spelling.ts +1106 -0
  122. package/src/structure/redundant-test.ts +99 -0
  123. package/src/structure/structure.ts +3478 -975
  124. package/src/structure/switch-recover.ts +403 -178
  125. package/src/symbols.ts +44 -3
  126. package/src/target.ts +1387 -71
  127. package/src/trace.ts +66 -28
  128. package/src/variation-definitions.ts +243 -13
  129. package/src/variation-gates.ts +2 -0
  130. package/src/variation-tokens.ts +25 -7
package/src/l3/ast.ts CHANGED
@@ -6,15 +6,49 @@ import { type IrType, typeEquals } from '../ir/types';
6
6
  export type Expr =
7
7
  | { k: 'var'; name: string }
8
8
  | { k: 'const'; value: number }
9
- | { k: 'bin'; op: BinOp; l: Expr; r: Expr }
10
- | { k: 'un'; op: '-' | '~' | '!'; e: Expr }
9
+ // A `double` literal, carried as its IEEE bit pattern (ir/float-bits.ts) — a kind apart from the
10
+ // integer `const`, so no integer fold, range test or signedness reading ever takes it for one.
11
+ | { k: 'fconst'; bits: string }
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 }
27
+ | { k: 'un'; op: '-' | '~' | '!' | 'f-'; e: Expr }
28
+ // A post-increment `name++` (`by` 1) or post-decrement `name--` (`by` -1): the value the local
29
+ // held BEFORE the update, with the update as a side effect. The only expression form here that
30
+ // WRITES anything, so `exprHasEffect` answers for it alongside `call` and `marker` — that is what
31
+ // every pass asking "may I move or duplicate this" already consults. It also NAMES a local, so
32
+ // `mentionedName` answers for it beside `var` and `addr`, and the two walks that classify their
33
+ // own leaves — `l3/mentions.ts` and `contracts.ts`'s `assertLocalsWritten` — count it on both
34
+ // sides, because `v++` reads the name and writes it.
35
+ //
36
+ // The target is a NAME rather than an Expr because its one producer has one: structure.ts's
37
+ // `emitDoWhile`, folding a loop update into a bottom test that reads the variable at its
38
+ // pre-update value (`while (v0 != 0 && v1++ <= 9)`). A general lvalue has no inhabitant.
39
+ //
40
+ // WHERE IT MAY STAND IS THE PRODUCER'S OBLIGATION, not a property of the node: C89 leaves the
41
+ // program undefined when the same object is also read elsewhere between two sequence points, and
42
+ // short-circuit operators decide whether the update happens at all. `PREUPDATE_COND_GATES`
43
+ // (structure/hazards.ts) holds both rules for the one producer.
44
+ | { k: 'postincr'; name: string; by: 1 | -1 }
11
45
  // A C-style value cast `(T)e`. Tree-level producers: a width-narrowing cast `(u8)e` (the
12
46
  // recovered form of a byte/half extend idiom — zext/sext IR ops), the STRUCT-pointer cast
13
47
  // (structure.ts memAccess/arrayAccess struct paths — see the note on `field` below), and the
14
- // integer legalization of a pointer operand under an operator C rejects (structure.ts intify,
15
- // `3 & (s32)p`). Scalar deref casts are backend-owned — the C-family printer synthesizes
16
- // them from the `index` node's width. Each backend spells the cast in its own syntax
17
- // (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).
18
52
  //
19
53
  // `volatile` qualifies the POINTEE of a pointer cast (`(volatile u16 *)0x4000208`) — the one
20
54
  // place asmlift can say "this access is to a volatile object" when there is no declaration to
@@ -22,12 +56,58 @@ export type Expr =
22
56
  // cv-qualifier, deliberately: volatility is a SPELLING, carried at the declaration or the
23
57
  // cast, the same split SFn.locals makes for `volatile`/`pointeeVolatile`.
24
58
  | { k: 'cast'; to: IrType; e: Expr; volatile?: true }
25
- | { k: 'call'; fn: string; args: Expr[] }
59
+ // A call. `wide64` is the INTEGER RANK the callee's return renders with, and it is carried on the
60
+ // node because nothing downstream can derive it: a callee's return type comes from a prototype
61
+ // outside the emitted function, so `exprCType` answers `undefined` for a call and every reader of
62
+ // the rendered rank would otherwise read C's implicit `int`.
63
+ //
64
+ // IT IS TRUE OF THE C THAT IS COMPILED BECAUSE THE SOURCES AGREE, and that agreement is a
65
+ // property of one lookup rather than of two modules happening to match. The width is set where
66
+ // the frontend was TOLD it — a compiler's own runtime-helper signature, whose callee the
67
+ // candidate never declares because the helper table IS the declaration, or a project's
68
+ // `FnProto.returns`, which `proto.ts` `spellableProto` admits only when the same entry can be
69
+ // PRINTED as the callee's own prototype. `prototypesFromSymbols` resolves that table once, so
70
+ // the frontend that stamps this and the collector that prints the declaration read one answer.
71
+ // The filters in `rank-declare.ts` can still withhold a printed declaration (a name the emitted
72
+ // C uses for its own storage, a non-identifier, a reserved name) — every one of them kills the
73
+ // SPELLING or the whole row rather than leaving this stamp standing over an implicitly-`int`
74
+ // callee.
75
+ //
76
+ // `sret` is the type of a struct the callee returns BY VALUE through memory. `args[0]` is then the
77
+ // hidden pointer the frontend read from the first argument register — the address of the struct
78
+ // the call fills — and the rest are the declared arguments. The only C spelling is a statement,
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.
88
+ //
89
+ // `fn` is the callee's NAME, or for an indirect call the EXPRESSION whose value is the address
90
+ // called (ir/core.ts `calleeValue`) — a child of the node like its arguments, listed first in
91
+ // `exprChildren`. That is no evaluation order: C leaves it unspecified, and agbcc loads the callee
92
+ // after the arguments.
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
+ }
26
102
  // The ADDRESS of a named global, `&gSym` (agbcc pool `.word gSym`, frontend `gaddr` op). A
27
103
  // DEREF of it collapses to the bare global: memAccess/arrayAccess spell `*(&gSym)` as `gSym`
28
104
  // and `(&gSym)[i]` as `gSym[i]` (a global name decays to a pointer). Only a genuinely
29
105
  // address-TAKEN global (passed by address, `&gSym` as a call arg) prints the `&` form. The
30
106
  // global's type comes from the project headers, so it is never declared as a local.
107
+ //
108
+ // An ARRAY's address is spelled by the bare name, and that is a fact of C's declarator grammar
109
+ // rather than of the address — so the C printer decides it from the name's declared type, and
110
+ // this node carries nothing about it (backend/cfamily.ts, `case 'addr'`).
31
111
  | { k: 'addr'; name: string }
32
112
  // A memory access `base[idx]` (printed `*base` when idx is the constant 0), CARRYING the
33
113
  // access's element width (bytes) and signedness. `idx` counts elements of `width` bytes.
@@ -142,6 +222,15 @@ export type Expr =
142
222
  operandOff?: number;
143
223
  baseOrdered?: true;
144
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;
145
234
  }
146
235
  // A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
147
236
  // pointer, so the byte offset resolves to a named field instead of a scaled array index).
@@ -176,15 +265,36 @@ export type Expr =
176
265
  // The divides earned it second, on pokeemerald:GetAnchorCoord — `(u32)(coord * a1) / (u32)a0`
177
266
  // standing beside two arithmetic shifts of the same values. Their only other channel is the operand
178
267
  // TYPES, reached by flipping a declaration, and there that flip is unreachable and unsound at
179
- // once: the divisor also feeds a signed compare, so the /uns-cmp reconciliation correctly refuses
180
- // it, and forcing it anyway makes agbcc delete the comparison as always-false. A per-operand pin is
181
- // 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".
182
272
  //
183
273
  // The COMPARISONS stay collapsed, and that asymmetry is the rule applying rather than an omission:
184
274
  // which side a compare was spelled from genuinely underdetermines — a signed spelling that
185
275
  // byte-matched was proved non-negative by the compiler — so it is refereed as a variation, while a
186
276
  // division helper is a pure function of the expression's C type with no such proof available.
277
+ //
278
+ // THE FLOAT OPERATORS (`f+` `f-` `f*` `f/`) are the third split, and they are earned by what an
279
+ // integer operator MEANS to the passes that read one rather than by a spelling: C writes both with
280
+ // the same token, but `/` here is the SIGNED divide the C-family backend pins with `(s32)` casts,
281
+ // and `+`/`-` are the pointer walks, reassociations and constant folds of a dozen L3 passes. A float
282
+ // under any of those is a wrong program, so it gets operators none of them match — the same reason
283
+ // the IR has `fadd` rather than a float-typed `add`. The unary minus splits the same way (`f-` on
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.
187
287
  export type BinOp =
288
+ | 'f+'
289
+ | 'f-'
290
+ | 'f*'
291
+ | 'f/'
292
+ | 'f<'
293
+ | 'f<='
294
+ | 'f>'
295
+ | 'f>='
296
+ | 'f=='
297
+ | 'f!='
188
298
  | '+'
189
299
  | '-'
190
300
  | '*'
@@ -234,7 +344,15 @@ export type Stmt =
234
344
  // `break;` / `continue;` — a loop early-exit / next-iteration jump. Emitted ONLY when the target is
235
345
  // the innermost enclosing loop (bare C break/continue cannot express a multi-level exit; a deeper
236
346
  // target declines). SGI/IDO Pascal has neither, so its backend loud-fails them (like `field`/`cast`).
237
- | { 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 }
238
356
  | { k: 'continue' }
239
357
  // A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
240
358
  // a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
@@ -337,11 +455,27 @@ export interface SFn {
337
455
  * one are deliberately left unstamped even where the offset is in hand — see the refusal at
338
456
  * the structurer's build site for the measurement that decided it. */
339
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;
340
465
  }[];
341
466
  /** project globals referenced with a known declaration shape (symbol map) — typed for the
342
467
  * legalization env (exprCType) but NEVER declared by a backend: the project's own headers
343
468
  * declare them, exactly like every other global name asmlift emits. */
344
469
  globals?: { name: string; type: IrType }[];
470
+ /** The function-scope statics this function DEFINES (structure/local-statics.ts), declared at
471
+ * the top of its body ahead of the locals. Unlike `globals`, the backend declares these: no
472
+ * header does, because the source defined them here. */
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[];
345
479
  retType: IrType;
346
480
  body: Stmt[];
347
481
  /** Struct types this function's fields reference, declared above it by the backend. Empty
@@ -359,6 +493,63 @@ export interface SFn {
359
493
  * `TargetDescription.compilerBehaviors`. ABSENT means the direction is unknown for this target
360
494
  * and `l3/slotorder.ts` is the identity — never "ascending by default". */
361
495
  slotOrder?: 'ascending' | 'descending';
496
+ /** Each declared callee's parameter types (an entry the printer cannot spell is `undefined`):
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). */
501
+ declaredArgs?: Readonly<Record<string, readonly (string | undefined)[]>>;
502
+ /** Each declared callee's return type as the project spells it, never printed: whether a call a
503
+ * pointer cell is compared with returns a pointer (l3/ptrcell.ts). */
504
+ declaredReturns?: Readonly<Record<string, string>>;
505
+ /** The emitted C is compiled AS C++. An emission policy like `slotOrder`. */
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;
525
+ }
526
+
527
+ /** One function-scope static's definition: `static [const] T name[count] = { init };`. `init` is
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. */
531
+ export interface SStatic {
532
+ name: string;
533
+ type: IrType;
534
+ const?: true;
535
+ align?: number;
536
+ init?: (number | InitAddress)[];
537
+ /** the initializer is spelled as a string literal — a byte array the compiler aligns as a
538
+ * string (structure/local-statics.ts) */
539
+ string?: true;
540
+ }
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;
362
553
  }
363
554
 
364
555
  /** A struct declaration surfaced to the backend (name + field list). Mirrors the IR struct
@@ -381,6 +572,13 @@ export interface LanguageBackend {
381
572
  * cannot print it turns a whole function that used to decompile into a loud stub. The
382
573
  * structurer reads it through `StructureOptions.spellSwitchFallthrough`. */
383
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;
384
582
  emit(fn: SFn): string;
385
583
  // Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
386
584
  // Used by the annotate-mode stub path to carry the failure reason + the original asm
@@ -427,6 +625,8 @@ export function exprEquals(a: Expr, b: Expr): boolean {
427
625
  return a.name === (b as typeof a).name;
428
626
  case 'const':
429
627
  return a.value === (b as typeof a).value;
628
+ case 'fconst':
629
+ return a.bits === (b as typeof a).bits;
430
630
  case 'bin': {
431
631
  const bb = b as typeof a;
432
632
  return a.op === bb.op && exprEquals(a.l, bb.l) && exprEquals(a.r, bb.r);
@@ -435,6 +635,10 @@ export function exprEquals(a: Expr, b: Expr): boolean {
435
635
  const bb = b as typeof a;
436
636
  return a.op === bb.op && exprEquals(a.e, bb.e);
437
637
  }
638
+ case 'postincr': {
639
+ const bb = b as typeof a;
640
+ return a.name === bb.name && a.by === bb.by;
641
+ }
438
642
  case 'cast': {
439
643
  const bb = b as typeof a;
440
644
  // `volatile` is part of the SPELLING, compared for the same reason `lead` and `dot` are: a
@@ -448,7 +652,29 @@ export function exprEquals(a: Expr, b: Expr): boolean {
448
652
  }
449
653
  case 'call': {
450
654
  const bb = b as typeof a;
451
- return a.fn === bb.fn && a.args.length === bb.args.length && a.args.every((x, i) => exprEquals(x, bb.args[i]));
655
+ // `wide64` is part of the node's IDENTITY, compared for the same reason `volatile` is one
656
+ // cast up: it decides the rank every operand pin is taken at, so two calls that differ in it
657
+ // are two different expressions and a dedup that kept one would respell the other's
658
+ // arithmetic.
659
+ //
660
+ // NO PIPELINE INPUT REACHES THIS TERM TODAY, and saying so is cheaper than a witness that
661
+ // cannot exist: the stamp is a pure function of the callee name (`structure.ts` reads the
662
+ // call result's IR width, which the frontend set from the callee's declared return), so two
663
+ // `call` nodes sharing `fn` and `args` always share `wide64` and deleting the term leaves
664
+ // the whole core suite green. What pins it is a direct test of THIS function's contract in
665
+ // test/int64-repr.test.ts, over two nodes built by hand — which is the honest bar, because
666
+ // the claim being made is about `exprEquals` and not about a path through the pipeline.
667
+ return (
668
+ (typeof a.fn === 'string' || typeof bb.fn === 'string' ? a.fn === bb.fn : exprEquals(a.fn, bb.fn)) &&
669
+ (a.wide64 ?? false) === (bb.wide64 ?? false) &&
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() &&
675
+ a.args.length === bb.args.length &&
676
+ a.args.every((x, i) => exprEquals(x, bb.args[i]))
677
+ );
452
678
  }
453
679
  case 'index': {
454
680
  const bb = b as typeof a;
@@ -493,12 +719,103 @@ export function exprEquals(a: Expr, b: Expr): boolean {
493
719
  }
494
720
  }
495
721
 
496
- /** THE spelling of an unmodelled instruction's gap reason, in one place: `structure.ts` writes it
497
- * into the marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
722
+ /** Do two statements print the same C? The leaf statements compare through {@link exprEquals},
723
+ * and an `if` arm by arm; every other kind answers no, which can only keep apart two statements a
724
+ * caller might have merged. */
725
+ export function stmtEquals(a: Stmt, b: Stmt): boolean {
726
+ switch (a.k) {
727
+ case 'assign':
728
+ return b.k === 'assign' && a.name === b.name && exprEquals(a.value, b.value);
729
+ case 'store':
730
+ return b.k === 'store' && exprEquals(a.lval, b.lval) && exprEquals(a.value, b.value);
731
+ case 'exprstmt':
732
+ return b.k === 'exprstmt' && exprEquals(a.value, b.value);
733
+ case 'return':
734
+ return (
735
+ b.k === 'return' &&
736
+ (a.unspelled ?? false) === (b.unspelled ?? false) &&
737
+ (a.value === undefined ? b.value === undefined : b.value !== undefined && exprEquals(a.value, b.value))
738
+ );
739
+ case 'if':
740
+ return b.k === 'if' && exprEquals(a.cond, b.cond) && stmtsEqual(a.then, b.then) && stmtsEqual(a.else, b.else);
741
+ default:
742
+ return false;
743
+ }
744
+ }
745
+
746
+ /** {@link stmtEquals} over two lists, in order. */
747
+ export function stmtsEqual(a: readonly Stmt[], b: readonly Stmt[]): boolean {
748
+ return a.length === b.length && a.every((s, i) => stmtEquals(s, b[i]));
749
+ }
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
+
805
+ /** THE spelling of an `opaque`'s gap reason, in one place: `structure.ts` writes it into the
806
+ * marker, `contracts.ts` matches on it to prove the gap was not dropped, and the benchmark
498
807
  * classifies declines by it. Two spellings make that contract silently vacuous — enforced-looking
499
- * and never firing. `?` when a frontend stamps no mnemonic. */
500
- export function gapReasonFor(mnemonic: unknown): string {
501
- return `unmodelled instruction '${typeof mnemonic === 'string' ? mnemonic : '?'}'`;
808
+ * and never firing, since it is an EQUALITY between what the IR carries and what the output shows.
809
+ *
810
+ * TWO PRODUCERS, TWO SENTENCES, and they name different capabilities: an INSTRUCTION no frontend
811
+ * decodes (`adc`), and a call into the compiler's own RUNTIME that no recogniser folded
812
+ * (`__div2i`). The attrs decide which, so a producer chooses by what it stamps rather than by
813
+ * spelling a string. `?` when a frontend stamps neither. */
814
+ export function gapReasonFor(attrs: { mnemonic?: unknown; helper?: unknown }): string {
815
+ if (typeof attrs.helper === 'string') {
816
+ return `no model for the runtime helper '${attrs.helper}'`;
817
+ }
818
+ return `unmodelled instruction '${typeof attrs.mnemonic === 'string' ? attrs.mnemonic : '?'}'`;
502
819
  }
503
820
 
504
821
  // ── the ONE traversal vocabulary ───────────────────────────────────────────────────────────────
@@ -513,7 +830,9 @@ export function exprChildren(e: Expr): Expr[] {
513
830
  switch (e.k) {
514
831
  case 'var':
515
832
  case 'const':
833
+ case 'fconst':
516
834
  case 'addr':
835
+ case 'postincr':
517
836
  return [];
518
837
  case 'bin':
519
838
  return [e.l, e.r];
@@ -521,7 +840,7 @@ export function exprChildren(e: Expr): Expr[] {
521
840
  case 'cast':
522
841
  return [e.e];
523
842
  case 'call':
524
- return e.args;
843
+ return typeof e.fn === 'string' ? e.args : [e.fn, ...e.args];
525
844
  case 'index':
526
845
  return [e.base, ...(e.lead ?? []), e.idx];
527
846
  case 'field':
@@ -536,7 +855,9 @@ export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
536
855
  switch (e.k) {
537
856
  case 'var':
538
857
  case 'const':
858
+ case 'fconst':
539
859
  case 'addr':
860
+ case 'postincr':
540
861
  return e;
541
862
  case 'bin':
542
863
  return { ...e, l: f(e.l), r: f(e.r) };
@@ -544,7 +865,7 @@ export function mapExprChildren(e: Expr, f: (c: Expr) => Expr): Expr {
544
865
  case 'cast':
545
866
  return { ...e, e: f(e.e) };
546
867
  case 'call':
547
- return { ...e, args: e.args.map(f) };
868
+ return { ...e, ...(typeof e.fn === 'string' ? {} : { fn: f(e.fn) }), args: e.args.map(f) };
548
869
  case 'index':
549
870
  return { ...e, base: f(e.base), ...(e.lead ? { lead: e.lead.map(f) } : {}), idx: f(e.idx) };
550
871
  case 'field':
@@ -654,7 +975,7 @@ export function rematerializableAddress(e: Expr): boolean {
654
975
  case 'un':
655
976
  break;
656
977
  default:
657
- ok = false; // var, addr, index, field, call, marker
978
+ ok = false; // var, addr, postincr, index, field, call, marker
658
979
  return;
659
980
  }
660
981
  for (const c of exprChildren(x)) {
@@ -665,10 +986,27 @@ export function rematerializableAddress(e: Expr): boolean {
665
986
  return ok && nonZero;
666
987
  }
667
988
 
668
- /** Whether the tree contains a node with an EFFECT no re-ordering may move: a call, or a marker
669
- * standing in for an unmodelled instruction (annotate mode). */
989
+ /** The local or global a node NAMES, or undefined for one that names none — the whole Expr
990
+ * vocabulary a "which names does this tree mention" walk has to know about, in one place, because
991
+ * a collector that misses a kind reports a SHORT live range and a short range is a clobber where a
992
+ * long one is only a missed merge.
993
+ *
994
+ * `call`'s `fn` is not one of them: it names a function, and the walks that care about that ask
995
+ * for it beside this (l3/hoist.ts). A walk that must tell the kinds APART — `&v` is a write where
996
+ * a bare `v` is a read — keeps its own arms; this answers the name, not what is done to it. */
997
+ export function mentionedName(e: Expr): string | undefined {
998
+ return e.k === 'var' || e.k === 'addr' || e.k === 'postincr' ? e.name : undefined;
999
+ }
1000
+
1001
+ /** The callee a call names, or undefined for an indirect one. */
1002
+ export function calleeOf(e: Extract<Expr, { k: 'call' }>): string | undefined {
1003
+ return typeof e.fn === 'string' ? e.fn : undefined;
1004
+ }
1005
+
1006
+ /** Whether the tree contains a node with an EFFECT no re-ordering may move: a call, a marker
1007
+ * standing in for an unmodelled instruction (annotate mode), or a post-increment's write. */
670
1008
  export function exprHasEffect(e: Expr): boolean {
671
- return e.k === 'call' || e.k === 'marker' || exprChildren(e).some(exprHasEffect);
1009
+ return e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || exprChildren(e).some(exprHasEffect);
672
1010
  }
673
1011
 
674
1012
  /** Whether the tree performs an access to a `volatile` object — the OTHER thing no re-ordering may
@@ -703,6 +1041,44 @@ function* subterms(e: Expr): Generator<Expr> {
703
1041
  }
704
1042
  }
705
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
+
706
1082
  /** The statements a statement DIRECTLY contains, in the order a backend prints them — a `switch`
707
1083
  * splices its default in at `defaultAt` for that reason. NOTE for document-order walks: a `for`'s
708
1084
  * init/inc are listed here while its cond is in stmtExprs — a walker visiting exprs-then-stmts
@@ -905,8 +1281,76 @@ export const NEGATE_REL: Partial<Record<BinOp, BinOp>> = {
905
1281
  '<=': '>',
906
1282
  '==': '!=',
907
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==',
908
1288
  };
909
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
+
910
1354
  export function negateCond(e: Expr): Expr {
911
1355
  if (e.k === 'bin') {
912
1356
  const flipped = NEGATE_REL[e.op];