@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.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- 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/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- 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 +176 -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 +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- 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 +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -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 +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- 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
|
|
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 {
|
|
19
|
-
import {
|
|
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 {
|
|
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
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
-
|
|
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
|
-
...(
|
|
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 {
|
|
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
|
|
146
|
-
// decompileWithReport + its score probe (report.ts) all raise a lifted fn through the SAME
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
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)
|
|
181
|
-
* changed the IR.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
251
|
-
//
|
|
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
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
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
|
|
327
|
-
//
|
|
328
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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 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`;
|
|
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-variation.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++;
|