@asmlift/core 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/pipeline.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
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
|
+
import { aggregateSize, ownArgumentWordsOn, returnedAggregate, returnsInMemory } from './aggregate';
|
|
3
4
|
import { cBackend } from './backend/c';
|
|
4
5
|
import {
|
|
5
6
|
ContractError,
|
|
6
7
|
assertDerefsTyped,
|
|
7
8
|
assertEffectsPreserved,
|
|
9
|
+
assertFrameUnionsViewed,
|
|
8
10
|
assertLocalsWritten,
|
|
11
|
+
assertPostIncrUnshared,
|
|
9
12
|
assertResolved,
|
|
10
13
|
assertTypesRecovered,
|
|
11
14
|
} from './contracts';
|
|
12
15
|
import type { AsmData } from './frontend/asmdata';
|
|
16
|
+
import { returnedStruct, structLocalType } from './frontend/call-plan';
|
|
13
17
|
import { FrontendUnsupportedError } from './frontend/errors';
|
|
18
|
+
import type { LiftOptions } from './frontend/frontend';
|
|
19
|
+
import { liftOwnRegisterStruct, liftOwnStructReturn } from './frontend/own-struct-return';
|
|
20
|
+
import { liftRegisterStructCalls } from './frontend/register-struct-call';
|
|
14
21
|
import { frontendFor } from './frontend/registry';
|
|
22
|
+
import { ABSORBED_KEYS, HOMED_UNDEF } from './frontend/ssa';
|
|
15
23
|
import { type Fn, reachableBlocks } from './ir/core';
|
|
24
|
+
import { deadOps } from './ir/discipline';
|
|
16
25
|
import { print } from './ir/print';
|
|
17
|
-
import { firstTrivialPhi } from './ir/simplify';
|
|
18
|
-
import { T } from './ir/types';
|
|
26
|
+
import { firstTrivialPhi, pruneDeadParams } from './ir/simplify';
|
|
27
|
+
import { type IrType, T, intWidth } from './ir/types';
|
|
19
28
|
import { VerifyError, verify } from './ir/verify';
|
|
20
29
|
import { LanguageBackend, SFn, gapReasonFor, walkExprs } from './l3/ast';
|
|
21
30
|
import { BASECSE_GATES, hoistBaseLocals } from './l3/basecse';
|
|
@@ -23,11 +32,21 @@ import { eliminateDeadStores } from './l3/dce';
|
|
|
23
32
|
import { mergeCommonTails } from './l3/tailmerge';
|
|
24
33
|
import { dropUnspelledReturns } from './l3/tailret';
|
|
25
34
|
import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
|
|
26
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
type FnProto,
|
|
37
|
+
type Prototypes,
|
|
38
|
+
declaresAggregateReturn,
|
|
39
|
+
declaresVoidReturn,
|
|
40
|
+
ownReturnWidth,
|
|
41
|
+
prototypesFromSymbols,
|
|
42
|
+
} from './proto';
|
|
43
|
+
import { type ContextGlobals, splitOwnContext } from './proto-context';
|
|
44
|
+
import { type StampBehaviors, stampDeclaredVolatileUnder } from './raise/declared-volatile';
|
|
27
45
|
import { RaiseUnsupportedError } from './raise/errors';
|
|
28
46
|
import { assumedShapes, inferGlobalArrays, orderLicensedGlobals } from './raise/globalshape';
|
|
29
|
-
import {
|
|
47
|
+
import { OFFSET_NAME_PASS } from './raise/offsetnames';
|
|
30
48
|
import { type PreRecoveryOptions, type PreRecoveryPass, runPreRecovery } from './raise/pre-recovery';
|
|
49
|
+
import { PRE_STRUCTURE_FOLDS, type PreStructureFold } from './raise/pre-structure';
|
|
31
50
|
import { recoverTypes } from './raise/recover';
|
|
32
51
|
import { sinkReturns } from './raise/retsink';
|
|
33
52
|
import { StructureError, structure } from './structure/structure';
|
|
@@ -70,6 +89,18 @@ export interface DecompileOptions {
|
|
|
70
89
|
* declaration shapes (DWARF types-sidecar). Drives the Thumb numeric-pool promotion and the
|
|
71
90
|
* byte-sensitive global spellings. Absent ⇒ byte-identical to today. */
|
|
72
91
|
symbols?: SymbolMap;
|
|
92
|
+
/** what the project's declaration context states about its globals (proto-context.ts
|
|
93
|
+
* `globalsFromContext`): no aggregate is spelled as a bare scalar name, and every access to a
|
|
94
|
+
* volatile object the map does not lay out, or through a pointer to one, is stamped
|
|
95
|
+
* (raise/declared-volatile.ts), so the cast it is read through carries the qualifier. */
|
|
96
|
+
contextGlobals?: ContextGlobals;
|
|
97
|
+
/** the declaration context's own entry for `name` (proto-context.ts `ownContextEntry`), which
|
|
98
|
+
* bounds the argument registers SOFTLY where `prototypes` states no entry for it, and whose
|
|
99
|
+
* struct return, where it declares one, is lifted where the machine proves it (`liftStamped`);
|
|
100
|
+
* absent ⇒ nothing bounds them softly. A `prototypes` entry for `name` marked
|
|
101
|
+
* `fromContext` is read as this, and taken out of the table (proto-context.ts
|
|
102
|
+
* `splitOwnContext`). */
|
|
103
|
+
ownContextProto?: FnProto;
|
|
73
104
|
/** gap policy — see `OnGap`. Default "strict". */
|
|
74
105
|
onGap?: OnGap;
|
|
75
106
|
}
|
|
@@ -109,6 +140,21 @@ export interface DecompileResult {
|
|
|
109
140
|
* bare, and every name the caller's own map described (raise/globalshape.ts `assumedShapes`
|
|
110
141
|
* computes that narrowing and names the corpus row behind each half). */
|
|
111
142
|
assumedSymbols: SymbolInfo[];
|
|
143
|
+
/** THE NAMES THIS SOURCE REACHED BY ARITHMETIC, not by the map alone (raise/offsetnames.ts).
|
|
144
|
+
* A pool-loaded `gaddr` is the map answering about an address the machine code carries; a
|
|
145
|
+
* walked one is asmlift computing `addr(base) + K` and asking about THAT — so the base's own
|
|
146
|
+
* address in the map is load-bearing for a name the reader sees spelled bare. Sibling of
|
|
147
|
+
* `assumedSymbols` and reported beside it (`main.ts`'s `[walked]` block, the trace's
|
|
148
|
+
* `stage:offsetnames`), and it is also the only thing in a result that says a walk spelling
|
|
149
|
+
* stopped being enumerable here. Empty on every run with no symbol map. */
|
|
150
|
+
walkedNames: string[];
|
|
151
|
+
/** WHERE THE LIFT WENT PAST ITS CONTEXT DECLARATION (`liftStamped`'s `[own-arity]` notes): each
|
|
152
|
+
* names the declaration and either the argument registers the body reads past it, kept as
|
|
153
|
+
* parameters, or the refusal that made the lift drop the bound. A note is no gap, but the
|
|
154
|
+
* parameter list printed contradicts the header, which every compiler here rejects as
|
|
155
|
+
* conflicting types. Empty where no context entry was handed over, or where the lift stayed
|
|
156
|
+
* within it. */
|
|
157
|
+
ownArityNotes: string[];
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
export function decompile(
|
|
@@ -142,10 +188,22 @@ function runTower(
|
|
|
142
188
|
const backend = opts.backend ?? cBackend;
|
|
143
189
|
// The project's own DWARF signatures fill in what the caller did not state — in practice the
|
|
144
190
|
// CALLEES (a function still in assembly has none), which is what makes this transferable.
|
|
145
|
-
const
|
|
191
|
+
const own = splitOwnContext(opts.prototypes, name, opts.ownContextProto);
|
|
192
|
+
const prototypes = prototypesFromSymbols(opts.symbols, own.prototypes ?? {});
|
|
146
193
|
// (1) lift: ISA frontend (resolved by target) → L1 with block-argument SSA
|
|
147
|
-
const
|
|
148
|
-
|
|
194
|
+
const ownArityNotes: string[] = [];
|
|
195
|
+
const fn = liftStamped(
|
|
196
|
+
name,
|
|
197
|
+
asm,
|
|
198
|
+
target,
|
|
199
|
+
prototypes,
|
|
200
|
+
opts.asmData,
|
|
201
|
+
opts.symbols,
|
|
202
|
+
opts.contextGlobals,
|
|
203
|
+
undefined,
|
|
204
|
+
own.ownContextProto,
|
|
205
|
+
ownArityNotes,
|
|
206
|
+
);
|
|
149
207
|
// Every dump carries the write-order record (ir/print.ts `PrintOptions`): it decides the
|
|
150
208
|
// edge-copy order and the raising folds mutate it, so two dumps compare whole program states.
|
|
151
209
|
const raw = print(fn, { writeOrder: true });
|
|
@@ -157,6 +215,20 @@ function runTower(
|
|
|
157
215
|
// …and the ORDER half of the same reading, which reaches names the shape derivation refuses (a
|
|
158
216
|
// struct element among them). Read off the same lifted fn, for the same reason.
|
|
159
217
|
const orderLicensed = orderLicensedGlobals(fn, target);
|
|
218
|
+
// (1.6) …and the NAMES the map holds for the addresses this function's machine code built by
|
|
219
|
+
// arithmetic off a named one (raise/offsetnames.ts). After the two readings above, which are
|
|
220
|
+
// about the lift as the frontend emitted it; before everything below, so the whole raising
|
|
221
|
+
// tower sees a walked-to cell as the named global it is.
|
|
222
|
+
//
|
|
223
|
+
// The two readings therefore attribute a walked access to the base rather than to the name this
|
|
224
|
+
// pass gives it, and that costs nothing either way: a SHAPE is a declaration, so a name the map
|
|
225
|
+
// describes — which every base here is, or the pass could not have fired — is dropped from
|
|
226
|
+
// `inferredSymbols`, and the ORDER licence only enables a cast spelling that is byte-correct
|
|
227
|
+
// under any declaration. Measured: with this call moved ABOVE both readings, the three corpus
|
|
228
|
+
// rows the pass fires on keep their fan size, their winner and their score exactly.
|
|
229
|
+
const walkedNames = opts.symbols
|
|
230
|
+
? OFFSET_NAME_PASS.run(fn, opts.symbols, stampBehaviorsFor(target, opts.contextGlobals))
|
|
231
|
+
: [];
|
|
160
232
|
|
|
161
233
|
// (2) idiom fold: apply serializable patterns on the IR (the AI-improvement surface),
|
|
162
234
|
// gated generically by the Target's capabilities (not an `arch ==` branch).
|
|
@@ -172,13 +244,15 @@ function runTower(
|
|
|
172
244
|
// every unresolved value spelled as a loud ASMLIFT_ERROR marker (annotate).
|
|
173
245
|
const mapSymbols = opts.symbols ? symbolsByName(opts.symbols) : undefined;
|
|
174
246
|
const sfn = structureChecked(fn, {
|
|
175
|
-
...structureOptionsFor(target, prototypes[name]
|
|
247
|
+
...structureOptionsFor(target, declaresVoidReturn(prototypes[name]), prototypes),
|
|
176
248
|
// What the EMITTED LANGUAGE can say is a structuring input wherever two recoveries of one
|
|
177
249
|
// shape are behaviourally identical and only one of them is printable (switch fall-through
|
|
178
250
|
// vs plain if-nesting): recovery must not mint a tree this backend would refuse.
|
|
179
251
|
spellSwitchFallthrough: backend.spellsSwitchFallthrough,
|
|
252
|
+
spellSwitchBreak: backend.spellsSwitchBreak,
|
|
180
253
|
onGap,
|
|
181
254
|
...(mapSymbols ? { symbols: mapSymbols } : {}),
|
|
255
|
+
...(opts.contextGlobals ? { contextGlobals: opts.contextGlobals } : {}),
|
|
182
256
|
...(inferredSymbols.size ? { inferredSymbols } : {}),
|
|
183
257
|
...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
|
|
184
258
|
});
|
|
@@ -196,9 +270,459 @@ function runTower(
|
|
|
196
270
|
// spell bare, and a name the caller's own map described, are both obligations this reader
|
|
197
271
|
// does not have (raise/globalshape.ts `assumedShapes`).
|
|
198
272
|
assumedSymbols: assumedShapes(inferredSymbols, sfn, mapSymbols),
|
|
273
|
+
walkedNames,
|
|
274
|
+
ownArityNotes,
|
|
199
275
|
};
|
|
200
276
|
}
|
|
201
277
|
|
|
278
|
+
/** The `[own-arity]` notes (`DecompileResult.ownArityNotes`) of one lift under `opts`, for a caller
|
|
279
|
+
* whose result carries none — the ranked path, whose every candidate lifts as this one does. Empty
|
|
280
|
+
* where the lift fails: the run reports that failure itself. */
|
|
281
|
+
export function ownArityNotesOf(
|
|
282
|
+
name: string,
|
|
283
|
+
asm: string,
|
|
284
|
+
target: TargetDescription,
|
|
285
|
+
opts: Pick<DecompileOptions, 'prototypes' | 'asmData' | 'symbols' | 'contextGlobals' | 'ownContextProto'>,
|
|
286
|
+
): string[] {
|
|
287
|
+
const own = splitOwnContext(opts.prototypes, name, opts.ownContextProto);
|
|
288
|
+
if (own.ownContextProto === undefined) {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
const notes: string[] = [];
|
|
292
|
+
try {
|
|
293
|
+
liftStamped(
|
|
294
|
+
name,
|
|
295
|
+
asm,
|
|
296
|
+
target,
|
|
297
|
+
prototypesFromSymbols(opts.symbols, own.prototypes ?? {}),
|
|
298
|
+
opts.asmData,
|
|
299
|
+
opts.symbols,
|
|
300
|
+
opts.contextGlobals,
|
|
301
|
+
undefined,
|
|
302
|
+
own.ownContextProto,
|
|
303
|
+
notes,
|
|
304
|
+
);
|
|
305
|
+
} catch {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
return notes;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** What the declared-volatile stamp reads besides the IR and the map: the compiler's fact and the
|
|
312
|
+
* declaration context's globals. Every stamp a driver puts reads the same two. */
|
|
313
|
+
export function stampBehaviorsFor(
|
|
314
|
+
target: TargetDescription,
|
|
315
|
+
contextGlobals: ContextGlobals | undefined,
|
|
316
|
+
): StampBehaviors {
|
|
317
|
+
return {
|
|
318
|
+
volatileReadsExtendInRegister: target.compilerBehaviors.volatileReadsExtendInRegister,
|
|
319
|
+
contextGlobals,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Stage 1 — lift: the target's ISA frontend → L1, verified, with every read of an object
|
|
324
|
+
* `symbols` or `contextGlobals` declares volatile stamped `declared` (raise/declared-volatile.ts)
|
|
325
|
+
* before any pass asks whether it may be deleted. Every entry path lifts through here, each lift
|
|
326
|
+
* under the map its structuring will be handed, and each bounded by the context's own entry
|
|
327
|
+
* (`ownContext`) the same way (`liftWithinContextBound`); `notes` collects that bound's
|
|
328
|
+
* `[own-arity]` notes. */
|
|
329
|
+
export function liftStamped(
|
|
330
|
+
name: string,
|
|
331
|
+
asm: string,
|
|
332
|
+
target: TargetDescription,
|
|
333
|
+
prototypes: Prototypes,
|
|
334
|
+
asmData: AsmData | undefined,
|
|
335
|
+
symbols: SymbolMap | undefined,
|
|
336
|
+
contextGlobals: ContextGlobals | undefined,
|
|
337
|
+
liftOptions?: LiftOptions,
|
|
338
|
+
ownContext?: FnProto,
|
|
339
|
+
notes?: string[],
|
|
340
|
+
): Fn {
|
|
341
|
+
// Its callers split the table already; split again here, where every lift passes, so that no
|
|
342
|
+
// frontend is handed a marked context entry to read as a stated, hard bound.
|
|
343
|
+
const split = splitOwnContext(prototypes, name, ownContext);
|
|
344
|
+
const table = split.prototypes ?? {};
|
|
345
|
+
const own = Object.hasOwn(table, name) ? table[name] : undefined;
|
|
346
|
+
const ownReturn = ownStructReturnThroughMemory(name, target, own, split.ownContextProto, notes);
|
|
347
|
+
// a refusal about the return's width that the declared struct explains waits for the proof
|
|
348
|
+
let deferred: ((unproved: string) => never) | undefined;
|
|
349
|
+
const bytes = ownReturn?.register === true ? undefined : ownReturn?.type.size;
|
|
350
|
+
const fn = liftWithinContextBound(
|
|
351
|
+
name,
|
|
352
|
+
asm,
|
|
353
|
+
target,
|
|
354
|
+
table,
|
|
355
|
+
asmData,
|
|
356
|
+
symbols,
|
|
357
|
+
contextGlobals,
|
|
358
|
+
bytes === undefined
|
|
359
|
+
? liftOptions
|
|
360
|
+
: { ...liftOptions, ownMemoryReturn: { bytes, defer: (refuse) => void (deferred ??= refuse) } },
|
|
361
|
+
split.ownContextProto,
|
|
362
|
+
notes,
|
|
363
|
+
);
|
|
364
|
+
// a struct a callee returns in a register is the local the call assigns, or the function's own
|
|
365
|
+
// return where every return hands that struct back whole
|
|
366
|
+
const calls = liftRegisterStructCalls(
|
|
367
|
+
fn,
|
|
368
|
+
table,
|
|
369
|
+
target,
|
|
370
|
+
own ?? split.ownContextProto,
|
|
371
|
+
ownReturn?.register === true ? ownReturn.type : undefined,
|
|
372
|
+
);
|
|
373
|
+
verify(fn);
|
|
374
|
+
if (ownReturn !== undefined && !calls.handedBack) {
|
|
375
|
+
returnOwnStruct(fn, name, target, ownReturn, notes, deferred, calls.readsCallStruct);
|
|
376
|
+
verify(fn);
|
|
377
|
+
}
|
|
378
|
+
refuseNarrowReturnsOfWideDeclaration(fn, name, own);
|
|
379
|
+
stampDeclaredVolatileUnder(fn, symbols, stampBehaviorsFor(target, contextGlobals));
|
|
380
|
+
return fn;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** THE CONTEXT'S OWN ENTRY BOUNDS THE ARGUMENT REGISTERS, SOFTLY: an argument register past the
|
|
384
|
+
* words it fills (`ownArgumentWordsOn`, the layout a stated entry gets) is lifted as an uninitialised
|
|
385
|
+
* local's home, and where the finished lift still READS one, the function is lifted again with
|
|
386
|
+
* the bound raised past it, so that register stays the parameter a context-less lift makes it,
|
|
387
|
+
* with an `[own-arity]` note naming the declaration and the register.
|
|
388
|
+
*
|
|
389
|
+
* WHY SOFT, where a stated entry is hard. A header entry for a function still in assembly was
|
|
390
|
+
* written with no compiler checking it against a definition, and the two directions of its error
|
|
391
|
+
* cost differently. Too long, it only keeps a parameter. Too short, a hard bound turns a real
|
|
392
|
+
* argument into `uninit_<reg>`, which compiles silently: sbk2's task callbacks are declared
|
|
393
|
+
* `void f(void);` ahead of a definition that reads `a0`, 407 of 525 such headers were short when
|
|
394
|
+
* their functions were decompiled, and a hard bound lifted 16 of 33 sampled ones as `s32
|
|
395
|
+
* uninit_a0; … memcpy(uninit_a0 + 28, …)`. Kept as a parameter instead, the lift is the right
|
|
396
|
+
* function and contradicts the header, which every compiler here rejects as conflicting types —
|
|
397
|
+
* loud — and the note says which register. What the bound buys is the other direction: a
|
|
398
|
+
* register nothing in the lift reads is no parameter (sa3's `void Task_802F698(void)` lifted
|
|
399
|
+
* `(s32 a0, s32 a1, s32 a2, s32 a3)`).
|
|
400
|
+
*
|
|
401
|
+
* "READS" MEANS THE FINISHED LIFT, not the SSA builder. A register is read as the argument of a
|
|
402
|
+
* call whose arity was guessed, and the guess then trims it (`k4(a, b, 2, 0); zq(a); sl();` reads
|
|
403
|
+
* a2/a3 as `zq`'s and `sl`'s possible arguments); so the test is that an undefined value of a
|
|
404
|
+
* dropped register survives into the lift — an `undef` keyed by it, or by a join that took it,
|
|
405
|
+
* that `dce` would not delete, or one the machine homes to a slot nothing reads
|
|
406
|
+
* (`readDroppedArgRegs`). A read the raising tower would delete later still counts: that only
|
|
407
|
+
* keeps the parameter.
|
|
408
|
+
*
|
|
409
|
+
* A LIFT THAT REFUSES WITHIN THE BOUND IS LIFTED WITHOUT IT, with a note, so the bound turns no
|
|
410
|
+
* lift into a decline. The one refusal it adds is a stated entry's (frontend/ssa.ts
|
|
411
|
+
* `LiveInModel.ownArgWords`): an incoming stack argument past it, which no local can be, and which,
|
|
412
|
+
* named by position past the registers the bound dropped, would be one slot low. A stated entry for
|
|
413
|
+
* `name` — `prototypes`, which includes what the symbol map derives — wins, and the context's is
|
|
414
|
+
* not read. */
|
|
415
|
+
function liftWithinContextBound(
|
|
416
|
+
name: string,
|
|
417
|
+
asm: string,
|
|
418
|
+
target: TargetDescription,
|
|
419
|
+
prototypes: Prototypes,
|
|
420
|
+
asmData: AsmData | undefined,
|
|
421
|
+
symbols: SymbolMap | undefined,
|
|
422
|
+
contextGlobals: ContextGlobals | undefined,
|
|
423
|
+
liftOptions: LiftOptions | undefined,
|
|
424
|
+
ownContext: FnProto | undefined,
|
|
425
|
+
notes: string[] | undefined,
|
|
426
|
+
): Fn {
|
|
427
|
+
const frontend = frontendFor(target);
|
|
428
|
+
// THE CONTEXT'S OWN ENTRY RIDES ON EVERY LIFT IT MAY BIND, the stated entry's absence permitting.
|
|
429
|
+
// A lift bounded by exactly its words, or one it fills every register of, may take it as proof
|
|
430
|
+
// (`LiftOptions.contextOwnProto.proves`). Any other lift may only refuse on it: the unbounded
|
|
431
|
+
// relift below, and a bound raised past a register the body reads. Otherwise a decided arity gap
|
|
432
|
+
// (frontend/call-plan.ts `decideGap`) that the entry refuses would come back from the relift as a
|
|
433
|
+
// lift without it. With `int pr(void)` in the context, `logv(s, 1)` would lift `logv(a0, 1)`, the
|
|
434
|
+
// same as with no declaration.
|
|
435
|
+
const context = (proves: boolean): LiftOptions =>
|
|
436
|
+
ownContext === undefined || Object.hasOwn(prototypes, name)
|
|
437
|
+
? {}
|
|
438
|
+
: { contextOwnProto: { proto: ownContext, proves } };
|
|
439
|
+
const unbounded = () =>
|
|
440
|
+
forgetAbsorbedKeys(
|
|
441
|
+
frontend.lift(name, asm, target, prototypes, asmData, symbols, contextGlobals, {
|
|
442
|
+
...liftOptions,
|
|
443
|
+
...context(false),
|
|
444
|
+
}),
|
|
445
|
+
);
|
|
446
|
+
let words =
|
|
447
|
+
ownContext === undefined || Object.hasOwn(prototypes, name)
|
|
448
|
+
? undefined
|
|
449
|
+
: ownArgumentWordsOn(name, ownContext, target);
|
|
450
|
+
if (words === undefined || words >= target.argRegs.length) {
|
|
451
|
+
return words === undefined
|
|
452
|
+
? unbounded()
|
|
453
|
+
: forgetAbsorbedKeys(
|
|
454
|
+
frontend.lift(name, asm, target, prototypes, asmData, symbols, contextGlobals, {
|
|
455
|
+
...liftOptions,
|
|
456
|
+
...context(true),
|
|
457
|
+
}),
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
const declared = spelledDeclaration(name, ownContext!);
|
|
461
|
+
const declaredWords = words;
|
|
462
|
+
for (;;) {
|
|
463
|
+
let fn: Fn;
|
|
464
|
+
try {
|
|
465
|
+
fn = frontend.lift(name, asm, target, prototypes, asmData, symbols, contextGlobals, {
|
|
466
|
+
...liftOptions,
|
|
467
|
+
contextOwnWords: words,
|
|
468
|
+
...context(words === declaredWords),
|
|
469
|
+
});
|
|
470
|
+
} catch (e) {
|
|
471
|
+
if (!(e instanceof FrontendUnsupportedError)) {
|
|
472
|
+
throw e;
|
|
473
|
+
}
|
|
474
|
+
const lifted = unbounded();
|
|
475
|
+
notes?.push(
|
|
476
|
+
`${name}: lifted without its context declaration \`${declared}\`, which it could not be lifted within ` +
|
|
477
|
+
`(${e.message.split('\n')[0]})`,
|
|
478
|
+
);
|
|
479
|
+
return lifted;
|
|
480
|
+
}
|
|
481
|
+
const read = readDroppedArgRegs(fn, target.argRegs.slice(words));
|
|
482
|
+
if (read.length === 0) {
|
|
483
|
+
return forgetAbsorbedKeys(fn);
|
|
484
|
+
}
|
|
485
|
+
notes?.push(
|
|
486
|
+
`${name}: its context declaration \`${declared}\` fills ${words} argument word${words === 1 ? '' : 's'}, ` +
|
|
487
|
+
`and the body reads ${read.join(', ')} past ${words === 1 ? 'it' : 'them'} — kept as ` +
|
|
488
|
+
`${read.length === 1 ? 'a parameter' : 'parameters'}, which the header contradicts`,
|
|
489
|
+
);
|
|
490
|
+
words = Math.max(...read.map((r) => target.argRegs.indexOf(r))) + 1;
|
|
491
|
+
if (words >= target.argRegs.length) {
|
|
492
|
+
return unbounded();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** The dropped argument registers whose undefined value the lift still reads: an `undef` keyed by
|
|
498
|
+
* one, or that absorbed one when its join collapsed (frontend/ssa.ts `ABSORBED_KEYS`), which is
|
|
499
|
+
* not dead (`deadOps`, the definition `dce` deletes by) or which the machine stored to a slot
|
|
500
|
+
* nothing reads back (`HOMED_UNDEF`). That store reaches no op, and it is still a read: IDO -g3 and
|
|
501
|
+
* -O0 home every argument register the definition declares, so a header naming fewer contradicts
|
|
502
|
+
* the asm, and the main lift's `(a0, a1)` is the one that recompiles to the same words. */
|
|
503
|
+
function readDroppedArgRegs(fn: Fn, dropped: readonly string[]): string[] {
|
|
504
|
+
const dead = deadOps(fn.blocks);
|
|
505
|
+
const read = new Set<string>();
|
|
506
|
+
for (const b of fn.blocks) {
|
|
507
|
+
for (const op of b.ops) {
|
|
508
|
+
if (op.opcode === 'undef' && (!dead.has(op) || op.attrs[HOMED_UNDEF] === true)) {
|
|
509
|
+
for (const k of [String(op.attrs.key), ...String(op.attrs[ABSORBED_KEYS] ?? '').split(',')]) {
|
|
510
|
+
read.add(k);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return dropped.filter((r) => read.has(r));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** `fn` with no `undef` carrying `ABSORBED_KEYS` or `HOMED_UNDEF`, which nothing past the own-entry
|
|
519
|
+
* bound reads. */
|
|
520
|
+
function forgetAbsorbedKeys(fn: Fn): Fn {
|
|
521
|
+
for (const b of fn.blocks) {
|
|
522
|
+
for (const op of b.ops) {
|
|
523
|
+
if (op.opcode === 'undef') {
|
|
524
|
+
delete op.attrs[ABSORBED_KEYS];
|
|
525
|
+
delete op.attrs[HOMED_UNDEF];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return fn;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** A declaration as a note spells it: `int f(short, int)`, `void f(void)`, `int f(int, ...)`, and
|
|
533
|
+
* `int f()` for one that states no parameters. */
|
|
534
|
+
function spelledDeclaration(name: string, p: FnProto): string {
|
|
535
|
+
const ret = p.returns ?? (p.returnsVoid === true ? 'void' : 'int');
|
|
536
|
+
const params =
|
|
537
|
+
p.variadicParams !== undefined
|
|
538
|
+
? [...p.variadicParams, '...'].join(', ')
|
|
539
|
+
: Array.isArray(p.params)
|
|
540
|
+
? p.params.length > 0
|
|
541
|
+
? p.params.join(', ')
|
|
542
|
+
: 'void'
|
|
543
|
+
: '';
|
|
544
|
+
return `${ret} ${name}(${params})`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** A FUNCTION THAT ITSELF RETURNS A STRUCT BY VALUE, as its declaration says: a stated entry for
|
|
548
|
+
* `name` (`own`), or else the context's own entry (`ownContext`, proto-context.ts
|
|
549
|
+
* `ownContextEntry`). Where its target's rule (`returnsInMemory`) returns the struct through
|
|
550
|
+
* memory, its caller hands it the storage in the first argument register, so its declared
|
|
551
|
+
* parameters sit one register up, and the lift is the pointer function `s32 *f(s32 *a0, s32 a1)`
|
|
552
|
+
* until `returnOwnStruct` proves that pointer is the storage and lifts the struct return the
|
|
553
|
+
* declaration names. This answers what that proof is handed — the struct laid out, and which
|
|
554
|
+
* declaration said so — and refuses, before any frontend runs, where no lift can be the declared
|
|
555
|
+
* function:
|
|
556
|
+
* - a struct the rule hands back in two registers (mwcc's svr4 eight-byte struct in r3:r4): the
|
|
557
|
+
* lift returns one word, and would drop the second register — on both declarations, since a
|
|
558
|
+
* context's header that says so is the only thing that says the second register is live;
|
|
559
|
+
* - a stated declaration whose struct the rule cannot place, because the target states no rule
|
|
560
|
+
* or nothing lays the struct out, or that names no type a local can be declared with. A context
|
|
561
|
+
* entry that cannot be placed keeps the pointer lift, with a note in `notes`: what it costs is
|
|
562
|
+
* a signature the header contradicts, which the compiler rejects.
|
|
563
|
+
* A struct the rule hands back in one register (agbcc's and mwcc's one-word struct in r0/r3) is
|
|
564
|
+
* that register's word: the plan says so (`register`), and a struct that is all one word-wide
|
|
565
|
+
* integer member is spelled as that member (frontend/own-struct-return.ts
|
|
566
|
+
* `liftOwnRegisterStruct`), where any other one stays the word the lift returns, with a note in
|
|
567
|
+
* `notes` saying why (`returnOwnStruct`). */
|
|
568
|
+
function ownStructReturnThroughMemory(
|
|
569
|
+
name: string,
|
|
570
|
+
target: TargetDescription,
|
|
571
|
+
own: FnProto | undefined,
|
|
572
|
+
ownContext: FnProto | undefined,
|
|
573
|
+
notes: string[] | undefined,
|
|
574
|
+
): OwnStructReturn | undefined {
|
|
575
|
+
const stated = own !== undefined;
|
|
576
|
+
const declared = stated ? own : ownContext;
|
|
577
|
+
if (!declaresAggregateReturn(declared)) {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
const spelled = declared!.returns ?? 'a struct or union';
|
|
581
|
+
const read = returnedStruct(declared!, target);
|
|
582
|
+
if (read === 'register') {
|
|
583
|
+
// A struct wider than a word that comes back in registers is a pair the lift returns one word
|
|
584
|
+
// of, dropping the second register; one of a word or less is that word
|
|
585
|
+
const size = aggregateSize(returnedAggregate(declared!), target)?.size;
|
|
586
|
+
if (size !== undefined && size <= 4) {
|
|
587
|
+
const type = structLocalType(declared!, target);
|
|
588
|
+
return 'refused' in type
|
|
589
|
+
? undefined
|
|
590
|
+
: { type, stated, declaration: spelledDeclaration(name, declared!), register: true };
|
|
591
|
+
}
|
|
592
|
+
if (size !== undefined) {
|
|
593
|
+
const pair =
|
|
594
|
+
`this target hands its ${size} bytes back in two registers — a function that itself returns a struct ` +
|
|
595
|
+
'in a register pair is not modelled';
|
|
596
|
+
if (stated) {
|
|
597
|
+
throw new FrontendUnsupportedError(
|
|
598
|
+
`cannot lift '${name}': it is declared to return ${spelled} by value, and ${pair}`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
throw new FrontendUnsupportedError(
|
|
602
|
+
`cannot lift '${name}': its context declaration says it returns ${spelled} by value, and ${pair} ` +
|
|
603
|
+
`(\`${spelledDeclaration(name, declared!)}\`)`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
if ('refused' in read) {
|
|
609
|
+
const how =
|
|
610
|
+
returnsInMemory(returnedAggregate(declared!), target) === true
|
|
611
|
+
? `this target returns that through a hidden pointer in ${target.argRegs[0]}, and ${read.refused}`
|
|
612
|
+
: target.compilerBehaviors.aggregateReturn === undefined
|
|
613
|
+
? `nothing here says whether this target returns it through a hidden pointer in ${target.argRegs[0]}`
|
|
614
|
+
: `nothing here lays it out, and this target's rule needs its layout to say whether it comes back through a ` +
|
|
615
|
+
`hidden pointer in ${target.argRegs[0]}`;
|
|
616
|
+
if (stated) {
|
|
617
|
+
throw new FrontendUnsupportedError(
|
|
618
|
+
`cannot lift '${name}': it is declared to return ${spelled} by value, and ${how}`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
notes?.push(
|
|
622
|
+
`[own-return] ${name}: its context declaration \`${spelledDeclaration(name, declared!)}\` returns a struct by value, ` +
|
|
623
|
+
`and ${how} — lifted as the machine code's function, which the header contradicts`,
|
|
624
|
+
);
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
return { type: read.type, stated, declaration: spelledDeclaration(name, declared!) };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** The struct a function's own declaration returns by value, and which declaration said so. */
|
|
631
|
+
interface OwnStructReturn {
|
|
632
|
+
type: Extract<IrType, { kind: 'struct' }>;
|
|
633
|
+
stated: boolean;
|
|
634
|
+
declaration: string;
|
|
635
|
+
/** the target hands it back in the return register, not through a hidden pointer */
|
|
636
|
+
register?: true;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** Lifts `fn`'s struct return (frontend/own-struct-return.ts `liftOwnStructReturn`), where the
|
|
640
|
+
* lift proves the hidden pointer is the struct's storage. Where it does not, a STATED declaration
|
|
641
|
+
* refuses — the pointer function contradicts it — and a context's keeps the pointer lift, as the
|
|
642
|
+
* machine code's function, with a note in `notes`: the context's own entry is a header nothing
|
|
643
|
+
* checked against a definition (`liftWithinContextBound` says why that makes it soft). Either way, a
|
|
644
|
+
* refusal the frontend `deferred` because this struct would explain it (agbcc's 8-byte epilogue,
|
|
645
|
+
* `LiftOptions.ownMemoryReturn`) is thrown where the proof fails, since nothing else explains it, and
|
|
646
|
+
* says why the proof failed. */
|
|
647
|
+
function returnOwnStruct(
|
|
648
|
+
fn: Fn,
|
|
649
|
+
name: string,
|
|
650
|
+
target: TargetDescription,
|
|
651
|
+
own: OwnStructReturn,
|
|
652
|
+
notes: string[] | undefined,
|
|
653
|
+
deferred: ((unproved: string) => never) | undefined,
|
|
654
|
+
readsCallStruct: boolean,
|
|
655
|
+
): void {
|
|
656
|
+
if (own.register) {
|
|
657
|
+
// a word the struct may not be all of stays the lift's own return, as the machine has it, and
|
|
658
|
+
// the note says why the declared struct is not what it returns
|
|
659
|
+
const kept = liftOwnRegisterStruct(fn, own.type);
|
|
660
|
+
// … save in a function that reads a struct a call returned (frontend/register-struct-call.ts):
|
|
661
|
+
// the word it returns may be computed from that struct — through a frame slot as well as an
|
|
662
|
+
// op — and the word return would print it as the scalar the declaration contradicts
|
|
663
|
+
if (kept !== undefined && readsCallStruct) {
|
|
664
|
+
throw new FrontendUnsupportedError(
|
|
665
|
+
`cannot lift '${name}': it is declared to return ${own.type.declared ?? 'a struct'} by value, which comes back in ` +
|
|
666
|
+
`${target.returnReg} — ${kept}, and it reads a struct a call returned there, which what it returns may be computed from`,
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (kept !== undefined) {
|
|
670
|
+
notes?.push(
|
|
671
|
+
`[own-return] ${name}: its ${own.stated ? '' : 'context '}declaration \`${own.declaration}\` returns ` +
|
|
672
|
+
`${own.type.declared ?? 'a struct'} by value in ${target.returnReg}, and ${kept} — lifted as the word ` +
|
|
673
|
+
'the machine code returns, which the header contradicts',
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const why = liftOwnStructReturn(fn, own.type, target);
|
|
679
|
+
if (why === undefined) {
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
// a refusal the frontend deferred because this struct would explain it stands where it does not
|
|
683
|
+
deferred?.(why);
|
|
684
|
+
const declared = `${own.type.declared ?? 'a struct'} by value, which this target returns through a hidden pointer in ${target.argRegs[0]}`;
|
|
685
|
+
if (own.stated) {
|
|
686
|
+
throw new FrontendUnsupportedError(
|
|
687
|
+
`cannot lift '${name}': it is declared to return ${own.type.declared ?? 'a struct'} by value, which this target returns ` +
|
|
688
|
+
`through a hidden pointer in ${target.argRegs[0]}, and the lift cannot show that pointer is only its storage: ${why}`,
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
notes?.push(
|
|
692
|
+
`[own-return] ${name}: its context declaration \`${own.declaration}\` returns ${declared}, and the lift cannot show ` +
|
|
693
|
+
`that pointer is only its storage (${why}) — lifted as the machine code's pointer function, which the header contradicts`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** A FUNCTION DECLARED TO RETURN AN INTEGER WIDER THAN A REGISTER, whose lift returns one word or
|
|
698
|
+
* none, refuses — on every frontend, since this reads only the lifted `ret`s.
|
|
699
|
+
*
|
|
700
|
+
* Only the declaration can decide this. A `long long` comes back in a register pair (Thumb r0:r1,
|
|
701
|
+
* O32 v0:v1, PowerPC r3:r4), and where the body never reads the pair's second register back, the
|
|
702
|
+
* asm of `long long f(long long *p, long long *q) { long long x = *p; *q = x; return x; }` is
|
|
703
|
+
* byte-identical to that of the `s32` function storing the same two words (kmc-gcc and gcc 2.7.2
|
|
704
|
+
* compile both to one object). A frontend that builds the pair returns one 64-bit value and
|
|
705
|
+
* passes; one that lowered a word would print `s32 f(...)` against the project's own `long long
|
|
706
|
+
* f(...)` — a candidate the compiler rejects as conflicting types. The frontends' asm rules
|
|
707
|
+
* (Thumb's `refuseWordReturns`, O32's `v1MayHoldLowWord`) stay as the backstop for a function
|
|
708
|
+
* nothing declares, and that byte-identical shape is their blind spot. */
|
|
709
|
+
function refuseNarrowReturnsOfWideDeclaration(fn: Fn, name: string, own: FnProto | undefined): void {
|
|
710
|
+
const width = ownReturnWidth(own);
|
|
711
|
+
if (width === undefined || width <= 32) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
for (const blk of fn.blocks) {
|
|
715
|
+
for (const op of blk.ops) {
|
|
716
|
+
if (op.opcode === 'ret' && !op.operands.some((v) => (intWidth(v.type) ?? 0) > 32)) {
|
|
717
|
+
throw new FrontendUnsupportedError(
|
|
718
|
+
`cannot lift '${name}': its declaration returns a ${width}-bit integer, which comes back as the halves of a ` +
|
|
719
|
+
'64-bit pair, and this lowering returns one word',
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
202
726
|
// ── the shared raising tower ────────────────────────────────────────────────────────────────
|
|
203
727
|
// decompile(), decompileTraced (trace.ts), rank.ts's decompileRanked and the cli's
|
|
204
728
|
// decompileWithReport + its score probe (report.ts) all raise a lifted fn through the SAME stage
|
|
@@ -231,19 +755,20 @@ export interface RaiseHooks {
|
|
|
231
755
|
afterRecover?: () => void;
|
|
232
756
|
/** after return-sinking, only when it changed the fn (fires after its verify) */
|
|
233
757
|
afterRetsink?: () => void;
|
|
234
|
-
/** after
|
|
235
|
-
|
|
758
|
+
/** after each pre-structure fold (raise/pre-structure.ts) that changed the fn (fires after its verify) */
|
|
759
|
+
afterFold?: (fold: PreStructureFold) => void;
|
|
236
760
|
}
|
|
237
761
|
|
|
238
762
|
/** Stages 2.35–3.5 — pre-recovery recognizers (the shared ordered list in raise/pre-recovery.ts)
|
|
239
763
|
* → type recovery (boundary contract: no `unknown` survives) → return-sinking (tail-duplicate a
|
|
240
|
-
* return-only merge so short-circuits emit early returns) →
|
|
241
|
-
*
|
|
764
|
+
* return-only merge so short-circuits emit early returns) → the pre-structure folds
|
|
765
|
+
* (raise/pre-structure.ts: empty latches, exit trampolines, peeled loop tests). `verify` after
|
|
766
|
+
* every pass that changed the IR.
|
|
242
767
|
*
|
|
243
|
-
*
|
|
244
|
-
* nothing across a 3337-function agbcc corpus. Worth saying, because folding an empty block ahead
|
|
768
|
+
* Return-sinking and latch folding look ordered and are not: running latch folding FIRST instead
|
|
769
|
+
* changes nothing across a 3337-function agbcc corpus. Worth saying, because folding an empty block ahead
|
|
245
770
|
* of return-sinking does take away `br` predecessors it needs — the dominance gate is what makes
|
|
246
|
-
* that unreachable, since the blocks retsink wants are never back-edge sources.
|
|
771
|
+
* that unreachable, since the blocks retsink wants are never back-edge sources. The folds go last
|
|
247
772
|
* because that is where the CFG stops moving.
|
|
248
773
|
*
|
|
249
774
|
* The tail is three separate parameters rather than an options bag on purpose: this is the
|
|
@@ -273,6 +798,9 @@ export function raiseRecovered(
|
|
|
273
798
|
pre,
|
|
274
799
|
);
|
|
275
800
|
hooks.beforeRecover?.();
|
|
801
|
+
if (declaresVoidReturn(self)) {
|
|
802
|
+
dropVoidReturnOperands(fn);
|
|
803
|
+
}
|
|
276
804
|
recoverTypes(fn);
|
|
277
805
|
verify(fn);
|
|
278
806
|
assertTypesRecovered(fn);
|
|
@@ -289,9 +817,11 @@ export function raiseRecovered(
|
|
|
289
817
|
verify(fn);
|
|
290
818
|
hooks.afterRetsink?.();
|
|
291
819
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
820
|
+
for (const fold of PRE_STRUCTURE_FOLDS) {
|
|
821
|
+
if (fold.run(fn) > 0) {
|
|
822
|
+
verify(fn);
|
|
823
|
+
hooks.afterFold?.(fold);
|
|
824
|
+
}
|
|
295
825
|
}
|
|
296
826
|
// THE BOUNDARY POSTCONDITION. Above this line passes move the CFG; below it nothing does, and the
|
|
297
827
|
// structurer reads a block parameter as a JOIN — a name it must give a local of its own. A param
|
|
@@ -311,10 +841,35 @@ export function raiseRecovered(
|
|
|
311
841
|
}
|
|
312
842
|
}
|
|
313
843
|
|
|
844
|
+
/** A FUNCTION ITS DECLARATION SAYS RETURNS NOTHING RETURNS NOTHING: each `ret` drops the return
|
|
845
|
+
* register the frontend hands it (frontend/ssa.ts returns that register at every `ret`, whatever
|
|
846
|
+
* the declared type), and the joins only that operand read go with it. Before type recovery, since
|
|
847
|
+
* a join read by nothing else is no value of the program, and recovery would otherwise unify what
|
|
848
|
+
* two paths leave in that register: mwcc's `if (*p == 7) { gHit += 1; break; }` leaves r3 holding
|
|
849
|
+
* gHit's value on one exit and the walked pointer on the other, and the one join typed both as
|
|
850
|
+
* `u8 *` (`gHit = (void *)(v0 + 1)`, which mwcc rejects). The structurer already reads no `ret`
|
|
851
|
+
* operand under `returnsVoid`, so this changes no `return` it writes. */
|
|
852
|
+
function dropVoidReturnOperands(fn: Fn): void {
|
|
853
|
+
let dropped = false;
|
|
854
|
+
for (const blk of fn.blocks) {
|
|
855
|
+
for (const op of blk.ops) {
|
|
856
|
+
if (op.opcode === 'ret' && op.operands.length > 0) {
|
|
857
|
+
op.operands = [];
|
|
858
|
+
dropped = true;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (dropped) {
|
|
863
|
+
pruneDeadParams(fn);
|
|
864
|
+
dce(fn);
|
|
865
|
+
verify(fn);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
314
869
|
/** Run `body`; if it declines, name the unmodelled instructions the function carries.
|
|
315
870
|
*
|
|
316
871
|
* An `opaque` degrades its own value AND makes its block impure, so a shape recognizer refuses:
|
|
317
|
-
*
|
|
872
|
+
* a loop none of whose recognizers takes it declines with "unrecovered back-edge …".
|
|
318
873
|
* True and useless — the shape is fine, an instruction is missing — and the benchmark classifies
|
|
319
874
|
* declines by that text, so the round is filed as a loop-capability gap and the improvement loop
|
|
320
875
|
* builds the wrong thing.
|
|
@@ -331,20 +886,21 @@ function attributeOpaques<T>(fn: Fn, body: () => T): T {
|
|
|
331
886
|
throw e;
|
|
332
887
|
}
|
|
333
888
|
const seen = reachableBlocks(fn);
|
|
334
|
-
|
|
889
|
+
// THE REASONS, not the mnemonics: an `opaque` a recogniser left behind for a runtime helper it
|
|
890
|
+
// could not fold carries no mnemonic at all, and asking for one would attribute the decline to
|
|
891
|
+
// an instruction named `?`.
|
|
892
|
+
const reasons = new Set<string>();
|
|
335
893
|
for (const b of seen) {
|
|
336
894
|
for (const op of b.ops) {
|
|
337
895
|
if (op.opcode === 'opaque') {
|
|
338
|
-
|
|
896
|
+
reasons.add(gapReasonFor(op.attrs));
|
|
339
897
|
}
|
|
340
898
|
}
|
|
341
899
|
}
|
|
342
|
-
if (!
|
|
900
|
+
if (!reasons.size || /unmodelled instruction|no model for the runtime helper/.test(e.message)) {
|
|
343
901
|
throw e;
|
|
344
902
|
}
|
|
345
|
-
|
|
346
|
-
// misses the mnemonic-anchored classes and every attributed decline lands in the generic bucket.
|
|
347
|
-
const list = [...names].sort().map(gapReasonFor).join(', ');
|
|
903
|
+
const list = [...reasons].sort().join(', ');
|
|
348
904
|
throw new StructureError(`${e.message} — and the function carries ${list}, which is the more likely cause`);
|
|
349
905
|
}
|
|
350
906
|
}
|
|
@@ -371,8 +927,9 @@ export function structureChecked(
|
|
|
371
927
|
fn: Fn,
|
|
372
928
|
opts: Parameters<typeof structure>[1],
|
|
373
929
|
hooks?: Parameters<typeof structure>[2],
|
|
930
|
+
memo?: Parameters<typeof structure>[3],
|
|
374
931
|
): SFn {
|
|
375
|
-
const raw = attributeOpaques(fn, () => structure(fn, opts, hooks));
|
|
932
|
+
const raw = attributeOpaques(fn, () => structure(fn, opts, hooks, memo));
|
|
376
933
|
// The boundary contracts run on the pre-DCE tree: the readability pass must never be able to
|
|
377
934
|
// hide a structuring defect by dropping the dead statement that carries it. assertResolved
|
|
378
935
|
// catches an unresolved `?` value; assertDerefsTyped catches an ill-typed deref (e.g. a pointer
|
|
@@ -382,11 +939,14 @@ export function structureChecked(
|
|
|
382
939
|
assertResolved(raw);
|
|
383
940
|
assertDerefsTyped(raw);
|
|
384
941
|
assertLocalsWritten(raw);
|
|
942
|
+
assertPostIncrUnshared(raw);
|
|
385
943
|
assertEffectsPreserved(fn, raw);
|
|
386
944
|
// The hoist moves the deref cast from each `index` node onto the local's initializer, so
|
|
387
945
|
// re-validate deref typing on the rewritten tree.
|
|
388
946
|
const sfn = readabilityRewrites(raw);
|
|
389
947
|
assertDerefsTyped(sfn);
|
|
948
|
+
assertFrameUnionsViewed(sfn);
|
|
949
|
+
assertPostIncrUnshared(sfn);
|
|
390
950
|
// Re-checked after the readability rewrites for the same reason deref typing is: a pass that
|
|
391
951
|
// merges arms or drops statements must not be able to lose or duplicate a call.
|
|
392
952
|
assertEffectsPreserved(fn, sfn);
|
|
@@ -445,8 +1005,10 @@ export function stubResult(name: string, asm: string, backend: LanguageBackend,
|
|
|
445
1005
|
ir: { raw: '', folded: '', recovered: '' },
|
|
446
1006
|
patternHits: 0,
|
|
447
1007
|
diagnostics: [{ stage, reason: msg }],
|
|
448
|
-
// A stub spells no global, so it assumes nothing about one.
|
|
1008
|
+
// A stub spells no global, so it assumes nothing about one and walked to none.
|
|
449
1009
|
assumedSymbols: [],
|
|
1010
|
+
walkedNames: [],
|
|
1011
|
+
ownArityNotes: [],
|
|
450
1012
|
};
|
|
451
1013
|
}
|
|
452
1014
|
|