@asmlift/core 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
package/src/l3/reindex.ts
CHANGED
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
// Past the table, the counter's DECLARATION is retired with its writes: nothing mentions it any
|
|
71
71
|
// more, and a declared-but-untouched local reads as the deliberate `uninit` slot it is not.
|
|
72
72
|
//
|
|
73
|
-
// v3 SCOPE — the up-counting
|
|
73
|
+
// v3 SCOPE — the up-counting walk with an EXPRESSION base (`p = (T *)(EXPR)` ahead of a
|
|
74
74
|
// counter-carried do-while): full rules at `tryExprWalk` below.
|
|
75
75
|
//
|
|
76
76
|
// v4 SCOPE — the UNGUARDED countdown, the shape agbcc emits for `for(i=0;i<C;i++) a[i]` with a
|
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
rematerializableAddress,
|
|
100
100
|
stmtChildren,
|
|
101
101
|
stmtExprs,
|
|
102
|
+
walkExprs,
|
|
102
103
|
} from './ast';
|
|
103
104
|
import { type Gate, firstRejection } from './gates';
|
|
104
105
|
import { takenNames } from './hoist';
|
|
@@ -301,11 +302,13 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
|
|
|
301
302
|
];
|
|
302
303
|
|
|
303
304
|
/** A `break`/`continue` that would target the ENCLOSING loop: found at if/switch depth, but not
|
|
304
|
-
* inside a nested loop (whose own exits target itself).
|
|
305
|
+
* inside a nested loop (whose own exits target itself). A switch's own `break` (`ofSwitch`)
|
|
306
|
+
* leaves only its switch, and the loop's tail still runs after it, so it is not one. */
|
|
305
307
|
function hasLoopExit(stmts: Stmt[]): boolean {
|
|
306
308
|
return stmts.some((s) => {
|
|
307
309
|
switch (s.k) {
|
|
308
310
|
case 'break':
|
|
311
|
+
return s.ofSwitch !== true;
|
|
309
312
|
case 'continue':
|
|
310
313
|
return true;
|
|
311
314
|
case 'if':
|
|
@@ -424,6 +427,11 @@ function reindexCond(cond: Expr, walk: WalkLoop, iv: string): Expr | null {
|
|
|
424
427
|
return null;
|
|
425
428
|
}
|
|
426
429
|
const i: Expr = { k: 'var', name: iv };
|
|
430
|
+
// A fresh node, so the compare's `unsignedCmp` evidence (l3/ast.ts) is dropped, and that is
|
|
431
|
+
// PROVED rather than lost: `p < base + n` compared addresses, which C compares unsigned, and the
|
|
432
|
+
// walk makes `p == base + i` (scaled), so `i < n` compares the same values. Kept, the evidence
|
|
433
|
+
// would have rank.ts withhold the index spelling over an `s32` counter that the pointer
|
|
434
|
+
// spelling, judged on the same rule, never owed.
|
|
427
435
|
return swap ? { k: 'bin', op: cond.op, l: n, r: i } : { k: 'bin', op: cond.op, l: i, r: n };
|
|
428
436
|
}
|
|
429
437
|
|
|
@@ -504,46 +512,81 @@ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
|
|
|
504
512
|
}
|
|
505
513
|
}
|
|
506
514
|
|
|
507
|
-
|
|
515
|
+
/** Whether any statement here, at any depth, writes memory (`store`). */
|
|
516
|
+
function stmtsStore(stmts: Stmt[]): boolean {
|
|
517
|
+
return stmts.some((s) => s.k === 'store' || stmtsStore(stmtChildren(s)));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** `e + c`, spelled `e - |c|` for a negative c and `e` alone for 0. */
|
|
521
|
+
function plusConst(e: Expr, c: number): Expr {
|
|
522
|
+
return c === 0 ? e : { k: 'bin', op: c < 0 ? '-' : '+', l: e, r: { k: 'const', value: Math.abs(c) } };
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ── v3: the up-counting walk with an EXPRESSION base ─────────────────────────────────────────
|
|
508
526
|
//
|
|
509
|
-
// p = (
|
|
527
|
+
// i = S; p = (T *)(EXPR); do { … *p … ; p = p + 1; i = i + 1; } while (i < n);
|
|
510
528
|
//
|
|
511
529
|
// The loop already carries its own integer counter, so the walk pointer is pure strength
|
|
512
|
-
// reduction: deleting its init and step and spelling the deref as BASE[i + REST]
|
|
513
|
-
//
|
|
514
|
-
// indexed source the compiler reduces back.
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
530
|
+
// reduction: deleting its init and step and spelling the deref as BASE[i - S + REST], REST in
|
|
531
|
+
// elements — the sole base addend of EXPR as the index base, the counter FIRST in the index sum —
|
|
532
|
+
// is the indexed source the compiler reduces back. One counter may carry SEVERAL walk pointers
|
|
533
|
+
// (KMC reduces `for (i = 1; …) { gH[i] = 7; gW[i] = 8; }` to one pointer per array): each keeps
|
|
534
|
+
// its own base and unit, and each re-spells on its own merits — one out of scope keeps walking,
|
|
535
|
+
// whatever the order of the steps. v3 SCOPE (decline over approximate):
|
|
536
|
+
// • each `p` is a `T *` over an integer T of W bytes, and every deref of `p` reads W. EXPR's sum
|
|
537
|
+
// counts in its BASE's unit. Over a W-element pointer base that is already elements, and REST
|
|
538
|
+
// stays verbatim, symbolic addends included. Over a byte base (a non-pointer var, a byte
|
|
539
|
+
// pointer, `(u32)&g`) a wider walk needs a constant total W divides (it is rescaled to
|
|
540
|
+
// elements), and a symbolic addend, which would need dividing, stays in the BYTE sum instead:
|
|
541
|
+
// the index base is `(T *)(BASE + SYM)`, the spelling `p = b + k; … p[i]` reduces back to;
|
|
542
|
+
// • the inits `i = S` and `p_k = EXPR_k` are the run of assignments immediately ahead of the
|
|
543
|
+
// `dowhile` (any order); EXPR (its pointer and 32-bit integer casts stripped; a narrowing cast
|
|
544
|
+
// declines) flattens over `+` into exactly ONE base addend — a bare var declared a non-pointer,
|
|
545
|
+
// a byte pointer or a pointer to W-byte integers (any other pointer strides a unit no element
|
|
546
|
+
// index names), or an object's address, `(u32)&g` beside other addends or `&g` alone — plus
|
|
547
|
+
// pure invariant addends (var/const/mul/shl trees whose vars the body never assigns, and,
|
|
548
|
+
// beside a call or a store in the loop, only params and locals whose address never escapes);
|
|
549
|
+
// • the body's contiguous TAIL is exactly one `p_k = p_k + 1` per walk pointer and one
|
|
550
|
+
// `i = i + 1` (any order), with `i` read by the do-while condition; each `p_k` is mentioned
|
|
551
|
+
// nowhere else in the function beyond its init, its step, and derefs AT OFFSET 0 inside this
|
|
552
|
+
// body, and not by the condition;
|
|
553
|
+
// • S is a constant and `i` a declared integer: any S at 32 bits, only S = 0 when narrower and
|
|
554
|
+
// unsigned, never when narrower and signed. The index is `i - S + REST` in elements, which is
|
|
555
|
+
// plain `i` for the init KMC folds from `for (i = S; …) base[i]`: `p = (u32)&g + S*W` over an
|
|
556
|
+
// object, `p = a0 + S` over a W-element pointer.
|
|
557
|
+
|
|
558
|
+
/** One v3 walk pointer's re-spelling: its deref `*p` becomes `base[i - S + idx… + elems]`. */
|
|
559
|
+
interface ExprWalkSpelling {
|
|
560
|
+
readonly p: string;
|
|
561
|
+
readonly base: Expr;
|
|
562
|
+
readonly idx: readonly Expr[];
|
|
563
|
+
readonly elems: number;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** The per-pointer half of v3: `init` (`p = EXPR`) decomposed into the index base and REST in
|
|
567
|
+
* elements, or null when `p`'s walk is out of scope. The loop-wide half is `tryExprWalkRun`. */
|
|
568
|
+
function exprWalkSpelling(
|
|
569
|
+
init: Stmt & { k: 'assign' },
|
|
527
570
|
dw: Stmt & { k: 'dowhile' },
|
|
528
571
|
ptrVars: Map<string, IrType>,
|
|
529
|
-
fnBody: Stmt[],
|
|
530
572
|
declTypes: VarTypes,
|
|
531
573
|
volatileLocals: ReadonlySet<string>,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const pT = ptrVars.get(p)!;
|
|
538
|
-
if (pT.kind !== 'ptr' || pT.to.kind !== 'int' || pT.to.width !== 8) {
|
|
574
|
+
privateNames: ReadonlySet<string>,
|
|
575
|
+
): ExprWalkSpelling | null {
|
|
576
|
+
const p = init.name;
|
|
577
|
+
const pT = ptrVars.get(p);
|
|
578
|
+
if (pT?.kind !== 'ptr' || pT.to.kind !== 'int' || pT.to.width % 8 !== 0 || volatileLocals.has(p)) {
|
|
539
579
|
return null;
|
|
540
580
|
}
|
|
541
|
-
|
|
581
|
+
const W = pT.to.width / 8;
|
|
582
|
+
if (!derefWidths([dw], p).every((w) => w === W)) {
|
|
542
583
|
return null;
|
|
543
584
|
}
|
|
544
|
-
// EXPR decomposition
|
|
545
|
-
|
|
546
|
-
|
|
585
|
+
// EXPR decomposition. Only casts that keep all 32 bits are stripped — a pointer cast, or an
|
|
586
|
+
// integer cast at 32 bits. A narrowing cast over EXPR (`(u16 *)(s16)((u32)&g + 2)`) truncates the
|
|
587
|
+
// address, which no index off the untruncated base reproduces, so it stays and the walk declines.
|
|
588
|
+
let expr: Expr = init.value;
|
|
589
|
+
while (expr.k === 'cast' && (expr.to.kind === 'ptr' || (expr.to.kind === 'int' && expr.to.width === 32))) {
|
|
547
590
|
expr = expr.e;
|
|
548
591
|
}
|
|
549
592
|
const addends: Expr[] = [];
|
|
@@ -556,21 +599,40 @@ function tryExprWalk(
|
|
|
556
599
|
}
|
|
557
600
|
};
|
|
558
601
|
flatten(expr);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
602
|
+
// The base candidates: a bare var, or an object's address. Beside other addends the address
|
|
603
|
+
// must sit under a 32-bit integer cast — the cast is what makes EXPR's `+` byte arithmetic (a
|
|
604
|
+
// bare `&g + 2` strides g's own size). ALONE it needs no cast: there is no `+` to stride, and
|
|
605
|
+
// the zero-offset init structure prints is `p = (T *)&g`, whose casts the strip above removed.
|
|
606
|
+
const addrOf = (a: Expr): Extract<Expr, { k: 'addr' }> | null =>
|
|
607
|
+
a.k === 'addr' && addends.length === 1
|
|
608
|
+
? a
|
|
609
|
+
: a.k === 'cast' && a.to.kind === 'int' && a.to.width === 32 && a.e.k === 'addr'
|
|
610
|
+
? a.e
|
|
611
|
+
: null;
|
|
612
|
+
const bases = addends.filter((a) => a.k === 'var' || addrOf(a) !== null);
|
|
613
|
+
if (bases.length !== 1) {
|
|
570
614
|
return null;
|
|
571
615
|
}
|
|
572
|
-
|
|
573
|
-
|
|
616
|
+
const rest = addends.filter((a) => a !== bases[0]);
|
|
617
|
+
const base: Extract<Expr, { k: 'var' | 'addr' }> = bases[0].k === 'var' ? bases[0] : addrOf(bases[0])!;
|
|
618
|
+
// The UNIT EXPR's `+` counts in is the base's own stride (declTypes carries params, locals AND
|
|
619
|
+
// globals). KMC folds `for (i = S; …) a0[i]` over a `u16 *a0` to `p = a0 + S`, element
|
|
620
|
+
// arithmetic. A var declared nowhere has an unknowable stride (its project declaration decides).
|
|
621
|
+
// An address needs no declaration: `&g` is g's address whatever g's type, and the printer always
|
|
622
|
+
// casts it to the access width (cfamily.ts legalizedIndexBase: the type walk answers nothing for
|
|
623
|
+
// an `addr`, so even a g declared with W-byte elements prints the cast).
|
|
624
|
+
let unit = 1;
|
|
625
|
+
if (base.k === 'var') {
|
|
626
|
+
const baseT = declTypes(base.name);
|
|
627
|
+
if (baseT === undefined) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
if (baseT.kind === 'ptr') {
|
|
631
|
+
if (baseT.to.kind !== 'int' || (baseT.to.width !== 8 && baseT.to.width !== W * 8)) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
unit = baseT.to.width / 8;
|
|
635
|
+
}
|
|
574
636
|
}
|
|
575
637
|
const pure = (e: Expr): boolean =>
|
|
576
638
|
e.k === 'var' || e.k === 'const'
|
|
@@ -580,7 +642,6 @@ function tryExprWalk(
|
|
|
580
642
|
: e.k === 'cast'
|
|
581
643
|
? pure(e.e)
|
|
582
644
|
: false;
|
|
583
|
-
const assignedIn = (stmts: Stmt[], name: string): boolean => stmts.some((st) => stmtAssigns(st, name));
|
|
584
645
|
const restVars = new Set<string>();
|
|
585
646
|
const collectVars = (e: Expr): void => {
|
|
586
647
|
if (e.k === 'var') {
|
|
@@ -592,69 +653,192 @@ function tryExprWalk(
|
|
|
592
653
|
});
|
|
593
654
|
};
|
|
594
655
|
rest.forEach(collectVars);
|
|
595
|
-
|
|
596
|
-
|
|
656
|
+
if (base.k === 'var') {
|
|
657
|
+
restVars.add(base.name);
|
|
658
|
+
}
|
|
659
|
+
// INVARIANT across the loop, because the indexed spelling re-reads every name on every pass
|
|
660
|
+
// where the walk read it once, at the init: no `assign` in the body writes it, and — unless it
|
|
661
|
+
// is a param or a local whose address is never taken — no call and no memory store in the loop
|
|
662
|
+
// can either. A global (`p = (u16 *)(b + gK * 2)`, `g()` in the body) or an escaped local is
|
|
663
|
+
// written by a callee or through an aliasing pointer with no `assign` in sight.
|
|
664
|
+
// (a `postincr` is an assign in expression clothing; a `marker` is an instruction nobody modelled)
|
|
665
|
+
const loopExprs = [...walkExprs([dw])];
|
|
666
|
+
const loopWritesMemory = loopExprs.some((e) => e.k === 'call' || e.k === 'marker') || stmtsStore(dw.body);
|
|
667
|
+
if (
|
|
668
|
+
!rest.every(pure) ||
|
|
669
|
+
[...restVars].some(
|
|
670
|
+
(n) =>
|
|
671
|
+
volatileLocals.has(n) ||
|
|
672
|
+
dw.body.some((st) => stmtAssigns(st, n)) ||
|
|
673
|
+
loopExprs.some((e) => e.k === 'postincr' && e.name === n) ||
|
|
674
|
+
(loopWritesMemory && !privateNames.has(n)),
|
|
675
|
+
)
|
|
676
|
+
) {
|
|
597
677
|
return null;
|
|
598
678
|
}
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
679
|
+
// REST in ELEMENTS. Counted in the walk's own unit (a byte walk, or a W-element pointer base)
|
|
680
|
+
// it is already elements: the symbolic addends stay verbatim and the constants fold into one.
|
|
681
|
+
// Counted in bytes under a wider walk, the constants must total a multiple of W (rescaled to
|
|
682
|
+
// elements), and the symbolic addends would need dividing — so they stay in the BYTE sum, which
|
|
683
|
+
// becomes the index base: `(T *)(a0 + (a1 << 1))`, every non-constant addend in EXPR's order,
|
|
684
|
+
// the address keeping its `(u32)` (the printer casts the sum to the access width).
|
|
685
|
+
const consts = rest.filter((a): a is Extract<Expr, { k: 'const' }> => a.k === 'const');
|
|
686
|
+
const symbolic = rest.filter((a) => a.k !== 'const');
|
|
687
|
+
const restUnits = consts.reduce((acc, c) => acc + c.value, 0) | 0; // the 32-bit sum, signed
|
|
688
|
+
if (unit === W) {
|
|
689
|
+
return { p, base: { ...base }, idx: symbolic, elems: restUnits };
|
|
690
|
+
}
|
|
691
|
+
if (restUnits % W !== 0) {
|
|
610
692
|
return null;
|
|
611
693
|
}
|
|
612
|
-
const
|
|
613
|
-
|
|
694
|
+
const byteSum = addends
|
|
695
|
+
.filter((a) => a.k !== 'const')
|
|
696
|
+
.reduce<Expr | null>((acc, a) => (acc === null ? a : { k: 'bin', op: '+', l: acc, r: a }), null)!;
|
|
697
|
+
return { p, base: symbolic.length === 0 ? { ...base } : byteSum, idx: [], elems: restUnits / W };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** v3 on one do-while: `out` is the statement list already walked ahead of it. Returns the
|
|
701
|
+
* re-spelled loop and the statements that replace `out`'s trailing init run (the counter's init
|
|
702
|
+
* kept, the walk pointers' deleted), or null. Tries the longest init run first. */
|
|
703
|
+
function tryExprWalk(
|
|
704
|
+
out: readonly Stmt[],
|
|
705
|
+
dw: Stmt & { k: 'dowhile' },
|
|
706
|
+
ptrVars: Map<string, IrType>,
|
|
707
|
+
fnBody: Stmt[],
|
|
708
|
+
declTypes: VarTypes,
|
|
709
|
+
volatileLocals: ReadonlySet<string>,
|
|
710
|
+
privateNames: ReadonlySet<string>,
|
|
711
|
+
): { loop: Stmt; run: number; kept: Stmt[] } | null {
|
|
712
|
+
let nTail = 0;
|
|
713
|
+
while (nTail < dw.body.length && isUnitIncrement(dw.body[dw.body.length - 1 - nTail]) !== null) {
|
|
714
|
+
nTail++;
|
|
715
|
+
}
|
|
716
|
+
let nInit = 0;
|
|
717
|
+
while (nInit < out.length && out[out.length - 1 - nInit].k === 'assign') {
|
|
718
|
+
nInit++;
|
|
719
|
+
}
|
|
720
|
+
for (let n = Math.min(nTail, nInit); n >= 2; n--) {
|
|
721
|
+
const r = tryExprWalkRun(
|
|
722
|
+
out.slice(-n) as (Stmt & { k: 'assign' })[],
|
|
723
|
+
n,
|
|
724
|
+
dw,
|
|
725
|
+
ptrVars,
|
|
726
|
+
fnBody,
|
|
727
|
+
declTypes,
|
|
728
|
+
volatileLocals,
|
|
729
|
+
privateNames,
|
|
730
|
+
);
|
|
731
|
+
if (r) {
|
|
732
|
+
return r;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** v3 with the last `n` statements of the body as the step tail and `inits` (the last `n` ahead
|
|
739
|
+
* of the loop) as their inits: one counter, n - 1 walk pointers. */
|
|
740
|
+
function tryExprWalkRun(
|
|
741
|
+
inits: (Stmt & { k: 'assign' })[],
|
|
742
|
+
n: number,
|
|
743
|
+
dw: Stmt & { k: 'dowhile' },
|
|
744
|
+
ptrVars: Map<string, IrType>,
|
|
745
|
+
fnBody: Stmt[],
|
|
746
|
+
declTypes: VarTypes,
|
|
747
|
+
volatileLocals: ReadonlySet<string>,
|
|
748
|
+
privateNames: ReadonlySet<string>,
|
|
749
|
+
): { loop: Stmt; run: number; kept: Stmt[] } | null {
|
|
750
|
+
// BOTH kinds of step as the body's contiguous TAIL (any order): every deref then runs with `i`
|
|
751
|
+
// completed steps equal to each `p`'s, which is what `BASE[i + REST]` states. A step anywhere
|
|
752
|
+
// earlier (the `*++p` walk, a deref straddling the step) reads a different address. The tail
|
|
753
|
+
// and the init run name the same variables, each once.
|
|
754
|
+
const tail = dw.body.slice(-n);
|
|
755
|
+
const tailNames = tail.map((st) => isUnitIncrement(st)!);
|
|
756
|
+
const initNames = inits.map((st) => st.name);
|
|
757
|
+
if (
|
|
758
|
+
new Set(tailNames).size !== n ||
|
|
759
|
+
new Set(initNames).size !== n ||
|
|
760
|
+
!initNames.every((x) => tailNames.includes(x))
|
|
761
|
+
) {
|
|
614
762
|
return null;
|
|
615
763
|
}
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
if (ivT !== undefined && ivT.kind !== 'int') {
|
|
764
|
+
const counters = inits.filter((st) => !ptrVars.has(st.name));
|
|
765
|
+
if (counters.length !== 1) {
|
|
619
766
|
return null;
|
|
620
767
|
}
|
|
621
|
-
|
|
768
|
+
const ivInit = counters[0];
|
|
769
|
+
const iv = ivInit.name;
|
|
770
|
+
if (!mentionsVar(dw.cond, iv)) {
|
|
622
771
|
return null;
|
|
623
772
|
}
|
|
624
|
-
// The counter
|
|
625
|
-
// steps
|
|
626
|
-
|
|
773
|
+
// The counter starts at a CONSTANT S and its ONLY write anywhere in the body is the tail step:
|
|
774
|
+
// k completed steps put each `p` at EXPR + k*W and `i` at S + k, so the deref is
|
|
775
|
+
// BASE[i - S + REST]. That holds while `i` takes the values S + k, which a DECLARED integer
|
|
776
|
+
// counter does: at 32 bits always (it wraps with the address), and narrower only from S = 0 and
|
|
777
|
+
// unsigned, where it names elements 0 … 2^w - 1 before it wraps. A signed narrow counter wraps
|
|
778
|
+
// at 2^(w-1) (an s8 goes 127 -> -128 and names an element 256 behind the walk), and from a
|
|
779
|
+
// nonzero S `i - S` wraps sooner still; an undeclared one has no width to argue from.
|
|
780
|
+
if (ivInit.value.k !== 'const') {
|
|
627
781
|
return null;
|
|
628
782
|
}
|
|
629
|
-
|
|
783
|
+
const start = ivInit.value.value | 0;
|
|
784
|
+
const ivT = declTypes(iv);
|
|
785
|
+
if (ivT?.kind !== 'int' || !(ivT.width === 32 || (start === 0 && !ivT.signed)) || volatileLocals.has(iv)) {
|
|
630
786
|
return null;
|
|
631
787
|
}
|
|
632
|
-
if (
|
|
788
|
+
if (dw.body.slice(0, -n).some((st) => stmtAssigns(st, iv))) {
|
|
633
789
|
return null;
|
|
634
790
|
}
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
791
|
+
// Each walk pointer re-spells on its OWN merits: one out of scope (its init, an off-0 deref, a
|
|
792
|
+
// mention outside the walk, a mention in the condition) keeps walking, init and step in place,
|
|
793
|
+
// beside the ones that re-spell — each `p` is its own address, so deleting one walk changes no
|
|
794
|
+
// other. Which pointers re-spell is therefore a property of each, not of the tail's step order.
|
|
795
|
+
// Dropping one can only remove rewrites, so the loop below shrinks the set to a fixpoint.
|
|
796
|
+
const spellings = new Map<string, ExprWalkSpelling>();
|
|
797
|
+
for (const st of inits) {
|
|
798
|
+
if (st === ivInit || mentionsVar(dw.cond, st.name)) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const s = exprWalkSpelling(st, dw, ptrVars, declTypes, volatileLocals, privateNames);
|
|
802
|
+
if (s !== null) {
|
|
803
|
+
spellings.set(s.p, s);
|
|
646
804
|
}
|
|
647
|
-
return m;
|
|
648
|
-
};
|
|
649
|
-
const body = dw.body.filter((_, k) => k !== pStepIdx).map((st) => mapStmtExprs(st, rewriteExpr));
|
|
650
|
-
const dw2: Stmt = { ...dw, body };
|
|
651
|
-
if (!respelled || !confinedToWalk(fnBody, p, countMentions([prev], p), dw)) {
|
|
652
|
-
return null;
|
|
653
805
|
}
|
|
654
|
-
|
|
655
|
-
|
|
806
|
+
const idxOf = (s: ExprWalkSpelling): Expr =>
|
|
807
|
+
plusConst(
|
|
808
|
+
s.idx.reduce<Expr>((acc, r) => ({ k: 'bin', op: '+', l: acc, r }), { k: 'var', name: iv }),
|
|
809
|
+
(s.elems - start) | 0,
|
|
810
|
+
);
|
|
811
|
+
while (spellings.size > 0) {
|
|
812
|
+
// rewrite the derefs, then verify nothing of any re-spelled `p` survives
|
|
813
|
+
const offsetDerefs = new Set<string>();
|
|
814
|
+
const rewriteExpr = (e: Expr): Expr => {
|
|
815
|
+
const m = mapExprChildren(e, rewriteExpr);
|
|
816
|
+
const s = m.k === 'index' && m.base.k === 'var' ? spellings.get(m.base.name) : undefined;
|
|
817
|
+
if (m.k === 'index' && s !== undefined) {
|
|
818
|
+
if (m.idx.k !== 'const' || m.idx.value !== 0) {
|
|
819
|
+
offsetDerefs.add(s.p);
|
|
820
|
+
return m;
|
|
821
|
+
}
|
|
822
|
+
return { ...m, base: { ...s.base }, idx: idxOf(s) };
|
|
823
|
+
}
|
|
824
|
+
return m;
|
|
825
|
+
};
|
|
826
|
+
const steps = new Set(tail.filter((st) => spellings.has(isUnitIncrement(st)!)));
|
|
827
|
+
const body = dw.body.filter((st) => !steps.has(st)).map((st) => mapStmtExprs(st, rewriteExpr));
|
|
828
|
+
const dw2: Stmt = { ...dw, body };
|
|
829
|
+
const failed = inits.filter(
|
|
830
|
+
(st) =>
|
|
831
|
+
spellings.has(st.name) &&
|
|
832
|
+
(offsetDerefs.has(st.name) ||
|
|
833
|
+
!confinedToWalk(fnBody, st.name, countMentions([st], st.name), dw) ||
|
|
834
|
+
countMentions([dw2], st.name) !== 0),
|
|
835
|
+
);
|
|
836
|
+
if (failed.length === 0) {
|
|
837
|
+
return { loop: dw2, run: n, kept: inits.filter((st) => !spellings.has(st.name)) };
|
|
838
|
+
}
|
|
839
|
+
failed.forEach((st) => spellings.delete(st.name));
|
|
656
840
|
}
|
|
657
|
-
return
|
|
841
|
+
return null;
|
|
658
842
|
}
|
|
659
843
|
|
|
660
844
|
/** Try the walk→index re-spelling on one function. Returns the transformed COPY when at least
|
|
@@ -679,6 +863,10 @@ export function reindexWalks(
|
|
|
679
863
|
sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
|
|
680
864
|
);
|
|
681
865
|
const storage = nameStorage(sfn);
|
|
866
|
+
// v3's invariant-addend test: the names no callee and no aliasing store can write — a param or a
|
|
867
|
+
// local whose address the function never takes
|
|
868
|
+
const addressTaken = new Set([...walkExprs(sfn.body)].flatMap((e) => (e.k === 'addr' ? [e.name] : [])));
|
|
869
|
+
const privateNames = new Set([...storage].filter(([n, c]) => c !== 'global' && !addressTaken.has(n)).map(([n]) => n));
|
|
682
870
|
for (const v of [...sfn.params, ...sfn.locals]) {
|
|
683
871
|
if (v.type.kind === 'ptr') {
|
|
684
872
|
ptrVars.set(v.name, v.type);
|
|
@@ -781,15 +969,14 @@ export function reindexWalks(
|
|
|
781
969
|
}
|
|
782
970
|
}
|
|
783
971
|
}
|
|
784
|
-
// v3 — the up-counting
|
|
972
|
+
// v3 — the up-counting walk with an expression base: `p = (T *)(EXPR)` immediately
|
|
785
973
|
// ahead of a counter-carried do-while (see tryExprWalk)
|
|
786
974
|
if (s.k === 'dowhile') {
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
out.
|
|
792
|
-
out.push(recurse(dw2));
|
|
975
|
+
const v3 = tryExprWalk(out, s, ptrVars, sfn.body, declTypes, volatileLocals, privateNames);
|
|
976
|
+
if (v3) {
|
|
977
|
+
// the walk inits are subsumed by the indexed spelling; the counter's init stays
|
|
978
|
+
out.splice(out.length - v3.run, v3.run, ...v3.kept);
|
|
979
|
+
out.push(recurse(v3.loop));
|
|
793
980
|
fired++;
|
|
794
981
|
continue;
|
|
795
982
|
}
|
package/src/l3/storage.ts
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
// silently a different object: `addressableGlobals`;
|
|
18
18
|
// • it asks which of the three classes a bare `assign` name denotes at all: `nameStorage`, the
|
|
19
19
|
// primitive the other two are the shadow-sensitive projections over.
|
|
20
|
-
|
|
20
|
+
// One question about LOCAL storage sits beside them, because it is asked from the same lists:
|
|
21
|
+
// which expressions are a local object's ADDRESS (`localAddresses`), the frame bytes a respelled
|
|
22
|
+
// tree must keep reading as integers (contracts.ts `assertLocalStorageStaysInteger`).
|
|
23
|
+
import { type Expr, type SFn, type Stmt, assignsIn, dotBase, stmtChildren, taintOverAssigns, walkExprs } from './ast';
|
|
21
24
|
|
|
22
25
|
export type Storage = 'local' | 'param' | 'global';
|
|
23
26
|
|
|
@@ -46,3 +49,97 @@ export function declaredGlobals(sfn: SFn): Set<string> {
|
|
|
46
49
|
export function addressableGlobals(sfn: SFn): Set<string> {
|
|
47
50
|
return new Set([...nameStorage(sfn)].filter(([, s]) => s === 'global').map(([n]) => n));
|
|
48
51
|
}
|
|
52
|
+
|
|
53
|
+
/** The ADDRESSES of a tree's local objects — the frame bytes, which nothing but the function's own
|
|
54
|
+
* accesses and the callees it hands an address to can reach.
|
|
55
|
+
*
|
|
56
|
+
* `isAddress(e)`: `e` evaluates to an address inside a local object — a local array (`sp0`,
|
|
57
|
+
* which decays), a local's address (`&v`), a local ever assigned one (`p0 = (s32 *)sp0;`, a
|
|
58
|
+
* flow-insensitive taint), under a cast or moved by `+`/`-`. PRECISE in the direction that
|
|
59
|
+
* matters to its readers: a value READ OUT of the bytes (`((s32 *)sp0)[i]`, a pointer the frame
|
|
60
|
+
* holds) is not an address of them, so an access through it is not an access to the frame.
|
|
61
|
+
* Known gap, on the side of answering no: an array MEMBER spelled as a `field` (`s.arr`) is an
|
|
62
|
+
* address C decays, and this does not follow it.
|
|
63
|
+
*
|
|
64
|
+
* `accessAddress(e)`: the operand an access node reads its address from — an `index`'s base, an
|
|
65
|
+
* arrow `field`'s base, a dot `field`'s array element's base, or a struct VALUE's own address
|
|
66
|
+
* (`s.f` reads at `&s`). A param counts as a local throughout: its home is the frame once its
|
|
67
|
+
* address is taken, and a param assigned an address holds it as a local would. */
|
|
68
|
+
export function localAddresses(sfn: SFn): {
|
|
69
|
+
isAddress: (e: Expr) => boolean;
|
|
70
|
+
accessAddress: (e: Extract<Expr, { k: 'index' } | { k: 'field' }>) => Expr;
|
|
71
|
+
} {
|
|
72
|
+
const storage = nameStorage(sfn);
|
|
73
|
+
// a param is a local variable of the callee: its home is the frame once its address is taken
|
|
74
|
+
const isLocal = (n: string): boolean => storage.get(n) === 'local' || storage.get(n) === 'param';
|
|
75
|
+
const addressIn = (e: Expr, tainted: ReadonlySet<string>): boolean => {
|
|
76
|
+
switch (e.k) {
|
|
77
|
+
case 'var':
|
|
78
|
+
case 'postincr':
|
|
79
|
+
return tainted.has(e.name);
|
|
80
|
+
case 'addr':
|
|
81
|
+
return isLocal(e.name);
|
|
82
|
+
case 'cast':
|
|
83
|
+
return addressIn(e.e, tainted);
|
|
84
|
+
case 'bin':
|
|
85
|
+
return (
|
|
86
|
+
(e.op === '+' && (addressIn(e.l, tainted) || addressIn(e.r, tainted))) ||
|
|
87
|
+
(e.op === '-' && addressIn(e.l, tainted))
|
|
88
|
+
);
|
|
89
|
+
default:
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const tainted = taintOverAssigns(
|
|
94
|
+
assignsIn(sfn.body),
|
|
95
|
+
sfn.locals.filter((l) => l.type.kind === 'array').map((l) => l.name),
|
|
96
|
+
(a, t) => isLocal(a.name) && addressIn(a.value, t),
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
isAddress: (e) => addressIn(e, tainted),
|
|
100
|
+
accessAddress: (e) =>
|
|
101
|
+
e.k === 'index'
|
|
102
|
+
? e.base
|
|
103
|
+
: (dotBase(e)?.base ?? (e.dot === true && e.base.k === 'var' ? { k: 'addr', name: e.base.name } : e.base)),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Whether a local object's address LEAVES the function — passed to a call, stored to memory,
|
|
108
|
+
* assigned to a non-local, or returned — AND the function itself writes local storage. Both
|
|
109
|
+
* halves are what let a pointer the function obtains LATER (a call's result, a load) name bytes
|
|
110
|
+
* the function also stores to: a callee that keeps the address can hand it back.
|
|
111
|
+
*
|
|
112
|
+
* OVER-WIDE ON PURPOSE, and what it costs is measured: a store to a DEVICE register counts as an
|
|
113
|
+
* escape, since the tree cannot tell a DMA source register (`REG_DMA3SAD = sp0;`, the GBA's fill
|
|
114
|
+
* idiom) from memory a later load reads back. On kleod:PauseMenuScreenHandler that refuses every
|
|
115
|
+
* `/ptr-field` candidate, none of which was its winner. */
|
|
116
|
+
export function localStorageEscapesWritten(sfn: SFn): boolean {
|
|
117
|
+
const { isAddress, accessAddress } = localAddresses(sfn);
|
|
118
|
+
const storage = nameStorage(sfn);
|
|
119
|
+
const held = (n: string): boolean => storage.get(n) === 'local' || storage.get(n) === 'param';
|
|
120
|
+
let escapes = false;
|
|
121
|
+
let written = false;
|
|
122
|
+
const addressTaken = new Set<string>();
|
|
123
|
+
for (const e of walkExprs(sfn.body)) {
|
|
124
|
+
if (e.k === 'call' && e.args.some(isAddress)) {
|
|
125
|
+
escapes = true;
|
|
126
|
+
}
|
|
127
|
+
if (e.k === 'addr' && held(e.name)) {
|
|
128
|
+
addressTaken.add(e.name);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const visit = (s: Stmt): void => {
|
|
132
|
+
if (s.k === 'store') {
|
|
133
|
+
escapes ||= isAddress(s.value);
|
|
134
|
+
written ||= (s.lval.k === 'index' || s.lval.k === 'field') && isAddress(accessAddress(s.lval));
|
|
135
|
+
} else if (s.k === 'assign') {
|
|
136
|
+
escapes ||= !held(s.name) && isAddress(s.value);
|
|
137
|
+
written ||= addressTaken.has(s.name);
|
|
138
|
+
} else if (s.k === 'return' && s.value !== undefined) {
|
|
139
|
+
escapes ||= isAddress(s.value);
|
|
140
|
+
}
|
|
141
|
+
stmtChildren(s).forEach(visit);
|
|
142
|
+
};
|
|
143
|
+
sfn.body.forEach(visit);
|
|
144
|
+
return escapes && written;
|
|
145
|
+
}
|