@asmlift/core 0.4.0 → 0.5.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/package.json +1 -1
- package/src/backend/cfamily.ts +5 -2
- package/src/contracts.ts +166 -2
- package/src/frontend/mips.ts +13 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +18 -7
- package/src/frontend/ssa.ts +249 -5
- package/src/frontend/thumb.ts +1082 -72
- package/src/ir/alias.ts +75 -0
- package/src/ir/opcodes.ts +24 -14
- package/src/l3/argbase.ts +6 -1
- package/src/l3/ast.ts +9 -1
- package/src/l3/basecse.ts +57 -24
- package/src/l3/coalesce.ts +107 -38
- package/src/l3/dce.ts +31 -18
- package/src/l3/gates.ts +67 -0
- package/src/l3/scopebase.ts +11 -7
- package/src/l3/tailmerge.ts +8 -4
- package/src/pipeline.ts +60 -4
- package/src/raise/divpow2.ts +2 -1
- package/src/raise/gvn.ts +16 -6
- package/src/raise/pre-recovery.ts +4 -2
- package/src/raise/retsink.ts +5 -4
- package/src/raise/shortcircuit.ts +3 -5
- package/src/raise/struct-arrays.ts +2 -1
- package/src/raise/structs.ts +29 -1
- package/src/rank.ts +26 -2
- package/src/structure/analysis.ts +168 -123
- package/src/structure/structure.ts +228 -63
- package/src/structure/switch-recover.ts +96 -27
|
@@ -36,9 +36,11 @@
|
|
|
36
36
|
// code): multi-latch headers, irreducible/overlapping loops, conditional `continue`, a `break`
|
|
37
37
|
// whose exit copies would clobber, switch fall-through, and mixed-entry self-loops (a guarded
|
|
38
38
|
// header also entered by a plain br).
|
|
39
|
+
import { type GlobalCell, globalCellOf, mayWriteGlobal } from '../ir/alias';
|
|
39
40
|
import { Block, Fn, Op, Value, defOpMap, successorsOf } from '../ir/core';
|
|
41
|
+
import { EFFECTFUL_OPS } from '../ir/opcodes';
|
|
40
42
|
import { type IrType, T, scalarTypeForAccess, typeEquals } from '../ir/types';
|
|
41
|
-
import { BinOp, Expr, SFn, Stmt, SwitchCase, exprChildren, mapExprChildren, negateCond } from '../l3/ast';
|
|
43
|
+
import { BinOp, Expr, SFn, Stmt, SwitchCase, exprChildren, gapReasonFor, mapExprChildren, negateCond } from '../l3/ast';
|
|
42
44
|
import { exprCType, ptrElemBytes } from '../l3/typing';
|
|
43
45
|
import { returnType } from '../raise/recover';
|
|
44
46
|
import { collectStructs } from '../raise/structs';
|
|
@@ -585,6 +587,13 @@ interface WhileLoopInfo {
|
|
|
585
587
|
body: Set<Block>; // the pure natural-loop body (for in-body vs exit classification)
|
|
586
588
|
}
|
|
587
589
|
|
|
590
|
+
// Opcodes whose NUMBER OF EXECUTIONS is observable. Moving one of these out of a loop changes what
|
|
591
|
+
// the program does — a call that ran per iteration would run once. A `load`/`aload` is deliberately
|
|
592
|
+
// NOT here: it is a pure read, so running it once instead of per-iteration is unobservable as long
|
|
593
|
+
// as it reads the same memory, which is exactly what the existing def→render barrier scan
|
|
594
|
+
// (structure/analysis.ts) already proves before it lets one inline at all.
|
|
595
|
+
const REPEATED_EFFECT = new Set(['call', 'opaque']);
|
|
596
|
+
|
|
588
597
|
// A bottom-tested `do { body } while(cond)`. The header is the body entry (entered before any
|
|
589
598
|
// test); the LATCH holds the loop condition and the single exit. Body = header..latch structured, then
|
|
590
599
|
// the latch's own ops + the loop-update; the latch test is the do-while condition. The condition is
|
|
@@ -629,6 +638,11 @@ export interface StructureOptions {
|
|
|
629
638
|
// read recompiles at the DECLARATION's access width — where that diverges from the asm's load
|
|
630
639
|
// width the honest shift spelling is the one that matches, and the differ referees.
|
|
631
640
|
spellBitfieldMembers?: boolean;
|
|
641
|
+
// Let a read of a named global render at its use across writes that PROVABLY cannot reach it
|
|
642
|
+
// (a store to a different named global), instead of caching it in a local. Off by default;
|
|
643
|
+
// rank.ts enumerates the ON spelling as the `/reread-globals` axis — see analysis.ts
|
|
644
|
+
// AnalyzeOptions for why this is a differ-refereed lever and not a fix.
|
|
645
|
+
rereadGlobals?: boolean;
|
|
632
646
|
// How an unresolvable VALUE degrades (a live `opaque`, an unlowered transient op, a dropped def):
|
|
633
647
|
// "strict" (default) — the `"?"` sentinel, tripping assertResolved at the boundary (loud in
|
|
634
648
|
// the PROCESS);
|
|
@@ -652,6 +666,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
652
666
|
anchorConstCopies = false,
|
|
653
667
|
littleEndian = true,
|
|
654
668
|
spellBitfieldMembers = true,
|
|
669
|
+
rereadGlobals = false,
|
|
655
670
|
onGap = 'strict',
|
|
656
671
|
symbols,
|
|
657
672
|
} = opts;
|
|
@@ -664,6 +679,15 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
664
679
|
const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom, emitPos, memWriteBetween } = analyze(
|
|
665
680
|
fn,
|
|
666
681
|
returnsVoid,
|
|
682
|
+
{
|
|
683
|
+
defs,
|
|
684
|
+
rereadGlobals,
|
|
685
|
+
// the map's own declaration truth: a volatile object's read may not be duplicated or moved
|
|
686
|
+
volatileGlobal: (n) => {
|
|
687
|
+
const si = symbols?.get(n);
|
|
688
|
+
return si?.volatile === true || (si?.layout ?? []).some((f) => f.volatile === true);
|
|
689
|
+
},
|
|
690
|
+
},
|
|
667
691
|
);
|
|
668
692
|
|
|
669
693
|
// SCALAR-vs-AGGREGATE globals: a `gaddr` symbol accessed EXCLUSIVELY at offset 0 is a scalar
|
|
@@ -676,13 +700,68 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
676
700
|
// widths is a union/type-pun, which the downstream struct-layout recovery rejects LOUD
|
|
677
701
|
// ("overlapping fields ... unions not modelled") before this classification is consumed — so a
|
|
678
702
|
// width collision at off-0 declines honestly rather than reaching a wrong bare-`gSym` emission.
|
|
703
|
+
// FRAME-LOCAL OBJECT NAMES (laddr). Minted HERE, not in the frontend, because identifiers live
|
|
704
|
+
// in this layer's namespace: params, locals, every gaddr symbol, and the project's symbol map —
|
|
705
|
+
// none of which the frontend can see. A frontend-chosen `sp0` silently shadowed a project global
|
|
706
|
+
// of the same name. `sp<off>` uniquified with underscores until free; one name per offset.
|
|
707
|
+
const laddrName = (() => {
|
|
708
|
+
const taken = new Set<string>();
|
|
709
|
+
if (symbols) {
|
|
710
|
+
for (const [n] of symbols) {
|
|
711
|
+
taken.add(n);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
for (const b of fn.blocks) {
|
|
715
|
+
for (const op of b.ops) {
|
|
716
|
+
if (op.opcode === 'gaddr') {
|
|
717
|
+
taken.add(op.attrs.sym as string);
|
|
718
|
+
} else if (op.opcode === 'call') {
|
|
719
|
+
// a CALLEE's name is in this namespace too: a function really named sp0 would be
|
|
720
|
+
// shadowed by the minted local, and `sp0()` on a u16 object is a compile error
|
|
721
|
+
taken.add(op.attrs.target as string);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const byOff = new Map<number, string>();
|
|
726
|
+
const names = new Map<Op, string>();
|
|
727
|
+
for (const b of fn.blocks) {
|
|
728
|
+
for (const op of b.ops) {
|
|
729
|
+
if (op.opcode !== 'laddr') {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const off = op.attrs.off as number;
|
|
733
|
+
let n = byOff.get(off);
|
|
734
|
+
if (n === undefined) {
|
|
735
|
+
n = `sp${off}`;
|
|
736
|
+
while (taken.has(n)) {
|
|
737
|
+
n += '_';
|
|
738
|
+
}
|
|
739
|
+
taken.add(n);
|
|
740
|
+
byOff.set(off, n);
|
|
741
|
+
}
|
|
742
|
+
names.set(op, n);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return names;
|
|
746
|
+
})();
|
|
747
|
+
|
|
679
748
|
const scalarGlobals = new Set<string>();
|
|
680
749
|
{
|
|
681
750
|
const offsets = new Map<string, Set<number>>();
|
|
682
751
|
const bumpAgg = (sym: string) => offsets.set(sym, new Set([-1])); // -1 marks "variable index"
|
|
683
752
|
for (const b of fn.blocks) {
|
|
684
753
|
for (const op of b.ops) {
|
|
685
|
-
const gaddrSym = (v: Value) =>
|
|
754
|
+
const gaddrSym = (v: Value) => {
|
|
755
|
+
const dv = defs.get(v);
|
|
756
|
+
// laddr participates identically: `sp0` is scalar-spelled at off 0 and cast-spelled
|
|
757
|
+
// anywhere else, exactly as a global of its shape would be — by its MINTED name
|
|
758
|
+
// (laddrName): the op carries no name attr, that namespace is this layer's
|
|
759
|
+
return dv?.opcode === 'gaddr'
|
|
760
|
+
? (dv.attrs.sym as string)
|
|
761
|
+
: dv?.opcode === 'laddr'
|
|
762
|
+
? (laddrName.get(dv) ?? null)
|
|
763
|
+
: null;
|
|
764
|
+
};
|
|
686
765
|
if (op.opcode === 'load' || op.opcode === 'store') {
|
|
687
766
|
const s = gaddrSym(op.operands[0]);
|
|
688
767
|
if (s) {
|
|
@@ -963,8 +1042,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
963
1042
|
* computes the right address anyway. That leniency is not something to rely on: the Klonoa
|
|
964
1043
|
* project's own build template treats these as fatal, so the row's emitted C does not build
|
|
965
1044
|
* where its author would put it. The cast is the always-valid spelling — the same fallback
|
|
966
|
-
* `bareArrayLead` documents for the indexed form — and it is byte-identical
|
|
967
|
-
*
|
|
1045
|
+
* `bareArrayLead` documents for the indexed form — and it is byte-identical, so no benchmark row
|
|
1046
|
+
* moves either way and the rule that decides it is pinned in test/deref-typing.test.ts instead.
|
|
968
1047
|
*
|
|
969
1048
|
* The test is whether `&gSym`'s rendered type PROVABLY equals the destination's, not whether the
|
|
970
1049
|
* symbol looks like an aggregate. A shape enumeration got this wrong three ways, each a real
|
|
@@ -1351,43 +1430,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1351
1430
|
const absorbedLoads = new Set<Op>();
|
|
1352
1431
|
if (symCtx && littleEndian && spellBitfieldMembers) {
|
|
1353
1432
|
// the (name, byte) of a load's address when it resolves through defs alone — `gaddr` or
|
|
1354
|
-
// `add(gaddr, const)`; anything else (a materialized base, a variable index) declines
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
return { name: d0.attrs.sym as string, byte: off };
|
|
1360
|
-
}
|
|
1361
|
-
if (d0?.opcode === 'add' && d0.operands.length === 2) {
|
|
1362
|
-
for (const [x, y] of [
|
|
1363
|
-
[d0.operands[0], d0.operands[1]],
|
|
1364
|
-
[d0.operands[1], d0.operands[0]],
|
|
1365
|
-
] as const) {
|
|
1366
|
-
const g0 = defs.get(x);
|
|
1367
|
-
const c0 = defs.get(y);
|
|
1368
|
-
if (g0?.opcode === 'gaddr' && c0?.opcode === 'const') {
|
|
1369
|
-
return { name: g0.attrs.sym as string, byte: (c0.attrs.value as number) + off };
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
return null;
|
|
1374
|
-
};
|
|
1433
|
+
// `add(gaddr, const)`; anything else (a materialized base, a variable index) declines. THE
|
|
1434
|
+
// shared L2 disjointness query (ir/alias.ts), which the materialization model consults with
|
|
1435
|
+
// the same rule, so the fold and the model cannot disagree about what a store can reach.
|
|
1436
|
+
const loadTargets = new Map<Op, GlobalCell>();
|
|
1437
|
+
const addrOf = (v: Value, off: number): GlobalCell | null => globalCellOf(defs, v, off);
|
|
1375
1438
|
// A write for the fold's purposes: calls and opaques always; a store/astore unless its base
|
|
1376
|
-
// resolves to a global PROVABLY different from the folded one.
|
|
1377
|
-
|
|
1378
|
-
// two names within a function.)
|
|
1379
|
-
const mayWrite =
|
|
1380
|
-
(sym: string) =>
|
|
1381
|
-
(x: Op): boolean => {
|
|
1382
|
-
if (x.opcode === 'call' || x.opcode === 'opaque') {
|
|
1383
|
-
return true;
|
|
1384
|
-
}
|
|
1385
|
-
if (x.opcode !== 'store' && x.opcode !== 'astore') {
|
|
1386
|
-
return false;
|
|
1387
|
-
}
|
|
1388
|
-
const t = addrOf(x.operands[0], 0);
|
|
1389
|
-
return !(t && t.name !== sym);
|
|
1390
|
-
};
|
|
1439
|
+
// resolves to a global PROVABLY different from the folded one.
|
|
1440
|
+
const mayWrite = (sym: string) => mayWriteGlobal(defs, sym);
|
|
1391
1441
|
for (const blk of fn.blocks) {
|
|
1392
1442
|
for (const op of blk.ops) {
|
|
1393
1443
|
if ((op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
|
|
@@ -1648,6 +1698,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1648
1698
|
if (d.opcode === 'call') {
|
|
1649
1699
|
return { k: 'call', fn: d.attrs.target as string, args: d.operands.map(e) };
|
|
1650
1700
|
}
|
|
1701
|
+
if (d.opcode === 'laddr') {
|
|
1702
|
+
// gaddr's local twin: the address of the frame-local object the Thumb frontend PROVED
|
|
1703
|
+
// (frame-object audit — width/signed are stamped machine facts). The NAME is this layer's:
|
|
1704
|
+
// see laddrName. Renders `&sp0`; the object itself is declared in `locals`.
|
|
1705
|
+
return { k: 'addr', name: laddrName.get(d)! };
|
|
1706
|
+
}
|
|
1651
1707
|
if (d.opcode === 'gaddr') {
|
|
1652
1708
|
// A promoted CODE symbol (frontend `code: true`) is a function pointer stored as an
|
|
1653
1709
|
// integer: spelled `(u32)Name` — the source idiom — never `&Name` (defect G of the
|
|
@@ -1684,7 +1740,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1684
1740
|
);
|
|
1685
1741
|
}
|
|
1686
1742
|
return d.opcode === 'opaque'
|
|
1687
|
-
? mkGap(
|
|
1743
|
+
? mkGap(gapReasonFor(d.attrs.mnemonic), d.operands.map(e))
|
|
1688
1744
|
: mkGap(`no lowering for op '${d.opcode}'`, d.operands.map(e));
|
|
1689
1745
|
};
|
|
1690
1746
|
|
|
@@ -1781,10 +1837,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1781
1837
|
return succ ? argAssignsFor(pred, succ, sub) : [];
|
|
1782
1838
|
};
|
|
1783
1839
|
|
|
1784
|
-
// Side-effecting ops of a block, emitted as statements in program order: memory stores
|
|
1785
|
-
//
|
|
1786
|
-
//
|
|
1787
|
-
// temp here, at its own program position.
|
|
1840
|
+
// Side-effecting ops of a block, emitted as statements in program order: memory stores; any
|
|
1841
|
+
// EFFECTFUL op whose result nothing consumes (a void/discarded call, and an `opaque` standing for
|
|
1842
|
+
// an instruction asmlift could not model); and MATERIALIZED defs — a call/load whose value cannot
|
|
1843
|
+
// soundly render at its use is assigned to its named temp here, at its own program position.
|
|
1788
1844
|
const sideEffects = (b: Block): Stmt[] => {
|
|
1789
1845
|
const out: Stmt[] = [];
|
|
1790
1846
|
for (const op of b.ops) {
|
|
@@ -1827,7 +1883,17 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1827
1883
|
),
|
|
1828
1884
|
value: expr(op.operands[2]),
|
|
1829
1885
|
});
|
|
1830
|
-
} else if (op.opcode
|
|
1886
|
+
} else if (EFFECTFUL_OPS.has(op.opcode) && op.results.length && !useSitesOf.has(op.results[0])) {
|
|
1887
|
+
// An effectful op whose result nobody reads is still an execution. `store`/`astore` have no
|
|
1888
|
+
// result and were handled above, so what reaches here is `call` and `opaque` — and an
|
|
1889
|
+
// `opaque` missing from this walk is an instruction the frontend could not model
|
|
1890
|
+
// disappearing with no diagnostic, which is the one thing this project refuses to do.
|
|
1891
|
+
//
|
|
1892
|
+
// Keyed on EFFECTFUL_OPS rather than the two opcode names: the deciding property is "has an
|
|
1893
|
+
// effect the result does not account for", which is what the flag already means, so the next
|
|
1894
|
+
// op to acquire it needs no edit here. Statement, not expression — `expr` on the result
|
|
1895
|
+
// routes through `lowerDef`, already where `opaque` becomes the gap, so this reuses the SAME
|
|
1896
|
+
// degradation a live opaque gets rather than inventing a second way to be loud.
|
|
1831
1897
|
out.push({ k: 'exprstmt', value: expr(op.results[0]) });
|
|
1832
1898
|
} else if (materialize.has(op) && !absorbedLoads.has(op)) {
|
|
1833
1899
|
// (an absorbed load's every consumer spells a named bitfield read — emitting its temp
|
|
@@ -1888,7 +1954,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1888
1954
|
|
|
1889
1955
|
// ── Regime-A switch recovery (structure/switch-recover.ts): the recognizer's case bodies call
|
|
1890
1956
|
// back into structureRegion, and Regime B (switch_br, below) shares its fall-through predicate.
|
|
1891
|
-
const { recognizeSwitch,
|
|
1957
|
+
const { recognizeSwitch, analyzeArmExit } = makeSwitchRecovery({
|
|
1892
1958
|
fn,
|
|
1893
1959
|
defs,
|
|
1894
1960
|
dom,
|
|
@@ -1926,23 +1992,18 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1926
1992
|
}
|
|
1927
1993
|
// Regime B: a `switch_br` (jump-table dispatch) lowers directly to the `switch` node — scrutinee,
|
|
1928
1994
|
// per-successor case value, last successor = default. Case bodies delegate to structureRegion (as in
|
|
1929
|
-
// Regime A).
|
|
1930
|
-
//
|
|
1995
|
+
// Regime A). Two table slots naming ONE block are one arm carrying both `case` labels; an arm whose
|
|
1996
|
+
// region flows into the NEXT arm is C fall-through (no `break`). Any other shape needs a `goto` and
|
|
1997
|
+
// fails LOUD rather than being duplicated or silently closed.
|
|
1931
1998
|
if (term.opcode === 'switch_br') {
|
|
1932
1999
|
const merge = ipdom.get(b) ?? stop;
|
|
1933
2000
|
const succ = term.successors;
|
|
1934
2001
|
const caseVals = term.attrs.cases as number[];
|
|
1935
|
-
const targets = new Set<Block>(succ.map((s) => s.block));
|
|
1936
|
-
if (caseRegionReachesSibling(targets, b, merge)) {
|
|
1937
|
-
throw new StructureError(
|
|
1938
|
-
`cannot structure '${fn.name}': fall-through between jump-table cases is not yet supported`,
|
|
1939
|
-
);
|
|
1940
|
-
}
|
|
1941
2002
|
// Switch edges CARRY phi args (frontend/ssa.ts appends them terminator-generically) — each
|
|
1942
2003
|
// case/default body must open with its edge's copies, exactly as cond_br edges do; dropping
|
|
1943
2004
|
// them leaves the target's params uninitialized on the switch path. Two case values sharing
|
|
1944
2005
|
// a target must agree on their args (else the copies are ambiguous → loud decline); the
|
|
1945
|
-
// shared body is then
|
|
2006
|
+
// shared body is then emitted ONCE under both labels.
|
|
1946
2007
|
const argsSeen = new Map<Block, Value[]>();
|
|
1947
2008
|
for (const s of succ) {
|
|
1948
2009
|
const prev = argsSeen.get(s.block);
|
|
@@ -1953,16 +2014,79 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1953
2014
|
}
|
|
1954
2015
|
argsSeen.set(s.block, s.args as Value[]);
|
|
1955
2016
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2017
|
+
// Group the case slots by target block, in TABLE order — `case 5: case 6:` is one arm with two
|
|
2018
|
+
// labels, not two copies of one body. Emission order is the array order (load-bearing: see the
|
|
2019
|
+
// l3/ast.ts fall-through note), so the adjacency check below is against this same order.
|
|
2020
|
+
const defEdge = succ[succ.length - 1];
|
|
2021
|
+
const arms: { entry: Block; edge: (typeof succ)[number]; values: number[] }[] = [];
|
|
2022
|
+
const armOf = new Map<Block, (typeof arms)[number]>();
|
|
2023
|
+
succ.slice(0, -1).forEach((s, i) => {
|
|
2024
|
+
let a = armOf.get(s.block);
|
|
2025
|
+
if (!a) {
|
|
2026
|
+
a = { entry: s.block, edge: s, values: [] };
|
|
2027
|
+
armOf.set(s.block, a);
|
|
2028
|
+
arms.push(a);
|
|
2029
|
+
}
|
|
2030
|
+
a.values.push(caseVals[i]);
|
|
2031
|
+
});
|
|
2032
|
+
// The blocks an arm could fall INTO. The default block counts only when it is a block of its
|
|
2033
|
+
// own: when it IS the merge, "the default" is just where the switch ends, and an arm reaching
|
|
2034
|
+
// it is a plain `break`.
|
|
2035
|
+
const siblings = new Set<Block>(arms.map((a) => a.entry));
|
|
2036
|
+
if (defEdge.block !== merge) {
|
|
2037
|
+
siblings.add(defEdge.block);
|
|
2038
|
+
}
|
|
2039
|
+
// ONE emission order for the whole statement: the case arms in table order, then the default —
|
|
2040
|
+
// which is exactly where C puts it. Adjacency is read off this array, so "falls into the next
|
|
2041
|
+
// arm" needs no separate rule for a case that falls into the default (it is the arm after the
|
|
2042
|
+
// last case, and legal C).
|
|
2043
|
+
const emitOrder = [...arms, { entry: defEdge.block, edge: defEdge, values: null as number[] | null }];
|
|
2044
|
+
// Each arm's switch-edge copies, computed ONCE and in emission order: `argAssignsFor` mints
|
|
2045
|
+
// swap-cycle temp names, so calling it twice for one edge burns a temp number and changes the
|
|
2046
|
+
// output (the same reason emitDoWhile reuses its `updates`).
|
|
2047
|
+
const edgeCopies = emitOrder.map((a) => argAssignsFor(b, a.edge));
|
|
2048
|
+
const bodies = emitOrder.map((a, i) => {
|
|
2049
|
+
const exit = analyzeArmExit(a.entry, b, merge, siblings);
|
|
2050
|
+
if (exit.kind === 'unstructurable') {
|
|
2051
|
+
throw new StructureError(`cannot structure '${fn.name}': ${exit.why}`);
|
|
2052
|
+
}
|
|
2053
|
+
const ft = exit.kind === 'fallthrough';
|
|
2054
|
+
const next = emitOrder[i + 1];
|
|
2055
|
+
if (ft && next?.entry !== exit.to) {
|
|
2056
|
+
throw new StructureError(
|
|
2057
|
+
`cannot structure '${fn.name}': ${a.values ? `case ${a.values.join('/')}` : 'the default arm'} falls ` +
|
|
2058
|
+
`through into an arm that is not the next one emitted — C fall-through only reaches the arm below`,
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
// The arm fallen INTO opens with its own switch-edge copies, which are how the dispatch hands
|
|
2062
|
+
// it its block parameters. On the fall-through path those copies would RE-RUN and overwrite
|
|
2063
|
+
// what the falling arm just computed (`case 0: v=7; case 1: v=5;` — the case-0 path calling
|
|
2064
|
+
// with 5). They cannot simply be dropped either: entering that arm by its own case value
|
|
2065
|
+
// needs them. Hoisting them above the switch is possible but not always safe (another arm may
|
|
2066
|
+
// read the same name first), so this shape declines LOUD; recovering it is future work.
|
|
2067
|
+
if (ft && edgeCopies[i + 1].length) {
|
|
2068
|
+
throw new StructureError(
|
|
2069
|
+
`cannot structure '${fn.name}': the case fallen into takes a value from the switch edge, ` +
|
|
2070
|
+
`which the fall-through path would re-run`,
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
return {
|
|
2074
|
+
// A falling-through arm stops AT its successor arm, which then emits that body once under
|
|
2075
|
+
// its own labels; a closed arm runs to the merge as before.
|
|
2076
|
+
body: [...edgeCopies[i], ...structureRegion(a.entry, ft ? exit.to : merge)],
|
|
2077
|
+
fallsThrough: ft,
|
|
2078
|
+
};
|
|
2079
|
+
});
|
|
2080
|
+
const outCases: SwitchCase[] = arms.map((a, i) => ({ values: a.values, ...bodies[i] }));
|
|
2081
|
+
// An EMPTY default arm is not a default at all: it is where the switch ends, which is where
|
|
2082
|
+
// an unmatched scrutinee goes anyway. Emitting the label with nothing under it says nothing
|
|
2083
|
+
// and is not even valid C89 (a label needs a statement).
|
|
2084
|
+
const defBody = bodies[bodies.length - 1].body;
|
|
1961
2085
|
const sw: Stmt = {
|
|
1962
2086
|
k: 'switch',
|
|
1963
2087
|
scrutinee: expr(term.operands[0]),
|
|
1964
2088
|
cases: outCases,
|
|
1965
|
-
|
|
2089
|
+
...(defBody.length ? { default: defBody } : {}),
|
|
1966
2090
|
};
|
|
1967
2091
|
out.push(sw);
|
|
1968
2092
|
if (merge && merge !== stop) {
|
|
@@ -2218,6 +2342,26 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2218
2342
|
`cannot structure '${fn.name}': do-while condition or a post-loop value reads a pre-update loop variable`,
|
|
2219
2343
|
);
|
|
2220
2344
|
}
|
|
2345
|
+
// The exit copies render AFTER the `dowhile` statement, but the analysis judged where each
|
|
2346
|
+
// value they carry may inline as if the copies sat on the latch's terminator — INSIDE the loop.
|
|
2347
|
+
// For a pure def that is only a naming question (the pre-update guard above covers the rest);
|
|
2348
|
+
// for an EFFECTFUL one it moves the effect out of the loop: a call that ran once per iteration
|
|
2349
|
+
// would render once, after it. Nothing can re-place it here — `materialize` was decided before
|
|
2350
|
+
// emission — so decline LOUD rather than emit a plausible loop that calls the wrong number of
|
|
2351
|
+
// times.
|
|
2352
|
+
const movedEffect = exitArgs.find((v) => {
|
|
2353
|
+
const d = defs.get(v);
|
|
2354
|
+
return (
|
|
2355
|
+
d && REPEATED_EFFECT.has(d.opcode) && dw.body.has(opBlock.get(d)!) && !materialize.has(d) && !varName.has(v)
|
|
2356
|
+
);
|
|
2357
|
+
});
|
|
2358
|
+
if (movedEffect) {
|
|
2359
|
+
const d = defs.get(movedEffect)!;
|
|
2360
|
+
throw new StructureError(
|
|
2361
|
+
`cannot structure '${fn.name}': a post-loop value inlines a '${d.opcode}' from inside the loop, ` +
|
|
2362
|
+
`which would move that effect out of it`,
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2221
2365
|
dwActive.add(dw.header);
|
|
2222
2366
|
// structure the header's own block up to the latch. Call structureBlock DIRECTLY (not
|
|
2223
2367
|
// structureRegion): the header is already on `onStack` from the caller's structureRegion, so
|
|
@@ -2264,7 +2408,28 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2264
2408
|
return {
|
|
2265
2409
|
name: fn.name,
|
|
2266
2410
|
params: entry.params.map((p, i) => ({ name: `a${i}`, type: p.type })),
|
|
2267
|
-
locals:
|
|
2411
|
+
locals: [
|
|
2412
|
+
...localNames.map((n) => ({ name: n, type: varType.get(n)! })),
|
|
2413
|
+
// frame-local objects (laddr): declared with EXACTLY the access type the machine used —
|
|
2414
|
+
// the frontend's frame-object audit proved all accesses agree, so this is a fact, not a guess
|
|
2415
|
+
...[
|
|
2416
|
+
...new Map(
|
|
2417
|
+
fn.blocks
|
|
2418
|
+
.flatMap((b) => b.ops)
|
|
2419
|
+
.filter((op) => op.opcode === 'laddr')
|
|
2420
|
+
.map((op) => [
|
|
2421
|
+
laddrName.get(op)!,
|
|
2422
|
+
{
|
|
2423
|
+
name: laddrName.get(op)!,
|
|
2424
|
+
type: T.int((op.attrs.width as number) * 8, op.attrs.signed as boolean),
|
|
2425
|
+
// an ESCAPED address makes every store observable (the DMA hardware reads it);
|
|
2426
|
+
// without volatile, gcc-2.9 deletes a store to a local nothing in-function reads
|
|
2427
|
+
...(op.attrs.volatile === true ? { volatile: true as const } : {}),
|
|
2428
|
+
},
|
|
2429
|
+
]),
|
|
2430
|
+
).values(),
|
|
2431
|
+
],
|
|
2432
|
+
],
|
|
2268
2433
|
...(shapedGlobalTypes.size
|
|
2269
2434
|
? {
|
|
2270
2435
|
globals: [...shapedGlobalTypes]
|
|
@@ -28,10 +28,23 @@ export interface SwitchRecoverDeps {
|
|
|
28
28
|
structureRegion: (b: Block, stop: Block | null) => Stmt[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Where ONE switch arm's region leaves it — the fact that decides whether the arm can be spelled
|
|
32
|
+
* as C at all, and with or without a `break`.
|
|
33
|
+
*
|
|
34
|
+
* - `break` every path out of the arm reaches the switch's merge (or returns / loops
|
|
35
|
+
* inside the arm). The ordinary closed arm.
|
|
36
|
+
* - `fallthrough` every path out leaves into exactly ONE sibling arm's entry: C's fall-through.
|
|
37
|
+
* Only spellable when that sibling is the arm emitted NEXT (the caller checks
|
|
38
|
+
* emission adjacency — see the l3/ast.ts non-neutrality note).
|
|
39
|
+
* - `unstructurable` anything else: two different siblings, or a mix of "into a sibling" and
|
|
40
|
+
* "out to the merge". C needs a `goto` for those, so callers decline LOUD. */
|
|
41
|
+
export type ArmExit = { kind: 'break' } | { kind: 'fallthrough'; to: Block } | { kind: 'unstructurable'; why: string };
|
|
42
|
+
|
|
31
43
|
export interface SwitchRecovery {
|
|
32
44
|
recognizeSwitch: (b: Block, stop: Block | null) => Stmt[] | null;
|
|
33
|
-
/** shared with the Regime-B (`switch_br`) path in structure.ts, which
|
|
34
|
-
|
|
45
|
+
/** shared with the Regime-B (`switch_br`) path in structure.ts, which recovers the fall-through
|
|
46
|
+
* this returns; Regime A only accepts `break` arms and otherwise declines to if-recovery. */
|
|
47
|
+
analyzeArmExit: (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>) => ArmExit;
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
@@ -198,31 +211,83 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
198
211
|
}
|
|
199
212
|
};
|
|
200
213
|
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
// Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
|
|
215
|
+
// sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
|
|
216
|
+
// caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
|
|
217
|
+
// switch whose default block IS the merge (agbcc's usual "the default just leaves") reads as an
|
|
218
|
+
// ordinary `break`, not as falling into the default.
|
|
219
|
+
//
|
|
220
|
+
// Region membership is `dom(blk) ∋ b` as before: a block NOT dominated by the switch is outside
|
|
221
|
+
// this switch's region and is not walked. It IS recorded as an escape, because an arm that can
|
|
222
|
+
// leave sideways does not fall into the next case — but only the fall-through verdict consults
|
|
223
|
+
// that, so no arm that used to be accepted as closed becomes a decline.
|
|
224
|
+
//
|
|
225
|
+
// A CONSEQUENCE, not a hole: a sibling reachable only THROUGH such a block is never seen, so the
|
|
226
|
+
// arm reads as closed and `structureRegion` walks into the sibling's blocks and emits them again
|
|
227
|
+
// under this arm. That is duplication, not a wrong dispatch — the same duplication the structurer
|
|
228
|
+
// already does for any tail two arms share, and how the case bodies agbcc tail-merged are put
|
|
229
|
+
// back. Costly for matching, correct to run.
|
|
230
|
+
const analyzeArmExit = (entry: Block, b: Block, merge: Block | null, siblings: Set<Block>): ArmExit => {
|
|
231
|
+
if (entry === merge) {
|
|
232
|
+
return { kind: 'break' }; // an empty arm (a table slot pointing straight at the switch's end)
|
|
233
|
+
}
|
|
234
|
+
const into = new Set<Block>(); // sibling entries this arm flows into
|
|
235
|
+
let toMerge = false,
|
|
236
|
+
escapes = false;
|
|
237
|
+
const seen = new Set<Block>([entry]);
|
|
238
|
+
const q = [entry];
|
|
239
|
+
while (q.length) {
|
|
240
|
+
const cur = q.pop()!;
|
|
241
|
+
for (const s of successorsOf(cur)) {
|
|
242
|
+
if (s === merge) {
|
|
243
|
+
toMerge = true;
|
|
244
|
+
} else if (s !== entry && siblings.has(s)) {
|
|
245
|
+
into.add(s);
|
|
246
|
+
} else if (s !== entry && !seen.has(s)) {
|
|
247
|
+
if (!dom.get(s)!.has(b)) {
|
|
248
|
+
escapes = true;
|
|
213
249
|
continue;
|
|
214
250
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
if (inRegion(s) && !rseen.has(s)) {
|
|
219
|
-
rseen.add(s);
|
|
220
|
-
q.push(s);
|
|
221
|
-
}
|
|
251
|
+
seen.add(s);
|
|
252
|
+
q.push(s);
|
|
222
253
|
}
|
|
223
254
|
}
|
|
224
255
|
}
|
|
225
|
-
|
|
256
|
+
if (into.size === 0) {
|
|
257
|
+
return { kind: 'break' };
|
|
258
|
+
}
|
|
259
|
+
if (into.size === 1 && !toMerge && !escapes) {
|
|
260
|
+
return { kind: 'fallthrough', to: [...into][0] };
|
|
261
|
+
}
|
|
262
|
+
// Name what is actually missing. These three are different facts, and only the first is a shape
|
|
263
|
+
// C has no spelling for — the other two are asmlift's own limits, so say so rather than blame C.
|
|
264
|
+
const names = () => [...into].map((x) => `#${fn.blocks.indexOf(x)}`).join(', ');
|
|
265
|
+
if (into.size > 1) {
|
|
266
|
+
return {
|
|
267
|
+
kind: 'unstructurable',
|
|
268
|
+
why: `a case body reaches several sibling cases (${names()}) — C fall-through reaches only one, so this needs a goto`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (escapes) {
|
|
272
|
+
return {
|
|
273
|
+
kind: 'unstructurable',
|
|
274
|
+
why: `a case body reaches sibling case ${names()} on one path and, on another, a block the switch does not dominate`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
kind: 'unstructurable',
|
|
279
|
+
// `case 0: if (c) { …; break; } /* fall through */ case 1:` is the C for this, and the reason
|
|
280
|
+
// asmlift cannot write it is its own: `{k:'break'}` is emitted only for the innermost LOOP
|
|
281
|
+
// (l3/ast.ts), never switch-scoped. That is the capability this shape is waiting on.
|
|
282
|
+
why: `a case body reaches sibling case ${names()} on one path and the end of the switch on another — a switch-scoped \`break\` inside a case body is not emitted yet`,
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/** Every arm closed (`break`)? The precondition Regime A needs — it has a behaviourally identical
|
|
287
|
+
* fallback (if-recovery), so it declines on anything else instead of recovering fall-through. */
|
|
288
|
+
const allArmsClosed = (targets: Set<Block>, b: Block, merge: Block | null): boolean => {
|
|
289
|
+
const siblings = new Set([...targets].filter((t) => t !== merge));
|
|
290
|
+
return [...siblings].every((t) => analyzeArmExit(t, b, merge, siblings).kind === 'break');
|
|
226
291
|
};
|
|
227
292
|
|
|
228
293
|
const recognizeSwitch = (b: Block, stop: Block | null): Stmt[] | null => {
|
|
@@ -385,11 +450,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
385
450
|
}
|
|
386
451
|
|
|
387
452
|
// PRE2 (fall-through): only NON-fall-through switches are handled — decline if any case body
|
|
388
|
-
// can reach ANOTHER case body (or
|
|
389
|
-
//
|
|
453
|
+
// can reach ANOTHER case body (or a default that has its own block) while staying inside the
|
|
454
|
+
// region. (The SAME analysis serves the Regime-B path, which RECOVERS the adjacent-sibling
|
|
455
|
+
// case as C fall-through instead of declining; A has if-recovery to fall back on, B does not.)
|
|
390
456
|
const merge = ipdom.get(b) ?? stop;
|
|
391
457
|
const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
|
|
392
|
-
if (
|
|
458
|
+
if (!allArmsClosed(targets, b, merge)) {
|
|
393
459
|
return null;
|
|
394
460
|
}
|
|
395
461
|
|
|
@@ -412,11 +478,14 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
412
478
|
body: structureRegion(blk, merge),
|
|
413
479
|
fallsThrough: false,
|
|
414
480
|
}));
|
|
481
|
+
// An empty default arm is not a default (see the Regime-B note in structure.ts): the label
|
|
482
|
+
// would carry no statement, which says nothing and is not valid C89.
|
|
483
|
+
const defBody = defaultBlk ? structureRegion(defaultBlk, merge) : [];
|
|
415
484
|
const sw: Stmt = {
|
|
416
485
|
k: 'switch',
|
|
417
486
|
scrutinee: scrutExpr,
|
|
418
487
|
cases: outCases,
|
|
419
|
-
...(
|
|
488
|
+
...(defBody.length ? { default: defBody } : {}),
|
|
420
489
|
};
|
|
421
490
|
const out: Stmt[] = [sw];
|
|
422
491
|
if (merge && merge !== stop) {
|
|
@@ -424,5 +493,5 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
424
493
|
}
|
|
425
494
|
return out;
|
|
426
495
|
};
|
|
427
|
-
return { recognizeSwitch,
|
|
496
|
+
return { recognizeSwitch, analyzeArmExit };
|
|
428
497
|
}
|