@asmlift/core 0.3.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/README.md +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +130 -4
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +181 -4
- package/src/declare.ts +35 -9
- package/src/frontend/mips.ts +37 -29
- package/src/frontend/opaque.ts +70 -20
- package/src/frontend/ppc.ts +18 -7
- package/src/frontend/ssa.ts +279 -56
- package/src/frontend/thumb.ts +1372 -87
- package/src/ir/alias.ts +75 -0
- package/src/ir/opcodes.ts +57 -3
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +221 -0
- package/src/l3/ast.ts +127 -5
- package/src/l3/basecse.ts +58 -62
- package/src/l3/coalesce.ts +215 -0
- package/src/l3/dce.ts +33 -41
- package/src/l3/gates.ts +67 -0
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +440 -0
- package/src/l3/tailmerge.ts +124 -0
- package/src/macros.ts +222 -13
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +65 -6
- package/src/raise/divpow2.ts +227 -0
- package/src/raise/gvn.ts +151 -0
- package/src/raise/pre-recovery.ts +39 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +37 -7
- package/src/raise/shortcircuit.ts +262 -22
- package/src/raise/struct-arrays.ts +2 -1
- package/src/raise/structs.ts +41 -3
- package/src/rank.ts +196 -20
- package/src/structure/analysis.ts +175 -89
- package/src/structure/structure.ts +588 -55
- package/src/structure/switch-recover.ts +117 -30
- package/src/symbols.ts +128 -13
- package/src/target.ts +4 -2
- package/src/trace.ts +9 -0
|
@@ -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 } 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';
|
|
@@ -46,9 +48,13 @@ import {
|
|
|
46
48
|
type DeclaredField,
|
|
47
49
|
type SymbolInfo,
|
|
48
50
|
type SymbolStructField,
|
|
51
|
+
arrayInnerExtents,
|
|
49
52
|
declaredFields,
|
|
50
53
|
isArrayField,
|
|
54
|
+
isBitfieldField,
|
|
55
|
+
isScalarCellSize,
|
|
51
56
|
pointeeFields,
|
|
57
|
+
scalarCellType,
|
|
52
58
|
} from '../symbols';
|
|
53
59
|
import { analyze } from './analysis';
|
|
54
60
|
import { makeLoopHazards, updateWriteSet } from './hazards';
|
|
@@ -108,6 +114,32 @@ function globalOf(e: Expr, width: number): { name: string; idx: Expr } | null {
|
|
|
108
114
|
return null;
|
|
109
115
|
}
|
|
110
116
|
|
|
117
|
+
// THE one gate on the BARE-NAME array-global spelling (`gSym[i]` rather than `((T *)&gSym)[i]`),
|
|
118
|
+
// shared by the constant-offset and variable-index access paths so the two cannot disagree.
|
|
119
|
+
// Returns the `index` node's `lead` fragment when the bare form is spellable, or null to fall
|
|
120
|
+
// through to the always-valid `&gSym` cast form.
|
|
121
|
+
//
|
|
122
|
+
// Two facts are required, not one. The element WIDTH must match, as it always has. And the RANK
|
|
123
|
+
// must be SPELLABLE, because one subscript reaches an element only on a rank-1 array: on `u16
|
|
124
|
+
// g[4][0x400]`, `g[i]` is a ROW. Against the project's own header that is usually a type error,
|
|
125
|
+
// but where the row address flows into an integer context it is merely a warning and the emitted C
|
|
126
|
+
// then addresses a different object than the asm did — silently.
|
|
127
|
+
//
|
|
128
|
+
// A rank > 1 pins the leading dimensions at 0 and puts the whole flat element index in the last
|
|
129
|
+
// subscript (`g[0][i]`) — the same address arithmetic, and the idiom decomp sources themselves use
|
|
130
|
+
// when the split is not observable in the asm either (`gBgTilemapBufs[0][…]` in kleod,
|
|
131
|
+
// `gNatureStatTable[nature][…]` in pokeemerald). A rank the map states but cannot spell (an unknown
|
|
132
|
+
// inner extent) gets no bare form at all; `((T *)&gSym)[i]` is byte-identical and valid under ANY
|
|
133
|
+
// declaration, which is why it is the safe fallback. See symbols.ts arrayInnerExtents for why an
|
|
134
|
+
// ABSENT rank is read as 1 rather than as unknown.
|
|
135
|
+
function bareArrayLead(si: SymbolInfo, width: number): { lead?: number[] } | null {
|
|
136
|
+
if (si.shape !== 'array' || si.elemSize !== width) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const inner = arrayInnerExtents(si);
|
|
140
|
+
return inner === null ? null : inner.length === 0 ? {} : { lead: new Array<number>(inner.length).fill(0) };
|
|
141
|
+
}
|
|
142
|
+
|
|
111
143
|
// A BYTE residual read as an ELEMENT index of `elemSize`-wide elements, or null when it is not one
|
|
112
144
|
// — the residual then addresses mid-element and no whole-element spelling can express it, so the
|
|
113
145
|
// caller falls through to the honest cast forms. THE one copy of the rule, indexing the
|
|
@@ -305,8 +337,10 @@ function pointeeAccess(
|
|
|
305
337
|
// Constant offset: the member must match EXACTLY — offset, read width, and the SPELLED type
|
|
306
338
|
// (spellsAccessType). An ARRAY member is excluded whatever its size: `u8 x[1]` would match a
|
|
307
339
|
// byte access by (offset, size) and spell `->x`, which is not an lvalue of that width at all.
|
|
340
|
+
// A BITFIELD member likewise: its `size` is the byte span its bits touch, so a 7-bit field
|
|
341
|
+
// would match a plain u16 read and spell a 7-bit lvalue for a 16-bit access.
|
|
308
342
|
const p = spellablePointee(pg.name, sym);
|
|
309
|
-
const f = p?.fields.find((m) => m.offset === total && m.size === width && !isArrayField(m));
|
|
343
|
+
const f = p?.fields.find((m) => m.offset === total && m.size === width && !isArrayField(m) && !isBitfieldField(m));
|
|
310
344
|
return p && f && spellsAccessType(f.signed, width, signed) && memberQualsAllow(f, p.const, isStore)
|
|
311
345
|
? { k: 'field', base: { k: 'var', name: pg.name }, name: f.name }
|
|
312
346
|
: null;
|
|
@@ -363,8 +397,12 @@ function memAccess(
|
|
|
363
397
|
// declaration on: a layout it declines whole is a layout with no nameable members, and a
|
|
364
398
|
// union alias it drops for the first view at that offset is a name no declaration carries.
|
|
365
399
|
// An ARRAY member is excluded for the same reason as in pointeeAccess: `u8 x[1]` would match
|
|
366
|
-
// a byte access by (offset, size) and spell `.x`, which is not an lvalue of that width.
|
|
367
|
-
|
|
400
|
+
// a byte access by (offset, size) and spell `.x`, which is not an lvalue of that width. A
|
|
401
|
+
// BITFIELD member likewise — a plain read of its bytes is not a read of its bits (the named
|
|
402
|
+
// bitfield spelling has its own recognizer, on the extract shape: see lowerDef).
|
|
403
|
+
const fld = declaredFields(si.layout)?.find(
|
|
404
|
+
(f) => f.offset === gb.byte && f.size === width && !isArrayField(f) && !isBitfieldField(f),
|
|
405
|
+
);
|
|
368
406
|
if (fld && memberQualsAllow(fld, si.const, isStore)) {
|
|
369
407
|
return { k: 'field', base: { k: 'var', name: gb.name }, name: fld.name, dot: true };
|
|
370
408
|
}
|
|
@@ -402,9 +440,10 @@ function memAccess(
|
|
|
402
440
|
// dogfood proved agbcc needs for ROM tables — with the element type registered in the env
|
|
403
441
|
// so the stride check passes and no cast is added. Element-width match only.
|
|
404
442
|
const siArr = sym?.info(g.name);
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
443
|
+
const lead = siArr === undefined ? null : bareArrayLead(siArr, width);
|
|
444
|
+
if (lead !== null) {
|
|
445
|
+
sym!.noteGlobal(g.name, T.ptr(T.int(width * 8, siArr!.elemSigned ?? false)));
|
|
446
|
+
return { k: 'index', base: { k: 'var', name: g.name }, idx, width, signed, ...lead };
|
|
408
447
|
}
|
|
409
448
|
return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed };
|
|
410
449
|
}
|
|
@@ -447,9 +486,10 @@ function arrayAccess(
|
|
|
447
486
|
if (baseExpr.k === 'addr' && fieldOff === undefined) {
|
|
448
487
|
// ARRAY-declared global (symbol map): the bare-name spelling, same rule as memAccess.
|
|
449
488
|
const si = sym?.info(baseExpr.name);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
489
|
+
const lead = si === undefined ? null : bareArrayLead(si, elemSize);
|
|
490
|
+
if (lead !== null) {
|
|
491
|
+
sym!.noteGlobal(baseExpr.name, T.ptr(T.int(elemSize * 8, si!.elemSigned ?? false)));
|
|
492
|
+
return { k: 'index', base: { k: 'var', name: baseExpr.name }, idx: idxExpr, width: elemSize, signed, ...lead };
|
|
453
493
|
}
|
|
454
494
|
return { k: 'index', base: baseExpr, idx: idxExpr, width: elemSize, signed };
|
|
455
495
|
}
|
|
@@ -520,12 +560,11 @@ const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
|
520
560
|
and: '&',
|
|
521
561
|
xor: '^',
|
|
522
562
|
shl: '<<',
|
|
523
|
-
shr_u: '
|
|
563
|
+
shr_u: '>>>', // the LOGICAL right shift; the C backend spells it `>>` over an unsigned operand
|
|
524
564
|
shr_s: '>>',
|
|
525
565
|
logic_and: '&&',
|
|
526
566
|
logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
|
|
527
567
|
};
|
|
528
|
-
const NEGATE: Record<string, BinOp> = { '<': '>=', '>=': '<', '>': '<=', '<=': '>', '==': '!=', '!=': '==' };
|
|
529
568
|
|
|
530
569
|
// Recovered info for a self-loop header: its exit block and the per-parameter back-edge
|
|
531
570
|
// arg it feeds (the value on the header→header edge). The back-edge arg is the "next"
|
|
@@ -548,6 +587,13 @@ interface WhileLoopInfo {
|
|
|
548
587
|
body: Set<Block>; // the pure natural-loop body (for in-body vs exit classification)
|
|
549
588
|
}
|
|
550
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
|
+
|
|
551
597
|
// A bottom-tested `do { body } while(cond)`. The header is the body entry (entered before any
|
|
552
598
|
// test); the LATCH holds the loop condition and the single exit. Body = header..latch structured, then
|
|
553
599
|
// the latch's own ops + the loop-update; the latch test is the do-while condition. The condition is
|
|
@@ -577,6 +623,26 @@ export interface StructureOptions {
|
|
|
577
623
|
// body). GCC freely uses `!=`; IDO prefers `==`/`<`. A per-compiler DATA lever, not an `arch ==`
|
|
578
624
|
// branch — default true (permissive; the decline path keeps it sound either way).
|
|
579
625
|
switchAllowsNeqCase?: boolean;
|
|
626
|
+
// Anchor a constant merge copy at its const op's ORIGINAL position instead of at the CFG edge:
|
|
627
|
+
// `movs r9, #0` at entry ahead of a single-armed overwrite emits as a pre-initialization above
|
|
628
|
+
// the `if`, not as its else-arm. A differ-refereed candidate axis (rank.ts `/defsite`), never a
|
|
629
|
+
// default — see the refusal conditions where it is computed.
|
|
630
|
+
anchorConstCopies?: boolean;
|
|
631
|
+
// HARDWARE fact from TargetDescription.capabilities.endianness, threaded by structureOptionsFor:
|
|
632
|
+
// the bitfield extract recognizer solves an LSB-first equation, so it only runs on little-endian
|
|
633
|
+
// data. The provider already refuses to EMIT bitfield facts for a big-endian ELF; this is the
|
|
634
|
+
// same boundary enforced on core's side, against a hand-built map that never went through it.
|
|
635
|
+
littleEndian?: boolean;
|
|
636
|
+
// Spell `(x << a) >> b` extracts of a struct global as the map's named bitfield member. On by
|
|
637
|
+
// default; rank.ts enumerates the OFF spelling as the `/no-bitfield` axis, because the named
|
|
638
|
+
// read recompiles at the DECLARATION's access width — where that diverges from the asm's load
|
|
639
|
+
// width the honest shift spelling is the one that matches, and the differ referees.
|
|
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;
|
|
580
646
|
// How an unresolvable VALUE degrades (a live `opaque`, an unlowered transient op, a dropped def):
|
|
581
647
|
// "strict" (default) — the `"?"` sentinel, tripping assertResolved at the boundary (loud in
|
|
582
648
|
// the PROCESS);
|
|
@@ -597,6 +663,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
597
663
|
preserveDivergentBranchSense = true,
|
|
598
664
|
orderArgCopiesByComputation = true,
|
|
599
665
|
switchAllowsNeqCase = true,
|
|
666
|
+
anchorConstCopies = false,
|
|
667
|
+
littleEndian = true,
|
|
668
|
+
spellBitfieldMembers = true,
|
|
669
|
+
rereadGlobals = false,
|
|
600
670
|
onGap = 'strict',
|
|
601
671
|
symbols,
|
|
602
672
|
} = opts;
|
|
@@ -606,7 +676,19 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
606
676
|
const dom = dominators(fn);
|
|
607
677
|
|
|
608
678
|
// ── analysis phase (structure/analysis.ts): use registry, liveness, materialization ──
|
|
609
|
-
const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom } = analyze(
|
|
679
|
+
const { useSitesOf, opIndex, opBlock, liveIn, materialize, reachFrom, emitPos, memWriteBetween } = analyze(
|
|
680
|
+
fn,
|
|
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
|
+
},
|
|
691
|
+
);
|
|
610
692
|
|
|
611
693
|
// SCALAR-vs-AGGREGATE globals: a `gaddr` symbol accessed EXCLUSIVELY at offset 0 is a scalar
|
|
612
694
|
// global → the bare name `gSym` (byte-exact, matches the source). A symbol accessed at any
|
|
@@ -618,13 +700,68 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
618
700
|
// widths is a union/type-pun, which the downstream struct-layout recovery rejects LOUD
|
|
619
701
|
// ("overlapping fields ... unions not modelled") before this classification is consumed — so a
|
|
620
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
|
+
|
|
621
748
|
const scalarGlobals = new Set<string>();
|
|
622
749
|
{
|
|
623
750
|
const offsets = new Map<string, Set<number>>();
|
|
624
751
|
const bumpAgg = (sym: string) => offsets.set(sym, new Set([-1])); // -1 marks "variable index"
|
|
625
752
|
for (const b of fn.blocks) {
|
|
626
753
|
for (const op of b.ops) {
|
|
627
|
-
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
|
+
};
|
|
628
765
|
if (op.opcode === 'load' || op.opcode === 'store') {
|
|
629
766
|
const s = gaddrSym(op.operands[0]);
|
|
630
767
|
if (s) {
|
|
@@ -895,6 +1032,50 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
895
1032
|
// The C static type of a rendered expression, over the declared variable types — what decides
|
|
896
1033
|
// whether a memory access's base may be dereferenced as spelled (memAccess/arrayAccess).
|
|
897
1034
|
const ctype = (e0: Expr): IrType | undefined => exprCType(e0, (n) => varType.get(n));
|
|
1035
|
+
|
|
1036
|
+
/** `&gSym` assigned to a `T *` local: the address of an AGGREGATE is not a pointer to its
|
|
1037
|
+
* element. `&gArr` is `T (*)[n]`, `&gStruct` is `struct S *`, and neither is assignable to
|
|
1038
|
+
* `T *` — yet the IR's `gaddr` value legitimately has type `T *`, because that is what the asm
|
|
1039
|
+
* loaded. The bare spelling therefore states a type the project's own header contradicts.
|
|
1040
|
+
*
|
|
1041
|
+
* It survived because agbcc only WARNS ("assignment from incompatible pointer type") and
|
|
1042
|
+
* computes the right address anyway. That leniency is not something to rely on: the Klonoa
|
|
1043
|
+
* project's own build template treats these as fatal, so the row's emitted C does not build
|
|
1044
|
+
* where its author would put it. The cast is the always-valid spelling — the same fallback
|
|
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.
|
|
1047
|
+
*
|
|
1048
|
+
* The test is whether `&gSym`'s rendered type PROVABLY equals the destination's, not whether the
|
|
1049
|
+
* symbol looks like an aggregate. A shape enumeration got this wrong three ways, each a real
|
|
1050
|
+
* miss: `shape:'pointer'` declares a pointer cell (`void *gSym`, or `struct Tag *gSym` when the
|
|
1051
|
+
* pointee has a declarable layout), so `&gSym` is a pointer-to-pointer either way; a `shape:'scalar'`
|
|
1052
|
+
* whose width differs from the destination's pointee gives `s32 *` for a `u16 *` slot; and a
|
|
1053
|
+
* NAME-ONLY symbol is synthesized as `extern u32 gSym;` (declare.ts), which is `u32 *` — not the
|
|
1054
|
+
* `T *` the older comment here claimed. So the default is to CAST, and the cast is omitted only
|
|
1055
|
+
* where the declared cell type is known and matches exactly. Byte-identical either way, so the
|
|
1056
|
+
* cost of casting one time too many is a redundant `(T *)`, never a wrong address. */
|
|
1057
|
+
const castAggregateAddr = (name: string, value: Expr): Expr => {
|
|
1058
|
+
const t = varType.get(name);
|
|
1059
|
+
if (t?.kind !== 'ptr' || value.k !== 'addr') {
|
|
1060
|
+
return value;
|
|
1061
|
+
}
|
|
1062
|
+
// The only provably-redundant case: a NON-VOLATILE scalar cell whose DECLARED type is the
|
|
1063
|
+
// destination's pointee, where `&gSym` already denotes exactly `T *`.
|
|
1064
|
+
//
|
|
1065
|
+
// `scalarCellType` and not `scalarTypeForAccess`: the latter answers what an ACCESS of that
|
|
1066
|
+
// width reads and collapses every 4-byte access to `s32`, so it called a `u32` cell equal to an
|
|
1067
|
+
// `s32 *` destination and let the incompatible assignment through. And a `volatile` cell makes
|
|
1068
|
+
// `&gSym` a `volatile T *`, so omitting the cast would DISCARD the qualifier — the same class of
|
|
1069
|
+
// fatal-under-a-strict-build defect this rule exists to remove.
|
|
1070
|
+
const si = symCtx?.info(value.name);
|
|
1071
|
+
if (si?.shape === 'scalar' && !si.volatile && isScalarCellSize(si.size)) {
|
|
1072
|
+
if (typeEquals(scalarCellType(si.size, si.signed), t.to)) {
|
|
1073
|
+
return value;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return { k: 'cast', to: t, e: value };
|
|
1077
|
+
};
|
|
1078
|
+
|
|
898
1079
|
let fresh = 0;
|
|
899
1080
|
// Materialized defs are named FIRST: the temp is the register the compiler held the
|
|
900
1081
|
// value in, so downstream coalescing (loop inits, merge params) may adopt it — subject to the
|
|
@@ -1101,6 +1282,227 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1101
1282
|
}
|
|
1102
1283
|
}
|
|
1103
1284
|
|
|
1285
|
+
// ── def-site anchoring of constant merge copies (anchorConstCopies) ──────────────────────────
|
|
1286
|
+
// An edge copy `v = K` places the constant where the EDGE is, but the asm often materialized K
|
|
1287
|
+
// earlier: `movs r9, #0` at entry ahead of a single-armed overwrite, `movs r5, #1` at the top
|
|
1288
|
+
// of an arm ahead of a nested if. Anchoring the copy at the const op's own program position
|
|
1289
|
+
// reproduces that placement — the write is emitted as a statement there (sideEffects reads
|
|
1290
|
+
// `anchoredAt`) and the edge copies it replaces are suppressed (argAssignsFor reads
|
|
1291
|
+
// `suppressedArgs`). Where the surviving arm then empties, mkIf's empty-then peephole yields
|
|
1292
|
+
// the single-armed positive `if` the source wrote.
|
|
1293
|
+
//
|
|
1294
|
+
// REFUSAL CONDITIONS — each keeps the edge placement, never producing a different write:
|
|
1295
|
+
// - the arg is not an UNNAMED `const` op (only a rematerializable constant carries
|
|
1296
|
+
// unambiguous placement evidence; a named value's position is its materialized def's);
|
|
1297
|
+
// - the merge is a loop header (loop copies have their own placement discipline);
|
|
1298
|
+
// - the const's block does not dominate every edge source passing it (the anchored write
|
|
1299
|
+
// must precede the edge on every path);
|
|
1300
|
+
// - the const's block or any edge source sits inside ANY loop. Block-level dominance does
|
|
1301
|
+
// not give per-ITERATION precedence — a path may pass the def in iteration 1 and take the
|
|
1302
|
+
// suppressed edge in iteration 2 with the variable overwritten in between, the /preinit
|
|
1303
|
+
// sticky-arm failure class (PR #13) — so in-loop shapes are declined outright;
|
|
1304
|
+
// - the merge variable names any OTHER SSA value (a shared name has readers and writers
|
|
1305
|
+
// between the def site and the edge that edge placement respects and anchoring would not);
|
|
1306
|
+
// - another anchored const of the same variable lies on a path from this one to this one's
|
|
1307
|
+
// edge (the later write would clobber this arg's value; both stay at their edges instead).
|
|
1308
|
+
const anchoredAt = new Map<Op, { name: string; arg: Value }[]>();
|
|
1309
|
+
const suppressedArgs = new Map<object, Set<number>>();
|
|
1310
|
+
if (anchorConstCopies) {
|
|
1311
|
+
const nameCount = new Map<string, number>();
|
|
1312
|
+
for (const n of varName.values()) {
|
|
1313
|
+
nameCount.set(n, (nameCount.get(n) ?? 0) + 1);
|
|
1314
|
+
}
|
|
1315
|
+
const inLoop = (b: Block): boolean => {
|
|
1316
|
+
for (const nl of forest.byHeader.values()) {
|
|
1317
|
+
if (nl.body.has(b)) {
|
|
1318
|
+
return true;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return false;
|
|
1322
|
+
};
|
|
1323
|
+
// conservative "a write in `a` may execute between one in `b` and `b`'s terminator": same
|
|
1324
|
+
// block counts (op order refined by the caller where it matters), else CFG reachability
|
|
1325
|
+
const mayFollow = (a: Block, b: Block): boolean => a === b || reachFrom(a).has(b);
|
|
1326
|
+
for (const M of fn.blocks) {
|
|
1327
|
+
if (M === entry || M.params.length === 0 || forest.byHeader.has(M)) {
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
M.params.forEach((p, i) => {
|
|
1331
|
+
const name = varName.get(p)!;
|
|
1332
|
+
if (nameCount.get(name) !== 1) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
// every in-edge record into M, grouped by the SSA value it passes for param i
|
|
1336
|
+
const groups = new Map<Value, { rec: { block: Block; args: Value[] }; src: Block }[]>();
|
|
1337
|
+
for (const pr of new Set(preds.get(M) ?? [])) {
|
|
1338
|
+
for (const s of pr.ops[pr.ops.length - 1].successors) {
|
|
1339
|
+
if (s.block === M) {
|
|
1340
|
+
const g = groups.get(s.args[i]);
|
|
1341
|
+
if (g) {
|
|
1342
|
+
g.push({ rec: s, src: pr });
|
|
1343
|
+
} else {
|
|
1344
|
+
groups.set(s.args[i], [{ rec: s, src: pr }]);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
const candidates: { arg: Value; def: Op; defBlock: Block; edges: { rec: object; src: Block }[] }[] = [];
|
|
1350
|
+
for (const [arg, edges] of groups) {
|
|
1351
|
+
const def = defs.get(arg);
|
|
1352
|
+
if (!def || def.opcode !== 'const' || varName.has(arg)) {
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
const defBlock = opBlock.get(def)!;
|
|
1356
|
+
if (inLoop(defBlock) || edges.some(({ src }) => inLoop(src))) {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
if (edges.some(({ src }) => !dom.get(src)!.has(defBlock))) {
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
candidates.push({ arg, def, defBlock, edges });
|
|
1363
|
+
}
|
|
1364
|
+
// pairwise clobber check: candidate `c` is unsafe when another candidate's write can lie
|
|
1365
|
+
// between c's def and one of c's edges (def_c → def_o → edge_c); both then keep their edges
|
|
1366
|
+
const safe = candidates.filter((c) =>
|
|
1367
|
+
candidates.every((o) => {
|
|
1368
|
+
if (o === c) {
|
|
1369
|
+
return true;
|
|
1370
|
+
}
|
|
1371
|
+
const oAfterC =
|
|
1372
|
+
c.defBlock === o.defBlock ? opIndex.get(o.def)! > opIndex.get(c.def)! : mayFollow(c.defBlock, o.defBlock);
|
|
1373
|
+
return !(oAfterC && c.edges.some(({ src }) => mayFollow(o.defBlock, src)));
|
|
1374
|
+
}),
|
|
1375
|
+
);
|
|
1376
|
+
for (const c of safe) {
|
|
1377
|
+
const at = anchoredAt.get(c.def);
|
|
1378
|
+
if (at) {
|
|
1379
|
+
at.push({ name, arg: c.arg });
|
|
1380
|
+
} else {
|
|
1381
|
+
anchoredAt.set(c.def, [{ name, arg: c.arg }]);
|
|
1382
|
+
}
|
|
1383
|
+
for (const { rec } of c.edges) {
|
|
1384
|
+
const sup = suppressedArgs.get(rec);
|
|
1385
|
+
if (sup) {
|
|
1386
|
+
sup.add(i);
|
|
1387
|
+
} else {
|
|
1388
|
+
suppressedArgs.set(rec, new Set([i]));
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// ── BITFIELD member reads (symbol map) ──────────────────────────────────────────────────────
|
|
1397
|
+
// The `(x << a) >> b` extract of a struct global's loaded bytes IS a bitfield access when the
|
|
1398
|
+
// map declares a bitfield at exactly those bits: spelled `gSym.field`, the source form, whose
|
|
1399
|
+
// declared `u32 field : n` then makes C's own integer promotion reproduce the signedness every
|
|
1400
|
+
// downstream operator compiled with (a 7-bit unsigned field promotes to signed int — sdiv
|
|
1401
|
+
// renders `/` and recompiles to __divsi3, where the raw-shift spelling stays u32).
|
|
1402
|
+
//
|
|
1403
|
+
// Semantically EXACT, never approximate: the window must lie inside the loaded bytes (so the
|
|
1404
|
+
// load's extension bits cannot reach it), the field's position, width and signedness must all
|
|
1405
|
+
// match the extract (a logical shift is an unsigned read, an arithmetic one a signed read —
|
|
1406
|
+
// a signless field never matches), and the member must be nameable at all (memberQualsAllow;
|
|
1407
|
+
// the map only carries bitfield facts for little-endian ELFs — see SymbolStructField). Any
|
|
1408
|
+
// mismatch keeps the honest shift spelling.
|
|
1409
|
+
//
|
|
1410
|
+
// Precomputed over the ops (not folded during rendering) for the load's sake: a load whose
|
|
1411
|
+
// EVERY use is a spelled extract chain must not also emit its materialized `v = *(u16 *)&g;`
|
|
1412
|
+
// temp — the compiler CSEs the repeated member reads back to one load, but the leftover temp
|
|
1413
|
+
// would be a second one. A VOLATILE container refuses the whole fold: N member reads are N
|
|
1414
|
+
// volatile accesses where the asm did one load. (Byte-level residual, differ-refereed: a load
|
|
1415
|
+
// only PARTIALLY absorbed — one extract spelled, another use kept — emits both the temp and
|
|
1416
|
+
// the named reads, one load more than the asm; semantics hold, the score decides.)
|
|
1417
|
+
//
|
|
1418
|
+
// ORDERING GATE (adversarial round, CRITICAL 1 — twice): the named spelling replaces a
|
|
1419
|
+
// REGISTER value — the bits captured at the load's program position — with a fresh memory
|
|
1420
|
+
// read at each render position. Every other memory read in this file goes through the
|
|
1421
|
+
// materialization model (analysis.ts) for exactly that hazard, so the fold clears the SAME
|
|
1422
|
+
// bar with the SAME machinery: `emitPos` resolves where each extract actually renders
|
|
1423
|
+
// (transitively through its inlining consumers — an unresolvable position refuses), and
|
|
1424
|
+
// `memWriteBetween` walks every def-avoiding load→render path for a call, an opaque, or a
|
|
1425
|
+
// store not provably to a DIFFERENT named global. Path-based on purpose: the second audit
|
|
1426
|
+
// pass broke the first fix's linear-position scan with a block laid out AFTER the render in
|
|
1427
|
+
// address order but executing between load and render on the taken path — fn.blocks order is
|
|
1428
|
+
// address order, not topological order.
|
|
1429
|
+
const bitfieldSpelling = new Map<Op, { global: string; field: string }>();
|
|
1430
|
+
const absorbedLoads = new Set<Op>();
|
|
1431
|
+
if (symCtx && littleEndian && spellBitfieldMembers) {
|
|
1432
|
+
// the (name, byte) of a load's address when it resolves through defs alone — `gaddr` or
|
|
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);
|
|
1438
|
+
// A write for the fold's purposes: calls and opaques always; a store/astore unless its base
|
|
1439
|
+
// resolves to a global PROVABLY different from the folded one.
|
|
1440
|
+
const mayWrite = (sym: string) => mayWriteGlobal(defs, sym);
|
|
1441
|
+
for (const blk of fn.blocks) {
|
|
1442
|
+
for (const op of blk.ops) {
|
|
1443
|
+
if ((op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
const b = op.attrs.imm as number | undefined;
|
|
1447
|
+
const inner = defs.get(op.operands[0]);
|
|
1448
|
+
if (typeof b !== 'number' || b <= 0 || b >= 32 || inner?.opcode !== 'shl' || inner.operands.length !== 1) {
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
const a = inner.attrs.imm as number | undefined;
|
|
1452
|
+
if (typeof a !== 'number' || a < 0 || b < a) {
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
const w = 32 - b; // extract width
|
|
1456
|
+
const lo = b - a; // low bit within the loaded value
|
|
1457
|
+
const load = defs.get(inner.operands[0]);
|
|
1458
|
+
if (load?.opcode !== 'load' || lo + w > (load.attrs.width as number) * 8) {
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
// a materialized shl would still emit its `v = x << a` temp reading the load — the fold
|
|
1462
|
+
// would then ADD member reads on top of it; rare, refuse
|
|
1463
|
+
if (materialize.has(inner)) {
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
const gb = addrOf(load.operands[0], load.attrs.off as number);
|
|
1467
|
+
const si = gb ? symCtx.info(gb.name) : undefined;
|
|
1468
|
+
if (!gb || si?.shape !== 'struct' || si.volatile) {
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
// where does the member read RENDER? at the extract's own position when materialized,
|
|
1472
|
+
// else wherever each of its consumers ultimately renders (emitPos, transitively —
|
|
1473
|
+
// unresolvable refuses); every load→render path must be write-free
|
|
1474
|
+
const renders = materialize.has(op)
|
|
1475
|
+
? [{ blk: opBlock.get(op)!, idx: opIndex.get(op)! }]
|
|
1476
|
+
: [...new Set((useSitesOf.get(op.results[0]) ?? []).map((s) => s.op))].map((c) => emitPos(c));
|
|
1477
|
+
const writes = mayWrite(gb.name);
|
|
1478
|
+
if (renders.some((r) => r === null) || renders.some((r) => memWriteBetween(load, r!, writes))) {
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
const signedRead = op.opcode === 'shr_s';
|
|
1482
|
+
const fld = declaredFields(si.layout)?.find(
|
|
1483
|
+
(f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === gb.byte * 8 + lo && f.signed === signedRead,
|
|
1484
|
+
);
|
|
1485
|
+
if (fld && memberQualsAllow(fld, si.const, false)) {
|
|
1486
|
+
bitfieldSpelling.set(op, { global: gb.name, field: fld.name });
|
|
1487
|
+
loadTargets.set(load, gb);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
// a load is ABSORBED when every use is an shl whose every use is a spelled extract
|
|
1492
|
+
for (const load of loadTargets.keys()) {
|
|
1493
|
+
const shls = useSitesOf.get(load.results[0]) ?? [];
|
|
1494
|
+
const absorbed =
|
|
1495
|
+
shls.length > 0 &&
|
|
1496
|
+
shls.every(
|
|
1497
|
+
(u) =>
|
|
1498
|
+
u.op.opcode === 'shl' && (useSitesOf.get(u.op.results[0]) ?? []).every((v) => bitfieldSpelling.has(v.op)),
|
|
1499
|
+
);
|
|
1500
|
+
if (absorbed) {
|
|
1501
|
+
absorbedLoads.add(load);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1104
1506
|
// An unresolvable value: strict mode keeps the `"?"` sentinel AND records the reason — the
|
|
1105
1507
|
// decline thrown below names the actual gaps ("unmodelled instruction 'adde'"), the same
|
|
1106
1508
|
// reasons annotate mode's markers carry, instead of the anonymous `?` that assertResolved
|
|
@@ -1123,6 +1525,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1123
1525
|
if (d.opcode === 'const') {
|
|
1124
1526
|
return { k: 'const', value: d.attrs.value as number };
|
|
1125
1527
|
}
|
|
1528
|
+
// a bitfield extract recognized over the ops (see the precompute above): the member read,
|
|
1529
|
+
// not the shift pair
|
|
1530
|
+
const bf = bitfieldSpelling.get(d);
|
|
1531
|
+
if (bf) {
|
|
1532
|
+
return { k: 'field', base: { k: 'var', name: bf.global }, name: bf.field, dot: true };
|
|
1533
|
+
}
|
|
1126
1534
|
if (CMP_TO_BIN[d.opcode]) {
|
|
1127
1535
|
// A bare global address `&gSym` as a COMPARISON operand is the same unspelled escape as the
|
|
1128
1536
|
// arithmetic case below (see intifyAddr): its C type comes from the PROJECT's own
|
|
@@ -1231,6 +1639,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1231
1639
|
l = isPtrGlobal(l) ? intifyPtrGlobal(l) : l;
|
|
1232
1640
|
r = isPtrGlobal(r) ? intifyPtrGlobal(r) : r;
|
|
1233
1641
|
}
|
|
1642
|
+
// (The two right shifts stay DISTINCT ops here — `>>>` logical, `>>` arithmetic. Which token
|
|
1643
|
+
// a language spells each with, and what cast pins the choice, is a BACKEND decision; see
|
|
1644
|
+
// l3/ast.ts BinOp and backend/cfamily.ts's shift rule.)
|
|
1234
1645
|
// SCOPE: this and intifyAddr cover the ARITHMETIC escapes. A pointer global under a
|
|
1235
1646
|
// COMPARISON (`gPtr < K` — C compares unsigned whatever the asm's icmp_s* said) is the same
|
|
1236
1647
|
// class as intifyAddrCmp's `addr` rule and is deliberately left alone here: it is valid C
|
|
@@ -1242,8 +1653,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1242
1653
|
// The C rotate idiom — `x >> n | x << (32 - n)` (mirrored for rotl). Byte-exact round-trip
|
|
1243
1654
|
// on agbcc (thumb ror) and mwcc (rotlw/rotlwi), verified against both toolchains before the
|
|
1244
1655
|
// ops landed. `x` and `n` render twice — both pure by construction (SSA values; the rotate's
|
|
1245
|
-
// operands are register reads)
|
|
1246
|
-
//
|
|
1656
|
+
// operands are register reads). The right half is the LOGICAL shift `>>>` — the idiom is
|
|
1657
|
+
// wrong with an arithmetic one — stated on the node rather than left to the rotated value's
|
|
1658
|
+
// recovered unsignedness, which is a property of recovery rather than of the idiom.
|
|
1247
1659
|
//
|
|
1248
1660
|
// (The PPC mirror fold — `rotl(x, 32 - m)` ⇒ rotr(x, m) — lives in the PATTERN layer,
|
|
1249
1661
|
// engine.ts ROTL_MIRROR: it is a compiler-spelling idiom, mwcc-gated there, not a
|
|
@@ -1260,7 +1672,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1260
1672
|
n.k === 'const'
|
|
1261
1673
|
? { k: 'const', value: 32 - n.value }
|
|
1262
1674
|
: { k: 'bin', op: '-', l: { k: 'const', value: 32 }, r: n };
|
|
1263
|
-
const [near, far] = dir === 'rotr' ? (['
|
|
1675
|
+
const [near, far] = dir === 'rotr' ? (['>>>', '<<'] as const) : (['<<', '>>>'] as const);
|
|
1264
1676
|
return {
|
|
1265
1677
|
k: 'bin',
|
|
1266
1678
|
op: '|',
|
|
@@ -1286,6 +1698,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1286
1698
|
if (d.opcode === 'call') {
|
|
1287
1699
|
return { k: 'call', fn: d.attrs.target as string, args: d.operands.map(e) };
|
|
1288
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
|
+
}
|
|
1289
1707
|
if (d.opcode === 'gaddr') {
|
|
1290
1708
|
// A promoted CODE symbol (frontend `code: true`) is a function pointer stored as an
|
|
1291
1709
|
// integer: spelled `(u32)Name` — the source idiom — never `&Name` (defect G of the
|
|
@@ -1322,7 +1740,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1322
1740
|
);
|
|
1323
1741
|
}
|
|
1324
1742
|
return d.opcode === 'opaque'
|
|
1325
|
-
? mkGap(
|
|
1743
|
+
? mkGap(gapReasonFor(d.attrs.mnemonic), d.operands.map(e))
|
|
1326
1744
|
: mkGap(`no lowering for op '${d.opcode}'`, d.operands.map(e));
|
|
1327
1745
|
};
|
|
1328
1746
|
|
|
@@ -1382,13 +1800,17 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1382
1800
|
const target = succ.block;
|
|
1383
1801
|
const argExpr = sub ? exprWith(sub) : expr;
|
|
1384
1802
|
const copies: { name: string; value: Expr; arg: Value }[] = [];
|
|
1803
|
+
const suppressed = suppressedArgs.get(succ);
|
|
1385
1804
|
target.params.forEach((p, i) => {
|
|
1805
|
+
if (suppressed?.has(i)) {
|
|
1806
|
+
return;
|
|
1807
|
+
} // anchored at its const's def site — the write already ran before this edge
|
|
1386
1808
|
const name = varName.get(p)!;
|
|
1387
1809
|
const arg = succ.args[i];
|
|
1388
1810
|
if ((sub?.get(arg) ?? varName.get(arg)) === name) {
|
|
1389
1811
|
return;
|
|
1390
1812
|
} // identity copy — coalesced away
|
|
1391
|
-
copies.push({ name, value: argExpr(arg), arg });
|
|
1813
|
+
copies.push({ name, value: castAggregateAddr(name, argExpr(arg)), arg });
|
|
1392
1814
|
});
|
|
1393
1815
|
// Emit in the order the args are COMPUTED in `pred` — a compiler that lays the defining ops
|
|
1394
1816
|
// (and thus the copies that read them) out in that order matches with no spurious arg-swap.
|
|
@@ -1415,10 +1837,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1415
1837
|
return succ ? argAssignsFor(pred, succ, sub) : [];
|
|
1416
1838
|
};
|
|
1417
1839
|
|
|
1418
|
-
// Side-effecting ops of a block, emitted as statements in program order: memory stores
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1421
|
-
// 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.
|
|
1422
1844
|
const sideEffects = (b: Block): Stmt[] => {
|
|
1423
1845
|
const out: Stmt[] = [];
|
|
1424
1846
|
for (const op of b.ops) {
|
|
@@ -1461,10 +1883,27 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1461
1883
|
),
|
|
1462
1884
|
value: expr(op.operands[2]),
|
|
1463
1885
|
});
|
|
1464
|
-
} 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.
|
|
1465
1897
|
out.push({ k: 'exprstmt', value: expr(op.results[0]) });
|
|
1466
|
-
} else if (materialize.has(op)) {
|
|
1467
|
-
|
|
1898
|
+
} else if (materialize.has(op) && !absorbedLoads.has(op)) {
|
|
1899
|
+
// (an absorbed load's every consumer spells a named bitfield read — emitting its temp
|
|
1900
|
+
// here would recompile to a second load the asm does not have)
|
|
1901
|
+
const nm = varName.get(op.results[0])!;
|
|
1902
|
+
out.push({ k: 'assign', name: nm, value: castAggregateAddr(nm, lowerDef(op, expr)) });
|
|
1903
|
+
}
|
|
1904
|
+
// a merge copy anchored at this const's original position (anchorConstCopies, above)
|
|
1905
|
+
for (const a of anchoredAt.get(op) ?? []) {
|
|
1906
|
+
out.push({ k: 'assign', name: a.name, value: expr(a.arg) });
|
|
1468
1907
|
}
|
|
1469
1908
|
}
|
|
1470
1909
|
return out;
|
|
@@ -1515,7 +1954,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1515
1954
|
|
|
1516
1955
|
// ── Regime-A switch recovery (structure/switch-recover.ts): the recognizer's case bodies call
|
|
1517
1956
|
// back into structureRegion, and Regime B (switch_br, below) shares its fall-through predicate.
|
|
1518
|
-
const { recognizeSwitch,
|
|
1957
|
+
const { recognizeSwitch, analyzeArmExit } = makeSwitchRecovery({
|
|
1519
1958
|
fn,
|
|
1520
1959
|
defs,
|
|
1521
1960
|
dom,
|
|
@@ -1524,6 +1963,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1524
1963
|
isNamed: (v) => varName.has(v),
|
|
1525
1964
|
isCmpOpcode: (opcode) => !!CMP_TO_BIN[opcode],
|
|
1526
1965
|
switchAllowsNeqCase,
|
|
1966
|
+
emitsAnchoredWrite: (blk) => blk.ops.some((o) => anchoredAt.has(o)),
|
|
1527
1967
|
expr: (v) => expr(v),
|
|
1528
1968
|
structureRegion: (b, stop) => structureRegion(b, stop),
|
|
1529
1969
|
});
|
|
@@ -1552,23 +1992,18 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1552
1992
|
}
|
|
1553
1993
|
// Regime B: a `switch_br` (jump-table dispatch) lowers directly to the `switch` node — scrutinee,
|
|
1554
1994
|
// per-successor case value, last successor = default. Case bodies delegate to structureRegion (as in
|
|
1555
|
-
// Regime A).
|
|
1556
|
-
//
|
|
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.
|
|
1557
1998
|
if (term.opcode === 'switch_br') {
|
|
1558
1999
|
const merge = ipdom.get(b) ?? stop;
|
|
1559
2000
|
const succ = term.successors;
|
|
1560
2001
|
const caseVals = term.attrs.cases as number[];
|
|
1561
|
-
const targets = new Set<Block>(succ.map((s) => s.block));
|
|
1562
|
-
if (caseRegionReachesSibling(targets, b, merge)) {
|
|
1563
|
-
throw new StructureError(
|
|
1564
|
-
`cannot structure '${fn.name}': fall-through between jump-table cases is not yet supported`,
|
|
1565
|
-
);
|
|
1566
|
-
}
|
|
1567
2002
|
// Switch edges CARRY phi args (frontend/ssa.ts appends them terminator-generically) — each
|
|
1568
2003
|
// case/default body must open with its edge's copies, exactly as cond_br edges do; dropping
|
|
1569
2004
|
// them leaves the target's params uninitialized on the switch path. Two case values sharing
|
|
1570
2005
|
// a target must agree on their args (else the copies are ambiguous → loud decline); the
|
|
1571
|
-
// shared body is then
|
|
2006
|
+
// shared body is then emitted ONCE under both labels.
|
|
1572
2007
|
const argsSeen = new Map<Block, Value[]>();
|
|
1573
2008
|
for (const s of succ) {
|
|
1574
2009
|
const prev = argsSeen.get(s.block);
|
|
@@ -1579,16 +2014,79 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1579
2014
|
}
|
|
1580
2015
|
argsSeen.set(s.block, s.args as Value[]);
|
|
1581
2016
|
}
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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;
|
|
1587
2085
|
const sw: Stmt = {
|
|
1588
2086
|
k: 'switch',
|
|
1589
2087
|
scrutinee: expr(term.operands[0]),
|
|
1590
2088
|
cases: outCases,
|
|
1591
|
-
|
|
2089
|
+
...(defBody.length ? { default: defBody } : {}),
|
|
1592
2090
|
};
|
|
1593
2091
|
out.push(sw);
|
|
1594
2092
|
if (merge && merge !== stop) {
|
|
@@ -1725,7 +2223,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1725
2223
|
out.push(...updateCopies); // the loop update, RAW (i++, p>>=1, …)
|
|
1726
2224
|
let leaveCond = exprWith(sub)(term.operands[0]);
|
|
1727
2225
|
if (contIsTaken) {
|
|
1728
|
-
leaveCond =
|
|
2226
|
+
leaveCond = negateCond(leaveCond);
|
|
1729
2227
|
} // continue is `taken` → leave when NOT it
|
|
1730
2228
|
const exitArm = isBreak
|
|
1731
2229
|
? [...argAssigns(b, loopCtx.exit, sub), { k: 'break' } as Stmt] // break to the loop exit
|
|
@@ -1759,7 +2257,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1759
2257
|
// IDO/MIPS; agbcc/GCC canonicalise either way, so it is safe there too. A compiler that
|
|
1760
2258
|
// inverts branch canonicalization sets preserveDivergentBranchSense false and falls through
|
|
1761
2259
|
// to the positive form below.
|
|
1762
|
-
out.push({ k: 'if', cond:
|
|
2260
|
+
out.push({ k: 'if', cond: negateCond(cond), then: elseS, else: thenS });
|
|
1763
2261
|
return out;
|
|
1764
2262
|
}
|
|
1765
2263
|
out.push(mkIf(cond, thenS, elseS));
|
|
@@ -1788,7 +2286,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1788
2286
|
const term = li.header.ops[li.header.ops.length - 1];
|
|
1789
2287
|
let cond = exprWith(loopSub(li))(term.operands[0]);
|
|
1790
2288
|
if (term.successors[0].block !== li.header) {
|
|
1791
|
-
cond =
|
|
2289
|
+
cond = negateCond(cond);
|
|
1792
2290
|
} // loop-continue must be `taken`
|
|
1793
2291
|
const body = [...sideEffects(li.header), ...(updates ?? argAssigns(li.header, li.header))];
|
|
1794
2292
|
return { k: 'while', cond, body };
|
|
@@ -1802,7 +2300,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1802
2300
|
const term = wl.header.ops[wl.header.ops.length - 1];
|
|
1803
2301
|
let cond = expr(term.operands[0]);
|
|
1804
2302
|
if (term.successors[1].block === wl.bodyEntry) {
|
|
1805
|
-
cond =
|
|
2303
|
+
cond = negateCond(cond);
|
|
1806
2304
|
}
|
|
1807
2305
|
// The header→bodyEntry edge may carry non-identity phi args (a value the header COMPUTED and passes
|
|
1808
2306
|
// into the body). Those copies must open the body — dropping them reads an uninitialised local.
|
|
@@ -1844,6 +2342,26 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1844
2342
|
`cannot structure '${fn.name}': do-while condition or a post-loop value reads a pre-update loop variable`,
|
|
1845
2343
|
);
|
|
1846
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
|
+
}
|
|
1847
2365
|
dwActive.add(dw.header);
|
|
1848
2366
|
// structure the header's own block up to the latch. Call structureBlock DIRECTLY (not
|
|
1849
2367
|
// structureRegion): the header is already on `onStack` from the caller's structureRegion, so
|
|
@@ -1864,7 +2382,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1864
2382
|
const body = [...inner, ...sideEffects(dw.latch), ...updates];
|
|
1865
2383
|
let cond = exprWith(sub)(lterm.operands[0]);
|
|
1866
2384
|
if (lterm.successors[1].block === dw.header) {
|
|
1867
|
-
cond =
|
|
2385
|
+
cond = negateCond(cond);
|
|
1868
2386
|
} // continue edge must be `taken`
|
|
1869
2387
|
const out: Stmt[] = [{ k: 'dowhile', cond, body }];
|
|
1870
2388
|
// The exit region reads latch back-edge values under `sub` (post-loop they live in the loop vars).
|
|
@@ -1890,7 +2408,28 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1890
2408
|
return {
|
|
1891
2409
|
name: fn.name,
|
|
1892
2410
|
params: entry.params.map((p, i) => ({ name: `a${i}`, type: p.type })),
|
|
1893
|
-
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
|
+
],
|
|
1894
2433
|
...(shapedGlobalTypes.size
|
|
1895
2434
|
? {
|
|
1896
2435
|
globals: [...shapedGlobalTypes]
|
|
@@ -2047,16 +2586,10 @@ function substVar(e: Expr, from: string, to: string): Expr {
|
|
|
2047
2586
|
// empty-then peephole: `if (c) {} else { S }` → `if (!c) { S }`
|
|
2048
2587
|
function mkIf(cond: Expr, thenS: Stmt[], elseS: Stmt[]): Stmt {
|
|
2049
2588
|
if (thenS.length === 0 && elseS.length > 0) {
|
|
2050
|
-
return { k: 'if', cond:
|
|
2589
|
+
return { k: 'if', cond: negateCond(cond), then: elseS, else: [] };
|
|
2051
2590
|
}
|
|
2052
2591
|
return { k: 'if', cond, then: thenS, else: elseS };
|
|
2053
2592
|
}
|
|
2054
|
-
function negate(e: Expr): Expr {
|
|
2055
|
-
if (e.k === 'bin' && NEGATE[e.op]) {
|
|
2056
|
-
return { ...e, op: NEGATE[e.op] };
|
|
2057
|
-
}
|
|
2058
|
-
return { k: 'un', op: '!', e };
|
|
2059
|
-
}
|
|
2060
2593
|
|
|
2061
2594
|
// --- CFG utilities ---
|
|
2062
2595
|
function predecessorBlocks(fn: Fn): Map<Block, Block[]> {
|