@asmlift/core 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +25 -24
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +149 -0
  4. package/src/backend/c.ts +1 -0
  5. package/src/backend/cfamily.ts +165 -50
  6. package/src/backend/cpp.ts +2 -1
  7. package/src/backend/pascal.ts +31 -12
  8. package/src/contracts.ts +173 -16
  9. package/src/declare.ts +69 -20
  10. package/src/frontend/asmdata.ts +7 -1
  11. package/src/frontend/call-plan.ts +964 -60
  12. package/src/frontend/dead-reads.ts +628 -0
  13. package/src/frontend/device-pins.ts +23 -4
  14. package/src/frontend/emit.ts +4 -3
  15. package/src/frontend/fpu.ts +224 -17
  16. package/src/frontend/frame-objects.ts +1232 -192
  17. package/src/frontend/frontend.ts +64 -3
  18. package/src/frontend/high-half.ts +53 -4
  19. package/src/frontend/jump-table.ts +530 -0
  20. package/src/frontend/local-object.ts +158 -32
  21. package/src/frontend/mips.ts +2076 -215
  22. package/src/frontend/own-struct-return.ts +500 -0
  23. package/src/frontend/ppc.ts +2711 -232
  24. package/src/frontend/register-struct-call.ts +484 -0
  25. package/src/frontend/reloc-symbol.ts +22 -7
  26. package/src/frontend/ssa.ts +1460 -44
  27. package/src/frontend/stackargs.ts +106 -57
  28. package/src/frontend/thumb.ts +2044 -318
  29. package/src/ir/alias.ts +42 -0
  30. package/src/ir/bits.ts +681 -1
  31. package/src/ir/core.ts +100 -5
  32. package/src/ir/discipline.ts +86 -21
  33. package/src/ir/entry.ts +159 -0
  34. package/src/{structure → ir}/loops.ts +6 -2
  35. package/src/ir/opcodes.ts +109 -9
  36. package/src/ir/types.ts +86 -2
  37. package/src/ir/verify.ts +80 -11
  38. package/src/l3/ast.ts +290 -18
  39. package/src/l3/basecse.ts +33 -5
  40. package/src/l3/constinit.ts +382 -0
  41. package/src/l3/dce.ts +2 -2
  42. package/src/l3/initfirst.ts +42 -11
  43. package/src/l3/intcell.ts +2 -1
  44. package/src/l3/narrowdecl.ts +77 -36
  45. package/src/l3/pollguard.ts +8 -2
  46. package/src/l3/ptrcell.ts +31 -34
  47. package/src/l3/ptrfield.ts +123 -8
  48. package/src/l3/regspell.ts +9 -4
  49. package/src/l3/reindex.ts +283 -96
  50. package/src/l3/storage.ts +98 -1
  51. package/src/l3/symbol-refs.ts +84 -6
  52. package/src/l3/tailmerge.ts +4 -2
  53. package/src/l3/typing.ts +108 -15
  54. package/src/l3/unreduce.ts +6 -5
  55. package/src/l3/volatileptr.ts +17 -54
  56. package/src/mangle.ts +30 -0
  57. package/src/pattern/engine.ts +361 -73
  58. package/src/pipeline.ts +541 -24
  59. package/src/proto-context.ts +905 -45
  60. package/src/proto.ts +555 -40
  61. package/src/raise/aggmembers.ts +290 -0
  62. package/src/raise/arrays.ts +5 -1
  63. package/src/raise/declared-volatile.ts +207 -31
  64. package/src/raise/floathelpers.ts +254 -33
  65. package/src/raise/gvn.ts +3 -3
  66. package/src/raise/latch.ts +162 -6
  67. package/src/raise/memberarrays.ts +10 -4
  68. package/src/raise/offsetnames.ts +10 -26
  69. package/src/raise/pairparams.ts +33 -0
  70. package/src/raise/paramwidth.ts +196 -6
  71. package/src/raise/pre-recovery.ts +102 -19
  72. package/src/raise/pre-structure.ts +54 -0
  73. package/src/raise/recover.ts +12 -5
  74. package/src/raise/reroll.ts +694 -0
  75. package/src/raise/shortcircuit.ts +26 -12
  76. package/src/raise/softdiv.ts +1 -1
  77. package/src/raise/storesign.ts +153 -0
  78. package/src/raise/struct-arrays.ts +30 -11
  79. package/src/raise/structs.ts +396 -33
  80. package/src/raise/truncload.ts +4 -3
  81. package/src/raise/unthread.ts +400 -0
  82. package/src/rank-declare.ts +118 -12
  83. package/src/rank-variations.ts +89 -31
  84. package/src/rank.ts +307 -54
  85. package/src/runtime-helpers.ts +140 -22
  86. package/src/structure/analysis.ts +165 -2
  87. package/src/structure/arith-ops.ts +23 -0
  88. package/src/structure/globalaccess.ts +111 -9
  89. package/src/structure/hazards.ts +16 -64
  90. package/src/structure/local-statics.ts +145 -9
  91. package/src/structure/namecoalesce.ts +1 -1
  92. package/src/structure/pointer-spelling.ts +1106 -0
  93. package/src/structure/redundant-test.ts +1 -1
  94. package/src/structure/structure.ts +2180 -1054
  95. package/src/structure/switch-recover.ts +244 -100
  96. package/src/symbols.ts +6 -0
  97. package/src/target.ts +765 -52
  98. package/src/trace.ts +26 -8
  99. package/src/variation-definitions.ts +126 -14
  100. package/src/variation-gates.ts +2 -0
  101. package/src/variation-tokens.ts +5 -1
@@ -17,7 +17,7 @@
17
17
  // stale ref, transitively reintroducing the hazards the collector excludes). Deriving at the
18
18
  // consumption point makes staleness impossible by construction.
19
19
  import type { IrType } from '../ir/types';
20
- import { type AggregateLayout, type ParamType, type Prototypes, spellableProto } from '../proto';
20
+ import { type AggregateLayout, type FnProto, type ParamType, type Prototypes, spellableProto } from '../proto';
21
21
  import type { SymbolInfo } from '../symbols';
22
22
  import type { TargetDescription } from '../target';
23
23
  import { Expr, SFn, Stmt, exprChildren, mentionedName, stmtChildren, stmtExprs } from './ast';
@@ -63,10 +63,43 @@ export interface SymbolRef {
63
63
  * by its tag (`name`), from the members the declaration lists (`layout`, which the lift laid out
64
64
  * to type the call). */
65
65
  returned?: { readonly name: string; readonly declared: string; readonly layout: AggregateLayout };
66
+ /** The structs a call target is passed BY VALUE (`byValue` on the call), defined the same way
67
+ * from the declaration's `byValueLayouts`. */
68
+ passed?: readonly { readonly name: string; readonly declared: string; readonly layout: AggregateLayout }[];
69
+ /** A global the body passes BARE as a struct by value (`take(gS)`), as `struct <tag>`: a
70
+ * name-only declaration of it is that struct, which the call needs and an integer cell rejects. */
71
+ passedAs?: string;
66
72
  /** The bare name holds a pointer (l3/ptrcell.ts): the body stores one into it, or it meets such a
67
73
  * cell bare and nothing reads it as an integer (`pointerPartners`). A name-only declaration of it
68
74
  * is a pointer, which the store needs and an integer cell rejects on CodeWarrior. */
69
75
  holdsPointer?: true;
76
+ /** A call target with NO spellable prototype whose address a function-scope static's (or an
77
+ * anonymous image's) initializer holds. The static's `(u32)&F` precedes every call, so the
78
+ * implicit declaration a call makes comes too late for it; `declare.ts` prints that same
79
+ * declaration first, `int F();`, which states no parameter and so binds the call as before. */
80
+ implicitCall?: true;
81
+ /** THE STRUCT THE FUNCTION ITSELF RETURNS ({@link ownReturnDefinition}): a ref under the
82
+ * function's own name that declares nothing of it — its definition is its declaration — and only
83
+ * defines `returned`, the type its own signature spells. */
84
+ definesReturn?: true;
85
+ }
86
+
87
+ /** The ref that defines the struct `tree` itself returns by value — a type the project's headers
88
+ * own (ir/types.ts `declared`), so the candidate's declarations define it as they define a
89
+ * callee's (`SymbolRef.returned`) — from the members its own declaration `own` lists. Undefined
90
+ * where it returns no such struct, or its declaration lists no members. */
91
+ export function ownReturnDefinition(tree: SFn, own: FnProto | undefined): SymbolRef | undefined {
92
+ const t = tree.retType;
93
+ const layout = own?.returnLayout;
94
+ if (t.kind !== 'struct' || t.declared === undefined || layout?.members === undefined) {
95
+ return undefined;
96
+ }
97
+ return {
98
+ name: tree.name,
99
+ info: { name: tree.name, kind: 'code' },
100
+ returned: { name: t.name, declared: t.declared, layout },
101
+ definesReturn: true,
102
+ };
70
103
  }
71
104
 
72
105
  /** The declarable symbols a structured body references in a VALUE context — the input to the
@@ -101,15 +134,31 @@ export function collectSymbolRefs(
101
134
  onRefused?: (name: string, reason: 'call-target' | 'self-name') => void,
102
135
  ): SymbolRef[] {
103
136
  const called = new Set<string>();
104
- // the struct a callee returns through memory, as the lift typed it
137
+ // the struct a callee returns through memory or by value in registers, as the lift typed it
105
138
  const returned = new Map<string, Extract<IrType, { kind: 'struct' }>>();
139
+ // …and the structs it is passed by value, by declared parameter index
140
+ const passedTo = new Map<string, Map<number, Extract<IrType, { kind: 'struct' }>>>();
141
+ // …and the globals passed bare as one, by its spelling
142
+ const passedAs = new Map<string, string>();
106
143
  const valueRefs = new Set<string>();
107
144
  const visitExpr = (e: Expr): void => {
108
145
  const named = mentionedName(e);
109
146
  if (e.k === 'call' && typeof e.fn === 'string') {
110
147
  called.add(e.fn);
111
- if (e.sret?.kind === 'struct') {
112
- returned.set(e.fn, e.sret);
148
+ const struct = e.sret ?? e.returns;
149
+ if (struct?.kind === 'struct') {
150
+ returned.set(e.fn, struct);
151
+ }
152
+ for (const b of e.byValue ?? []) {
153
+ if (b.type.kind === 'struct') {
154
+ const params = passedTo.get(e.fn) ?? passedTo.set(e.fn, new Map()).get(e.fn)!;
155
+ params.set(b.param, b.type);
156
+ const arg = e.args[b.at];
157
+ if (arg?.k === 'var' && symbols.has(arg.name)) {
158
+ // by its tag, which declares even ahead of the definition and of a typedef of it
159
+ passedAs.set(arg.name, `struct ${b.type.name}`);
160
+ }
161
+ }
113
162
  }
114
163
  } else if (named !== undefined && symbols.has(named)) {
115
164
  valueRefs.add(named);
@@ -126,6 +175,18 @@ export function collectSymbolRefs(
126
175
  stmtChildren(s).forEach(visitStmt);
127
176
  };
128
177
  tree.body.forEach(visitStmt);
178
+ // A function-scope static's initializer names the globals whose addresses it holds
179
+ // (`(u32)&MetatileBehavior_IsTallGrass`), which the candidate must declare as much as the body's —
180
+ // and so does an anonymous image, which the emitter folds into a local's initializer.
181
+ const initAddresses = new Set<string>();
182
+ for (const st of [...(tree.statics ?? []), ...(tree.images ?? [])]) {
183
+ for (const v of st.init ?? []) {
184
+ if (v !== null && typeof v === 'object' && symbols.has(v.symbol)) {
185
+ valueRefs.add(v.symbol);
186
+ initAddresses.add(v.symbol);
187
+ }
188
+ }
189
+ }
129
190
  const holdsPointer = new Set([...pointerCellsOf(tree), ...pointerPartners(tree)]);
130
191
  // A call target is a ref in its own right, whether or not the body also names it as a value —
131
192
  // `Object.hasOwn`, because `prototypes` is caller-supplied JSON and a callee may be named
@@ -137,7 +198,9 @@ export function collectSymbolRefs(
137
198
  // function, and `proto.ts` `prototypesFromSymbols` has already taken the `returns` off such an
138
199
  // entry, so what arrives here states no printable prototype and the frontend read the call the
139
200
  // same way. Testing it a second time at this reader is what let the two answer differently.
140
- const p = Object.hasOwn(prototypes, n) ? spellableProto(prototypes[n], target, returned.get(n)) : undefined;
201
+ const p = Object.hasOwn(prototypes, n)
202
+ ? spellableProto(prototypes[n], target, returned.get(n), new Set(passedTo.get(n)?.keys()))
203
+ : undefined;
141
204
  if (p !== undefined) {
142
205
  proto.set(n, p);
143
206
  }
@@ -145,7 +208,11 @@ export function collectSymbolRefs(
145
208
  return [...new Set([...valueRefs, ...proto.keys()])]
146
209
  .sort()
147
210
  .filter((n) => {
148
- if (called.has(n) && !proto.has(n)) {
211
+ // …except where an initializer holds its address: there the name is spelled ahead of every
212
+ // call, so leaving it to the call's implicit declaration leaves the initializer naming an
213
+ // undeclared identifier, and the implicit declaration itself is the one that is printed
214
+ // (`SymbolRef.implicitCall`).
215
+ if (called.has(n) && !proto.has(n) && !initAddresses.has(n)) {
149
216
  onRefused?.(n, 'call-target');
150
217
  return false;
151
218
  }
@@ -164,12 +231,23 @@ export function collectSymbolRefs(
164
231
  const r = p ? returned.get(n) : undefined;
165
232
  const layout = r?.declared !== undefined ? prototypes[n].returnLayout : undefined;
166
233
  const definition = layout?.members !== undefined ? { name: r!.name, declared: r!.declared!, layout } : undefined;
234
+ const passed = p
235
+ ? [...(passedTo.get(n) ?? [])].flatMap(([k, t]) => {
236
+ const l = prototypes[n].byValueLayouts?.[k];
237
+ return l?.members !== undefined && t.declared !== undefined
238
+ ? [{ name: t.name, declared: t.declared, layout: l }]
239
+ : [];
240
+ })
241
+ : [];
167
242
  return {
168
243
  name: n,
169
244
  info,
170
245
  ...(p ? { proto: p } : {}),
171
246
  ...(definition ? { returned: definition } : {}),
247
+ ...(passed.length > 0 ? { passed } : {}),
248
+ ...(passedAs.has(n) ? { passedAs: passedAs.get(n)! } : {}),
172
249
  ...(holdsPointer.has(n) ? { holdsPointer: true as const } : {}),
250
+ ...(called.has(n) && !p ? { implicitCall: true as const } : {}),
173
251
  };
174
252
  });
175
253
  }
@@ -63,7 +63,7 @@
63
63
  // artifact still refuses to compile) but `collectMarkers` then reports it once rather than twice,
64
64
  // which is accurate — it is one gap that ran on both paths.
65
65
  import type { SFn, Stmt } from './ast';
66
- import { stmtEquals } from './ast';
66
+ import { stmtEquals, unionStmtEvidence } from './ast';
67
67
 
68
68
  /** Statements this pass may move. Deliberately narrow — see SCOPE. */
69
69
  type Mergeable = Extract<Stmt, { k: 'assign' } | { k: 'store' } | { k: 'exprstmt' }>;
@@ -89,7 +89,9 @@ function rewrite(s: Stmt): Stmt[] {
89
89
  // it, but it is the one shape where a zero-arm merge is qualitatively unlike a partial one.
90
90
  const tail: Stmt[] = [];
91
91
  while (then.length > 0 && els.length > 0 && sameStmt(then[then.length - 1], els[els.length - 1])) {
92
- tail.unshift(then[then.length - 1]);
92
+ // The two print alike, but an unsigned compare in one prints like the signed one in the
93
+ // other: the moved statement carries the evidence of both (ast.ts `unionEvidence`).
94
+ tail.unshift(unionStmtEvidence(then[then.length - 1], els[els.length - 1]));
93
95
  then.pop();
94
96
  els.pop();
95
97
  }
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, memberOf, scalarTypeForAccess } from '../ir/types';
21
- import { type BinOp, 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,7 +31,7 @@ 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 ?? []) {
34
+ for (const st of [...(fn.statics ?? []), ...(fn.images ?? [])]) {
35
35
  m.set(st.name, st.type);
36
36
  }
37
37
  for (const p of fn.params) {
@@ -58,8 +58,9 @@ export function ptrElemBytes(to: IrType): number {
58
58
  export interface PrintEnv {
59
59
  readonly type: VarTypes;
60
60
  readonly volatilePointee: (name: string) => boolean;
61
- /** a callee's declared parameter types, on a function compiled as C++ (`SFn.declaredArgs`,
62
- * `SFn.dialect`) */
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`) */
63
64
  readonly declaredArgs: (callee: string) => readonly (string | undefined)[] | undefined;
64
65
  /** a function-scope static this function defines `const` (`SFn.statics`) */
65
66
  readonly constStatic: (name: string) => boolean;
@@ -76,7 +77,9 @@ export function printEnv(fn: SFn): PrintEnv {
76
77
  type: declaredTypes(fn),
77
78
  volatilePointee: (n) => vol.has(n),
78
79
  declaredArgs: (callee) =>
79
- fn.dialect === 'c++' && fn.declaredArgs && Object.hasOwn(fn.declaredArgs, callee)
80
+ (fn.dialect === 'c++' || fn.strictConversions === true) &&
81
+ fn.declaredArgs &&
82
+ Object.hasOwn(fn.declaredArgs, callee)
80
83
  ? fn.declaredArgs[callee]
81
84
  : undefined,
82
85
  constStatic: (n) => constStatics.has(n),
@@ -184,8 +187,24 @@ export function exprIntWidth(e: Expr, varType: VarTypes): 32 | 64 {
184
187
  export const FLOAT_BINOPS: ReadonlySet<BinOp> = new Set<BinOp>(['f+', 'f-', 'f*', 'f/']);
185
188
 
186
189
  /** The binary operators whose RESULT is a C `int` whatever their operands are — read by both
187
- * halves of the conversion model, which is the point of naming them once. */
188
- const INT_RESULT_BINOPS = new Set<BinOp>(['<', '<=', '>', '>=', '==', '!=', '&&', '||']);
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
+ ]);
189
208
 
190
209
  /** The USUAL ARITHMETIC CONVERSIONS over two rendered operands.
191
210
  *
@@ -221,7 +240,7 @@ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes):
221
240
  *
222
241
  * THE one rendered-signedness judgment, and it lives beside the declarations it judges against
223
242
  * because every consumer shares it: the C-family backend's operand pin, structure.ts's
224
- * 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
225
244
  * `arithConversionSignedness` above — initfirst's compare-meaning gate. Two of those models
226
245
  * disagreeing about one expression is the drift this placement prevents.
227
246
  *
@@ -237,8 +256,8 @@ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes):
237
256
  * instruction-for-instruction the same where `f` really returns `int`); a missing one is a
238
257
  * miscompile — with `u32 f(void);` in scope `f() / a` calls `__udivsi3` where the machine called
239
258
  * `__divsi3`, and `if (f() >= 0)` compiles to `bl f; mov r0, #0`, comparison and both arms gone.
240
- * The one consumer that reads `undefined` the other way is the /uns-cmp variation's own gate, which is
241
- * 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.
242
261
  *
243
262
  * Anything narrower than 32 bits promotes to `int` and is therefore SIGNED, whatever it was
244
263
  * declared. Pointers, calls and markers are `undefined`.
@@ -308,16 +327,41 @@ export function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | und
308
327
  }
309
328
 
310
329
  /** Is the rendered expression provably in [0, 2^31) — the range where a signed and an unsigned
311
- * compare agree on every input (and where gcc itself emits the unsigned branch for the signed
312
- * spelling)? Narrow unsigned values are the everyday case: a `(u8)x` cast or a `u8`/`u16`
313
- * 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`). */
314
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 {
315
353
  switch (e.k) {
316
354
  case 'const':
317
355
  return e.value >= 0 && e.value < 0x80000000;
318
356
  case 'un':
319
357
  return e.op === '!';
320
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
+ }
321
365
  return ['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op);
322
366
  case 'cast':
323
367
  return e.to.kind === 'int' && e.to.width < 32 && !e.to.signed;
@@ -328,6 +372,47 @@ export function provablyNonNegative(e: Expr, varType: VarTypes): boolean {
328
372
  }
329
373
  }
330
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
+
331
416
  /** Does this write put a value that renders as a DEFINITELY-NON-POINTER C expression into a
332
417
  * pointer-declared slot? — `v2 = a1 + v0` with `v2: u8 *`, a `return a0 + v0` from a
333
418
  * pointer-returning function, a store through a pointer-element lvalue. The machine's register
@@ -421,8 +506,16 @@ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined
421
506
  // would type it `s96`-style garbage and make the field contract reject valid trees. The
422
507
  // node width must AGREE with the element size (when the struct declares one): a mismatch
423
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.
424
512
  const bt = rec(e.base);
425
- 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
+ ) {
426
519
  return bt.to;
427
520
  }
428
521
  if (bt?.kind === 'ptr' && derefStrideOk(bt, e.width, e.signed)) {
@@ -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
@@ -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 &&
package/src/mangle.ts CHANGED
@@ -129,6 +129,36 @@ export function demangle(sym: string): CppSig | null {
129
129
  return { name, cls, params };
130
130
  }
131
131
 
132
+ /** How a class opens where gcc 2 names one: a length (`3Foo`), a nesting count (`Q21A1B`, and
133
+ * `Q_12_` past nine levels) or a template (`t3Vec1Zi`). A member function's const and volatile
134
+ * marks come before it (`get__C3Fooi`, `get__V3Fooi`, `get__CQ21A1Bi`), and CodeWarrior's class
135
+ * opens the same way, its marks following the class (`getStartHour__11PlayerStateCFv`). */
136
+ const GCC2_CLASS = String.raw`(?:\d|Q(?:\d|_\d+_)|t\d)`;
137
+
138
+ /** How many argument words a C++ symbol may be handed ahead of its declared parameters: 1 where it
139
+ * may name a MEMBER function, whose `this` the declared list leaves out; 0 where it cannot; and
140
+ * `undefined` for a constructor or destructor, which may also take a flag no declaration states
141
+ * (an in-charge or delete flag, beside `this`). Either mangling scheme a target here compiles with
142
+ * puts the class right after the `__`, length-prefixed or `Q<n>`-nested: CodeWarrior's
143
+ * `pick__3FooFiPi` (`__ct__3FooFv`, `__dt__3FooFv`), and gcc 2's `pick__3FooiPi` (a const method
144
+ * `pick__C3FooiPi`, a volatile one `pick__V3FooiPi`, a constructor `__3Foo`, a destructor `_$_3Foo`
145
+ * or `_._3Foo`, every one of which this abstains on, nested `_._Q21A1B` included), where a
146
+ * TEMPLATE class is spelled `t<n><name><args>` in the class's place (`get__t3Vec1Zii`, a
147
+ * constructor `__t3Vec1Zii`, `operator=` `__as__t3Vec1ZiRCt3Vec1Zi`). A free function (`f__Fii`)
148
+ * and a plain C name answer 0. A STATIC member mangles as a method does and takes no
149
+ * `this`, so 1 is "may", and a reader that needs the exact count must not take it as one.
150
+ *
151
+ * THE PATTERNS ERR TOWARDS A MEMBER, because the errors cost differently: a method read as free
152
+ * loses its `this` word, and the own-declaration bound (`proto.ts` `ownArgumentWords`) then turns
153
+ * its last parameter into an uninitialised local, where a C name read as a method only leaves one
154
+ * more register a parameter. */
155
+ export function implicitArgumentWords(sym: string): 0 | 1 | undefined {
156
+ if (/^__(?:ct|dt)__/.test(sym) || new RegExp(`^__${GCC2_CLASS}`).test(sym) || /^_[$.]_/.test(sym)) {
157
+ return undefined;
158
+ }
159
+ return new RegExp(`__[CV]*${GCC2_CLASS}`).test(sym) ? 1 : 0;
160
+ }
161
+
132
162
  /** A class qualifier off the front of `s` — `12RefCountable`, or `Q23zen17particleGenerator` for a
133
163
  * nested one — as its scope names and the remaining string; null when `s` does not open with one. */
134
164
  function parseQualifier(s: string): { scopes: string[]; rest: string } | null {