@asmlift/core 0.4.0 → 0.6.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.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +649 -219
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +23 -4
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
package/src/pipeline.ts
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
// asmlift — the library entry point. `decompile(name, asm, target)` runs the raising tower and
|
|
2
2
|
// returns structured results: the source, the per-level IR dumps, and diagnostics.
|
|
3
3
|
import { cBackend } from './backend/c';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ContractError,
|
|
6
|
+
assertDerefsTyped,
|
|
7
|
+
assertEffectsPreserved,
|
|
8
|
+
assertLocalsWritten,
|
|
9
|
+
assertResolved,
|
|
10
|
+
assertTypesRecovered,
|
|
11
|
+
} from './contracts';
|
|
5
12
|
import type { AsmData } from './frontend/asmdata';
|
|
6
13
|
import { FrontendUnsupportedError } from './frontend/errors';
|
|
7
14
|
import { frontendFor } from './frontend/registry';
|
|
8
|
-
import type
|
|
15
|
+
import { type Fn, reachableBlocks } from './ir/core';
|
|
9
16
|
import { print } from './ir/print';
|
|
17
|
+
import { firstTrivialPhi } from './ir/simplify';
|
|
10
18
|
import { T } from './ir/types';
|
|
11
19
|
import { VerifyError, verify } from './ir/verify';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
20
|
+
import { LanguageBackend, SFn, gapReasonFor, walkExprs } from './l3/ast';
|
|
21
|
+
import { BASECSE_GATES, hoistBaseLocals } from './l3/basecse';
|
|
14
22
|
import { eliminateDeadStores } from './l3/dce';
|
|
15
23
|
import { mergeCommonTails } from './l3/tailmerge';
|
|
16
24
|
import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
|
|
17
|
-
import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
25
|
+
import { type FnProto, type Prototypes, prototypesFromSymbols } from './proto';
|
|
18
26
|
import { RaiseUnsupportedError } from './raise/errors';
|
|
19
|
-
import {
|
|
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';
|
|
20
30
|
import { recoverTypes } from './raise/recover';
|
|
21
31
|
import { sinkReturns } from './raise/retsink';
|
|
22
32
|
import { StructureError, structure } from './structure/structure';
|
|
23
|
-
import { type SymbolMap, symbolsByName } from './symbols';
|
|
33
|
+
import { type SymbolInfo, type SymbolMap, symbolsByName } from './symbols';
|
|
24
34
|
import { type TargetDescription, structureOptionsFor } from './target';
|
|
25
35
|
|
|
26
36
|
/** How a gap (a construct asmlift cannot faithfully model) degrades:
|
|
@@ -68,10 +78,36 @@ export interface DecompileResult {
|
|
|
68
78
|
sfn: SFn;
|
|
69
79
|
ir: { raw: string; folded: string; recovered: string }; // IR dumps: post-lift, post-idiom, post-recovery
|
|
70
80
|
patternHits: number;
|
|
71
|
-
/** structured gap list — ALWAYS present
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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. */
|
|
74
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[];
|
|
75
111
|
}
|
|
76
112
|
|
|
77
113
|
export function decompile(
|
|
@@ -109,39 +145,66 @@ function runTower(
|
|
|
109
145
|
// (1) lift: ISA frontend (resolved by target) → L1 with block-argument SSA
|
|
110
146
|
const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
|
|
111
147
|
verify(fn);
|
|
112
|
-
|
|
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);
|
|
113
159
|
|
|
114
160
|
// (2) idiom fold: apply serializable patterns on the IR (the AI-improvement surface),
|
|
115
161
|
// gated generically by the Target's capabilities (not an `arch ==` branch).
|
|
116
162
|
const patternHits = applyIdiomPatterns(fn, target, opts.patterns);
|
|
117
|
-
const folded = print(fn);
|
|
163
|
+
const folded = print(fn, { writeOrder: true });
|
|
118
164
|
|
|
119
165
|
// (2.35–3.5) pre-recovery recognizers → type recovery → return-sinking, the ONE shared spine
|
|
120
166
|
// (`raiseRecovered`) that trace.ts and the cli's rank.ts/report.ts also run.
|
|
121
|
-
raiseRecovered(fn, target);
|
|
122
|
-
const recovered = print(fn);
|
|
167
|
+
raiseRecovered(fn, target, {}, prototypes[name]);
|
|
168
|
+
const recovered = print(fn, { writeOrder: true });
|
|
123
169
|
|
|
124
170
|
// (4) structure: IR → neutral AST; boundary contract: no unresolved value leaked (strict), or
|
|
125
171
|
// every unresolved value spelled as a loud ASMLIFT_ERROR marker (annotate).
|
|
172
|
+
const mapSymbols = opts.symbols ? symbolsByName(opts.symbols) : undefined;
|
|
126
173
|
const sfn = structureChecked(fn, {
|
|
127
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,
|
|
128
179
|
onGap,
|
|
129
|
-
...(
|
|
180
|
+
...(mapSymbols ? { symbols: mapSymbols } : {}),
|
|
181
|
+
...(inferredSymbols.size ? { inferredSymbols } : {}),
|
|
182
|
+
...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
|
|
130
183
|
});
|
|
131
184
|
|
|
132
185
|
// (5) lower + print: neutral AST → target language
|
|
133
186
|
const source = backend.emit(sfn);
|
|
134
187
|
|
|
135
|
-
return {
|
|
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
|
+
};
|
|
136
199
|
}
|
|
137
200
|
|
|
138
201
|
// ── the shared raising tower ────────────────────────────────────────────────────────────────
|
|
139
|
-
// decompile(), decompileTraced (trace.ts), and the cli's
|
|
140
|
-
// decompileWithReport + its score probe (report.ts) all raise a lifted fn through the SAME
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// 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.
|
|
145
208
|
|
|
146
209
|
/** Stage 2 — idiom fold: filter the pattern set by target capabilities, apply, dce + verify.
|
|
147
210
|
* Returns total hits. `patterns` defaults to DEFAULT_IDIOM_PATTERNS exactly like decompile(). */
|
|
@@ -167,17 +230,47 @@ export interface RaiseHooks {
|
|
|
167
230
|
afterRecover?: () => void;
|
|
168
231
|
/** after return-sinking, only when it changed the fn (fires after its verify) */
|
|
169
232
|
afterRetsink?: () => void;
|
|
233
|
+
/** after empty-latch folding, only when it removed a block (fires after its verify) */
|
|
234
|
+
afterLatchFold?: () => void;
|
|
170
235
|
}
|
|
171
236
|
|
|
172
237
|
/** Stages 2.35–3.5 — pre-recovery recognizers (the shared ordered list in raise/pre-recovery.ts)
|
|
173
238
|
* → type recovery (boundary contract: no `unknown` survives) → return-sinking (tail-duplicate a
|
|
174
|
-
* return-only merge so short-circuits emit early returns)
|
|
175
|
-
* changed the IR.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
runPreRecovery(
|
|
265
|
+
fn,
|
|
266
|
+
target,
|
|
267
|
+
(pass, result) => {
|
|
268
|
+
verify(fn);
|
|
269
|
+
hooks.afterPass?.(pass, result);
|
|
270
|
+
},
|
|
271
|
+
self,
|
|
272
|
+
pre,
|
|
273
|
+
);
|
|
181
274
|
hooks.beforeRecover?.();
|
|
182
275
|
recoverTypes(fn);
|
|
183
276
|
verify(fn);
|
|
@@ -187,25 +280,93 @@ export function raiseRecovered(fn: Fn, target: TargetDescription, hooks: RaiseHo
|
|
|
187
280
|
verify(fn);
|
|
188
281
|
hooks.afterRetsink?.();
|
|
189
282
|
}
|
|
283
|
+
if (foldEmptyLatches(fn)) {
|
|
284
|
+
verify(fn);
|
|
285
|
+
hooks.afterLatchFold?.();
|
|
286
|
+
}
|
|
287
|
+
// THE BOUNDARY POSTCONDITION. Above this line passes move the CFG; below it nothing does, and the
|
|
288
|
+
// structurer reads a block parameter as a JOIN — a name it must give a local of its own. A param
|
|
289
|
+
// whose every in-edge carries one value is not a join, and leaving one standing is how a
|
|
290
|
+
// CFG-motion pass does its damage three stages away rather than where it happened: retsink's own
|
|
291
|
+
// stranded merge was destroyed into `v0 = 0; return v0;` and read by Regime-A switch recovery as a
|
|
292
|
+
// SECOND `default` candidate, declining every fall-through tree. This names it at retsink.
|
|
293
|
+
// Not an `ir/verify.ts` rule: a trivial phi is well-formed IR, and both SSA construction and the
|
|
294
|
+
// `addrnum` pass mint one and clear it inside their own scope (see `firstTrivialPhi`).
|
|
295
|
+
const stranded = firstTrivialPhi(fn);
|
|
296
|
+
if (stranded) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
`internal: raising left a trivial phi — block #${fn.blocks.indexOf(stranded.block)} takes ` +
|
|
299
|
+
`'${stranded.param}', whose every in-edge carries one value. A pass that retires an in-edge ` +
|
|
300
|
+
`must run simplifyTrivialPhis after it.`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Run `body`; if it declines, name the unmodelled instructions the function carries.
|
|
306
|
+
*
|
|
307
|
+
* An `opaque` degrades its own value AND makes its block impure, so a shape recognizer refuses:
|
|
308
|
+
* `headerPure` rejects a header holding one, and the loop declines with "unrecovered back-edge …".
|
|
309
|
+
* True and useless — the shape is fine, an instruction is missing — and the benchmark classifies
|
|
310
|
+
* declines by that text, so the round is filed as a loop-capability gap and the improvement loop
|
|
311
|
+
* builds the wrong thing.
|
|
312
|
+
*
|
|
313
|
+
* Only ADDS attribution: never converts a decline into a success, never fires without an
|
|
314
|
+
* unmodelled instruction, reachable blocks only (one in dead code did not cause the refusal). */
|
|
315
|
+
function attributeOpaques<T>(fn: Fn, body: () => T): T {
|
|
316
|
+
try {
|
|
317
|
+
return body();
|
|
318
|
+
} catch (e) {
|
|
319
|
+
// Attribution is a nicety, so it must not be able to throw: a crash here would replace a
|
|
320
|
+
// DESIGNED loud failure with an incidental one, which contract-invariant.test.ts rejects by name.
|
|
321
|
+
if (!(e instanceof StructureError) || !fn.blocks[0]) {
|
|
322
|
+
throw e;
|
|
323
|
+
}
|
|
324
|
+
const seen = reachableBlocks(fn);
|
|
325
|
+
const names = new Set<string>();
|
|
326
|
+
for (const b of seen) {
|
|
327
|
+
for (const op of b.ops) {
|
|
328
|
+
if (op.opcode === 'opaque') {
|
|
329
|
+
names.add(typeof op.attrs.mnemonic === 'string' ? op.attrs.mnemonic : '?');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!names.size || /unmodelled instruction/.test(e.message)) {
|
|
334
|
+
throw e;
|
|
335
|
+
}
|
|
336
|
+
// Through `gapReasonFor`, so the classifier sees its canonical text — a hand-written variant
|
|
337
|
+
// misses the mnemonic-anchored classes and every attributed decline lands in the generic bucket.
|
|
338
|
+
const list = [...names].sort().map(gapReasonFor).join(', ');
|
|
339
|
+
throw new StructureError(`${e.message} — and the function carries ${list}, which is the more likely cause`);
|
|
340
|
+
}
|
|
190
341
|
}
|
|
191
342
|
|
|
192
343
|
/** Stage 4 — structure + its boundary contracts, always as a pair. */
|
|
193
344
|
export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]): SFn {
|
|
194
|
-
const raw = structure(fn, opts);
|
|
195
|
-
//
|
|
345
|
+
const raw = attributeOpaques(fn, () => structure(fn, opts));
|
|
346
|
+
// The boundary contracts run on the pre-DCE tree: the readability pass must never be able to
|
|
196
347
|
// hide a structuring defect by dropping the dead statement that carries it. assertResolved
|
|
197
348
|
// catches an unresolved `?` value; assertDerefsTyped catches an ill-typed deref (e.g. a pointer
|
|
198
|
-
// under a rejected operator) — even one sitting in dead code structure emitted
|
|
199
|
-
//
|
|
349
|
+
// under a rejected operator) — even one sitting in dead code structure emitted;
|
|
350
|
+
// assertLocalsWritten catches a materialized def whose assignment no position emitted. DCE then
|
|
351
|
+
// only removes statements/flips branches over an already-validated tree.
|
|
200
352
|
assertResolved(raw);
|
|
201
353
|
assertDerefsTyped(raw);
|
|
354
|
+
assertLocalsWritten(raw);
|
|
355
|
+
assertEffectsPreserved(fn, raw);
|
|
202
356
|
// Then the readability/quality rewrites: merge a statement common to every arm of an if,
|
|
203
|
-
// drop dead stores (whose empty-then peephole flips the arm the merge empties), then hoist
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
357
|
+
// drop dead stores (whose empty-then peephole flips the arm the merge empties), then hoist each
|
|
358
|
+
// leaf base the DEFAULT gate table admits into a typed local pointer. The hoist moves the deref
|
|
359
|
+
// cast from each `index` node onto the local's initializer, so re-validate deref typing on the
|
|
360
|
+
// rewritten tree.
|
|
361
|
+
// Both arguments SPELLED, defaults or not: this is the one call to this pass that is committed
|
|
362
|
+
// rather than offered, so it is the one whose gate table and whose placement can cost a MATCH
|
|
363
|
+
// instead of a candidate (docs/level-tower.md). A committed policy that reads as "whatever the
|
|
364
|
+
// default is" is the policy nobody reviews.
|
|
365
|
+
const sfn = hoistBaseLocals(eliminateDeadStores(mergeCommonTails(raw)), BASECSE_GATES, 'head');
|
|
208
366
|
assertDerefsTyped(sfn);
|
|
367
|
+
// Re-checked after the readability rewrites for the same reason deref typing is: a pass that
|
|
368
|
+
// merges arms or drops statements must not be able to lose or duplicate a call.
|
|
369
|
+
assertEffectsPreserved(fn, sfn);
|
|
209
370
|
return sfn;
|
|
210
371
|
}
|
|
211
372
|
|
|
@@ -261,26 +422,22 @@ export function stubResult(name: string, asm: string, backend: LanguageBackend,
|
|
|
261
422
|
ir: { raw: '', folded: '', recovered: '' },
|
|
262
423
|
patternHits: 0,
|
|
263
424
|
diagnostics: [{ stage, reason: msg }],
|
|
425
|
+
// A stub spells no global, so it assumes nothing about one.
|
|
426
|
+
assumedSymbols: [],
|
|
264
427
|
};
|
|
265
428
|
}
|
|
266
429
|
|
|
267
430
|
/** Every ASMLIFT_ERROR marker in the emitted AST, as a structured diagnostic (one per marker,
|
|
268
431
|
* document order). The harness/self-improve loop reads THIS; the source text is for humans. */
|
|
269
432
|
function collectMarkers(sfn: SFn): Diagnostic[] {
|
|
270
|
-
// On the shared
|
|
271
|
-
//
|
|
272
|
-
//
|
|
433
|
+
// On the shared `walkExprs` traversal (l3/ast.ts). Order is exprs-then-nested-statements per
|
|
434
|
+
// statement — deterministic and near-document-order (a `for`'s cond is visited before its init;
|
|
435
|
+
// see the note on stmtChildren).
|
|
273
436
|
const out: Diagnostic[] = [];
|
|
274
|
-
const
|
|
437
|
+
for (const e of walkExprs(sfn.body)) {
|
|
275
438
|
if (e.k === 'marker') {
|
|
276
439
|
out.push({ stage: 'structure', reason: e.reason });
|
|
277
440
|
}
|
|
278
|
-
|
|
279
|
-
};
|
|
280
|
-
const ws = (s: Stmt): void => {
|
|
281
|
-
stmtExprs(s).forEach(we);
|
|
282
|
-
stmtChildren(s).forEach(ws);
|
|
283
|
-
};
|
|
284
|
-
sfn.body.forEach(ws);
|
|
441
|
+
}
|
|
285
442
|
return out;
|
|
286
443
|
}
|
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
|
|
5
|
-
// map, keyed by symbol, is threaded through every entry point and resolved at the
|
|
6
|
-
// use — a callee's `params` gives its call-site arity, a function's own entry gives its
|
|
7
|
-
// `returnsVoid
|
|
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 *"`).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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 an axis 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`;
|
|
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
|
-
/**
|
|
23
|
-
* return register that must not surface as a `return`
|
|
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
|
-
|
|
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-axis.test.ts). */
|
|
147
|
+
function typeSpelling(t: SymbolTypeFacts): ParamType | null {
|
|
50
148
|
if (t.pointer) {
|
|
51
149
|
return 'void *';
|
|
52
150
|
}
|
package/src/raise/arrays.ts
CHANGED
|
@@ -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
|
-
|
|
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++;
|
package/src/raise/divpow2.ts
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
// the biased arm must be the negative one — the consistency check m2c's `49b5d87` also adds),
|
|
42
42
|
// because every one of those is a way for a superficially similar diamond to mean something else.
|
|
43
43
|
import { Block, Fn, Op, Value, defOpMap, mkOp, mkValue, predecessors, replaceAllUsesWith } from '../ir/core';
|
|
44
|
-
import {
|
|
44
|
+
import { EFFECTFUL_OPS } from '../ir/opcodes';
|
|
45
45
|
import { T } from '../ir/types';
|
|
46
46
|
|
|
47
47
|
/** `shr_s v {imm=k}` → k, else null. */
|
|
@@ -103,13 +103,14 @@ export function recognizeDivPow2(fn: Fn): boolean {
|
|
|
103
103
|
for (const bias of preds.get(m)!) {
|
|
104
104
|
// The BIAS arm: sole predecessor is the head, and it does nothing but bias (and possibly
|
|
105
105
|
// shift). The whole block is DELETED, not hoisted, so anything else in it would be silently
|
|
106
|
-
// dropped — a store, a call or
|
|
106
|
+
// dropped — a store, a call or an opaque there would simply stop happening (an opaque
|
|
107
|
+
// whether or not its result is read: liveness says nothing about what the instruction did).
|
|
107
108
|
const bt = term(bias);
|
|
108
109
|
if (bt.opcode !== 'br' || bt.successors[0]?.block !== m || bias === fn.blocks[0]) {
|
|
109
110
|
continue;
|
|
110
111
|
}
|
|
111
112
|
const bp = preds.get(bias) ?? [];
|
|
112
|
-
if (bp.length !== 1 || bias.ops.some((op) =>
|
|
113
|
+
if (bp.length !== 1 || bias.ops.some((op) => EFFECTFUL_OPS.has(op.opcode))) {
|
|
113
114
|
continue;
|
|
114
115
|
}
|
|
115
116
|
const h = bp[0];
|