@asmlift/core 0.7.0 → 0.8.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 +48 -24
- package/package.json +1 -1
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +126 -29
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +207 -230
- package/src/ir/core.ts +62 -3
- package/src/ir/opcodes.ts +9 -0
- package/src/ir/parse.ts +7 -1
- package/src/l3/advance.ts +2 -2
- package/src/l3/argbase.ts +2 -2
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +45 -1
- package/src/l3/basecse.ts +2 -2
- package/src/l3/coalesce.ts +109 -52
- package/src/l3/scopebase.ts +4 -4
- package/src/l3/tailret.ts +70 -0
- package/src/l3/unmerge.ts +2 -2
- package/src/l3/unreduce.ts +2 -1
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +128 -13
- package/src/pipeline.ts +22 -11
- package/src/raise/extscale.ts +5 -2
- package/src/raise/paramwidth.ts +111 -3
- package/src/raise/pre-recovery.ts +11 -1
- package/src/raise/retsink.ts +8 -4
- package/src/raise/tailsink.ts +17 -2
- package/src/rank-declare.ts +17 -9
- package/src/rank.ts +45 -19
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +12 -3
- package/src/structure/switch-recover.ts +1 -1
- package/src/target.ts +224 -14
- package/src/trace.ts +27 -18
- package/src/variation-definitions.ts +52 -2
- package/src/variation-gates.ts +3 -0
- package/src/variation-tokens.ts +1 -0
package/src/raise/retsink.ts
CHANGED
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
//
|
|
240
240
|
// This does NOT recover the boolean-VALUE form `return a && b` — that is shortcircuit.ts's job
|
|
241
241
|
// (the `logic_and`/`logic_or` connective plus agbcc's `(-b|b)>>31` = `b!=0` normalisation).
|
|
242
|
-
import { Block, Fn, Op, Value, defOpMap, isBodyless, mkOp, predecessors, terminator } from '../ir/core';
|
|
242
|
+
import { Block, Fn, Op, Value, defOpMap, fallThroughOf, isBodyless, mkOp, predecessors, terminator } from '../ir/core';
|
|
243
243
|
import { NEGATED_ICMP } from '../ir/opcodes';
|
|
244
244
|
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
245
245
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
@@ -645,9 +645,13 @@ export function sinkReturns(
|
|
|
645
645
|
continue;
|
|
646
646
|
}
|
|
647
647
|
for (const p of brPreds) {
|
|
648
|
-
const
|
|
649
|
-
const sunk = ret.operands.map((o) => args[m.params.indexOf(o)]);
|
|
650
|
-
|
|
648
|
+
const t = p.ops[p.ops.length - 1];
|
|
649
|
+
const sunk = ret.operands.map((o) => t.successors[0].args[m.params.indexOf(o)]);
|
|
650
|
+
// The `br` being replaced carries whether the machine BRANCHED to this epilogue or fell into
|
|
651
|
+
// it, which is what `structure/retspell.ts` reads. Sinking puts the return ON that edge, so
|
|
652
|
+
// the edge's fact travels with it; ask the new return block's in-edges instead and they answer
|
|
653
|
+
// how control reached the statements above the return, not how it reached the epilogue.
|
|
654
|
+
p.ops[p.ops.length - 1] = mkOp('ret', { operands: sunk, attrs: fallThroughOf(t) });
|
|
651
655
|
changed = true;
|
|
652
656
|
}
|
|
653
657
|
// If no predecessor still branches to m (all were unconditional), it is unreachable — drop it.
|
package/src/raise/tailsink.ts
CHANGED
|
@@ -39,7 +39,16 @@
|
|
|
39
39
|
// NO GATE: which copy stays shared is the follow's question, and whether a sunk function is worth
|
|
40
40
|
// a candidate is rank.ts's, asked with the follow's own predicate (`hasDivergentSharedRet`) rather
|
|
41
41
|
// than a copy of it here.
|
|
42
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
type Block,
|
|
44
|
+
type Fn,
|
|
45
|
+
type Value,
|
|
46
|
+
fallThroughOf,
|
|
47
|
+
mkOp,
|
|
48
|
+
predecessors,
|
|
49
|
+
reachableBlocks,
|
|
50
|
+
terminator,
|
|
51
|
+
} from '../ir/core';
|
|
43
52
|
import { simplifyTrivialPhis } from '../ir/simplify';
|
|
44
53
|
|
|
45
54
|
/** One edge that supplies the tail its arguments. `resolve` sends a value the tail reads to the
|
|
@@ -110,7 +119,13 @@ export function sinkStoreTails(fn: Fn): boolean {
|
|
|
110
119
|
const body = tail.ops.slice(0, -1);
|
|
111
120
|
for (const src of sources) {
|
|
112
121
|
const copies = body.map((o) => mkOp('store', { operands: o.operands.map(src.resolve), attrs: { ...o.attrs } }));
|
|
113
|
-
|
|
122
|
+
// Carry the replaced edge's fall-through fact onto the duplicated `ret`, as `raise/retsink.ts`
|
|
123
|
+
// does. Only for a source that branches straight to the tail: seen THROUGH a forwarder the path
|
|
124
|
+
// is several edges and no one of them answers on its own, so the copy is left unmarked and
|
|
125
|
+
// read as a branch — the side that can never delete a return the object needs.
|
|
126
|
+
const t = src.from.ops[src.from.ops.length - 1];
|
|
127
|
+
const attrs = t.successors[0]?.block === tail ? fallThroughOf(t) : {};
|
|
128
|
+
src.from.ops.splice(src.from.ops.length - 1, 1, ...copies, mkOp('ret', { attrs }));
|
|
114
129
|
}
|
|
115
130
|
// By REACHABILITY, not predecessor count: the tail (unless a conditional edge keeps it) and
|
|
116
131
|
// every forwarder on the way are left unreachable, however long the chain, and a forwarder's
|
package/src/rank-declare.ts
CHANGED
|
@@ -118,18 +118,16 @@ const DECL_RESERVED = new Set<string>([
|
|
|
118
118
|
|
|
119
119
|
/** The emitter's own NAME GRAMMAR for storage it invents: parameters `a0, a1, …` (structure.ts
|
|
120
120
|
* names them positionally, so no rename can move one) and coalesced/temp locals `v0…`/`t0…`
|
|
121
|
-
* (structure.ts's `localNames` accepts exactly `/^[vt]\d+$/`). A
|
|
122
|
-
* these names cannot be declared beside the C that spells it — see the refusal in `refsOf`,
|
|
123
|
-
* is the one that kills the spelling rather than the line.
|
|
121
|
+
* (structure.ts's `localNames` accepts exactly `/^[vt]\d+$/`). A global the target names with one
|
|
122
|
+
* of these names cannot be declared beside the C that spells it — see the refusal in `refsOf`,
|
|
123
|
+
* which is the one that kills the spelling rather than the line.
|
|
124
124
|
*
|
|
125
125
|
* Checked as a grammar IN ADDITION to the tree's own bound names, because the collision that
|
|
126
126
|
* matters is the one the tree cannot show: `localNames` DROPS a local whose name a written
|
|
127
127
|
* global already claims, so where the global is stored `tree.locals` is silent about it. The
|
|
128
|
-
* price is refusing a
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* map's own name total is deliberately not quoted — it is a property of the checkouts the sweep
|
|
132
|
-
* ran over rather than of this repo, so nothing here can re-derive it. */
|
|
128
|
+
* price is refusing a global the target names `v3` in a function that never mints one. A map
|
|
129
|
+
* global the target does NOT name pays nothing: Pikmin's map holds `v0` and `v1`, the only
|
|
130
|
+
* grammar names in any vendored map, and no Pikmin row references either. */
|
|
133
131
|
const EMITTER_NAME = /^[avt]\d+$/;
|
|
134
132
|
|
|
135
133
|
/** Why a name the candidate's tree references got NO declaration. Reported rather than silently
|
|
@@ -201,10 +199,13 @@ export function makeRefCollector(ctx: {
|
|
|
201
199
|
accessFacts: ReadonlyMap<string, { width: number; signed: boolean }>;
|
|
202
200
|
/** the project map alone — a name it does NOT know makes the ref a `synthesized` hypothesis */
|
|
203
201
|
mapSymbols: ReadonlyMap<string, SymbolInfo> | undefined;
|
|
202
|
+
/** the globals the TARGET names — the pool/reloc names and the shapes the asm evidences, before
|
|
203
|
+
* the map is unioned in */
|
|
204
|
+
targetNames: ReadonlySet<string>;
|
|
204
205
|
/** reports a refusal at most once per (name, reason); the caller owns the dedup */
|
|
205
206
|
refuse: (name: string, reason: RefusedDeclarationReason) => void;
|
|
206
207
|
}): (tree: SFn) => { symbolRefs?: SymbolRef[] } {
|
|
207
|
-
const { declSymbols, accessFacts, mapSymbols, refuse } = ctx;
|
|
208
|
+
const { declSymbols, accessFacts, mapSymbols, targetNames, refuse } = ctx;
|
|
208
209
|
return (tree: SFn): { symbolRefs?: SymbolRef[] } => {
|
|
209
210
|
// The names THIS tree binds. Computed per tree because the emitter mints local names per
|
|
210
211
|
// spelling — but the test below is NOT `bound` alone, and the difference is a wrong answer.
|
|
@@ -226,6 +227,13 @@ export function makeRefCollector(ctx: {
|
|
|
226
227
|
// project trades nothing for — so the spelling dies here and `respellTree`'s catch reports it.
|
|
227
228
|
// If every spelling of every tree dies, the row declines LOUDLY naming the collision.
|
|
228
229
|
if (bound.has(r.name) || EMITTER_NAME.test(r.name)) {
|
|
230
|
+
// …but only a global the TARGET names can collide. A name that only the MAP supplies is a
|
|
231
|
+
// project global the function never touches, and the tree's `v0` is its own local: Pikmin
|
|
232
|
+
// holds `.sdata` statics named `v0` and `v1`, and refusing them killed every spelling of
|
|
233
|
+
// every function whose emitted C mints those locals.
|
|
234
|
+
if (!targetNames.has(r.name)) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
229
237
|
refuse(r.name, 'emitter-name');
|
|
230
238
|
throw new Error(
|
|
231
239
|
`cannot spell '${tree.name}': the target names a global '${r.name}', which is a name the ` +
|
package/src/rank.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { T } from './ir/types';
|
|
|
34
34
|
import { verify } from './ir/verify';
|
|
35
35
|
import { advancedBases } from './l3/advance';
|
|
36
36
|
import { materializeArgBases } from './l3/argbase';
|
|
37
|
+
import { argCopyCandidates } from './l3/argcopy';
|
|
37
38
|
import type { LanguageBackend, SFn } from './l3/ast';
|
|
38
39
|
import { type BaseKey, admittedBases, hoistBaseLocals } from './l3/basecse';
|
|
39
40
|
import { armDisjointCandidates, coalesceCandidates } from './l3/coalesce';
|
|
@@ -846,14 +847,17 @@ export function enumerateCandidates(
|
|
|
846
847
|
// spelling needs one, and the declaration a candidate spelling `gTbl[i]` cannot compile
|
|
847
848
|
// without), and the project map, which knows more than either.
|
|
848
849
|
const mapSymbols = baseOpts.symbols;
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
...sharedLiftShapes,
|
|
852
|
-
...(mapSymbols ?? []),
|
|
853
|
-
]);
|
|
850
|
+
const targetSymbols = new Map<string, SymbolInfo>([...bareGlobalSymbols(sharedLift), ...sharedLiftShapes]);
|
|
851
|
+
const declSymbols = new Map<string, SymbolInfo>([...targetSymbols, ...(mapSymbols ?? [])]);
|
|
854
852
|
// The four per-enumeration constants named at the seam rather than captured across 60 lines of
|
|
855
853
|
// closure (rank-declare.ts states why they belong on one object).
|
|
856
|
-
const refsOf = makeRefCollector({
|
|
854
|
+
const refsOf = makeRefCollector({
|
|
855
|
+
declSymbols,
|
|
856
|
+
accessFacts,
|
|
857
|
+
mapSymbols,
|
|
858
|
+
targetNames: new Set(targetSymbols.keys()),
|
|
859
|
+
refuse,
|
|
860
|
+
});
|
|
857
861
|
// THE RESPELL SET, as a function whose PARAMETER LIST is the invariant the tree skip below
|
|
858
862
|
// rests on: every source here is a pure function of the structured tree and this call's own
|
|
859
863
|
// constants, so a tree an earlier structure setting already produced can only re-emit sources
|
|
@@ -1254,6 +1258,25 @@ export function enumerateCandidates(
|
|
|
1254
1258
|
const v = regionVolatile();
|
|
1255
1259
|
return v ? volStore(v) : null;
|
|
1256
1260
|
});
|
|
1261
|
+
/** The results of a multi-result variation, under the same guard `respell` gives a
|
|
1262
|
+
* single-result one: a throw costs this variation and nothing else. Run at statement level
|
|
1263
|
+
* instead, a throwing source takes the WHOLE row with it — `reportThrow` never runs, so the
|
|
1264
|
+
* DEFAULT candidate is lost too and the row goes `noncompile`. A multi-result source is a
|
|
1265
|
+
* `for`-loop subject rather than a thunk, which is why the guard is a helper and not a try
|
|
1266
|
+
* inside `respell`. */
|
|
1267
|
+
const candidatesOf = (
|
|
1268
|
+
variations: readonly Variation[],
|
|
1269
|
+
from: () => SFn | null | undefined,
|
|
1270
|
+
resultsOf: (s: SFn) => { merged: string; sfn: SFn }[],
|
|
1271
|
+
): { merged: string; sfn: SFn }[] => {
|
|
1272
|
+
try {
|
|
1273
|
+
const base = from();
|
|
1274
|
+
return base ? resultsOf(base) : [];
|
|
1275
|
+
} catch (e) {
|
|
1276
|
+
reportThrow([...preRespellVariations, ...variations], e);
|
|
1277
|
+
return [];
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1257
1280
|
/** One candidate per result of a multi-result variation, `name` applied to the result's own
|
|
1258
1281
|
* subject after `prefix`. */
|
|
1259
1282
|
const respellEach = (
|
|
@@ -1265,19 +1288,8 @@ export function enumerateCandidates(
|
|
|
1265
1288
|
if (!offeredOn(target, [...prefix, name])) {
|
|
1266
1289
|
return;
|
|
1267
1290
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
const base = from();
|
|
1271
|
-
results = (base ? resultsOf(base) : []).map((c) => ({
|
|
1272
|
-
variations: [...prefix, withSubject(name, c.merged)],
|
|
1273
|
-
sfn: c.sfn,
|
|
1274
|
-
}));
|
|
1275
|
-
} catch (e) {
|
|
1276
|
-
reportThrow([...preRespellVariations, ...prefix, name], e);
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
for (const c of results) {
|
|
1280
|
-
respell(c.variations, () => c.sfn);
|
|
1291
|
+
for (const c of candidatesOf([...prefix, name], from, resultsOf)) {
|
|
1292
|
+
respell([...prefix, withSubject(name, c.merged)], () => c.sfn);
|
|
1281
1293
|
}
|
|
1282
1294
|
};
|
|
1283
1295
|
respellEach(['scopebase'], 'coalesce', () => hoistScopedBases(sfn));
|
|
@@ -1630,6 +1642,20 @@ export function enumerateCandidates(
|
|
|
1630
1642
|
respellEach(variations, 'coalesce', hoist, armDisjointCandidates);
|
|
1631
1643
|
respellEach([...variations, 'volatile'], 'coalesce', volatiles, armDisjointCandidates);
|
|
1632
1644
|
}
|
|
1645
|
+
// `/argcopy` — a pointer parameter copied into a local for ONE region (l3/argcopy.ts): the
|
|
1646
|
+
// copy frees the parameter's incoming register for that region, and every legal region is its
|
|
1647
|
+
// own candidate.
|
|
1648
|
+
//
|
|
1649
|
+
// PAIRED WITH `/coalesce`, because a freed register is worth nothing until something takes it:
|
|
1650
|
+
// on pokeemerald:SetMauvilleOldManLanguage the copy frees r5 and the counter shared between two
|
|
1651
|
+
// switch arms is what moves into it, and neither spelling alone reaches the bytes.
|
|
1652
|
+
if (offeredOn(target, ['argcopy'])) {
|
|
1653
|
+
for (const c of candidatesOf(['argcopy'], () => sfn, argCopyCandidates)) {
|
|
1654
|
+
const copied = withSubject('argcopy', c.merged);
|
|
1655
|
+
respell([copied], () => c.sfn);
|
|
1656
|
+
respellEach([copied], 'coalesce', () => c.sfn);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1633
1659
|
// `/parkfirst` — incoming-argument parks lead the entry prefix (l3/parkfirst.ts): the
|
|
1634
1660
|
// park's `mov` lifts to pure SSA aliasing, so its position is unrecoverable and the
|
|
1635
1661
|
// default order is emission's. Both orders are emitted; the differ referees.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Which `return;` statements the SOURCE wrote, read off the assembly.
|
|
2
|
+
//
|
|
3
|
+
// A void `return;` is a control transfer to the epilogue, and a compiler spells one as an
|
|
4
|
+
// unconditional `b <epilogue>`. So where the epilogue is a block of its OWN — nothing in it but the
|
|
5
|
+
// `ret` — its in-edges are the ways the body ends, and only one kind of arrival is a statement:
|
|
6
|
+
//
|
|
7
|
+
// - an unconditional branch instruction into it → a `return;` the source wrote.
|
|
8
|
+
// - FALLING into it → the body simply running out, which spells
|
|
9
|
+
// nothing.
|
|
10
|
+
// - a conditional branch's own edge → nothing either. Unoptimised code tests into a
|
|
11
|
+
// BODY label and never into the epilogue, so a
|
|
12
|
+
// `bxx` landing there is jump-optimisation having
|
|
13
|
+
// rerouted a branch the source did not write — and
|
|
14
|
+
// by then both spellings are one object anyway.
|
|
15
|
+
//
|
|
16
|
+
// The statement being decided is the one at the END of the body, so a fall-through in-edge normally
|
|
17
|
+
// settles it: that edge is the body running out, and it wrote no `return;`. A branch in-edge
|
|
18
|
+
// alongside it is a `return;`, just not this one — it ends an arm, and `l3/tailret.ts` may delete a
|
|
19
|
+
// return only in TAIL position, so an arm the function continues past keeps its own. An arm that IS
|
|
20
|
+
// in tail position is one the compiler must branch over regardless (the block laid out before the
|
|
21
|
+
// epilogue is the one that falls in, and only one block can be), so its `return;` and its `}`
|
|
22
|
+
// compile to the same instruction. That is what makes the answer safe per BLOCK.
|
|
23
|
+
//
|
|
24
|
+
// ONE KIND OF BRANCH IN-EDGE OVERRULES THE FALLING ONE: a branch out of an EMPTY block. That arm
|
|
25
|
+
// holds nothing but the `return;`, so dropping it does not shorten a statement list, it flips the
|
|
26
|
+
// branch sense — and the two senses are two different objects. Measured on `if (*p & 1) return; …`
|
|
27
|
+
// at agbcc -O0: the sense that writes the `return;` is 40 bytes, `beq` over a `b <epilogue>`; the
|
|
28
|
+
// flipped one is 36, `bne` over the body with no `b` at all. So that `b` is in the object because
|
|
29
|
+
// the source wrote a `return;`, and the falling edge beside it licenses nothing.
|
|
30
|
+
//
|
|
31
|
+
// With no fall-through in-edge, a branch in-edge is the only evidence there is, and it says the
|
|
32
|
+
// source wrote a `return;` — keep it. With neither, nothing reached the epilogue by a written
|
|
33
|
+
// transfer at all and there is nothing to spell. NO in-edge at all says the same thing without
|
|
34
|
+
// needing the epilogue to be a block of its own: a body that never branched anywhere ended by
|
|
35
|
+
// running out. That is the single-block function, which on an OBJECT has no epilogue label to
|
|
36
|
+
// split it — the shape the `.s` path never shows.
|
|
37
|
+
//
|
|
38
|
+
// A `ret` SUNK onto one edge (`raise/retsink.ts`, `raise/tailsink.ts`) carries that edge's own
|
|
39
|
+
// fall-through fact and answers for itself, which beats anything its block's in-edges could say:
|
|
40
|
+
// those are about reaching the statements above the return, not about reaching the epilogue. An
|
|
41
|
+
// epilogue block that ALSO holds statements and did not come from sinking is not asked at all, for
|
|
42
|
+
// the same reason.
|
|
43
|
+
//
|
|
44
|
+
// NOT DECIDED HERE: a `while` whose only exit branches straight to the epilogue. That `b` is the
|
|
45
|
+
// loop's `}`, and the same instruction to the same address is what a trailing `return;` compiles to
|
|
46
|
+
// — the two objects differ only by an empty forwarder block between them, which is a fact about
|
|
47
|
+
// branch chains rather than about return spelling. It is kept, which is the side that can never
|
|
48
|
+
// delete a return the object needs.
|
|
49
|
+
//
|
|
50
|
+
// The in-edges the reading is about are the ways the body ENDS, so only LIVE ones count. The thumb
|
|
51
|
+
// frontend hands over unreachable blocks on purpose, and one laid out just before the epilogue would
|
|
52
|
+
// otherwise contribute a fall-through no execution takes — deleting the `return;` every real edge
|
|
53
|
+
// branched in with.
|
|
54
|
+
//
|
|
55
|
+
// This decides SPELLING only; `l3/tailret.ts` owns whether a marked return is safe to delete.
|
|
56
|
+
import type { Block, Fn } from '../ir/core';
|
|
57
|
+
import { predecessors, reachableBlocks } from '../ir/core';
|
|
58
|
+
|
|
59
|
+
/** The arrival an in-edge stands for: a `return;` the source wrote, or the body running out. */
|
|
60
|
+
const isWrittenBranch = (p: Block): boolean => {
|
|
61
|
+
const term = p.ops[p.ops.length - 1];
|
|
62
|
+
return term.opcode === 'br' && term.attrs.fallthrough !== true;
|
|
63
|
+
};
|
|
64
|
+
const isFallThrough = (p: Block): boolean => {
|
|
65
|
+
const term = p.ops[p.ops.length - 1];
|
|
66
|
+
return term.opcode === 'br' && term.attrs.fallthrough === true;
|
|
67
|
+
};
|
|
68
|
+
/** A branch out of an EMPTY block is an arm with nothing else in it: the `return;` it stands for is
|
|
69
|
+
* the only statement that arm would hold, so it has nowhere else to live and is never dropped. */
|
|
70
|
+
const isBareBranch = (p: Block): boolean => p.ops.length === 1 && isWrittenBranch(p);
|
|
71
|
+
|
|
72
|
+
/** The blocks whose `ret` the assembly shows no `return;` for. */
|
|
73
|
+
export function unspelledEpilogues(fn: Fn): Set<Block> {
|
|
74
|
+
const preds = predecessors(fn);
|
|
75
|
+
const live = reachableBlocks(fn);
|
|
76
|
+
const out = new Set<Block>();
|
|
77
|
+
for (const b of fn.blocks) {
|
|
78
|
+
const term = b.ops[b.ops.length - 1];
|
|
79
|
+
if (term?.opcode !== 'ret') {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const inEdges = (preds.get(b) ?? []).filter((p) => live.has(p));
|
|
83
|
+
// Nothing arrived here from anywhere: the body never left its one block, so it transferred
|
|
84
|
+
// control to no epilogue and spelled no `return;`.
|
|
85
|
+
if (inEdges.length === 0) {
|
|
86
|
+
out.add(b);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const fellIn = inEdges.some(isFallThrough) && !inEdges.some(isBareBranch);
|
|
90
|
+
if (term.attrs.fallthrough === true || (b.ops.length === 1 && (fellIn || !inEdges.some(isWrittenBranch)))) {
|
|
91
|
+
out.add(b);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
@@ -93,6 +93,7 @@ import {
|
|
|
93
93
|
import { makeLoopHazards, sunkCopyOverDroppedUndef, updateWriteSet } from './hazards';
|
|
94
94
|
import { type NaturalLoop, analyzeLoops } from './loops';
|
|
95
95
|
import { type NameMerge, coalesceNames } from './namecoalesce';
|
|
96
|
+
import { unspelledEpilogues } from './retspell';
|
|
96
97
|
import { type ArmExit, makeSwitchRecovery } from './switch-recover';
|
|
97
98
|
|
|
98
99
|
// Lower a constant-offset memory access to its lvalue/rvalue Expr. If the base was recovered as a
|
|
@@ -446,7 +447,7 @@ function spellablePointee(
|
|
|
446
447
|
* materialised — and where the displacement carries it the cast spelling stands.
|
|
447
448
|
*
|
|
448
449
|
* THAT CHANNEL DOES NOT SAY THE SOURCE NAMED THE MEMBER: a HOISTED BASE LOCAL materialises the
|
|
449
|
-
* same constant. Compiled at
|
|
450
|
+
* same constant. Compiled at agbcc's canonical flags against `u8 unk8[6][8]`, all three of
|
|
450
451
|
* `gBlob->unk8[0][i]`, `u8 *p = (u8 *)gBlob->unk8; p[i]` and `u8 *p = (u8 *)gBlob + 8; p[i]` emit
|
|
451
452
|
* the identical `add r1, #0x8` · `add r1, r1, r0` · `ldrb r0, [r1]`, while `*((u8 *)gBlob + 8 + i)`
|
|
452
453
|
* and `((u8 *)gBlob + i)[8]` take the displacement — and the base-local form is a spelling three
|
|
@@ -1406,7 +1407,7 @@ export interface StructureOptions {
|
|
|
1406
1407
|
// and a `switch` over the same values produce the SAME candidate and the differ never sees the
|
|
1407
1408
|
// ladder — there is nothing in the fan for it to prefer.
|
|
1408
1409
|
//
|
|
1409
|
-
// The two spellings are different objects. Compiled at
|
|
1410
|
+
// The two spellings are different objects. Compiled at agbcc's canonical flags the same two-case
|
|
1410
1411
|
// body is 20 bytes either way (0x14, ten Thumb instructions — the pair is committed as
|
|
1411
1412
|
// `corpus/agbcc-sw{frontload,ladder}.s`) and disagrees instruction for instruction: the `switch` emits
|
|
1412
1413
|
// `cmp #0x1e; beq` then `cmp #0x64; bne` — both tests ahead of both bodies, and sorted ASCENDING,
|
|
@@ -4394,6 +4395,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4394
4395
|
// Branch-sense sites, numbered as the walk below first reaches them (`branchSenseFlipSites`).
|
|
4395
4396
|
const senseOrdinal = new Map<number, number>();
|
|
4396
4397
|
|
|
4398
|
+
// Which `return;` statements the SOURCE wrote — the reading, and why it is decidable, live in
|
|
4399
|
+
// `structure/retspell.ts`. `l3/tailret.ts` owns whether a marked return is safe to delete.
|
|
4400
|
+
const unspelledRets = unspelledEpilogues(fn);
|
|
4401
|
+
|
|
4397
4402
|
const structureRegion = (b: Block, stop: Block | null): Stmt[] => {
|
|
4398
4403
|
if (b === stop) {
|
|
4399
4404
|
return [];
|
|
@@ -4456,7 +4461,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4456
4461
|
const term = b.ops[b.ops.length - 1];
|
|
4457
4462
|
if (term.opcode === 'ret') {
|
|
4458
4463
|
// A void function's `bx lr` leaves whatever in r0; suppress that phantom return value.
|
|
4459
|
-
|
|
4464
|
+
const value = returnsVoid || !term.operands.length ? undefined : expr(term.operands[0]);
|
|
4465
|
+
// The `value === undefined` half is a GUARD: `l3/tailret.ts` re-checks it before deleting
|
|
4466
|
+
// anything, so marking a value-carrying return would change no output. The mark claims the asm
|
|
4467
|
+
// shows no `return;` STATEMENT here, which is a claim about a VOID return only.
|
|
4468
|
+
out.push({ k: 'return', value, ...(value === undefined && unspelledRets.has(b) ? { unspelled: true } : {}) });
|
|
4460
4469
|
return out;
|
|
4461
4470
|
}
|
|
4462
4471
|
if (term.opcode === 'br') {
|
|
@@ -900,7 +900,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
900
900
|
// WHY BLOCK IDENTITY IS THE KEY, and not a body-equality one like `sameBareExit` above. agbcc
|
|
901
901
|
// MERGES two written-out copies into one block — target.ts's `switchArmsFollowLayout` note
|
|
902
902
|
// says so from agbcc's own sources, SRCS compiling jump.c — and compiling both directions at
|
|
903
|
-
//
|
|
903
|
+
// agbcc's canonical flags says WHERE the merged block lands: at the last copy's position. So
|
|
904
904
|
// `case 0: A break; case 1: … case 2: A break;` and the grouped arm placed THERE are one
|
|
905
905
|
// object (.text md5 555abb1a), while the grouped arm placed at the first value is not
|
|
906
906
|
// (fe4d7d35). That is why the grouped spelling round-trips rather than merely reading shorter:
|