@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
package/src/pipeline.ts CHANGED
@@ -5,28 +5,32 @@ import {
5
5
  ContractError,
6
6
  assertDerefsTyped,
7
7
  assertEffectsPreserved,
8
+ assertLocalsWritten,
8
9
  assertResolved,
9
10
  assertTypesRecovered,
10
11
  } from './contracts';
11
12
  import type { AsmData } from './frontend/asmdata';
12
13
  import { FrontendUnsupportedError } from './frontend/errors';
13
14
  import { frontendFor } from './frontend/registry';
14
- import { type Block, type Fn, successorsOf } from './ir/core';
15
+ import { type Fn, reachableBlocks } from './ir/core';
15
16
  import { print } from './ir/print';
17
+ import { firstTrivialPhi } from './ir/simplify';
16
18
  import { T } from './ir/types';
17
19
  import { VerifyError, verify } from './ir/verify';
18
- import { Expr, LanguageBackend, SFn, Stmt, exprChildren, gapReasonFor, stmtChildren, stmtExprs } from './l3/ast';
19
- import { hoistReusedGlobalBases } from './l3/basecse';
20
+ import { LanguageBackend, SFn, gapReasonFor, walkExprs } from './l3/ast';
21
+ import { BASECSE_GATES, hoistBaseLocals } from './l3/basecse';
20
22
  import { eliminateDeadStores } from './l3/dce';
21
23
  import { mergeCommonTails } from './l3/tailmerge';
22
24
  import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
23
- import { type Prototypes, prototypesFromSymbols } from './proto';
25
+ import { type FnProto, type Prototypes, prototypesFromSymbols } from './proto';
24
26
  import { RaiseUnsupportedError } from './raise/errors';
25
- import { type PreRecoveryPass, runPreRecovery } from './raise/pre-recovery';
27
+ import { assumedShapes, inferGlobalArrays, orderLicensedGlobals } from './raise/globalshape';
28
+ import { foldEmptyLatches } from './raise/latch';
29
+ import { type PreRecoveryOptions, type PreRecoveryPass, runPreRecovery } from './raise/pre-recovery';
26
30
  import { recoverTypes } from './raise/recover';
27
31
  import { sinkReturns } from './raise/retsink';
28
32
  import { StructureError, structure } from './structure/structure';
29
- import { type SymbolMap, symbolsByName } from './symbols';
33
+ import { type SymbolInfo, type SymbolMap, symbolsByName } from './symbols';
30
34
  import { type TargetDescription, structureOptionsFor } from './target';
31
35
 
32
36
  /** How a gap (a construct asmlift cannot faithfully model) degrades:
@@ -74,10 +78,36 @@ export interface DecompileResult {
74
78
  sfn: SFn;
75
79
  ir: { raw: string; folded: string; recovered: string }; // IR dumps: post-lift, post-idiom, post-recovery
76
80
  patternHits: number;
77
- /** structured gap list — ALWAYS present; empty ⇔ the emission is gap-free (compiles + candidate
78
- * for scoring). Non-empty the source contains ASMLIFT_ERROR markers / a stub and will NOT
79
- * compile until the user defines that symbol (the loud-in-artifact contract). */
81
+ /** structured gap list — ALWAYS present. Non-empty ⇔ the source contains ASMLIFT_ERROR markers /
82
+ * a stub and will NOT compile until the user acts (the loud-in-artifact contract).
83
+ *
84
+ * EMPTY IS THE ABSENCE OF A GAP, NOT A PROMISE THAT THE C COMPILES. A candidate can also fail
85
+ * on a symbol the caller never declared, which is a CONTEXT question the benchmark answers by
86
+ * escalating to the vendored preprocessed context. Most such names survive K&R implicit
87
+ * declaration; a function ADDRESS does not, and klonoa's `UpdateStageSelectScreen` reports 0
88
+ * gaps while emitting `((s32 *)50345232)[1] = &HandlePauseMenuInput;`, which agbcc rejects
89
+ * where the plain call above it passes with a warning. Declaring an address-taken unknown
90
+ * callee would close it — an emitter change, moving source bytes on every row that has one. */
80
91
  diagnostics: Diagnostic[];
92
+ /** THE SHAPES THIS SOURCE'S SPELLING ASSUMES, which no symbol map supplied (raise/globalshape.ts).
93
+ *
94
+ * Everything else a backend emits is byte-correct under ANY declaration of the names it spells
95
+ * — that is exactly why `((T *)&gSym)[i]` is the fallback (structure/globalaccess.ts). A bare
96
+ * `gSym[i]` is not: it means what the DECLARATION of `gSym` says it means, and where that
97
+ * declaration was derived from the assembly rather than read from the project's map, the
98
+ * emitted source is right about the target's bytes only beside the declaration derived with it.
99
+ * The element SIGNEDNESS is the sharp case, and it is an assumption rather than a reading:
100
+ * compiled through the benchmark's own agbcc command, `(u16)gS[i]` over `extern const s16 gS[]`
101
+ * and `gS[i]` over `extern const u16 gS[]` are the SAME OBJECT, so the assembly cannot say which
102
+ * the source wrote — asmlift picks the one its own declaration block states.
103
+ *
104
+ * So this travels with the source on every path that can emit it: the scoring layer renders it
105
+ * (declare.ts, and main.ts's `[declared]` block), and a caller that shows the source alone must
106
+ * show these too, or it is publishing a spelling whose meaning it has not stated. Empty on every
107
+ * run that assumed nothing — which includes every derived shape the structurer did not spell
108
+ * bare, and every name the caller's own map described (raise/globalshape.ts `assumedShapes`
109
+ * computes that narrowing and names the corpus row behind each half). */
110
+ assumedSymbols: SymbolInfo[];
81
111
  }
82
112
 
83
113
  export function decompile(
@@ -115,39 +145,66 @@ function runTower(
115
145
  // (1) lift: ISA frontend (resolved by target) → L1 with block-argument SSA
116
146
  const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
117
147
  verify(fn);
118
- const raw = print(fn);
148
+ // Every dump carries the write-order record (ir/print.ts `PrintOptions`): it decides the
149
+ // edge-copy order and the raising folds mutate it, so two dumps compare whole program states.
150
+ const raw = print(fn, { writeOrder: true });
151
+ // (1.5) the ARRAY SHAPES this function's own assembly evidences, for globals the project map
152
+ // does not describe. Read HERE, off the lifted fn, because the fact it needs — whether the base
153
+ // was materialized before the index was scaled — is destroyed by the raising tower below
154
+ // (raise/globalshape.ts's module note). Empty unless the target opts in.
155
+ const inferredSymbols = inferGlobalArrays(fn, target);
156
+ // …and the ORDER half of the same reading, which reaches names the shape derivation refuses (a
157
+ // struct element among them). Read off the same lifted fn, for the same reason.
158
+ const orderLicensed = orderLicensedGlobals(fn, target);
119
159
 
120
160
  // (2) idiom fold: apply serializable patterns on the IR (the AI-improvement surface),
121
161
  // gated generically by the Target's capabilities (not an `arch ==` branch).
122
162
  const patternHits = applyIdiomPatterns(fn, target, opts.patterns);
123
- const folded = print(fn);
163
+ const folded = print(fn, { writeOrder: true });
124
164
 
125
165
  // (2.35–3.5) pre-recovery recognizers → type recovery → return-sinking, the ONE shared spine
126
166
  // (`raiseRecovered`) that trace.ts and the cli's rank.ts/report.ts also run.
127
- raiseRecovered(fn, target);
128
- const recovered = print(fn);
167
+ raiseRecovered(fn, target, {}, prototypes[name]);
168
+ const recovered = print(fn, { writeOrder: true });
129
169
 
130
170
  // (4) structure: IR → neutral AST; boundary contract: no unresolved value leaked (strict), or
131
171
  // every unresolved value spelled as a loud ASMLIFT_ERROR marker (annotate).
172
+ const mapSymbols = opts.symbols ? symbolsByName(opts.symbols) : undefined;
132
173
  const sfn = structureChecked(fn, {
133
174
  ...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false),
175
+ // What the EMITTED LANGUAGE can say is a structuring input wherever two recoveries of one
176
+ // shape are behaviourally identical and only one of them is printable (switch fall-through
177
+ // vs plain if-nesting): recovery must not mint a tree this backend would refuse.
178
+ spellSwitchFallthrough: backend.spellsSwitchFallthrough,
134
179
  onGap,
135
- ...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
180
+ ...(mapSymbols ? { symbols: mapSymbols } : {}),
181
+ ...(inferredSymbols.size ? { inferredSymbols } : {}),
182
+ ...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
136
183
  });
137
184
 
138
185
  // (5) lower + print: neutral AST → target language
139
186
  const source = backend.emit(sfn);
140
187
 
141
- return { source, sfn, ir: { raw, folded, recovered }, patternHits, diagnostics: collectMarkers(sfn) };
188
+ return {
189
+ source,
190
+ sfn,
191
+ ir: { raw, folded, recovered },
192
+ patternHits,
193
+ diagnostics: collectMarkers(sfn),
194
+ // WHAT THE SOURCE RESTS ON, not what the derivation found: a shape the structurer did not
195
+ // spell bare, and a name the caller's own map described, are both obligations this reader
196
+ // does not have (raise/globalshape.ts `assumedShapes`).
197
+ assumedSymbols: assumedShapes(inferredSymbols, sfn, mapSymbols),
198
+ };
142
199
  }
143
200
 
144
201
  // ── the shared raising tower ────────────────────────────────────────────────────────────────
145
- // decompile(), decompileTraced (trace.ts), and the cli's decompileRanked (rank.ts) /
146
- // decompileWithReport + its score probe (report.ts) all raise a lifted fn through the SAME
147
- // stage sequence. The optional hooks are the only per-caller
148
- // differences: rank pins its signedness candidate `beforeRecover`; the report pushes trace
149
- // entries after each stage. Every hook fires AFTER the stage's verify, so a hook can never
150
- // observe unverified IR.
202
+ // decompile(), decompileTraced (trace.ts), rank.ts's decompileRanked and the cli's
203
+ // decompileWithReport + its score probe (report.ts) all raise a lifted fn through the SAME stage
204
+ // sequence. Two things vary per caller and nothing else does: the optional HOOKS — rank pins its
205
+ // signedness candidate at `beforeRecover`, decompileTraced pushes a trace entry after each stage —
206
+ // and the `pre` options bag, which reaches the pre-recovery passes themselves. Every hook fires
207
+ // AFTER the stage's verify, so a hook can never observe unverified IR.
151
208
 
152
209
  /** Stage 2 — idiom fold: filter the pattern set by target capabilities, apply, dce + verify.
153
210
  * Returns total hits. `patterns` defaults to DEFAULT_IDIOM_PATTERNS exactly like decompile(). */
@@ -173,26 +230,84 @@ export interface RaiseHooks {
173
230
  afterRecover?: () => void;
174
231
  /** after return-sinking, only when it changed the fn (fires after its verify) */
175
232
  afterRetsink?: () => void;
233
+ /** after empty-latch folding, only when it removed a block (fires after its verify) */
234
+ afterLatchFold?: () => void;
176
235
  }
177
236
 
178
237
  /** Stages 2.35–3.5 — pre-recovery recognizers (the shared ordered list in raise/pre-recovery.ts)
179
238
  * → type recovery (boundary contract: no `unknown` survives) → return-sinking (tail-duplicate a
180
- * return-only merge so short-circuits emit early returns). `verify` after every pass that
181
- * changed the IR. */
182
- export function raiseRecovered(fn: Fn, target: TargetDescription, hooks: RaiseHooks = {}): void {
183
- runPreRecovery(fn, target, (pass, result) => {
184
- verify(fn);
185
- hooks.afterPass?.(pass, result);
186
- });
239
+ * return-only merge so short-circuits emit early returns) empty-latch folding (splice out a
240
+ * back-edge block SSA construction emptied). `verify` after every pass that changed the IR.
241
+ *
242
+ * The two CFG passes look ordered and are not: running latch folding FIRST instead changes
243
+ * nothing across a 3337-function agbcc corpus. Worth saying, because folding an empty block ahead
244
+ * of return-sinking does take away `br` predecessors it needs — the dominance gate is what makes
245
+ * that unreachable, since the blocks retsink wants are never back-edge sources. It goes last
246
+ * because that is where the CFG stops moving.
247
+ *
248
+ * The tail is three separate parameters rather than an options bag on purpose: this is the
249
+ * published package root export, so every caller outside this repo is pinned to the positions.
250
+ *
251
+ * @param hooks per-stage observers; see {@link RaiseHooks}. Each fires after that stage's verify.
252
+ * @param self this function's own prototype, where the caller has one — what the pre-recovery
253
+ * passes read to type the parameters they are narrowing.
254
+ * @param pre per-caller PRE-RECOVERY options. One shipped user: rank.ts's `/connective`
255
+ * candidate, which passes `{ shortCircuit: { foldTreeOwned: true } }` to take the
256
+ * fold the comparison-tree refusal owns (raise/shortcircuit.ts). */
257
+ export function raiseRecovered(
258
+ fn: Fn,
259
+ target: TargetDescription,
260
+ hooks: RaiseHooks = {},
261
+ self?: FnProto,
262
+ pre: PreRecoveryOptions = {},
263
+ ): void {
264
+ const lifted = runPreRecovery(
265
+ fn,
266
+ target,
267
+ (pass, result) => {
268
+ verify(fn);
269
+ hooks.afterPass?.(pass, result);
270
+ },
271
+ self,
272
+ pre,
273
+ );
187
274
  hooks.beforeRecover?.();
188
275
  recoverTypes(fn);
189
276
  verify(fn);
190
277
  assertTypesRecovered(fn);
191
278
  hooks.afterRecover?.();
192
- if (sinkReturns(fn)) {
279
+ // `lifted.mergeShapes` is the CFG as it ENTERED pre-recovery, and retsink's `pre-diamond` needs
280
+ // exactly that: `raise/shortcircuit.ts` manufactures two-armed diamonds out of condition trees the
281
+ // ROM never merged, and a diamond this pass reads at its own turn may be one of those.
282
+ if (
283
+ sinkReturns(fn, {
284
+ hoistsSingleSetArm: target.compilerBehaviors.hoistsSingleSetArm,
285
+ mergeShapes: lifted.mergeShapes,
286
+ })
287
+ ) {
193
288
  verify(fn);
194
289
  hooks.afterRetsink?.();
195
290
  }
291
+ if (foldEmptyLatches(fn)) {
292
+ verify(fn);
293
+ hooks.afterLatchFold?.();
294
+ }
295
+ // THE BOUNDARY POSTCONDITION. Above this line passes move the CFG; below it nothing does, and the
296
+ // structurer reads a block parameter as a JOIN — a name it must give a local of its own. A param
297
+ // whose every in-edge carries one value is not a join, and leaving one standing is how a
298
+ // CFG-motion pass does its damage three stages away rather than where it happened: retsink's own
299
+ // stranded merge was destroyed into `v0 = 0; return v0;` and read by Regime-A switch recovery as a
300
+ // SECOND `default` candidate, declining every fall-through tree. This names it at retsink.
301
+ // Not an `ir/verify.ts` rule: a trivial phi is well-formed IR, and both SSA construction and the
302
+ // `addrnum` pass mint one and clear it inside their own scope (see `firstTrivialPhi`).
303
+ const stranded = firstTrivialPhi(fn);
304
+ if (stranded) {
305
+ throw new Error(
306
+ `internal: raising left a trivial phi — block #${fn.blocks.indexOf(stranded.block)} takes ` +
307
+ `'${stranded.param}', whose every in-edge carries one value. A pass that retires an in-edge ` +
308
+ `must run simplifyTrivialPhis after it.`,
309
+ );
310
+ }
196
311
  }
197
312
 
198
313
  /** Run `body`; if it declines, name the unmodelled instructions the function carries.
@@ -214,15 +329,7 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
214
329
  if (!(e instanceof StructureError) || !fn.blocks[0]) {
215
330
  throw e;
216
331
  }
217
- const seen = new Set<Block>([fn.blocks[0]]);
218
- for (const stack = [fn.blocks[0]]; stack.length;) {
219
- for (const s of successorsOf(stack.pop()!)) {
220
- if (!seen.has(s)) {
221
- seen.add(s);
222
- stack.push(s);
223
- }
224
- }
225
- }
332
+ const seen = reachableBlocks(fn);
226
333
  const names = new Set<string>();
227
334
  for (const b of seen) {
228
335
  for (const op of b.ops) {
@@ -234,7 +341,7 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
234
341
  if (!names.size || /unmodelled instruction/.test(e.message)) {
235
342
  throw e;
236
343
  }
237
- // Through `gapReasonFor`, so the classifier sees its canonical text — a hand-written variant
344
+ // Through `gapReasonFor`, so the classifier sees its canonical text — a hand-written spelling
238
345
  // misses the mnemonic-anchored classes and every attributed decline lands in the generic bucket.
239
346
  const list = [...names].sort().map(gapReasonFor).join(', ');
240
347
  throw new StructureError(`${e.message} — and the function carries ${list}, which is the more likely cause`);
@@ -242,22 +349,32 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
242
349
  }
243
350
 
244
351
  /** Stage 4 — structure + its boundary contracts, always as a pair. */
245
- export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]): SFn {
246
- const raw = attributeOpaques(fn, () => structure(fn, opts));
247
- // BOTH boundary contracts run on the pre-DCE tree: the readability pass must never be able to
352
+ export function structureChecked(
353
+ fn: Fn,
354
+ opts: Parameters<typeof structure>[1],
355
+ hooks?: Parameters<typeof structure>[2],
356
+ ): SFn {
357
+ const raw = attributeOpaques(fn, () => structure(fn, opts, hooks));
358
+ // The boundary contracts run on the pre-DCE tree: the readability pass must never be able to
248
359
  // hide a structuring defect by dropping the dead statement that carries it. assertResolved
249
360
  // catches an unresolved `?` value; assertDerefsTyped catches an ill-typed deref (e.g. a pointer
250
- // under a rejected operator) — even one sitting in dead code structure emitted. DCE then only
251
- // removes statements/flips branches over an already-validated tree.
361
+ // under a rejected operator) — even one sitting in dead code structure emitted;
362
+ // assertLocalsWritten catches a materialized def whose assignment no position emitted. DCE then
363
+ // only removes statements/flips branches over an already-validated tree.
252
364
  assertResolved(raw);
253
365
  assertDerefsTyped(raw);
366
+ assertLocalsWritten(raw);
254
367
  assertEffectsPreserved(fn, raw);
255
368
  // Then the readability/quality rewrites: merge a statement common to every arm of an if,
256
- // drop dead stores (whose empty-then peephole flips the arm the merge empties), then hoist a
257
- // reused aggregate-global
258
- // base into a typed local pointer. The hoist moves the deref cast from each `index` node onto the
259
- // local's initializer, so re-validate deref typing on the rewritten tree.
260
- const sfn = hoistReusedGlobalBases(eliminateDeadStores(mergeCommonTails(raw)));
369
+ // drop dead stores (whose empty-then peephole flips the arm the merge empties), then hoist each
370
+ // leaf base the DEFAULT gate table admits into a typed local pointer. The hoist moves the deref
371
+ // cast from each `index` node onto the local's initializer, so re-validate deref typing on the
372
+ // rewritten tree.
373
+ // Both arguments SPELLED, defaults or not: this is the one call to this pass that is committed
374
+ // rather than offered, so it is the one whose gate table and whose placement can cost a MATCH
375
+ // instead of a candidate (docs/level-tower.md). A committed policy that reads as "whatever the
376
+ // default is" is the policy nobody reviews.
377
+ const sfn = hoistBaseLocals(eliminateDeadStores(mergeCommonTails(raw)), BASECSE_GATES, 'head');
261
378
  assertDerefsTyped(sfn);
262
379
  // Re-checked after the readability rewrites for the same reason deref typing is: a pass that
263
380
  // merges arms or drops statements must not be able to lose or duplicate a call.
@@ -317,26 +434,22 @@ export function stubResult(name: string, asm: string, backend: LanguageBackend,
317
434
  ir: { raw: '', folded: '', recovered: '' },
318
435
  patternHits: 0,
319
436
  diagnostics: [{ stage, reason: msg }],
437
+ // A stub spells no global, so it assumes nothing about one.
438
+ assumedSymbols: [],
320
439
  };
321
440
  }
322
441
 
323
442
  /** Every ASMLIFT_ERROR marker in the emitted AST, as a structured diagnostic (one per marker,
324
443
  * document order). The harness/self-improve loop reads THIS; the source text is for humans. */
325
444
  function collectMarkers(sfn: SFn): Diagnostic[] {
326
- // On the shared exprChildren/stmtExprs/stmtChildren traversal. Order is exprs-then-children
327
- // per statement — deterministic and near-document-order (a `for`'s cond is visited before its
328
- // init; see the note on stmtChildren).
445
+ // On the shared `walkExprs` traversal (l3/ast.ts). Order is exprs-then-nested-statements per
446
+ // statement — deterministic and near-document-order (a `for`'s cond is visited before its init;
447
+ // see the note on stmtChildren).
329
448
  const out: Diagnostic[] = [];
330
- const we = (e: Expr): void => {
449
+ for (const e of walkExprs(sfn.body)) {
331
450
  if (e.k === 'marker') {
332
451
  out.push({ stage: 'structure', reason: e.reason });
333
452
  }
334
- exprChildren(e).forEach(we);
335
- };
336
- const ws = (s: Stmt): void => {
337
- stmtExprs(s).forEach(we);
338
- stmtChildren(s).forEach(ws);
339
- };
340
- sfn.body.forEach(ws);
453
+ }
341
454
  return out;
342
455
  }
package/src/proto.ts CHANGED
@@ -1,15 +1,22 @@
1
- import type { SymbolMap } from './symbols';
1
+ import type { SymbolMap, SymbolTypeFacts } from './symbols';
2
2
 
3
3
  // asmlift — function prototypes: the single carrier for the caller-supplied facts a
4
- // matching-decomp project reads from its headers (arg counts, void-ness). One `Prototypes`
5
- // map, keyed by symbol, is threaded through every entry point and resolved at the point of
6
- // use — a callee's `params` gives its call-site arity, a function's own entry gives its
7
- // `returnsVoid`. It also keeps the frontend seam honest: a frontend receives prototypes,
8
- // not a grab-bag of ISA-specific options.
4
+ // matching-decomp project reads from its headers (arg counts, parameter widths, void-ness). One
5
+ // `Prototypes` map, keyed by symbol, is threaded through every entry point and resolved at the
6
+ // point of use — a callee's `params` gives its call-site arity, a function's own entry gives its
7
+ // `returnsVoid` and the widths raise/paramwidth.ts checks against. It also keeps the frontend seam
8
+ // honest: a frontend receives prototypes, not a grab-bag of ISA-specific options.
9
9
 
10
- /** One declared parameter, as its C type text (`"u8"`, `"s32"`, `"void *"`). asmlift consumes
11
- * only the COUNT today (call-site arity), but a project's header extraction naturally produces
12
- * the typed list, and keeping it lets a later pass pin an argument's width/signedness. */
10
+ /** One declared parameter, as its C type text (`"u8"`, `"s32"`, `"void *"`, `"int"`). Two facts
11
+ * are read off it: the list's LENGTH is the call-site arity (`protoArity`), and one entry's WIDTH
12
+ * (`declaredWidth`) is what raise/paramwidth.ts checks its inference against.
13
+ *
14
+ * A DECLARED WIDTH ONLY VETOES, never pins. Where the asm carries a prologue extension the
15
+ * declaration contradicts, the declaration wins — it is a fact from the project's headers, where
16
+ * the extension is an inference off an encoding two different C sources produce. Where the asm
17
+ * carries no extension, this list is NOT consulted: pinning there would type every parameter of
18
+ * every row from the declaration, and a declared `u32` kills rank.ts's signed arm before the
19
+ * differ ever sees it. That half is a variation question and is not answered here. */
13
20
  export type ParamType = string;
14
21
 
15
22
  /** What the headers know about one function. All fields optional: a partial table (only
@@ -17,10 +24,26 @@ export type ParamType = string;
17
24
  export interface FnProto {
18
25
  /** declared parameters — either a bare arity COUNT or the typed parameter list a header
19
26
  * extraction produces (`["u8", "s32"]`). BOTH forms yield the call-site arity via
20
- * `protoArity`; omit to let the frontend fall back to its contiguous-arg-register heuristic. */
27
+ * `protoArity`; only the typed form carries a width. Omit to let the frontend fall back to its
28
+ * contiguous-arg-register heuristic. */
21
29
  params?: number | ParamType[];
22
- /** the declared return type is `void`, so a trailing `bx lr` leaves a meaningless
23
- * return register that must not surface as a `return` value. */
30
+ /** The declared return type is `void`. Read for the function under decompilation, where a
31
+ * trailing `bx lr` leaves a meaningless return register that must not surface as a `return`
32
+ * value — and, since the out-parameter path landed, for a CALLEE, where it is the only thing
33
+ * that tells an out-parameter frame from a hidden struct-return pointer.
34
+ *
35
+ * THAT SECOND READER IS LOAD-BEARING AND THE FIELD IS UNCHECKED DATA, which is worth knowing
36
+ * before authoring one. `validatePrototypes` type-checks the boolean and can check no more:
37
+ * nothing in the assembly distinguishes the two frames, which is why the refusal exists. So a
38
+ * callee wrongly declared `void` turns a loud decline into a compiling, plausible, wrong
39
+ * program — measured on the shape the guard is for, `struct S4 mk(int); struct S4 s = mk(x);`
40
+ * lifts as `mk(&sp0); return (u8)sp0;` when `mk` is declared `returnsVoid` — where a callee
41
+ * wrongly declared NON-void, or left undeclared, only costs the lift. Under-declaring is the
42
+ * safe direction and this project has already shipped one wrong entry (a dataset row declaring
43
+ * `returnsVoid: true` for a function whose own reference returns `void *`).
44
+ *
45
+ * Both readers see one field through two trust levels: caller-supplied on `--proto`, and
46
+ * machine-derived from DWARF through `prototypesFromSymbols`. Neither is distinguished here. */
24
47
  returnsVoid?: boolean;
25
48
  }
26
49
 
@@ -43,10 +66,85 @@ export function protoArity(p: FnProto | undefined): number | undefined {
43
66
  return undefined;
44
67
  }
45
68
 
69
+ /** Bit width per C89 base type on every target asmlift lifts (all ILP32). `long` is 32 here and
70
+ * would not be on an LP64 host, so it is a target fact rather than a language one. */
71
+ const BASE_WIDTHS: ReadonlyMap<string, number> = new Map([
72
+ ['char', 8],
73
+ ['short', 16],
74
+ ['short int', 16],
75
+ ['int', 32],
76
+ ['long', 32],
77
+ ['long int', 32],
78
+ ]);
79
+
80
+ /** The bit width one declared parameter type spells, or `undefined` for a spelling this does not
81
+ * read — a project typedef, a struct, a `float`. UNDEFINED IS "NO OPINION", never "wide": the one
82
+ * consumer treats a width it can read as authority and a width it cannot as absence, so an
83
+ * unrecognized spelling leaves the asm's own inference standing.
84
+ *
85
+ * A pointer is register-wide whatever it points at, which is the fact the `*` test carries. */
86
+ export function declaredWidth(t: ParamType): number | undefined {
87
+ const s = t
88
+ .replace(/\b(?:const|volatile)\b/g, ' ')
89
+ .trim()
90
+ .replace(/\s+/g, ' ');
91
+ if (s.endsWith('*')) {
92
+ return 32;
93
+ }
94
+ const own = /^([su])(8|16|32)$/.exec(s);
95
+ if (own) {
96
+ return Number(own[2]);
97
+ }
98
+ // `unsigned`/`signed` alone is `unsigned int`/`signed int`; the signedness itself is not a width.
99
+ const base = s
100
+ .replace(/\b(?:signed|unsigned)\b/g, ' ')
101
+ .trim()
102
+ .replace(/\s+/g, ' ');
103
+ return BASE_WIDTHS.get(base === '' && s !== '' ? 'int' : base);
104
+ }
105
+
106
+ /** Problems with a HAND-WRITTEN prototype table — empty when it is well formed.
107
+ *
108
+ * `protoArity` above falls back to the arg-register heuristic on a `params` it cannot read, which
109
+ * is right when `params` is omitted and silent when it is mistyped: `params: "2"` then decompiles
110
+ * at a guessed arity, and a misspelled `returnsVoid` does nothing at all. Neither is visible in
111
+ * the output, so a table that came from outside is checked before it reaches either. */
112
+ export function validatePrototypes(value: unknown): string[] {
113
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
114
+ return ['must be an object mapping a symbol name to its prototype'];
115
+ }
116
+ const problems: string[] = [];
117
+ for (const [sym, proto] of Object.entries(value)) {
118
+ if (typeof proto !== 'object' || proto === null || Array.isArray(proto)) {
119
+ problems.push(`${sym}: must be an object, e.g. {"params": 2}`);
120
+ continue;
121
+ }
122
+ for (const key of Object.keys(proto)) {
123
+ if (key !== 'params' && key !== 'returnsVoid') {
124
+ problems.push(`${sym}: unknown key "${key}" (expected "params" or "returnsVoid")`);
125
+ }
126
+ }
127
+ const { params, returnsVoid } = proto as { params?: unknown; returnsVoid?: unknown };
128
+ if (params !== undefined) {
129
+ const countOk = typeof params === 'number' && Number.isInteger(params) && params >= 0;
130
+ const listOk = Array.isArray(params) && params.every((t) => typeof t === 'string');
131
+ if (!countOk && !listOk) {
132
+ problems.push(`${sym}: "params" must be a non-negative integer or a list of type strings`);
133
+ }
134
+ }
135
+ if (returnsVoid !== undefined && typeof returnsVoid !== 'boolean') {
136
+ problems.push(`${sym}: "returnsVoid" must be a boolean`);
137
+ }
138
+ }
139
+ return problems;
140
+ }
141
+
46
142
  /** The C type spelling for one declared parameter/return, or null when the facts do not
47
143
  * determine one. A pointer is `void *` — address-identical to any object pointer, and asmlift
48
- * makes every stride explicit — so nothing is guessed about what it points at. */
49
- function typeSpelling(t: { size: number | null; signed: boolean | null; pointer?: boolean }): ParamType | null {
144
+ * makes every stride explicit — so nothing is guessed about what it points at. A richer spelling
145
+ * would also be INERT: `declaredWidth` answers 32 for every `*`, and a CALLEE's parameter types
146
+ * are read for the list's length alone (test/param-pointee-variation.test.ts). */
147
+ function typeSpelling(t: SymbolTypeFacts): ParamType | null {
50
148
  if (t.pointer) {
51
149
  return 'void *';
52
150
  }
@@ -61,7 +61,12 @@ export function recognizeArrays(fn: Fn): number {
61
61
  b.ops[i] = mkOp('aload', {
62
62
  operands: [m.base, m.index],
63
63
  results: [res],
64
- attrs: { elemSize: op.attrs.width as number, signed: op.attrs.signed as boolean },
64
+ // listOrder rides along: an ldmia-expanded load keeps its stream-order caveat as an aload
65
+ attrs: {
66
+ elemSize: op.attrs.width as number,
67
+ signed: op.attrs.signed as boolean,
68
+ ...(op.attrs.listOrder === true && { listOrder: true }),
69
+ },
65
70
  });
66
71
  replaceAllUsesWith(fn, op.results[0], res);
67
72
  count++;