@asmlift/core 0.8.1 → 0.9.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 (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  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 +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +267 -79
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
package/src/ir/opcodes.ts CHANGED
@@ -13,8 +13,8 @@ export interface OpSig {
13
13
  successors?: number | 'variadic';
14
14
  requiredAttrs?: readonly string[];
15
15
  /** observable side effect (memory write / call): never deleted when dead, never hoisted into
16
- * an unconditional position. THE one effect vocabulary — DCE (pattern/engine.ts) and the
17
- * short-circuit hoist guard (raise/shortcircuit.ts) both derive from this flag. */
16
+ * an unconditional position. THE one effect vocabulary — every question ir/discipline.ts asks of an
17
+ * op derives from this flag and the two below. */
18
18
  effects?: boolean;
19
19
  /** reads memory. Deletable when dead — nothing observes a load nobody reads — but NOT movable:
20
20
  * a load answers whichever stores ran before it, so crossing one changes the value it yields.
@@ -109,6 +109,48 @@ export const OPCODES = {
109
109
  // which recompiles to the branch diamond the source emitted.
110
110
  logic_and: { operands: 2, results: 1 },
111
111
  logic_or: { operands: 2, results: 1 },
112
+ // --- a 64-bit value and its two halves ---
113
+ // A 64-bit integer is ONE value of `{kind:'int'|'unknown', width:64}`, never two results and never
114
+ // a new `IrType` kind. The rule that decides it: for TYPES, widen the number that is already
115
+ // there; for OPERATIONS, add a new opcode. Existing code tests `t.kind === 'int'` and reads
116
+ // `t.width`, so a wider width makes a pass that is wrong about it fail LOUDLY, while a new kind
117
+ // would make the same pass skip SILENTLY. Existing code switches on `op.opcode`, so a new opcode
118
+ // makes an existing pass skip, which is the safe direction there.
119
+ //
120
+ // So the arithmetic opcodes above gain NOTHING: `add`, `sub`, `mul`, the shifts and the `icmp_*`
121
+ // family carry the width in their operand types. Only the three ops that have no 32-bit spelling
122
+ // are new — building a 64-bit value out of two halves, and reading either half back.
123
+ //
124
+ // `concat lo, hi` is TRANSIENT the way `mulh` is: it renders only in the shapes something above
125
+ // it recognises (a widen, a literal, an ABI pair), and one that survives to the structurer is a
126
+ // loud gap. `lo32`/`hi32` invert that property and are PERMANENT, because C spells them exactly —
127
+ // `(u32)v` and `(u32)(v >> 32)`.
128
+ //
129
+ // WHY `hi32` AND NOT `lo32(shr_u v {imm:32})`, which is the same value in two ops rather than
130
+ // three: spelling a projection as a 64-bit SHIFT puts a 64-bit arithmetic op into the IR at every
131
+ // pair site, and hands it to every pass that matches a shift — `raise/divpow2.ts` on
132
+ // `shr_s {imm}`, `raise/extscale.ts` on `shl`, `raise/narrow.ts` on a narrow/re-widen pair. Those
133
+ // passes would then be REACHED by a 64-bit operand instead of skipping it. A third opcode nothing
134
+ // else matches is what buys the isolation.
135
+ concat: { operands: 2, results: 1 }, // concat lo, hi → the 64-bit value whose low half is `lo`
136
+ lo32: { operands: 1, results: 1 },
137
+ hi32: { operands: 1, results: 1 },
138
+ // --- hardware floating point ---
139
+ // IEEE arithmetic on `{kind:'float'}` values, and on nothing else (ir/verify.ts checks both
140
+ // directions). NEW OPCODES rather than `add`/`sub` over a float-typed operand, by the rule the
141
+ // 64-bit section above states for OPERATIONS: every pass that matches `add` is an integer rewrite
142
+ // — a pointer walk, a reassociation, a constant fold, an idiom pattern — and a new opcode is what
143
+ // makes each of them skip. Pure: under the default IEEE environment none of them traps, so a
144
+ // dead one is reaped like any integer op.
145
+ fadd: { operands: 2, results: 1 },
146
+ fsub: { operands: 2, results: 1 },
147
+ fmul: { operands: 2, results: 1 },
148
+ fdiv: { operands: 2, results: 1 },
149
+ fneg: { operands: 1, results: 1 },
150
+ // A double LITERAL: `bits` is its IEEE pattern as sixteen hex digits (ir/float-bits.ts). Its own
151
+ // opcode rather than a `const` over a float type for the reason the arithmetic above has its own:
152
+ // every pass that matches `const` is an integer fold.
153
+ fconst: { operands: 0, results: 1, requiredAttrs: ['bits'] },
112
154
  // --- memory ---
113
155
  load: { operands: 1, results: 1, requiredAttrs: ['off', 'width', 'signed'], reads: true },
114
156
  store: { operands: 2, results: 0, requiredAttrs: ['off', 'width'], effects: true },
@@ -119,8 +161,10 @@ export const OPCODES = {
119
161
  aload: { operands: 2, results: 1, requiredAttrs: ['elemSize', 'signed'], reads: true }, // aload base, index
120
162
  astore: { operands: 3, results: 0, requiredAttrs: ['elemSize'], effects: true }, // astore base, index, value
121
163
  // --- call: operands are the argument values (r0..), result is the return value (r0),
122
- // `target` attr is the callee symbol. Caller-saved clobbering is implicit. ---
123
- call: { operands: 'variadic', results: 1, requiredAttrs: ['target'], effects: true },
164
+ // `target` attr is the callee symbol — or, with `indirect: true` and no `target`, the
165
+ // callee is the address in the LAST operand (ir/core.ts `calleeValue`). Exactly one of the
166
+ // two, which `ir/verify.ts` checks. Caller-saved clobbering is implicit. ---
167
+ call: { operands: 'variadic', results: 1, effects: true },
124
168
  // The ADDRESS of a named global (agbcc `ldr rD, .Lpool` where the pool word is `.word gSym`).
125
169
  // Pure, 0 operands. Globals come from the project headers, so they are referenced by name, never
126
170
  // declared as locals. The structurer lowers it three ways (see scalarGlobals in structure.ts):
@@ -134,10 +178,17 @@ export const OPCODES = {
134
178
  // proving every access agrees, and the structurer declares the local and renders `&name` exactly
135
179
  // as it renders a gaddr's `&sym`. Operand-free and pure, so GVN numbers it like gaddr and a dead
136
180
  // one is reaped.
137
- // `width`/`signed` are stamped by the frontend's frame-object AUDIT — requiring them makes
138
- // "the audit ran" a verifier-checkable fact instead of a convention: a frontend that emits a
139
- // laddr and skips the audit fails verify loudly instead of rendering `&undefined`.
140
- laddr: { operands: 0, results: 1, requiredAttrs: ['off', 'width', 'signed'] },
181
+ // `width`/`signed`/`count` are stamped by the frontend's frame-object AUDIT — requiring them
182
+ // makes "the audit ran" a verifier-checkable fact instead of a convention: a frontend that emits
183
+ // a laddr and skips the audit fails verify loudly instead of rendering `&undefined`. The SHAPE
184
+ // the three of them encode is checked there too (ir/verify.ts), because the attrs are
185
+ // disjunctive and only `count` tells the two arms apart.
186
+ // The object is `count` elements of `width` bytes, so its storage spans `width * count`. A
187
+ // SCALAR is count 1, typed by the access width and signedness every access agreed on. Count
188
+ // above 1 is STORAGE the audit sized without typing — no access pins an element type, so it is
189
+ // width 1, unsigned, and declares as `u8 name[count]`; nothing else may mint one, because the
190
+ // only thing that licenses an untyped object is the frame accounting the audit solves.
191
+ laddr: { operands: 0, results: 1, requiredAttrs: ['off', 'width', 'signed', 'count'] },
141
192
  // An UNDEFINED value: a read of storage that carries no INPUT — nothing was entitled to hand this
142
193
  // function a value there, and none of its own stores reached it on this path. Deliberately NOT
143
194
  // "storage nobody could have written": a callee-saved register holds the CALLER's value at entry,
@@ -153,7 +204,7 @@ export const OPCODES = {
153
204
  // merely "owns the storage", because a frame the function owns can still be written by
154
205
  // someone else once an address into it escapes to a callee, which fills a wider object than
155
206
  // any in-function access reveals. Whoever mints one owes the retraction on escape
156
- // (frontend/thumb.ts, after the frame-object audit).
207
+ // (the frame-object audit, frontend/frame-objects.ts).
157
208
  // • a REGISTER the ABI does not pass arguments in AND this function's prologue SAVED cannot
158
209
  // carry a value a caller handed over, and has no address for anything else to reach it by, so
159
210
  // there is nothing to retract. The save is half of the premise, not a corroboration of it:
@@ -199,13 +250,28 @@ export const OPCODES = {
199
250
  * does not. */
200
251
  export type Opcode = keyof typeof OPCODES;
201
252
 
253
+ /** The width of a 64-bit IR value, named once so the three places that build one and the verifier
254
+ * that checks them cannot come to disagree about it. */
255
+ export const WIDE_BITS = 64;
256
+
202
257
  /** The widths a `zext`/`sext` carries — a fact about those two opcodes, so it lives with them
203
258
  * rather than re-declared per consumer (raise/narrow.ts pairs a narrowing op with its re-widening,
204
259
  * raise/paramwidth.ts declares a parameter at one). Every frontend that produces the pair produces
205
260
  * one of these: agbcc's gated shift-pair fold (pattern/engine.ts CAST_PATTERNS) and PPC's
206
- * `extsb`/`extsh` (frontend/ppc.ts). A third width would be a C type the backend cannot spell. */
261
+ * `extsb`/`extsh` (frontend/ppc.ts). A third width would be a C type the backend cannot spell.
262
+ *
263
+ * 32 IS NOT ONE OF THEM, and the widening to 64 does not come here. The machine spells that widen
264
+ * as a PAIR — agbcc `asr rN,rM,#31` or `mov rN,#0`, PPC `srawi`, MIPS `sra` — so it is a `concat`
265
+ * shape (`concat(x, shr_s(x,31))` signed, `concat(x, const 0)` unsigned), which is also the only
266
+ * form that says HOW the half was widened. This set keeps its one job: refuse an extension with no
267
+ * C spelling. */
207
268
  export const CAST_WIDTHS: ReadonlySet<number> = new Set([8, 16]);
208
269
 
270
+ /** The float opcodes: the only ops a `{kind:'float'}` value may be an operand or a result of,
271
+ * besides `ret` (ir/verify.ts). Authored beside the registry because no signature field says what
272
+ * an op computes ON. */
273
+ export const FLOAT_OPS: ReadonlySet<string> = new Set(['fadd', 'fsub', 'fmul', 'fdiv', 'fneg', 'fconst']);
274
+
209
275
  /** Signature lookup by RUNTIME opcode string (Op.opcode is a plain string — IR consumers switch
210
276
  * on it); undefined for an unregistered opcode. */
211
277
  export function opSig(opcode: string): OpSig | undefined {
@@ -214,17 +280,16 @@ export function opSig(opcode: string): OpSig | undefined {
214
280
 
215
281
  /** The comparison whose result is the logical NEGATION of each `icmp_*` — `!(a < b)` is `a >= b`.
216
282
  *
217
- * Unlike EFFECTFUL_OPS/HOIST_UNSAFE_OPS below, this is AUTHORED data seated beside the registry,
218
- * not a view derived from it: nothing in `OPCODES` states which comparison opposes which. What is
283
+ * AUTHORED data seated beside the registry, not a view derived from it: nothing in `OPCODES`
284
+ * states which comparison opposes which. What is
219
285
  * derived is its SYMMETRY — the five involutive pairs are expanded both ways, so `neg(neg(c)) === c`
220
286
  * holds by construction (a hand-written map is one typo away from breaking it, and the symptom is a
221
287
  * plainly inverted condition in the emitted C). Completeness against the icmp family is the part
222
288
  * construction cannot give, so a test asserts it (test/pattern.test.ts) — an eleventh comparison
223
289
  * added to `OPCODES` would otherwise degrade its consumers several different ways.
224
290
  *
225
- * It lives here for the reason HOIST_UNSAFE_OPS does: every consumer that has to say "the opposite
226
- * of this compare" reads THIS one, so they
227
- * cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
291
+ * It lives here so that every consumer that has to say "the opposite of this compare" reads THIS
292
+ * one, and they cannot drift apart the way inline copies did. Two adjacent facts worth knowing: raise/
228
293
  * shortcircuit.ts derives its `BOOL_OPS` from these keys (asserting negatable-icmp == boolean-op,
229
294
  * true today), and l3/ast.ts `NEGATE_REL` is the SAME relation over the neutral L3 operator
230
295
  * vocabulary — deliberately separate, because signedness lives in the operand types there, so the
@@ -243,103 +308,17 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
243
308
  ]),
244
309
  );
245
310
 
246
- /** Ops with an observable side effect: the flag on the signature, derived rather than re-listed.
247
- * `isDceSafe` asks the same question of the FLAG through `opSig` rather than of this set, so the
248
- * two cannot disagree. The SET's own consumers are `HOIST_UNSAFE_OPS` below, structure.ts's
249
- * block-purity tests (may this block be folded into a loop header, is this exit owned),
250
- * analysis.ts's memory-write barrier, divpow2's bias block (which is DELETED rather than moved),
251
- * and the idiom layer's de-sequencing guard (pattern/engine.ts). Derived rather than re-listed
252
- * because three of those consumers each carried a hand-written copy of this membership, which is
253
- * how the models drifted apart. */
254
- export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
255
- (Object.keys(OPCODES) as Opcode[]).filter((k) => (OPCODES[k] as OpSig).effects),
256
- );
257
-
258
- /** Ops that may not be SPECULATED — run on a path that did not run them before. Identical to
259
- * `EFFECTFUL_OPS`, and kept as its own name because its call sites ask the speculation question
260
- * rather than the deletion one.
261
- *
262
- * A memory read is deliberately absent, and that is the one entry worth arguing: its only consumer
263
- * is raise/shortcircuit.ts, which hoists an arm's body into the block above, and the structurer
264
- * inlines an unnamed value back into the `&&`/`||` right-hand side, where C's own short-circuit
265
- * re-guards it. Adding the two reads
266
- * here cost three byte-matches (kleod:UpdateHUDCounterDisplay, retired 2026-09-13 with its source,
267
- * plus synthetic:breakloop and synthetic:strcmp1), so the argument was load-bearing rather than
268
- * merely plausible. Not re-measured since the kleod row's retirement.
269
- *
270
- * KNOWN GAP: the trapping divides are absent too, and there the re-guard argument does NOT carry
271
- * — a hoisted `sdiv` that the structurer NAMES becomes an unconditional statement. Left as it is
272
- * because closing it is a separate change with its own measurement; `REEVAL_UNSAFE_OPS` does
273
- * refuse them, so the pre-update sink is not exposed to it.
274
- *
275
- * AND THE EXEMPTION IS NOT TRANSFERABLE, which is worth saying beside it: a second consumer once
276
- * read this set to answer "would gcc have SPECULATED this arm above a compare", where nothing
277
- * re-guards anything, and admitted single-load arms `gcc/jump.c:483`'s `! may_trap_p` refuses.
278
- * Pointing that consumer back at this set costs `synthetic:mergeldcast:agbcc` its byte-match
279
- * (MATCH -> diff:6), measured. That consumer is `raise/narrowlocal.ts` and it reads
280
- * `REEVAL_UNSAFE_OPS` instead. Reach for this set only when the argument above — a C-level
281
- * re-guard at the new point — actually holds at your call site. */
282
- export const HOIST_UNSAFE_OPS: ReadonlySet<string> = EFFECTFUL_OPS;
283
-
284
311
  /** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
285
312
  * `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
286
313
  * Authored rather than derived — no registry field records the operand ROLE, and the only other
287
314
  * memory-touching opcode is `call`, whose variadic operands are arguments and not a base.
288
315
  *
289
- * Two consumers ask two different questions of it and both need the same answer, which is why it
290
- * is here and not next to either: `structure/analysis.ts` uses it for the address-home variation's slot
291
- * model, and `raise/const.ts` to recognise a folded literal that IS an address. */
316
+ * Three consumers ask three different questions of it and all need the same answer, which is why
317
+ * it is here and not next to any: `structure/analysis.ts` uses it for the address-home variation's
318
+ * slot model, `raise/const.ts` to recognise a folded literal that IS an address, and
319
+ * `ir/discipline.ts` to tell a device access's `volatile` from a frame object's. */
292
320
  export const MEM_BASE_OPS: ReadonlySet<string> = new Set(['load', 'store', 'aload', 'astore']);
293
321
 
294
- /** Ops whose answer depends on WHERE they run: an effect (its order against other effects is
295
- * observable) or a memory read (it answers whichever stores ran before it). The question a pass
296
- * asks before moving a computation to another point on the SAME path. `SPELLED_WHEN_DEAD_OPS`
297
- * below asks a DIFFERENT question and derives, today, the same set. */
298
- export const ORDER_SENSITIVE_OPS: ReadonlySet<string> = new Set(
299
- (Object.keys(OPCODES) as Opcode[]).filter((k) => {
300
- const sig = OPCODES[k] as OpSig;
301
- return sig.effects || sig.reads;
302
- }),
303
- );
304
-
305
- /** Ops the structurer must still SPELL when nothing consumes their result — an effect, or a memory
306
- * READ. Consumer: structure.ts's `sideEffects` walk.
307
- *
308
- * The read half is the entry worth arguing, because `reads` documents the OPPOSITE about C — a
309
- * load nobody reads is deletable, nothing observes it. That is the C claim. The COMPILER claim
310
- * points the other way: an optimizing compiler deletes every dead read it is ALLOWED to delete, so
311
- * one still in the target is evidence the source's access was `volatile`. Membership here only
312
- * says the structurer may not drop the op silently; whether a `volatile` can actually reach the
313
- * access is a second, ADDRESS-level question the call site asks separately
314
- * (structure.ts `volatileQualifiable`), and a read it answers no to is dropped as before.
315
- *
316
- * DERIVED FROM THE REGISTRY, not aliased to `ORDER_SENSITIVE_OPS`, even though the two are
317
- * extensionally identical today and `HOIST_UNSAFE_OPS` above does alias `EFFECTFUL_OPS`. An alias
318
- * makes two DIFFERENT questions incapable of ever differing, so the day one of them acquires an
319
- * opcode the other should not have, the edit lands on both silently. The identity is pinned by a
320
- * test instead (test/pattern.test.ts), where a future divergence surfaces as a decision to make
321
- * rather than a coupling nobody sees. */
322
- export const SPELLED_WHEN_DEAD_OPS: ReadonlySet<string> = new Set(
323
- (Object.keys(OPCODES) as Opcode[]).filter((k) => {
324
- const sig = OPCODES[k] as OpSig;
325
- return sig.effects || sig.reads;
326
- }),
327
- );
328
-
329
- /** Ops that may not be RE-EVALUATED at another program point — order-sensitive, or trapping. The
330
- * trap half is what separates this from `ORDER_SENSITIVE_OPS`: it only matters when the new point
331
- * can be reached on a path the old one was not, so a consumer that merely re-orders on one path
332
- * wants the smaller set. Both are needed because the two consumers differ on exactly the divides:
333
- * a collapsed switch re-renders a test block's ops AT THEIR USES, and every use is dominated by
334
- * the def, so it evaluates them on a SUBSET of the original paths — a narrowing, never a
335
- * speculation. */
336
- export const REEVAL_UNSAFE_OPS: ReadonlySet<string> = new Set(
337
- (Object.keys(OPCODES) as Opcode[]).filter((k) => {
338
- const sig = OPCODES[k] as OpSig;
339
- return sig.effects || sig.reads || sig.traps;
340
- }),
341
- );
342
-
343
322
  /** Ops that MATERIALIZE a value out of nothing: no operands, no state read, no effect, no trap, no
344
323
  * control flow — so where one sits in a block says nothing about what ran before it. Derived, so a
345
324
  * future pure nullary opcode joins without a second edit. Consumed by raise/paramwidth.ts, whose
@@ -352,10 +331,3 @@ export const MATERIALIZING_OPS: ReadonlySet<string> = new Set(
352
331
  return sig.operands === 0 && !sig.terminator && !sig.effects && !sig.reads && !sig.traps;
353
332
  }),
354
333
  );
355
-
356
- /** May a dead result of this opcode be deleted? Registered, no observable effects, not control
357
- * flow. `opaque` is excluded via its `effects` flag — see the note on its signature. */
358
- export function isDceSafe(opcode: string): boolean {
359
- const sig = opSig(opcode);
360
- return !!sig && !sig.effects && !sig.terminator;
361
- }
package/src/ir/parse.ts CHANGED
@@ -127,6 +127,7 @@ export function parse(text: string): Fn {
127
127
  writeOrder: undefined,
128
128
  slotHomes: undefined,
129
129
  paramEvidence: undefined,
130
+ localObjects: undefined,
130
131
  };
131
132
  }
132
133
 
package/src/ir/types.ts CHANGED
@@ -17,13 +17,36 @@ export type IrType =
17
17
  // from `ptr(int)`+array-index because its access pattern is inconsistent with a homogeneous
18
18
  // array (mixed widths / non-uniform offsets). `name` is synthetic today (`Struct0`); a later
19
19
  // DWARF pass supplies real names. Fields are sorted by `off`.
20
- | { kind: 'struct'; name: string; fields: StructField[]; size?: number }
20
+ //
21
+ // `declared` is a struct the project's HEADERS own — one a callee is declared to return
22
+ // (aggregate.ts `aggregateType`) — holding the headers' own spelling of it (`struct Blob64`,
23
+ // `BlobT`). No source this lift prints defines it: the declarations block does, which a candidate
24
+ // compiled inside the project's headers drops, since the headers define it there.
25
+ | { kind: 'struct'; name: string; fields: StructField[]; size?: number; declared?: string }
21
26
  // A fixed-length array `elem[count]`. Today its sole inhabitant is struct padding (a `u8[N]`
22
27
  // pad member seats fields at their exact offsets, raise/struct-arrays.ts) — a REAL type,
23
28
  // not a printed string. Array-typed fields declare with the length AFTER the name in C
24
29
  // (`u8 _pad[4]`), so the backend routes them through a declarator-aware `cDeclare`, not the
25
30
  // prefix `cType`.
26
31
  | { kind: 'array'; elem: IrType; count: number }
32
+ // Several VIEWS of one storage cell (raise/structs.ts): a base read or written at more than one
33
+ // width or extension over the same bytes. Every member sits at offset 0. It carries no name
34
+ // because it is only ever declared INLINE, as the type of the struct member that holds it, and its
35
+ // `size` is the one the target's compiler gives it — which is not always its widest view
36
+ // (`T.union` computes it from the compiler's aggregate boundary).
37
+ | { kind: 'union'; members: StructField[]; size: number }
38
+ // An IEEE floating-point value, single (32) or double (64). A KIND and not an `int` width, which is
39
+ // the one exception to the 64-bit rule at `ir/opcodes.ts` `concat` ("widen the number that is already
40
+ // there"): a float is not an integer of any width, so every pass that tests `kind === 'int'` must
41
+ // SKIP it rather than compute on it, and skipping is what a new kind buys. Its values live in the
42
+ // FPU's own register file, or in a soft-float runtime's register pair, and are computed only by
43
+ // the float opcodes (`fadd` …); `docs/floating-point.md` says which frontends mint one and what
44
+ // they still refuse. Two floats of different widths are different types (`typeEquals`), so a
45
+ // float op mixing them fails `ir/verify.ts`. A null width is a float whose code states no precision:
46
+ // a PowerPC FPR holds a double whichever the source declared, and `fneg`/`fmr` are one instruction
47
+ // for both, so `double dneg(double a){ return -a; }` and its `float` twin compile to one object.
48
+ // Either spelling reproduces it, and a declaration the caller supplies decides between them.
49
+ | { kind: 'float'; width: 32 | 64 | null }
27
50
  | { kind: 'void' }; // a function that returns nothing
28
51
 
29
52
  /** The scalar type of a memory access of `width` bytes: word ⇒ the s32 integer default;
@@ -34,6 +57,23 @@ export function scalarTypeForAccess(width: number, signed: boolean): IrType {
34
57
  return width === 4 ? T.s(32) : T.int(width * 8, signed);
35
58
  }
36
59
 
60
+ /** The width in bits of the integer a type carries, or null where a type carries no integer. A
61
+ * pointer's own width is the machine's and says nothing about what it addresses; an aggregate and
62
+ * `void` have no single width at all. `unknown` DOES carry one, which is the point — before type
63
+ * recovery every value is `unknown`, so a rule that skipped that kind would be vacuous exactly
64
+ * where the frontends build these.
65
+ *
66
+ * THE one copy, for the reason `scalarTypeForAccess` above is: its two consumers read the same
67
+ * question under OPPOSITE policies, and a per-consumer copy is how they drift apart. `ir/verify.ts`
68
+ * reads null as "this type does not take part in the width rule, so pass"; `runtime-helpers.ts`
69
+ * reads null as "this operand carries no integer width, so refuse to fold a 64-bit operation over
70
+ * it". Both want exactly this predicate — a pointer handed to `__muldi3` has no width either of
71
+ * them may claim — so an `IrType` kind added here reaches the verifier and the recogniser together
72
+ * or neither. */
73
+ export function intWidth(t: IrType): number | null {
74
+ return t.kind === 'int' || t.kind === 'unknown' ? t.width : null;
75
+ }
76
+
37
77
  export const T = {
38
78
  unk: (width = 32): IrType => ({ kind: 'unknown', width }),
39
79
  int: (width: number, signed: boolean): IrType => ({ kind: 'int', width, signed }),
@@ -42,9 +82,68 @@ export const T = {
42
82
  ptr: (to: IrType): IrType => ({ kind: 'ptr', to }),
43
83
  struct: (name: string, fields: StructField[], size?: number): IrType => ({ kind: 'struct', name, fields, size }),
44
84
  array: (elem: IrType, count: number): IrType => ({ kind: 'array', elem, count }),
85
+ // Its size is its widest view rounded up to the compiler's aggregate boundary (target.ts).
86
+ union: (members: StructField[], boundary: number): IrType => {
87
+ const extent = Math.max(...members.map((m) => (m.type.kind === 'array' ? m.type.count : 1) * viewBytes(m.type)));
88
+ return { kind: 'union', members, size: Math.ceil(extent / boundary) * boundary };
89
+ },
90
+ f32: (): IrType => ({ kind: 'float', width: 32 }),
91
+ f64: (): IrType => ({ kind: 'float', width: 64 }),
92
+ fUnstated: (): IrType => ({ kind: 'float', width: null }),
45
93
  void: (): IrType => ({ kind: 'void' }),
46
94
  };
47
95
 
96
+ /** The byte width of a union view's element (an array view's element, or the scalar itself). */
97
+ function viewBytes(t: IrType): number {
98
+ const e = t.kind === 'array' ? t.elem : t;
99
+ if (e.kind !== 'int') {
100
+ throw new Error(`a union view is an integer or an array of one, not '${typeToString(t)}'`);
101
+ }
102
+ return e.width / 8;
103
+ }
104
+
105
+ /** The member `name` of an aggregate — a struct's field or a union's view — or undefined when `t`
106
+ * is not an aggregate or declares no such member. THE one lookup the typing walk and the deref
107
+ * contract share, so the two cannot disagree on which member access is well-typed. */
108
+ export function memberOf(t: IrType | undefined, name: string): StructField | undefined {
109
+ if (t?.kind === 'struct') {
110
+ return t.fields.find((f) => f.name === name);
111
+ }
112
+ if (t?.kind === 'union') {
113
+ return t.members.find((m) => m.name === name);
114
+ }
115
+ return undefined;
116
+ }
117
+
118
+ /** The union member of a struct that holds byte `off`, and the view of it an access of `width`
119
+ * bytes reads — the view of that width and, for a narrow LOAD, of that extension; a store, which
120
+ * carries none, takes the width's only view or the unsigned one of two — with the element index
121
+ * inside an ARRAY view (null for a scalar one). Undefined when no union member holds `off`; `view`
122
+ * undefined when the member has no view for the access. */
123
+ export function unionViewAt(
124
+ st: Extract<IrType, { kind: 'struct' }>,
125
+ off: number,
126
+ width: number,
127
+ signed: boolean,
128
+ isStore: boolean,
129
+ ): { member: StructField; view: StructField | undefined; index: number | null } | undefined {
130
+ const member = st.fields.find((f) => f.type.kind === 'union' && f.off <= off && off < f.off + f.type.size);
131
+ if (member === undefined || member.type.kind !== 'union') {
132
+ return undefined;
133
+ }
134
+ const elemOf = (t: IrType): IrType => (t.kind === 'array' ? t.elem : t);
135
+ const wide = member.type.members.filter((m) => intWidth(elemOf(m.type)) === width * 8);
136
+ const signOf = (m: StructField): boolean | undefined => {
137
+ const e = elemOf(m.type);
138
+ return e.kind === 'int' ? e.signed : undefined;
139
+ };
140
+ const view =
141
+ width === 4 || (isStore && wide.length === 1)
142
+ ? wide[0]
143
+ : wide.find((m) => signOf(m) === (isStore ? false : signed));
144
+ return { member, view, index: view?.type.kind === 'array' ? (off - member.off) / width : null };
145
+ }
146
+
48
147
  export function typeToString(t: IrType): string {
49
148
  switch (t.kind) {
50
149
  case 'unknown':
@@ -57,6 +156,10 @@ export function typeToString(t: IrType): string {
57
156
  return t.name;
58
157
  case 'array':
59
158
  return `${typeToString(t.elem)}[${t.count}]`;
159
+ case 'union':
160
+ return `union{${t.members.map((m) => `${typeToString(m.type)} ${m.name}`).join(';')}}`;
161
+ case 'float':
162
+ return `f${t.width ?? '?'}`;
60
163
  case 'void':
61
164
  return 'void';
62
165
  }
@@ -67,6 +170,9 @@ export function parseType(s: string): IrType {
67
170
  if (s.endsWith('*')) {
68
171
  return T.ptr(parseType(s.slice(0, -1)));
69
172
  }
173
+ if (s === 'f32' || s === 'f64' || s === 'f?') {
174
+ return s === 'f32' ? T.f32() : s === 'f64' ? T.f64() : T.fUnstated();
175
+ }
70
176
  const m = s.match(/^(unk|s|u)(\d+)$/);
71
177
  if (!m) {
72
178
  throw new Error(`bad type '${s}'`);
@@ -85,12 +191,22 @@ export function typeEquals(a: IrType, b: IrType): boolean {
85
191
  if (a.kind === 'int' && b.kind === 'int') {
86
192
  return a.width === b.width && a.signed === b.signed;
87
193
  }
88
- if (a.kind === 'unknown' && b.kind === 'unknown') {
194
+ if (a.kind === 'void' && b.kind === 'void') {
195
+ return true;
196
+ }
197
+ if ((a.kind === 'unknown' && b.kind === 'unknown') || (a.kind === 'float' && b.kind === 'float')) {
89
198
  return a.width === b.width;
90
199
  }
91
200
  if (a.kind === 'array' && b.kind === 'array') {
92
201
  return a.count === b.count && typeEquals(a.elem, b.elem);
93
202
  }
203
+ if (a.kind === 'union' && b.kind === 'union') {
204
+ return (
205
+ a.size === b.size &&
206
+ a.members.length === b.members.length &&
207
+ a.members.every((m, i) => m.name === b.members[i].name && typeEquals(m.type, b.members[i].type))
208
+ );
209
+ }
94
210
  // Two structs are equal when their name + field layout match (recovered structs are named
95
211
  // by layout-discovery order, so equal name ⇒ equal layout in practice).
96
212
  if (a.kind === 'struct' && b.kind === 'struct') {
package/src/ir/verify.ts CHANGED
@@ -3,16 +3,75 @@
3
3
  // 1. every block ends in exactly one terminator (and it is the last op)
4
4
  // 2. operands well-formed: opcode registered, correct arity/attrs
5
5
  // 3. SSA: each value defined once; every use is defined; def dominates use
6
- // 4. side data: a fn that carries a write-order record carries one for EVERY block, with every
6
+ // 4. a float value is an operand or result of a float op or `ret` only — or a `double` operand of
7
+ // a call whose callee declares one there — and crosses an edge only into a block parameter of
8
+ // its own type; an `fconst` is a double, sixteen hex digits into an f64
9
+ // 5. side data: a fn that carries a write-order record carries one for EVERY block, with every
7
10
  // ordinal inside that block's own write count (ir/core.ts `WriteOrder`)
8
- import { Block, Fn, Value, dominators } from './core';
9
- import { opSig } from './opcodes';
11
+ import { Block, Fn, Op, Value, dominators } from './core';
12
+ import { isDoubleBits } from './float-bits';
13
+ import { FLOAT_OPS, WIDE_BITS, opSig } from './opcodes';
14
+ import { type IrType, T, intWidth, typeEquals, typeToString } from './types';
10
15
 
11
16
  export class VerifyError extends Error {}
12
17
 
18
+ /** A call hands a float only to a parameter its callee is declared to take a `double` in — the
19
+ * operands the frontend lists in `doubles` — and returns none. */
20
+ function doublesDeclared(op: Op): boolean {
21
+ const at = op.attrs.doubles;
22
+ return (
23
+ op.opcode === 'call' &&
24
+ Array.isArray(at) &&
25
+ op.results.every((r) => r.type.kind !== 'float') &&
26
+ op.operands.every((o, i) => o.type.kind !== 'float' || (at.includes(i) && typeEquals(o.type, T.f64())))
27
+ );
28
+ }
29
+
13
30
  // Opcodes admitting EITHER a 2-operand register form OR a 1-operand + `imm` attr form.
14
31
  const TWO_OR_IMM = new Set(['sdiv', 'shl', 'shr_u', 'shr_s']);
15
32
 
33
+ // ── 64 DOES NOT MIX ────────────────────────────────────────────────────────────────────────────
34
+ // A 64-bit value is one `Value` whose type carries width 64 (ir/opcodes.ts, `concat`), so every
35
+ // pass that reads `t.width` and is wrong about it is wrong HERE rather than three stages later. The
36
+ // rule below is a QUARANTINE and not full width agreement, and the difference is measured rather
37
+ // than stylistic: full agreement is already false on 32-bit IR, because `raise/paramwidth.ts`
38
+ // narrows a parameter to 8 or 16 bits and `add(p_u8, x_s32)` is an ordinary correct shape. What
39
+ // must hold is that a 64-bit operand never meets a 32-bit one in an op that computes on both.
40
+ //
41
+ // `unknown` PARTICIPATES, and that is the whole point: at L1 every value is `unknown`, so a rule
42
+ // that skipped it would be vacuous exactly where this is meant to be red. The shifts count only
43
+ // their SHIFTED operand — a 64-bit shift by a 32-bit count is what the machine does.
44
+ const WIDTH_UNIFORM = new Set(['add', 'sub', 'mul', 'and', 'or', 'xor', 'neg', 'not', 'sdiv', 'udiv', 'smod', 'umod']);
45
+ // The shifts count only their SHIFTED operand: a 64-bit value shifted by a 32-bit count is what
46
+ // every one of these machines does. The comparisons count only their OPERANDS: the result of a C
47
+ // comparison is an `int` whatever it compared, which is what `raise/recover.ts` already types it.
48
+ const SHIFTS = new Set(['shl', 'shr_u', 'shr_s']);
49
+ const COMPARES = new Set([
50
+ 'icmp_slt',
51
+ 'icmp_sle',
52
+ 'icmp_sgt',
53
+ 'icmp_sge',
54
+ 'icmp_ult',
55
+ 'icmp_ule',
56
+ 'icmp_ugt',
57
+ 'icmp_uge',
58
+ 'icmp_eq',
59
+ 'icmp_ne',
60
+ ]);
61
+ /** Whether `t` takes part in the width rule at all: it does exactly when it carries an integer
62
+ * width. `ir/types.ts` owns that question — `raise/widehelpers.ts` asks it too, of the same kinds,
63
+ * and reads the null with the opposite polarity. */
64
+ const widthOf = intWidth;
65
+ /** The types an op's width rule quantifies over. */
66
+ const widthParticipants = (op: Op): IrType[] => {
67
+ const operands = op.operands.map((o) => o.type);
68
+ if (COMPARES.has(op.opcode)) {
69
+ return operands;
70
+ }
71
+ const results = op.results.map((r) => r.type);
72
+ return SHIFTS.has(op.opcode) ? [...results, ...operands.slice(0, 1)] : [...results, ...operands];
73
+ };
74
+
16
75
  export function verify(fn: Fn): void {
17
76
  if (fn.blocks.length === 0) {
18
77
  throw new VerifyError(`fn '${fn.name}' has no blocks`);
@@ -104,11 +163,90 @@ export function verify(fn: Fn): void {
104
163
  if (!isTerm && op.successors.length) {
105
164
  throw new VerifyError(`non-terminator '${op.opcode}' has successors`);
106
165
  }
166
+ // `laddr` carries two shapes in one set of attrs and `count` is the only thing telling
167
+ // them apart: one element is a SCALAR typed by the accesses the frame-object audit read
168
+ // off the machine, and more than one is STORAGE that audit sized without typing, whose
169
+ // width and signedness are therefore not an access at all. The structurer declares the
170
+ // second as `u8 name[count]`, so a typed element there would be a type nothing pinned —
171
+ // the exact answer the audit refuses to invent. (One BYTE encodes like a real `u8`
172
+ // scalar either way; the declaration is the same one, so nothing has to tell them apart.)
173
+ if (op.opcode === 'laddr') {
174
+ const count = op.attrs.count;
175
+ if (typeof count !== 'number' || !Number.isInteger(count) || count < 1) {
176
+ throw new VerifyError(`'laddr' count must be a positive integer, got ${String(count)}`);
177
+ }
178
+ if (count > 1 && (op.attrs.width !== 1 || op.attrs.signed !== false)) {
179
+ throw new VerifyError(
180
+ `'laddr' of ${count} elements is storage no access typed, so it must be unsigned bytes — ` +
181
+ `got width ${String(op.attrs.width)}, signed ${String(op.attrs.signed)}`,
182
+ );
183
+ }
184
+ }
185
+ // A 64-bit value is BUILT and TAKEN APART by exactly these three, so their shape is
186
+ // checked here rather than left to whoever reads a half. `concat` is the only producer.
187
+ if (op.opcode === 'concat' || op.opcode === 'lo32' || op.opcode === 'hi32') {
188
+ const wide = op.opcode === 'concat' ? op.results[0].type : op.operands[0].type;
189
+ const narrow = op.opcode === 'concat' ? op.operands.map((o) => o.type) : op.results.map((r) => r.type);
190
+ const wideW = widthOf(wide);
191
+ if (wideW !== WIDE_BITS) {
192
+ throw new VerifyError(
193
+ `'${op.opcode}' ${op.opcode === 'concat' ? 'result' : 'operand'} must be an integer of ` +
194
+ `width ${WIDE_BITS}, got ${String(wideW ?? wide.kind)}`,
195
+ );
196
+ }
197
+ for (const t of narrow) {
198
+ if (widthOf(t) !== 32) {
199
+ throw new VerifyError(
200
+ `'${op.opcode}' half must be an integer of width 32, got ${String(widthOf(t) ?? t.kind)}`,
201
+ );
202
+ }
203
+ }
204
+ }
205
+ // …and everywhere else, 64 does not mix (see the header above this file's checks).
206
+ if (WIDTH_UNIFORM.has(op.opcode) || SHIFTS.has(op.opcode) || COMPARES.has(op.opcode)) {
207
+ const ws = widthParticipants(op)
208
+ .map(widthOf)
209
+ .filter((w): w is number => w !== null);
210
+ if (ws.some((w) => w === WIDE_BITS) && ws.some((w) => w !== WIDE_BITS)) {
211
+ throw new VerifyError(
212
+ `'${op.opcode}' mixes a ${WIDE_BITS}-bit operand with a narrower one (widths ${ws.join(', ')})`,
213
+ );
214
+ }
215
+ }
216
+ // A FLOAT IS COMPUTED ON BY THE FLOAT OPS AND RETURNED, AND NOTHING ELSE TOUCHES IT. Both
217
+ // halves are checked, because each is a silent wrong the other cannot see: a float op over
218
+ // an integer is a frontend that lost track of a register file, and an integer op over a
219
+ // float is a pass that matched an opcode without asking what it computes on.
220
+ const floats = [...op.operands, ...op.results].map((v) => v.type).filter((t) => t.kind === 'float');
221
+ if (FLOAT_OPS.has(op.opcode)) {
222
+ const all = [...op.operands, ...op.results].map((v) => v.type);
223
+ if (floats.length !== all.length || all.some((t) => !typeEquals(t, all[0]))) {
224
+ throw new VerifyError(`'${op.opcode}' computes on floats only, got ${all.map(typeToString).join(', ')}`);
225
+ }
226
+ } else if (floats.length > 0 && op.opcode !== 'ret' && !doublesDeclared(op)) {
227
+ throw new VerifyError(`a float value reaches '${op.opcode}', which does not compute on floats`);
228
+ }
229
+ // A literal's width is its pattern's, so the pattern and the result must name one width.
230
+ if (op.opcode === 'fconst' && (!isDoubleBits(op.attrs.bits) || !typeEquals(op.results[0].type, T.f64()))) {
231
+ throw new VerifyError(
232
+ `'fconst' is a double literal: bits ${JSON.stringify(op.attrs.bits)} into ${typeToString(op.results[0].type)}`,
233
+ );
234
+ }
107
235
  for (const k of sig.requiredAttrs ?? []) {
108
236
  if (!(k in op.attrs)) {
109
237
  throw new VerifyError(`'${op.opcode}' missing required attr '${k}'`);
110
238
  }
111
239
  }
240
+ if (op.opcode === 'call') {
241
+ const named = typeof op.attrs.target === 'string';
242
+ const indirect = op.attrs.indirect === true;
243
+ if (named === indirect || 'target' in op.attrs !== named || (indirect && op.operands.length === 0)) {
244
+ throw new VerifyError(
245
+ "'call' names its callee in a string 'target' or calls the value in its last operand under " +
246
+ "'indirect', and does exactly one of the two",
247
+ );
248
+ }
249
+ }
112
250
  for (const u of op.operands) {
113
251
  if (!defined.has(u)) {
114
252
  throw new VerifyError(`use of undefined value in '${op.opcode}'`);
@@ -128,6 +266,15 @@ export function verify(fn: Fn): void {
128
266
  throw new VerifyError(`use of undefined value in successor args of '${op.opcode}'`);
129
267
  }
130
268
  }
269
+ // …and a float crosses an edge only into a block parameter of its own type.
270
+ s.args.forEach((u, i) => {
271
+ const p = s.block.params[i].type;
272
+ if ((u.type.kind === 'float' || p.kind === 'float') && !typeEquals(u.type, p)) {
273
+ throw new VerifyError(
274
+ `successor of '${op.opcode}' passes a ${typeToString(u.type)} to a ${typeToString(p)} block parameter`,
275
+ );
276
+ }
277
+ });
131
278
  }
132
279
  },
133
280
  () => at(b, idx),