@asmlift/core 0.5.0 → 0.7.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 (94) hide show
  1. package/README.md +22 -16
  2. package/package.json +1 -1
  3. package/src/backend/c.ts +1 -0
  4. package/src/backend/cfamily.ts +270 -171
  5. package/src/backend/cpp.ts +1 -0
  6. package/src/backend/pascal.ts +26 -12
  7. package/src/contracts.ts +243 -39
  8. package/src/declare.ts +41 -4
  9. package/src/frontend/mips.ts +11 -0
  10. package/src/frontend/ppc.ts +43 -7
  11. package/src/frontend/ssa.ts +404 -29
  12. package/src/frontend/thumb.ts +2176 -686
  13. package/src/ir/alias.ts +78 -0
  14. package/src/ir/bits.ts +75 -0
  15. package/src/ir/core.ts +345 -2
  16. package/src/ir/opcodes.ts +176 -21
  17. package/src/ir/parse.ts +19 -2
  18. package/src/ir/print.ts +27 -2
  19. package/src/ir/simplify.ts +190 -3
  20. package/src/ir/struct-names.ts +42 -0
  21. package/src/ir/verify.ts +43 -49
  22. package/src/l3/address.ts +62 -0
  23. package/src/l3/advance.ts +373 -0
  24. package/src/l3/argbase.ts +6 -5
  25. package/src/l3/ast.ts +510 -59
  26. package/src/l3/basecse.ts +686 -78
  27. package/src/l3/coalesce.ts +432 -46
  28. package/src/l3/dce.ts +31 -9
  29. package/src/l3/gates.ts +96 -1
  30. package/src/l3/hoist.ts +293 -14
  31. package/src/l3/homesplit.ts +285 -0
  32. package/src/l3/initfirst.ts +301 -0
  33. package/src/l3/inlinebase.ts +193 -0
  34. package/src/l3/mentions.ts +176 -0
  35. package/src/l3/mulfirst.ts +42 -0
  36. package/src/l3/nearbase.ts +152 -0
  37. package/src/l3/offmember.ts +371 -0
  38. package/src/l3/parkfirst.ts +96 -0
  39. package/src/l3/pollguard.ts +154 -0
  40. package/src/l3/ptrfield.ts +227 -0
  41. package/src/l3/regspell.ts +114 -89
  42. package/src/l3/reindex.ts +722 -80
  43. package/src/l3/scopebase.ts +649 -220
  44. package/src/l3/sinkinit.ts +40 -0
  45. package/src/l3/slotorder.ts +123 -0
  46. package/src/l3/storage.ts +48 -0
  47. package/src/l3/symbol-refs.ts +41 -8
  48. package/src/l3/tailmerge.ts +16 -1
  49. package/src/l3/typing.ts +198 -9
  50. package/src/l3/unmerge.ts +687 -0
  51. package/src/l3/unreduce.ts +971 -0
  52. package/src/l3/volatileptr.ts +207 -0
  53. package/src/l3/volatileval.ts +130 -0
  54. package/src/l3/volstore.ts +229 -0
  55. package/src/l3/zerosub.ts +62 -0
  56. package/src/pattern/engine.ts +239 -16
  57. package/src/pipeline.ts +173 -60
  58. package/src/proto.ts +112 -14
  59. package/src/raise/arrays.ts +6 -1
  60. package/src/raise/const.ts +203 -3
  61. package/src/raise/divpow2.ts +4 -4
  62. package/src/raise/extscale.ts +342 -0
  63. package/src/raise/globalshape.ts +1058 -0
  64. package/src/raise/gvn.ts +33 -18
  65. package/src/raise/latch.ts +126 -0
  66. package/src/raise/magicdiv.ts +2 -2
  67. package/src/raise/memberarrays.ts +594 -0
  68. package/src/raise/narrow.ts +124 -0
  69. package/src/raise/narrowlocal.ts +572 -0
  70. package/src/raise/paramwidth.ts +201 -0
  71. package/src/raise/pre-recovery.ts +169 -21
  72. package/src/raise/recover.ts +56 -23
  73. package/src/raise/retsink.ts +585 -19
  74. package/src/raise/shortcircuit.ts +1050 -89
  75. package/src/raise/struct-arrays.ts +19 -2
  76. package/src/raise/structs.ts +34 -4
  77. package/src/raise/tailsink.ts +126 -0
  78. package/src/rank-declare.ts +256 -0
  79. package/src/rank-variations.ts +760 -0
  80. package/src/rank.ts +2122 -326
  81. package/src/structure/analysis.ts +1398 -150
  82. package/src/structure/bitfields.ts +432 -0
  83. package/src/structure/globalaccess.ts +300 -0
  84. package/src/structure/hazards.ts +411 -20
  85. package/src/structure/loops.ts +2 -49
  86. package/src/structure/namecoalesce.ts +454 -0
  87. package/src/structure/structure.ts +3979 -612
  88. package/src/structure/switch-recover.ts +710 -145
  89. package/src/symbols.ts +188 -6
  90. package/src/target.ts +495 -32
  91. package/src/trace.ts +112 -33
  92. package/src/variation-definitions.ts +1540 -0
  93. package/src/variation-gates.ts +89 -0
  94. package/src/variation-tokens.ts +355 -0
@@ -10,7 +10,8 @@
10
10
  // Turbo/Delphi/FreePascal.
11
11
  import { IrType, typeToString } from '../ir/types';
12
12
  import { BinOp, Expr, LanguageBackend, SFn, Stmt } from '../l3/ast';
13
- import { type VarTypes, declaredTypes, derefStrideOk, exprCType } from '../l3/typing';
13
+ import { orderSlotLocals } from '../l3/slotorder';
14
+ import { type VarTypes, declaredTypes, derefStrideOk, exprCType, writesNonPointerIntoPointer } from '../l3/typing';
14
15
 
15
16
  // Infix operators IDO Pascal spells directly.
16
17
  const OP: Partial<Record<BinOp, string>> = {
@@ -18,6 +19,10 @@ const OP: Partial<Record<BinOp, string>> = {
18
19
  // match C's truncated `%` (sign of the DIVIDEND) — verified: `a mod 3` mis-scores against the
19
20
  // IDO C `a % 3` codegen. There is no faithful IDO-Pascal spelling of a signed C remainder, so the
20
21
  // backend fails LOUD on `%` (below) rather than emit a silently-wrong `mod`. `/`→`div` DOES match.
22
+ //
23
+ // And no `/u`/`%u` either, for the same reason `>>>` is absent from BIT_FN below: `div` over this
24
+ // backend's signed `Integer` is the SIGNED division, so lending it to the unsigned twin would
25
+ // emit `div` where the machine did `divu`. They reach the loud decline instead.
21
26
  '+': '+',
22
27
  '-': '-',
23
28
  '*': '*',
@@ -95,7 +100,7 @@ function makePrinter(vt: VarTypes) {
95
100
  throw new Error(`pascal backend: a multidimensional array access has no IDO Pascal spelling yet`);
96
101
  }
97
102
  const bt = exprCType(e.base, vt);
98
- if ((bt !== undefined && !derefStrideOk(bt, e.width)) || (bt === undefined && e.width !== 4)) {
103
+ if ((bt !== undefined && !derefStrideOk(bt, e.width, e.signed)) || (bt === undefined && e.width !== 4)) {
99
104
  throw new Error(
100
105
  `pascal backend: a ${e.width}-byte access through a base of type '${bt ? typeToString(bt) : '<unknowable>'}' has no faithful spelling (no reinterpret cast)`,
101
106
  );
@@ -111,7 +116,10 @@ function makePrinter(vt: VarTypes) {
111
116
  // Casts have no faithful IDO-Pascal spelling yet — fail LOUD rather than emit silently-wrong
112
117
  // source. Tree-level producers reaching here: the width-narrowing idiom casts (agbcc-gated,
113
118
  // so never on this path today), structure.ts's STRUCT-pointer casts (unreachable too — the
114
- // `field` case above throws first), and intify's `(s32)ptr` legalization (any target).
119
+ // `field` case above throws first), intify's `(s32)ptr` legalization (any target), and the
120
+ // byte-pointer walk of a pointer offset by a runtime value (any target, and reachable with no
121
+ // `field` in the tree — it declines two functions that used to emit here, whose Pascal was
122
+ // silently walking ELEMENTS where the asm walked bytes).
115
123
  // Scalar deref casts never appear in the tree — the index case above owns that judgment.
116
124
  case 'cast':
117
125
  throw new Error(`pascal backend: cast has no IDO Pascal spelling yet`);
@@ -148,15 +156,13 @@ function makePrinter(vt: VarTypes) {
148
156
  stmts.flatMap((x, i) => ps(fnName, x, ind, tl && i === stmts.length - 1));
149
157
  switch (s.k) {
150
158
  case 'assign': {
151
- // The write-side sibling of the index case's deref discipline: Pascal has no reinterpret
152
- // cast, so a definitely-non-pointer value assigned into a pointer-declared var (the shape
153
- // the C family legalizes with `(u8 *)…`, cfamily.ts legalizePointerWrites) declines LOUD
154
- // here instead of failing three stages later in upas.
155
- const dt = vt(s.name);
156
- const ct = exprCType(s.value, vt);
157
- if (dt?.kind === 'ptr' && ct && ct.kind !== 'ptr' && ct.kind !== 'array') {
159
+ // The write-side sibling of the index case's deref discipline. The C family answers the
160
+ // same question (l3/typing.ts writesNonPointerIntoPointer) with a reinterpret cast;
161
+ // Pascal has none, so it declines LOUD here instead of failing three stages later in upas.
162
+ if (writesNonPointerIntoPointer(vt(s.name), s.value, vt)) {
163
+ const ct = exprCType(s.value, vt);
158
164
  throw new Error(
159
- `pascal backend: assigning a ${typeToString(ct)} value into pointer var '${s.name}' has no faithful spelling (no reinterpret cast)`,
165
+ `pascal backend: assigning a ${ct ? typeToString(ct) : '<unknowable>'} value into pointer var '${s.name}' has no faithful spelling (no reinterpret cast)`,
160
166
  );
161
167
  }
162
168
  return [`${indent}${s.name} := ${pe(s.value)};`];
@@ -263,7 +269,15 @@ function makePrinter(vt: VarTypes) {
263
269
 
264
270
  export const pascalBackend: LanguageBackend = {
265
271
  id: 'pascal',
266
- emit(fn: SFn): string {
272
+ // `case-of` has no fall-through, and this file's `switch` printing loud-fails a `fallsThrough`
273
+ // arm. Declared so RECOVERY never mints one for this backend: a comparison-tree switch also
274
+ // spells as plain if-nesting, which Pascal prints, so the choice is between a decompiled
275
+ // function and a stub.
276
+ spellsSwitchFallthrough: false,
277
+ emit(fn0: SFn): string {
278
+ // The declaration list is put into the target's own frame order HERE, as the C family does it
279
+ // in its shared assembler — owned by `emit`, never by a `.emit(` call site (l3/slotorder.ts).
280
+ const fn = orderSlotLocals(fn0);
267
281
  // Same env discipline as the C family (cfamily.ts cFamilyBody): the printer judges derefs
268
282
  // against the exact declarations it emits.
269
283
  const ps = makePrinter(declaredTypes(fn));
package/src/contracts.ts CHANGED
@@ -3,10 +3,20 @@
3
3
  // decompileRanked / decompileWithReport).
4
4
  // A pass that regresses fails AT its boundary with a diagnostic, not three stages later as
5
5
  // wrong C.
6
- import { type Block, type Fn, type Value, successorsOf } from './ir/core';
6
+ import { type Fn, type Value, reachableBlocks } from './ir/core';
7
7
  import { type IrType, typeToString } from './ir/types';
8
8
  import type { BinOp, Expr, SFn, Stmt } from './l3/ast';
9
- import { exprChildren, fieldSpellsDot, gapReasonFor, stmtChildren, stmtExprs } from './l3/ast';
9
+ import {
10
+ exprChildren,
11
+ fieldSpellsDot,
12
+ gapReasonFor,
13
+ mapExprChildren,
14
+ stmtChildren,
15
+ stmtExprs,
16
+ stmtLists,
17
+ walkExprs,
18
+ } from './l3/ast';
19
+ import { mentionedLocals } from './l3/mentions';
10
20
  import { declaredTypes, exprCType } from './l3/typing';
11
21
 
12
22
  export class ContractError extends Error {
@@ -40,20 +50,35 @@ export function assertTypesRecovered(fn: Fn): void {
40
50
  }
41
51
  }
42
52
 
43
- /** Post structuring: the AST must reference no unresolved value. The structurer emits the
44
- * sentinel var `"?"` when it cannot resolve a value (a dropped def, or an opcode it has no
45
- * lowering for) — which would print as uncompilable source. Fail at the structuring boundary
46
- * instead of emitting garbage. */
53
+ /** Post structuring: the AST must reference no unresolved name. The structurer emits the sentinel
54
+ * var `"?"` when it cannot resolve a value (a dropped def, or an opcode it has no lowering for),
55
+ * which would print as uncompilable source. Fail at the boundary instead of emitting garbage.
56
+ *
57
+ * `undefined` is the same failure from the other side — not a spelling the structurer chooses
58
+ * (`Expr` declares `name: string`) but a `varName.get(v)!` whose value was never adopted, printing
59
+ * as the token `undefined`. Both are checked on every ROUTE a name takes into the AST, and those
60
+ * are not all expressions: `var` / `addr` / `field` / `call` carry one, and so does an `assign`'s
61
+ * DESTINATION — a bare string field the expression walk never reaches. */
47
62
  export function assertResolved(sfn: SFn): void {
48
63
  // Derived from the shared exprChildren/stmtExprs/stmtChildren traversal so no statement kind
49
64
  // can be missed. A gap `marker` is annotate-mode's DESIGNED spelling of an unresolved value
50
- // ("resolved" by construction); only its args could still hide a stray `"?"` — and args are
65
+ // ("resolved" by construction); only its args could still hide a stray name — and args are
51
66
  // exactly its children.
52
- const badExpr = (e: Expr): boolean => (e.k === 'var' && e.name === '?') || exprChildren(e).some(badExpr);
53
- const badStmt = (s: Stmt): boolean => stmtExprs(s).some(badExpr) || stmtChildren(s).some(badStmt);
67
+ const badName = (n: string | undefined): boolean => n === '?' || n === undefined;
68
+ // Every Expr kind that CARRIES a name, not just `var` — each is read through the same
69
+ // `map.get(d)!` / `attrs.x as string` and prints straight into the source. `carriesName` is asked
70
+ // separately because an ABSENT name is the case being caught: keying off `nameOf` alone refuses nothing.
71
+ const carriesName = (e: Expr): boolean => e.k === 'var' || e.k === 'addr' || e.k === 'field' || e.k === 'call';
72
+ const nameOf = (e: Expr): string | undefined =>
73
+ e.k === 'call' ? e.fn : e.k === 'marker' ? undefined : (e as { name?: string }).name;
74
+ const badExpr = (e: Expr): boolean => (carriesName(e) && badName(nameOf(e))) || exprChildren(e).some(badExpr);
75
+ // An `assign`'s DESTINATION is a bare string field, so the expression walk never reaches it.
76
+ const badStmt = (s: Stmt): boolean =>
77
+ (s.k === 'assign' && badName(s.name)) || stmtExprs(s).some(badExpr) || stmtChildren(s).some(badStmt);
54
78
  if (sfn.body.some(badStmt)) {
55
79
  throw new ContractError(
56
- `structuring left an unresolved value ('?') in '${sfn.name}' — a dropped def or unlowered opcode`,
80
+ `structuring left an unresolved name ('?' or one never adopted) in '${sfn.name}' — ` +
81
+ `a dropped def, an unlowered opcode, or a name the structurer assumed the naming pipeline gave it`,
57
82
  );
58
83
  }
59
84
  }
@@ -154,15 +179,7 @@ function countCalls(stmts: Stmt[]): { total: CallCounts; path: CallCounts } {
154
179
  */
155
180
  export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
156
181
  // Reachable blocks only: an unreachable block's call is legitimately never emitted.
157
- const seen = new Set<Block>([fn.blocks[0]]);
158
- for (const stack = [fn.blocks[0]]; stack.length;) {
159
- for (const s of successorsOf(stack.pop()!)) {
160
- if (!seen.has(s)) {
161
- seen.add(s);
162
- stack.push(s);
163
- }
164
- }
165
- }
182
+ const seen = reachableBlocks(fn);
166
183
  const irCalls: CallCounts = new Map();
167
184
  // Unmodelled instructions, by the mnemonic the frontend stamped. Same "never dropped" property as
168
185
  // a call, and it needs its own tally because an `opaque` carries no `target`.
@@ -186,17 +203,11 @@ export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
186
203
  // only mode with no other backstop against a silently dropped opaque.
187
204
  if (irOpaques.size) {
188
205
  const emitted = new Set<string>();
189
- const we = (e: Expr): void => {
206
+ for (const e of walkExprs(sfn.body)) {
190
207
  if (e.k === 'marker') {
191
208
  emitted.add(e.reason);
192
209
  }
193
- exprChildren(e).forEach(we);
194
- };
195
- const ws = (s: Stmt): void => {
196
- stmtExprs(s).forEach(we);
197
- stmtChildren(s).forEach(ws);
198
- };
199
- sfn.body.forEach(ws);
210
+ }
200
211
  for (const reason of irOpaques) {
201
212
  if (!emitted.has(reason)) {
202
213
  throw new ContractError(
@@ -222,6 +233,203 @@ export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
222
233
  }
223
234
  }
224
235
 
236
+ /** Post structuring: a local the body READS must be WRITTEN somewhere in it. A materialized value
237
+ * renders as one `v = …` statement at its def's position while every use reads the bare name, so
238
+ * any pass that DISCARDS the statement's position — a collapsed switch test block, a suppressed
239
+ * edge copy — leaves the reads standing over whatever the register allocator left behind. That is
240
+ * the one wrongness the byte differ rewards rather than catches: the candidate compiles, scores,
241
+ * and can win.
242
+ *
243
+ * PRESENCE, not reaching definitions. The stronger question needs path sensitivity through
244
+ * `switch` fall-through, `do-while` and `break`, where a false positive DECLINES a function that
245
+ * is fine; assigned nowhere at all needs none of that and has no legitimate producer. Two local
246
+ * kinds are exempt and both say so in their declaration: an `uninit` local stands on an `undef`,
247
+ * where the missing assignment IS the recovery, and a `frame` local is the machine's own slot,
248
+ * whose store the readability passes between here and L3 may have dropped. */
249
+ export function assertLocalsWritten(sfn: SFn): void {
250
+ const suspect = new Set(sfn.locals.filter((l) => !l.frame && !l.uninit).map((l) => l.name));
251
+ if (!suspect.size) {
252
+ return;
253
+ }
254
+ const read = new Set<string>();
255
+ const written = new Set<string>();
256
+ // `&v` is a write channel this walk cannot follow — the callee/store behind it may fill the
257
+ // object — so it counts as one.
258
+ const walkExpr = (e: Expr): void => {
259
+ if ((e.k === 'var' || e.k === 'addr') && suspect.has(e.name)) {
260
+ (e.k === 'addr' ? written : read).add(e.name);
261
+ }
262
+ exprChildren(e).forEach(walkExpr);
263
+ };
264
+ const walkStmt = (st: Stmt): void => {
265
+ if (st.k === 'assign' && suspect.has(st.name)) {
266
+ written.add(st.name);
267
+ }
268
+ stmtExprs(st).forEach(walkExpr);
269
+ stmtChildren(st).forEach(walkStmt);
270
+ };
271
+ sfn.body.forEach(walkStmt);
272
+ const orphans = [...read].filter((n) => !written.has(n));
273
+ if (orphans.length) {
274
+ throw new ContractError(
275
+ `structuring emitted local(s) ${orphans.map((n) => `'${n}'`).join(', ')} in '${sfn.name}' read but ` +
276
+ `never assigned — a def whose assignment no render position emitted`,
277
+ );
278
+ }
279
+ }
280
+
281
+ /** After a respell variation: a local a pass DELETED from the declaration list is named nowhere in the tree it
282
+ * produced.
283
+ *
284
+ * THE FAILURE THIS CATCHES is the mirror of `assertLocalsWritten` above, and the three contracts
285
+ * beside it do not see it. A pass that consumes a local — l3/unmerge.ts substituting a merge temp
286
+ * into the arms, l3/coalesce.ts folding two names into one, l3/inlinebase.ts deleting a
287
+ * const-address pointer — drops the name from `sfn.locals` on the strength of an in-pass count
288
+ * that it is no longer mentioned. If that count is ever wrong the result is not a loud variation
289
+ * error: it is a candidate handed to the compiler with an undeclared identifier. Normally that is
290
+ * a dropped candidate, but in the REAL tier the candidate is compiled inside the project's
291
+ * vendored translation unit, where an orphaned name that collides with a context symbol compiles
292
+ * and scores. Measured on the shape that inhabits it — `locals = [p]`, body `p = 0; v16 = 1;
293
+ * *p = v16;` — `assertResolved`, `assertDerefsTyped` and `assertLocalsWritten` all pass: the name
294
+ * is neither `?` nor `undefined`, it is well-typed, and the question the third one asks is the
295
+ * OPPOSITE one (read but never written).
296
+ *
297
+ * A DIFFERENTIAL, and that is what makes it safe to run on every respelled tree. "Every name the tree
298
+ * mentions is declared" is NOT the invariant and would refuse correct output everywhere:
299
+ * structure.ts spells a write to a scalar global as a bare `assign` whose name is declared in the
300
+ * project's headers and nowhere in the tree (`gBlendValue = v;` — 71 such occurrences across 22
301
+ * winning sources, per l3/unmerge.ts's own note), and `SFn.globals` is the symbol-map-shaped
302
+ * subset, not that population. So the check speaks only about names the tree ITSELF declared a
303
+ * moment ago and the pass then removed — a set with no legitimate inhabitant, because a pass that
304
+ * drops a declaration is asserting exactly this.
305
+ *
306
+ * `addr` counts, like everywhere else: `&v` names the object as surely as a read does. */
307
+ export function assertNoOrphanedLocals(before: SFn, after: SFn): void {
308
+ const kept = new Set(after.locals.map((l) => l.name));
309
+ const dropped = new Set(before.locals.map((l) => l.name).filter((n) => !kept.has(n)));
310
+ if (!dropped.size) {
311
+ return;
312
+ }
313
+ // `l3/mentions.ts`'s walk, not a third copy of the node vocabulary — this is the LOUD BACKSTOP
314
+ // for the mistake that predicate guards, so it is the last place that should own its own.
315
+ // Locals only, deliberately: no L3 respell variation drops `SFn.params` (`pruneDeadParams` is L1 block
316
+ // params, ir/simplify.ts), so a params arm here would be a refusal with no inhabitant.
317
+ const found = mentionedLocals(after.body, dropped);
318
+ if (found.size) {
319
+ throw new ContractError(
320
+ `a respell variation deleted the declaration of ${[...found]
321
+ .sort()
322
+ .map((n) => `'${n}'`)
323
+ .join(', ')} in '${after.name}' ` +
324
+ `while the tree still names ${found.size > 1 ? 'them' : 'it'} — an undeclared identifier in the candidate`,
325
+ );
326
+ }
327
+ }
328
+
329
+ /** After a respell variation: every read of a MINTED local — its ADDRESS being taken included — must sit where
330
+ * that local's assignment has already run.
331
+ *
332
+ * THE failure a placing variation can ship, and the only one the byte differ rewards: a base local whose
333
+ * assignment does not reach a use is a DIFFERENT VARIABLE — C that compiles, scores, and can win
334
+ * (the shape #106 shipped). `contracts.ts`'s `assertLocalsWritten` does not see it: it accumulates
335
+ * reads and writes as SETS over the whole body, so a local assigned in one arm and read after the
336
+ * `if` is written somewhere and passes.
337
+ *
338
+ * Checked on the EMITTED tree rather than argued from the plan, because the plan is what a bug
339
+ * would be in. `rank.ts`'s `respell` catches the throw and drops the candidate, so the wrong
340
+ * answer becomes a reported variation error instead of a scored spelling.
341
+ *
342
+ * IT LIVES HERE, beside `assertLocalsWritten`, because it has that check's population and that
343
+ * check's call site: respell variations that place a def — l3/sinkinit.ts, l3/basecse.ts's first-use policy,
344
+ * l3/nearbase.ts, l3/reindex.ts, l3/scopebase.ts, l3/argbase.ts — are the population that can
345
+ * produce the failure, so the check belongs on every respelled tree rather than on one variation's.
346
+ *
347
+ * Called ABSOLUTELY by the placing variations that put an init inside a nested list, each over its own
348
+ * plan; everywhere else it is reached through `assertPlacementSurvives` below, which is a
349
+ * DIFFERENTIAL — so a placement no respelled tree ever satisfied is not judged, and a variation that
350
+ * mints nothing is not judged at all.
351
+ *
352
+ * A nested list gets a COPY of the reaching set, so an assignment inside one arm does not count as
353
+ * reaching anything after the `if`. */
354
+ export function assertHoistsDominate(sfn: SFn, minted: ReadonlySet<string>): void {
355
+ if (minted.size === 0) {
356
+ return;
357
+ }
358
+ const readUndominated = (e: Expr, live: ReadonlySet<string>): string | null => {
359
+ // `&p` COUNTS, the same mention the placing passes query on (l3/hoist.ts): the address is what
360
+ // a callee reads the cell through, so an init has to precede it as surely as it must precede a
361
+ // read.
362
+ if ((e.k === 'var' || e.k === 'addr') && minted.has(e.name) && !live.has(e.name)) {
363
+ return e.name;
364
+ }
365
+ let bad: string | null = null;
366
+ mapExprChildren(e, (c) => {
367
+ bad ??= readUndominated(c, live);
368
+ return c;
369
+ });
370
+ return bad;
371
+ };
372
+ const judge = (heads: readonly Expr[], live: ReadonlySet<string>): void => {
373
+ for (const e of heads) {
374
+ const bad = readUndominated(e, live);
375
+ if (bad) {
376
+ throw new ContractError(
377
+ `'${sfn.name}' reads '${bad}' where its assignment does not reach — ` +
378
+ `a def placed below a use it claims to serve`,
379
+ );
380
+ }
381
+ }
382
+ };
383
+ const walk = (list: Stmt[], live: Set<string>): void => {
384
+ for (const st of list) {
385
+ // A `for`'s INIT runs once, before the condition, the inc and the body — so its assignment
386
+ // reaches all three, and the loop's own parts are statements with their own nested lists.
387
+ // `l3/reindex.ts` mints an induction variable whose ONLY def is that init, so reading the
388
+ // `for` as one flat head list rejects every counted walk it spells.
389
+ if (st.k === 'for') {
390
+ walk([st.init], live);
391
+ judge(stmtExprs(st), live);
392
+ walk([st.inc], new Set(live));
393
+ walk(st.body, new Set(live));
394
+ continue;
395
+ }
396
+ judge(stmtExprs(st), live);
397
+ for (const child of stmtLists(st)) {
398
+ walk(child, new Set(live));
399
+ }
400
+ if (st.k === 'assign' && minted.has(st.name)) {
401
+ live.add(st.name);
402
+ }
403
+ }
404
+ };
405
+ walk(sfn.body, new Set());
406
+ }
407
+
408
+ /** The same guarantee across a re-spelling that MOVES statements over a placement another pass
409
+ * already made — `rank.ts`'s stacked variations (`/initfirst`, `/pollguard`, `/pollread`), derived
410
+ * onto every respelled tree after the variation placed its defs, and the variation-on-variation compositions in
411
+ * the same file where a def-moving pass (`sinkInitsToFirstUse`, `nearBaseClusters`,
412
+ * `reindexWalks`) runs on a tree a placing variation built. `pollReads` folds a materialized re-read
413
+ * back into a loop condition, which is exactly such a move.
414
+ *
415
+ * A DIFFERENTIAL, which is what makes it safe on every variation: the walk judges the reshaped tree
416
+ * only where it already described the unshaped one, so a placement it cannot model (a def inside
417
+ * a loop body read earlier in the same body is assigned on every iteration but the first) is not
418
+ * judged either way. `minted` may name a local `before` does not carry — a mover mints its own —
419
+ * and that one is judged absolutely, which is the same thing: a name absent from `before` is
420
+ * never read there. */
421
+ export function assertPlacementSurvives(before: SFn, after: SFn, minted: ReadonlySet<string>): void {
422
+ if (minted.size === 0) {
423
+ return;
424
+ }
425
+ try {
426
+ assertHoistsDominate(before, minted);
427
+ } catch {
428
+ return;
429
+ }
430
+ assertHoistsDominate(after, minted);
431
+ }
432
+
225
433
  /** Post structuring: the AST's memory accesses and operators must be SPELLABLE — a `field`
226
434
  * node's base a pointer-to-struct (`->`) or a struct value (`.`, an array element) carrying
227
435
  * that field; no pointer operand under an operator C rejects; and every SCALAR `index` node's
@@ -249,7 +457,7 @@ export function assertDerefsTyped(sfn: SFn): void {
249
457
  }
250
458
  }
251
459
  // Ops C rejects outright on a pointer operand (the additive ops and &&/|| are legal C).
252
- const NO_PTR_OPS = new Set<BinOp>(['&', '|', '^', '<<', '>>', '>>>', '*', '/', '%']);
460
+ const NO_PTR_OPS = new Set<BinOp>(['&', '|', '^', '<<', '>>', '>>>', '*', '/', '/u', '%', '%u']);
253
461
  // The comparison operators — where a bare `&SYM` operand is SIGN-ambiguous, not ill-formed.
254
462
  const CMP_OPS = new Set(['<', '<=', '>', '>=', '==', '!=']);
255
463
  // 1/2/4 only: the decomp typedef vocabulary (C_TYPEDEFS) has no 64-bit scalar, so a width-8
@@ -259,11 +467,13 @@ export function assertDerefsTyped(sfn: SFn): void {
259
467
  // Dot-form field bases (struct-array elements) carry the struct STRIDE as their width — any
260
468
  // stride matching the element size is legal there (the tree-level struct cast governs the
261
469
  // spelling; a stride/size MISMATCH types scalar in exprCType and the field rule flags it).
262
- // Collected as fields are visited, BEFORE recursing into their children. Identity-keyed: a
263
- // future subtree-SHARING pass (CSE-style) would leak the exemption to aliased bare uses —
264
- // trees are freshly built per node today (structure.ts), which this relies on.
470
+ // Collected as fields are visited, BEFORE recursing into their children which is what makes
471
+ // `walkExprs`' PRE-ORDER load-bearing here rather than incidental: the exemption is recorded on
472
+ // the `field` node and read at the `index` node beneath it. Identity-keyed: a future
473
+ // subtree-SHARING pass (CSE-style) would leak the exemption to aliased bare uses — trees are
474
+ // freshly built per node today (structure.ts), which this relies on.
265
475
  const structElem = new Set<Expr>();
266
- const checkExpr = (e: Expr): void => {
476
+ for (const e of walkExprs(sfn.body)) {
267
477
  if (e.k === 'index' && !structElem.has(e) && !SCALAR_WIDTHS.has(e.width)) {
268
478
  bad.push(`index width ${e.width} is not a C scalar width`);
269
479
  }
@@ -323,13 +533,7 @@ export function assertDerefsTyped(sfn: SFn): void {
323
533
  }
324
534
  }
325
535
  }
326
- exprChildren(e).forEach(checkExpr);
327
- };
328
- const checkStmt = (s: Stmt): void => {
329
- stmtExprs(s).forEach(checkExpr);
330
- stmtChildren(s).forEach(checkStmt);
331
- };
332
- sfn.body.forEach(checkStmt);
536
+ }
333
537
  if (bad.length) {
334
538
  throw new ContractError(
335
539
  `structuring emitted ill-typed C in '${sfn.name}': ${bad[0]}${bad.length > 1 ? ` (+${bad.length - 1} more)` : ''}`,
package/src/declare.ts CHANGED
@@ -29,7 +29,25 @@
29
29
  // field) can only LOSE score — the target bytes derive from the truth decls, so a
30
30
  // divergent compile can never false-match. Exception two is the NAME-ONLY data symbol
31
31
  // (`extern u32 name;` — see the default case): required to reproduce symtab-only map
32
- // rows outside project headers, justified by the same only-loses-score argument.
32
+ // rows outside project headers.
33
+ //
34
+ // WHERE THE ONLY-LOSES-SCORE ARGUMENT STOPS. It rests on the target bytes coming from the
35
+ // project's own TRUTH declarations, so a divergent decl compiles to different bytes and simply
36
+ // scores worse. The line is not map-derived vs. synthesized, it is whether the ref carries
37
+ // `access`: that field is read out of the candidate's own IR — the very asm it is then scored
38
+ // against — so a declaration wearing it is FITTED and can only manufacture agreement. Every
39
+ // `synthesized` ref can wear it, and so can a MAP-KNOWN name whose map entry has no shape
40
+ // (symtab-only projects), which is the one fitted case `synthesized` does not mark. Measured
41
+ // over the 252 real benchmark rows with their vendored maps: fitted-and-marked 2, fitted-but-
42
+ // unmarked 0 — so the marker covers today's population, and a symtab-only project is where it
43
+ // would stop.
44
+ //
45
+ // A fitted declaration is a sound ARTIFACT (decls + source really do compile to those bytes) and
46
+ // an unsound CLAIM if the decls are hidden, so a consumer publishing a verdict must show the
47
+ // block beside the source. Its price, against the benchmark's own vendored maps: of 28 fitted
48
+ // NARROW declarations over the 126 rankable agbcc rows, 26 agree with the project's real
49
+ // declaration and 2 do not (27 of 28 agree on the offset-0 ACCESS WIDTH the declaration
50
+ // produces, which is the weaker question of whether the same load is emitted).
33
51
  import { type StructFieldDecl, renderStructDecl } from './backend/cfamily';
34
52
  import { T } from './ir/types';
35
53
  import type { SymbolRef } from './l3/symbol-refs';
@@ -42,6 +60,7 @@ import {
42
60
  pointeeFields,
43
61
  symbolFieldType,
44
62
  } from './symbols';
63
+ import { C_TYPEDEFS } from './target';
45
64
 
46
65
  /** The u8/s8/u16/s16/u32/s32 spelling for a 1/2/4-byte cell, or null (no faithful narrow type). */
47
66
  function intType(size: number, signed: boolean): string | null {
@@ -223,9 +242,11 @@ export function renderDeclarations(refs: SymbolRef[]): string {
223
242
  // tree performed only under a decl of that exact width (`extern u16 g;` is `sh` where
224
243
  // a guessed u32 is `sw`). Without a bare off-0 access fact, every core spelling goes
225
244
  // through `&name` casts, where any object decl is address-identical — u32 is the
226
- // fallback cell. A divergent decl can only LOSE score — the target bytes derive from
227
- // the truth decls, so a mis-declared compile can never false-match (same argument as
228
- // enumIsSigned).
245
+ // fallback cell.
246
+ // For a MAP-derived name-only symbol (symtab-only projects) the only-loses-score
247
+ // argument applies. For a `synthesized` one it does not — the width came from the
248
+ // target's own asm, so this line is a hypothesis fitted to the bytes; see the module
249
+ // note's "WHERE THE ONLY-LOSES-SCORE ARGUMENT STOPS".
229
250
  const t = access ? intType(access.width, access.signed) : null;
230
251
  lines.push(`extern ${quals(info)}${t ?? 'u32'} ${name};`);
231
252
  break;
@@ -249,3 +270,19 @@ export function macroDefinesOf(declarations: string | undefined): string {
249
270
  const lines = declarations.split('\n').filter((l) => l.startsWith('#define '));
250
271
  return lines.length ? lines.join('\n') + '\n' : '';
251
272
  }
273
+
274
+ /** THE self-declared world's compilation context: asmlift's typedef prelude followed by this
275
+ * candidate's declaration block. One composition with two callers — the cli's compile seam
276
+ * (compile-command.ts, whose probe decides whether the world is self-declared at all) and the
277
+ * webapp's wasm scorer, which is ALWAYS in it — because two hand-rolled copies of
278
+ * `C_TYPEDEFS + decls` is exactly how the two scoring worlds come to disagree about what a
279
+ * candidate was compiled in. */
280
+ export function selfDeclaredContext(declarations: string | undefined): string {
281
+ return C_TYPEDEFS + (declarations ?? '');
282
+ }
283
+
284
+ /** The same context straight from a candidate's refs — what a scorer holding `Candidate`s (the
285
+ * webapp) needs, and the one place that decides an empty ref list renders no block at all. */
286
+ export function selfDeclaredContextFor(refs: SymbolRef[] | undefined): string {
287
+ return selfDeclaredContext(refs?.length ? renderDeclarations(refs) : undefined);
288
+ }
@@ -517,6 +517,17 @@ export function lift(
517
517
  }
518
518
  });
519
519
 
520
+ // NO FRAME PARTITION IS CLAIMED, so every def-less slot read refuses (frontend/ssa.ts,
521
+ // LiveInModel). This frontend has no frame bound at all — `addiu sp,sp,±N` is transparent and
522
+ // every word sp-relative access becomes `sp@<rawOff>` — so its slot keys span O32's CALLER-owned
523
+ // register-parameter home area and the incoming stack arguments above it, where a def-less read
524
+ // is argument 5, not an uninitialised local.
525
+ //
526
+ // Claiming one needs the frame SIZE those offsets are measured against, which is not computed
527
+ // here, plus ensureParam for the register half. The ranges themselves are known: O32 reserves
528
+ // `[0,16)` as the caller-owned home area (in NEITHER range — caller-owned, but not an argument)
529
+ // with stack arguments from 16 up, which is what `mips32be.cspec`'s `<localrange>` and stack
530
+ // `<pentry offset="16">` encode.
520
531
  const ssa = makeSsaBuilder(name, blocks.length, preds);
521
532
  const { irBlocks, readVar, writeVar, paramReg } = ssa;
522
533
  const RET = target.returnReg;
@@ -353,6 +353,17 @@ export function lift(
353
353
  // it is exempted from the loud-fail below; an UNrecovered `bctr` still fails loud.
354
354
  const jts = asmData ? recoverPpcJumpTables(instrs, asmData) : new Map<number, PpcJT>();
355
355
  const recoveredBctr = new Set([...jts.values()].map((j) => j.bctrAddr));
356
+ // A data reloc on an immediate-forming instruction means objdump printed a LINK-TIME
357
+ // placeholder (`lis r4,0` + R_PPC_ADDR16_HA sym): the real value is the symbol's half, and
358
+ // lifting the 0 silently reads the wrong address — plausible-but-wrong C, the forbidden class.
359
+ // The jump-table idiom's own @tbl pair never reaches these guards: a recovered dispatch block
360
+ // is the bounds branch's replaced fall-through, pruned as unreachable before decode.
361
+ const relocPlaceholder = (ins: Instr): void => {
362
+ throw new PpcUnsupportedError(
363
+ `cannot lift '${name}': '${ins.mnemonic}' at 0x${ins.addr.toString(16)} carries a data relocation ` +
364
+ `('${ins.sym}') — the printed immediate is a link-time placeholder, not the value`,
365
+ );
366
+ };
356
367
  // TRUSTWORTHINESS: fail loud on an unmodelled control transfer rather than dropping it (which
357
368
  // would silently miscompile the control flow). CTR-counted loops and indirect branches land here.
358
369
  for (const ins of instrs) {
@@ -573,17 +584,17 @@ export function lift(
573
584
  for (let k = 0; k < argc; k++) {
574
585
  args.push(read(ARG_REGS[k]));
575
586
  }
576
- // Pushed with `tmp` rather than `emit` so the result register is written AFTER the clobber
577
- // is recordedthe order matters: r3.. are volatile under the EABI, so a GUESSED arity
578
- // that counted a register set up before an intervening call passes an argument the caller
579
- // never set up (`finish()` cuts those back — frontend/ssa.ts), while the call's OWN result
580
- // must stay fresh for the next call (`bar(foo())`).
587
+ // Pushed with `tmp` rather than `emit` so the result register is written separately from
588
+ // the op — r3.. are volatile under the EABI, so a GUESSED arity that counted a register
589
+ // set up before an intervening call passes an argument the caller never set up
590
+ // (`finish()` cuts those back — frontend/ssa.ts), and the call's OWN result is the
591
+ // CALLEE's write, so `noteCall` records the clobber after it rather than before.
581
592
  const res = kit.tmp('call', args, { target: sym });
582
593
  if (declared === undefined) {
583
- ssa.recordGuessedCall(ops[ops.length - 1], bi, ARG_REGS);
594
+ ssa.recordGuessedCall(ops[ops.length - 1], bi, { argRegs: ARG_REGS, returnReg: RET });
584
595
  }
585
- ssa.noteCall(bi);
586
596
  write(RET, res);
597
+ ssa.noteCall(bi);
587
598
  break;
588
599
  }
589
600
  // Stack-frame + link-register bookkeeping. `stwu r1,-N(r1)` / `addi r1,r1,N` adjust the frame
@@ -629,9 +640,16 @@ export function lift(
629
640
  write(d, read(s));
630
641
  break; // move register (or rD,rS,rS)
631
642
  case 'li':
643
+ // SDA21 address formation encodes rA=0, so objdump prints `li rD,0` + R_PPC_EMB_SDA21
644
+ if (ins.sym) {
645
+ relocPlaceholder(ins);
646
+ }
632
647
  write(d, constVal(parseImm(s)));
633
648
  break; // load immediate (addi rD,0,imm)
634
649
  case 'lis':
650
+ if (ins.sym) {
651
+ relocPlaceholder(ins);
652
+ }
635
653
  write(d, constVal((parseImm(s) << 16) >> 0));
636
654
  break; // load immediate shifted
637
655
  case 'add':
@@ -641,11 +659,25 @@ export function lift(
641
659
  // `addi r1,r1,N` is frame teardown (skip); any other addi is a real add-immediate.
642
660
  case 'addi':
643
661
  case 'addic':
662
+ // reloc first: a data reloc on a stack adjust is no known compiler's output — loud
663
+ if (ins.sym) {
664
+ relocPlaceholder(ins);
665
+ }
644
666
  if (d === 'r1') {
645
667
  break;
646
668
  }
647
669
  emitBin('add', d, read(s), constVal(parseImm(t)));
648
670
  break;
671
+ // add immediate SHIFTED — the register-based `%ha` anchor: mwcc derives an absolute base
672
+ // from a scaled index (`addis r4,r3,-32736` = r3 + 0x80200000). The jump-table lis/addi
673
+ // pair recognizer is the only reloc-carrying consumer; an addis over a register is plain
674
+ // arithmetic, and a reloc-carrying one is a placeholder (guard above).
675
+ case 'addis':
676
+ if (ins.sym) {
677
+ relocPlaceholder(ins);
678
+ }
679
+ emitBin('add', d, read(s), constVal((parseImm(t) << 16) >> 0));
680
+ break;
649
681
  case 'subf':
650
682
  case 'subfc':
651
683
  case 'subfo':
@@ -697,6 +729,10 @@ export function lift(
697
729
  emitBin('or', d, read(s), read(t));
698
730
  break;
699
731
  case 'ori':
732
+ // `ori rD,rA,sym@l` is the other @l half-former — same placeholder hazard as addi
733
+ if (ins.sym) {
734
+ relocPlaceholder(ins);
735
+ }
700
736
  emitBin('or', d, read(s), constVal(parseImm(t)));
701
737
  break;
702
738
  case 'xor':