@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/typing.ts CHANGED
@@ -17,8 +17,8 @@
17
17
  // Callers choose their conservative direction: the emission guard treats `undefined` as "not
18
18
  // provably a pointer" (adds a cast — valid C either way); the deref contract treats `undefined`
19
19
  // as "not provably wrong" (no error).
20
- import { IrType, T, scalarTypeForAccess } from '../ir/types';
21
- import { type Expr, type SFn, exprChildren } from './ast';
20
+ import { IrType, T, isCastView, memberOf, scalarTypeForAccess } from '../ir/types';
21
+ import { type BinOp, type Expr, type SFn, exprChildren, walkExprs } from './ast';
22
22
 
23
23
  /** The declared type of a printed variable — the env `exprCType` judges rendered C against.
24
24
  * THE one copy of the SFn→env derivation — printers, contracts and L3 respell variations alike: each
@@ -31,6 +31,9 @@ export function declaredTypes(fn: SFn): VarTypes {
31
31
  for (const g of fn.globals ?? []) {
32
32
  m.set(g.name, g.type);
33
33
  }
34
+ for (const st of [...(fn.statics ?? []), ...(fn.images ?? [])]) {
35
+ m.set(st.name, st.type);
36
+ }
34
37
  for (const p of fn.params) {
35
38
  m.set(p.name, p.type);
36
39
  }
@@ -55,6 +58,12 @@ export function ptrElemBytes(to: IrType): number {
55
58
  export interface PrintEnv {
56
59
  readonly type: VarTypes;
57
60
  readonly volatilePointee: (name: string) => boolean;
61
+ /** a callee's declared parameter types, on a function whose compiler refuses C's implicit pointer
62
+ * conversions: compiled as C++ (`SFn.dialect`), or by a C front end that refuses them too
63
+ * (`SFn.strictConversions`) (`SFn.declaredArgs`) */
64
+ readonly declaredArgs: (callee: string) => readonly (string | undefined)[] | undefined;
65
+ /** a function-scope static this function defines `const` (`SFn.statics`) */
66
+ readonly constStatic: (name: string) => boolean;
58
67
  }
59
68
 
60
69
  export function printEnv(fn: SFn): PrintEnv {
@@ -63,7 +72,18 @@ export function printEnv(fn: SFn): PrintEnv {
63
72
  const vol = new Set(
64
73
  fn.locals.filter((l) => l.type.kind === 'ptr' && (l.pointeeVolatile || l.volatile)).map((l) => l.name),
65
74
  );
66
- return { type: declaredTypes(fn), volatilePointee: (n) => vol.has(n) };
75
+ const constStatics = new Set((fn.statics ?? []).filter((s) => s.const).map((s) => s.name));
76
+ return {
77
+ type: declaredTypes(fn),
78
+ volatilePointee: (n) => vol.has(n),
79
+ declaredArgs: (callee) =>
80
+ (fn.dialect === 'c++' || fn.strictConversions === true) &&
81
+ fn.declaredArgs &&
82
+ Object.hasOwn(fn.declaredArgs, callee)
83
+ ? fn.declaredArgs[callee]
84
+ : undefined,
85
+ constStatic: (n) => constStatics.has(n),
86
+ };
67
87
  }
68
88
 
69
89
  /** Does anything under `e` assert a VOLATILE POINTEE? The two spellings that carry the qualifier
@@ -103,15 +123,97 @@ export function derefStrideOk(rt: IrType | undefined, width: number, signed: boo
103
123
  return false;
104
124
  }
105
125
 
106
- /** The USUAL ARITHMETIC CONVERSIONS over two rendered operands: at equal rank, unsigned wins.
107
- * Either side unknown leaves the result unknown — EXCEPT when the known side is unsigned, which
108
- * already decides it.
126
+ /** The INTEGER RANK a rendered expression carries, as a width in bits: 64 only where it can be
127
+ * PROVEN, 32 everywhere else. The complement of `renderedIntSignedness` — that one models the
128
+ * signedness `exprCType` reports uniformly as `s32`, this one models the width it reports the same
129
+ * way — and the two together are what the usual arithmetic conversions need.
130
+ *
131
+ * THE COMPLEMENT PARTS COMPANY WITH `exprCType` AT ONE NODE, AND IT IS `call`. There `exprCType`
132
+ * is UNINFORMED by design and answers `undefined`, while this reads the `wide64` stamp the
133
+ * structurer put on the node and can answer 64. A reader who reaches for `exprCType` on a call
134
+ * will be told the information does not exist; it does, and it is here.
135
+ *
136
+ * ITS SOUNDNESS IS AN ENUMERATION OF THE PRODUCERS, so here is the enumeration. A 64-bit value
137
+ * reaches a rendered expression in exactly four ways:
138
+ * 1. a `var`/`postincr` whose DECLARED type is 64 bits wide — read off `varType`;
139
+ * 2. a `cast` to a 64-bit type — read off the node;
140
+ * 3. a `call` the structurer STAMPED `wide64` — read off the node, and it is the one case where
141
+ * the rank cannot be derived from anything visible here: a callee's return type comes from a
142
+ * prototype outside the emitted function, which is why `exprCType` answers `undefined` for a
143
+ * call and why the stamp exists (l3/ast.ts carries what it means);
144
+ * 4. an arithmetic node over one of those — the recursion below.
145
+ * It does NOT arrive through MEMORY, because `contracts.ts` `SCALAR_WIDTHS` is {1,2,4} and there
146
+ * is no 64-bit global; nor through a `const`, because a 64-bit literal only folds inside C `int`
147
+ * range, where 32 is the right answer for the literal's own type.
109
148
  *
110
- * That exception is the one place this returns a DEFINITE answer from an unknown operand, and it
111
- * is sound only because every integer here is rank `int`: at UNEQUAL rank C converts to the wider
112
- * type first, so `unsigned int & long long` is SIGNED. Core has no 64-bit integer type at all
113
- * (the decomp typedef vocabulary stops at 32 — see contracts.ts SCALAR_WIDTHS), so the
114
- * unequal-rank case cannot arise. Adding one would invalidate this.
149
+ * AN UNSTAMPED CALL IS 32 AND THAT IS THE SAFE ANSWER, not a claim: it is what C makes of a callee
150
+ * nobody declared, and every consumer of this rank takes a cast on a 32 it doubts and takes none on
151
+ * a 64 — so the direction that would be wrong silently is the one this never guesses.
152
+ *
153
+ * A SHIFT takes the rank of its left operand alone; every other ARITHMETIC node takes the wider of
154
+ * the two. That is C, and it is also why this is not simply `exprCType(e).width`.
155
+ *
156
+ * THE NODES THAT ARE NOT ARITHMETIC ONES are the same list `renderedIntSignedness` enumerates, and
157
+ * they yield `int` however wide their operands: a comparison, a logical connective, and `!`. `-`
158
+ * and `~` are not in it, because each carries the promoted type of its operand. */
159
+ export function exprIntWidth(e: Expr, varType: VarTypes): 32 | 64 {
160
+ const wide = (t: IrType | undefined): boolean => t?.kind === 'int' && t.width === 64;
161
+ switch (e.k) {
162
+ case 'var':
163
+ case 'postincr':
164
+ case 'cast':
165
+ case 'index':
166
+ case 'field':
167
+ return wide(exprCType(e, varType)) ? 64 : 32;
168
+ case 'call':
169
+ return e.wide64 === true ? 64 : 32;
170
+ case 'un':
171
+ return e.op === '!' ? 32 : exprIntWidth(e.e, varType);
172
+ case 'bin':
173
+ if (INT_RESULT_BINOPS.has(e.op)) {
174
+ return 32;
175
+ }
176
+ if (e.op === '<<' || e.op === '>>' || e.op === '>>>') {
177
+ return exprIntWidth(e.l, varType);
178
+ }
179
+ return exprIntWidth(e.l, varType) === 64 || exprIntWidth(e.r, varType) === 64 ? 64 : 32;
180
+ default:
181
+ return 32;
182
+ }
183
+ }
184
+
185
+ /** The float operators (l3/ast.ts BinOp): their result is the float their operands are, never an
186
+ * integer, so none of the integer conversion rules below applies to one. */
187
+ export const FLOAT_BINOPS: ReadonlySet<BinOp> = new Set<BinOp>(['f+', 'f-', 'f*', 'f/']);
188
+
189
+ /** The binary operators whose RESULT is a C `int` whatever their operands are — read by both
190
+ * halves of the conversion model, which is the point of naming them once. A float compare is one:
191
+ * its operands are floats and its result is C's `int`. */
192
+ const INT_RESULT_BINOPS = new Set<BinOp>([
193
+ '<',
194
+ '<=',
195
+ '>',
196
+ '>=',
197
+ '==',
198
+ '!=',
199
+ '&&',
200
+ '||',
201
+ 'f<',
202
+ 'f<=',
203
+ 'f>',
204
+ 'f>=',
205
+ 'f==',
206
+ 'f!=',
207
+ ]);
208
+
209
+ /** The USUAL ARITHMETIC CONVERSIONS over two rendered operands.
210
+ *
211
+ * AT EQUAL RANK, unsigned wins, and either side unknown leaves the result unknown — EXCEPT when
212
+ * the known side is unsigned, which already decides it. AT UNEQUAL RANK C converts to the wider
213
+ * type first, so the WIDER side's signedness decides outright: `unsigned int / long long` is
214
+ * SIGNED, and taking the equal-rank shortcut there would spell an unsigned divide over a signed
215
+ * one. `exprIntWidth` is what tells the two cases apart, and it is TOTAL, so the rank is always
216
+ * known and only the signedness can be undetermined.
115
217
  *
116
218
  * Exported because two consumers ask it about an operator whose own rendering they are deciding,
117
219
  * so they cannot ask `renderedIntSignedness` about the node: the C-family backend's operand pin,
@@ -119,6 +221,11 @@ export function derefStrideOk(rt: IrType | undefined, width: number, signed: boo
119
221
  export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes): boolean | undefined {
120
222
  const ls = renderedIntSignedness(l, varType);
121
223
  const rs = renderedIntSignedness(r, varType);
224
+ const lw = exprIntWidth(l, varType);
225
+ const rw = exprIntWidth(r, varType);
226
+ if (lw !== rw) {
227
+ return lw > rw ? ls : rs;
228
+ }
122
229
  if (ls === false || rs === false) {
123
230
  return false;
124
231
  }
@@ -133,7 +240,7 @@ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes):
133
240
  *
134
241
  * THE one rendered-signedness judgment, and it lives beside the declarations it judges against
135
242
  * because every consumer shares it: the C-family backend's operand pin, structure.ts's
136
- * unsigned-compare gate (the /uns-cmp variation) and its signed-compare pin, and — through
243
+ * unsigned-compare gate (unsignedCompareSpelling) and its signed-compare pin, and — through
137
244
  * `arithConversionSignedness` above — initfirst's compare-meaning gate. Two of those models
138
245
  * disagreeing about one expression is the drift this placement prevents.
139
246
  *
@@ -149,8 +256,8 @@ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes):
149
256
  * instruction-for-instruction the same where `f` really returns `int`); a missing one is a
150
257
  * miscompile — with `u32 f(void);` in scope `f() / a` calls `__udivsi3` where the machine called
151
258
  * `__divsi3`, and `if (f() >= 0)` compiles to `bl f; mov r0, #0`, comparison and both arms gone.
152
- * The one consumer that reads `undefined` the other way is the /uns-cmp variation's own gate, which is
153
- * asking whether an operand is ALREADY unsigned.
259
+ * The one consumer that reads `undefined` the other way is the unsigned-compare spelling's own
260
+ * gate, which is asking whether an operand is ALREADY unsigned.
154
261
  *
155
262
  * Anything narrower than 32 bits promotes to `int` and is therefore SIGNED, whatever it was
156
263
  * declared. Pointers, calls and markers are `undefined`.
@@ -158,10 +265,16 @@ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes):
158
265
  export function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | undefined {
159
266
  const rec = (x: Expr): boolean | undefined => renderedIntSignedness(x, varType);
160
267
  // an lvalue-ish leaf: its C type is a declaration / an explicit cast / a carried access width
268
+ // Anything narrower than `int` promotes and comes out SIGNED whatever it was declared. `int` and
269
+ // `long long` keep their own signedness, because no promotion applies to either. A width that is
270
+ // NEITHER is not a C type at all — an `index` node carries a struct STRIDE as its width — and
271
+ // stays undetermined, which is what every consumer already takes a cast on.
161
272
  const promoted = (t: IrType | undefined): boolean | undefined =>
162
- t?.kind !== 'int' ? undefined : t.width < 32 ? true : t.width === 32 ? t.signed : undefined;
273
+ t?.kind !== 'int' ? undefined : t.width < 32 ? true : t.width === 32 || t.width === 64 ? t.signed : undefined;
163
274
  switch (e.k) {
164
275
  case 'var':
276
+ // `v++` renders the value the local held, so it promotes exactly as the bare name does.
277
+ case 'postincr':
165
278
  case 'cast':
166
279
  case 'index':
167
280
  case 'field':
@@ -174,10 +287,16 @@ export function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | und
174
287
  // folded to a shift — where `a / (s32)-2147483648` calls `__divsi3`.
175
288
  case 'const':
176
289
  return e.value > -2147483648 && e.value <= 2147483647;
177
- // `-x` / `~x` carry the PROMOTED type of the operand; `!x` is `int`.
290
+ case 'fconst':
291
+ return undefined;
292
+ // `-x` / `~x` carry the PROMOTED type of the operand; `!x` is `int`. A float has no integer
293
+ // signedness at all.
178
294
  case 'un':
179
- return e.op === '!' ? true : rec(e.e);
295
+ return e.op === 'f-' ? undefined : e.op === '!' ? true : rec(e.e);
180
296
  case 'bin': {
297
+ if (FLOAT_BINOPS.has(e.op)) {
298
+ return undefined;
299
+ }
181
300
  // The SIGNEDNESS-CARRYING pairs — the ops the tower keeps apart because C spells each pair
182
301
  // with one token and picks between them from the operand types (l3/ast.ts BinOp). The
183
302
  // C-family backend PINS their operands, so what gets printed renders as the op says
@@ -194,8 +313,8 @@ export function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | und
194
313
  if (e.op === '<<') {
195
314
  return rec(e.l);
196
315
  }
197
- // Comparisons and the logical connectives yield `int`.
198
- if (['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op)) {
316
+ // Comparisons and the logical connectives yield `int`, which is signed.
317
+ if (INT_RESULT_BINOPS.has(e.op)) {
199
318
  return true;
200
319
  }
201
320
  return arithConversionSignedness(e.l, e.r, varType);
@@ -208,16 +327,41 @@ export function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | und
208
327
  }
209
328
 
210
329
  /** Is the rendered expression provably in [0, 2^31) — the range where a signed and an unsigned
211
- * compare agree on every input (and where gcc itself emits the unsigned branch for the signed
212
- * spelling)? Narrow unsigned values are the everyday case: a `(u8)x` cast or a `u8`/`u16`
213
- * deref promotes to a non-negative `int`. Conservative false elsewhere. */
330
+ * compare agree on every input? Narrow unsigned values are the everyday case: a `(u8)x` cast or a
331
+ * `u8`/`u16` deref promotes to a non-negative `int`, and so is a mask, `x & 0xFFFF`, whose result
332
+ * has no bit its non-negative side lacks; `|` and `^` set a bit only where a side has it, so they
333
+ * are non-negative when both sides are. Conservative false elsewhere. A VALUE fact: it says the
334
+ * two compares mean the same, not that they compile the same (`spelledNonNegative`). */
214
335
  export function provablyNonNegative(e: Expr, varType: VarTypes): boolean {
336
+ return nonNegative(e, varType, true);
337
+ }
338
+
339
+ /** The subset of `provablyNonNegative` over which the DEFAULT leaves an unsigned compare uncast
340
+ * (structure.ts): everything but an `|`/`^`. Those are non-negative too, so their uncast
341
+ * spelling owes no proof, but the pin stays in the default because it is codegen-visible where
342
+ * the compiler does not track the bits: mwcc_242_81 compiles
343
+ * `if ((unsigned)((a & 0xf0) | (b & 3)) < 0x31u)` beside a `!= 0` test to `cmplwi`, and the lift
344
+ * scores 9/15 cast against 13/16 uncast over `s32` parameters (10/15 for the `u32` twin). Without
345
+ * the pin in the default that spelling is in no candidate at all, since `/signed-cmp` is the
346
+ * uncast one. A mask alone (gcc's own pick of the unsigned branch, kmc `selconst`) keeps the
347
+ * uncast default. */
348
+ export function spelledNonNegative(e: Expr, varType: VarTypes): boolean {
349
+ return nonNegative(e, varType, false);
350
+ }
351
+
352
+ function nonNegative(e: Expr, varType: VarTypes, orXor: boolean): boolean {
215
353
  switch (e.k) {
216
354
  case 'const':
217
355
  return e.value >= 0 && e.value < 0x80000000;
218
356
  case 'un':
219
357
  return e.op === '!';
220
358
  case 'bin':
359
+ if (e.op === '&') {
360
+ return nonNegative(e.l, varType, orXor) || nonNegative(e.r, varType, orXor);
361
+ }
362
+ if (orXor && (e.op === '|' || e.op === '^')) {
363
+ return nonNegative(e.l, varType, orXor) && nonNegative(e.r, varType, orXor);
364
+ }
221
365
  return ['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op);
222
366
  case 'cast':
223
367
  return e.to.kind === 'int' && e.to.width < 32 && !e.to.signed;
@@ -228,6 +372,47 @@ export function provablyNonNegative(e: Expr, varType: VarTypes): boolean {
228
372
  }
229
373
  }
230
374
 
375
+ /** Would C evaluate `l OP r` SIGNED, for an ordering compare the machine did unsigned? Both sides
376
+ * render as signed-promoting C (or as a type this env cannot see, which may be), neither is a
377
+ * pointer (C compares an address unsigned), and they are not both `nonNeg`. With the default,
378
+ * `provablyNonNegative`, a true answer means the C compares DIFFERENT values from the machine:
379
+ * the proof obligation, which `signedOverUnsignedCompare` asks of the finished tree.
380
+ * structure.ts also asks it with `spelledNonNegative` to place the `(u32)` pin. */
381
+ export function rendersSignedOverUnsigned(
382
+ l: Expr,
383
+ r: Expr,
384
+ varType: VarTypes,
385
+ nonNeg: (e: Expr, varType: VarTypes) => boolean = provablyNonNegative,
386
+ ): boolean {
387
+ const ptrSide = (x: Expr): boolean => {
388
+ const t = exprCType(x, varType);
389
+ return t?.kind === 'ptr' || t?.kind === 'array';
390
+ };
391
+ return (
392
+ !ptrSide(l) &&
393
+ !ptrSide(r) &&
394
+ renderedIntSignedness(l, varType) !== false &&
395
+ renderedIntSignedness(r, varType) !== false &&
396
+ !(nonNeg(l, varType) && nonNeg(r, varType))
397
+ );
398
+ }
399
+
400
+ /** Does the finished tree spell some unsigned compare (`Expr.unsignedCmp`) so that C evaluates it
401
+ * signed? Judged over the tree's own declarations, AFTER every pass that rewrites it: emission
402
+ * pins each compare it lowers, but a respell that substitutes an arm's value into an emitted
403
+ * compare (l3/unmerge.ts puts an `s16` global where a `u32` local stood) re-renders it without
404
+ * asking. A name the declarations do not hold — a global the symbol declarations type outside
405
+ * the tree — is not provably unsigned, so it counts: this errs toward withholding. */
406
+ export function signedOverUnsignedCompare(sfn: SFn): boolean {
407
+ const env = declaredTypes(sfn);
408
+ for (const e of walkExprs(sfn.body)) {
409
+ if (e.k === 'bin' && e.unsignedCmp === true && rendersSignedOverUnsigned(e.l, e.r, env)) {
410
+ return true;
411
+ }
412
+ }
413
+ return false;
414
+ }
415
+
231
416
  /** Does this write put a value that renders as a DEFINITELY-NON-POINTER C expression into a
232
417
  * pointer-declared slot? — `v2 = a1 + v0` with `v2: u8 *`, a `return a0 + v0` from a
233
418
  * pointer-returning function, a store through a pointer-element lvalue. The machine's register
@@ -257,17 +442,25 @@ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined
257
442
  const rec = (x: Expr): IrType | undefined => exprCType(x, varType);
258
443
  switch (e.k) {
259
444
  case 'var':
445
+ case 'postincr':
260
446
  return varType(e.name);
261
447
  // An integer literal spells as a plain C `int` — NEVER a pointer, whatever the value's
262
448
  // recovered type was. This is the exact gap the emission guard exists to bridge.
263
449
  case 'const':
264
450
  return T.s(32);
451
+ case 'fconst':
452
+ return T.f64();
265
453
  case 'cast':
266
454
  return e.to;
267
- // `-`/`~` yield the promoted integer; `!` yields int. None yields a pointer.
455
+ // `-`/`~` yield the promoted integer; `!` yields int. None yields a pointer. A float negation
456
+ // yields the float it negates.
268
457
  case 'un':
269
- return T.s(32);
458
+ return e.op === 'f-' ? rec(e.e) : T.s(32);
270
459
  case 'bin': {
460
+ // Both operands are one float type (ir/verify.ts holds the IR op to that), so the left names it.
461
+ if (FLOAT_BINOPS.has(e.op)) {
462
+ return rec(e.l);
463
+ }
271
464
  if (e.op === '+' || e.op === '-') {
272
465
  const l = rec(e.l);
273
466
  const r = rec(e.r);
@@ -291,7 +484,14 @@ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined
291
484
  return T.s(32);
292
485
  }
293
486
  // A callee's C return type comes from a prototype OUTSIDE the emitted function (the
294
- // project ctx / C89 implicit int) — not statically knowable here.
487
+ // project ctx / C89 implicit int) — not derivable from the tree here.
488
+ //
489
+ // THE NODE MAY STILL CARRY ITS RANK, and that is a different question from its TYPE:
490
+ // `exprIntWidth` reads the `wide64` stamp and answers 64 for a call whose callee the frontend
491
+ // was told returns a pair. Nothing is read off it here, because the stamp is a width and this
492
+ // owes its callers a full `IrType` — signedness included, which no source states. Widening
493
+ // this to answer `s64` from the stamp would be a signedness claim nobody made, over the one
494
+ // node every rendered function has; the rank-only reader is the honest half.
295
495
  case 'call':
296
496
  return undefined;
297
497
  case 'index': {
@@ -306,8 +506,16 @@ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined
306
506
  // would type it `s96`-style garbage and make the field contract reject valid trees. The
307
507
  // node width must AGREE with the element size (when the struct declares one): a mismatch
308
508
  // means the stride channel is corrupt, so it types scalar and the field contract flags it.
509
+ // A recovered struct declares no size, so the one other index on its pointer — a CAST VIEW of
510
+ // a `castViews` field, `((u8 *)p)[1]` (structure.ts `memAccess`) — is told apart by the
511
+ // same rule that minted it, and reads its scalar.
309
512
  const bt = rec(e.base);
310
- if (bt?.kind === 'ptr' && bt.to.kind === 'struct' && (bt.to.size === undefined || bt.to.size === e.width)) {
513
+ if (
514
+ bt?.kind === 'ptr' &&
515
+ bt.to.kind === 'struct' &&
516
+ (bt.to.size === undefined || bt.to.size === e.width) &&
517
+ !(e.idx.k === 'const' && isCastView(bt.to, e.idx.value * e.width, e.width, e.signed, false))
518
+ ) {
311
519
  return bt.to;
312
520
  }
313
521
  if (bt?.kind === 'ptr' && derefStrideOk(bt, e.width, e.signed)) {
@@ -319,13 +527,10 @@ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined
319
527
  return scalarTypeForAccess(e.width, e.signed);
320
528
  }
321
529
  case 'field': {
322
- // `base->name` (base: ptr-to-struct) or `base.name` (base: struct value, an array element).
530
+ // `base->name` (base: ptr-to-struct) or `base.name` (base: struct value, an array element,
531
+ // or a union member's view).
323
532
  const bt = rec(e.base);
324
- const st = bt?.kind === 'ptr' ? bt.to : bt;
325
- if (st?.kind !== 'struct') {
326
- return undefined;
327
- }
328
- return st.fields.find((f) => f.name === e.name)?.type;
533
+ return memberOf(bt?.kind === 'ptr' ? bt.to : bt, e.name)?.type;
329
534
  }
330
535
  case 'marker':
331
536
  return undefined;
@@ -137,11 +137,12 @@
137
137
  // read off `acc = acc + K`, so it counts in the units of the ACCUMULATOR's declared type: on a
138
138
  // `u16 *` a step of 32 advances 64 BYTES. The closed form spells that stride onto the INIT, whose
139
139
  // `+` scales by whatever the INIT's own C type says. Where the two disagree the candidate
140
- // addresses the wrong byte, compiles clean, and carries no marker — structure.ts's `bytePtr`
141
- // states the same rule from the other end ("a `u16 *` walked by a computed offset addresses TWICE
142
- // the intended byte, and nothing downstream can see the error"). `stride-units` refuses unless
143
- // both scales are KNOWN and equal; a narrow integer accumulator is the same question in the other
144
- // direction, since `u16 acc` wraps at 65536 where `init + (i << 6)` does not.
140
+ // addresses the wrong byte, compiles clean, and carries no marker — the POINTER STRIDE rule of
141
+ // `ARITH_ROWS` in structure/pointer-spelling.ts states the same rule from the other end: a
142
+ // runtime offset on a pointer whose pointee is not one byte walks `(u8 *)p`.
143
+ // `stride-units` refuses unless both scales are KNOWN and equal; a narrow integer accumulator is
144
+ // the same question in the other direction, since `u16 acc` wraps at 65536 where
145
+ // `init + (i << 6)` does not.
145
146
  //
146
147
  // THAT GATE HAS NO BENCHMARK REACH AT ALL, and neither tier can see it. Censused at the
147
148
  // `firstRejection` call site over both — 750 synthetic trees and all 252 real-tier rows, the real
@@ -440,10 +441,10 @@ function arithScale(t: IrType | undefined): number | null {
440
441
  * refusal.
441
442
  *
442
443
  * MEASURED, so this is a stated zero and not an assumption: instrumented at the deletion site, it
443
- * fires on none of the three agbcc rows that spill AND lift — `spillorder`, `dma_fill_uninit`,
444
- * `uninit_spill` — so the clause costs no candidate today. (`spill10` spills too but declines in the
445
- * Thumb frontend, so it never reaches this pass and its zero says nothing.) It is here for the day
446
- * one does. */
444
+ * fires on none of the agbcc rows that spill AND lift — `spillorder`, `dma_fill_uninit`,
445
+ * `uninit_spill`, `spill10`, `spillarg` — so the clause costs no candidate today. (The last two
446
+ * bring no local to these gates at all, so their zero says nothing.) It is here for the day one
447
+ * does. */
447
448
  function declarationPins(l: SFn['locals'][number]): boolean {
448
449
  return (
449
450
  l.volatile !== undefined ||
@@ -27,7 +27,15 @@
27
27
  // mapped global through a volatile view the map never granted. No qualifying local ⇒ decline
28
28
  // (null), so the variation never emits a duplicate of the default.
29
29
  import { addrConst, cellAddress, inRange } from './address';
30
- import { type Expr, type SFn, type Stmt, mapExprChildren, rematerializableAddress, walkExprs } from './ast';
30
+ import {
31
+ type Expr,
32
+ type SFn,
33
+ assignsIn,
34
+ mapExprChildren,
35
+ rematerializableAddress,
36
+ taintOverAssigns,
37
+ walkExprs,
38
+ } from './ast';
31
39
 
32
40
  const exprHas = (e: Expr, pred: (x: Expr) => boolean): boolean => {
33
41
  if (pred(e)) {
@@ -41,36 +49,6 @@ const exprHas = (e: Expr, pred: (x: Expr) => boolean): boolean => {
41
49
  return hit;
42
50
  };
43
51
 
44
- function collectAssigns(stmts: Stmt[], out: { name: string; value: Expr }[]): void {
45
- for (const s of stmts) {
46
- switch (s.k) {
47
- case 'assign':
48
- out.push({ name: s.name, value: s.value });
49
- break;
50
- case 'if':
51
- collectAssigns(s.then, out);
52
- collectAssigns(s.else, out);
53
- break;
54
- case 'while':
55
- case 'dowhile':
56
- collectAssigns(s.body, out);
57
- break;
58
- case 'for':
59
- collectAssigns([s.init, s.inc], out);
60
- collectAssigns(s.body, out);
61
- break;
62
- case 'switch':
63
- for (const c of s.cases) {
64
- collectAssigns(c.body, out);
65
- }
66
- collectAssigns(s.default ?? [], out);
67
- break;
68
- default:
69
- break;
70
- }
71
- }
72
- }
73
-
74
52
  /** How many of this tree's `volatile` claims land on a DEVICE REGISTER — the gate on rank.ts's
75
53
  * volatility tie-break, so it is counted here beside the variation that mints the claims.
76
54
  *
@@ -100,8 +78,7 @@ export function deviceVolatileClaims(sfn: SFn, window?: readonly [number, number
100
78
  underSubscript.add(e.base);
101
79
  }
102
80
  }
103
- const assigns: { name: string; value: Expr }[] = [];
104
- collectAssigns(sfn.body, assigns);
81
+ const assigns = assignsIn(sfn.body);
105
82
  for (const l of sfn.locals) {
106
83
  if (l.type.kind === 'ptr' && (l.pointeeVolatile !== undefined || l.volatile !== undefined)) {
107
84
  n += assigns.filter((a) => a.name === l.name && inWindow(a.value)).length;
@@ -143,27 +120,13 @@ export const volatileEligibleValue = (e: Expr): boolean => rematerializableAddre
143
120
 
144
121
  /** the shared eligibility predicate (the GATE in the header) for one function's locals */
145
122
  function eligibility(sfn: SFn): (l: SFn['locals'][number]) => boolean {
146
- const assigns: { name: string; value: Expr }[] = [];
147
- collectAssigns(sfn.body, assigns);
148
- const numericFed = new Set<string>();
149
- const tainted = new Set<string>();
150
- for (const a of assigns) {
151
- if (rematerializableAddress(a.value)) {
152
- numericFed.add(a.name);
153
- }
154
- if (feedsSymbolAddress(a.value)) {
155
- tainted.add(a.name);
156
- }
157
- }
158
- for (let grew = true; grew;) {
159
- grew = false;
160
- for (const a of assigns) {
161
- if (!tainted.has(a.name) && exprHas(a.value, (x) => x.k === 'var' && tainted.has(x.name))) {
162
- tainted.add(a.name);
163
- grew = true;
164
- }
165
- }
166
- }
123
+ const assigns = assignsIn(sfn.body);
124
+ const numericFed = new Set(assigns.filter((a) => rematerializableAddress(a.value)).map((a) => a.name));
125
+ const tainted = taintOverAssigns(
126
+ assigns,
127
+ assigns.filter((a) => feedsSymbolAddress(a.value)).map((a) => a.name),
128
+ (a, t) => exprHas(a.value, (x) => x.k === 'var' && t.has(x.name)),
129
+ );
167
130
  return (l) =>
168
131
  l.type.kind === 'ptr' &&
169
132
  l.volatile === undefined &&