@asmlift/core 0.9.0 → 0.11.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 +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- 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 +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -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 +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- 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 +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- 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/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- 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 +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
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
|
|
@@ -93,12 +93,15 @@ import {
|
|
|
93
93
|
Expr,
|
|
94
94
|
SFn,
|
|
95
95
|
Stmt,
|
|
96
|
+
SwitchCase,
|
|
97
|
+
caseEndGotoLeaves,
|
|
96
98
|
mapExprChildren,
|
|
97
99
|
mapStmtExprs,
|
|
98
100
|
mentionedName,
|
|
99
101
|
rematerializableAddress,
|
|
100
102
|
stmtChildren,
|
|
101
103
|
stmtExprs,
|
|
104
|
+
walkExprs,
|
|
102
105
|
} from './ast';
|
|
103
106
|
import { type Gate, firstRejection } from './gates';
|
|
104
107
|
import { takenNames } from './hoist';
|
|
@@ -301,17 +304,24 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
|
|
|
301
304
|
];
|
|
302
305
|
|
|
303
306
|
/** 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).
|
|
307
|
+
* inside a nested loop (whose own exits target itself). A switch's own `break` (`ofSwitch`)
|
|
308
|
+
* leaves only its switch, and the loop's tail still runs after it, so it is not one. A case-end
|
|
309
|
+
* `goto` whose label lies outside `stmts` leaves every loop it sits in, at any depth. */
|
|
305
310
|
function hasLoopExit(stmts: Stmt[]): boolean {
|
|
311
|
+
return caseEndGotoLeaves(stmts) || hasShallowLoopExit(stmts);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function hasShallowLoopExit(stmts: Stmt[]): boolean {
|
|
306
315
|
return stmts.some((s) => {
|
|
307
316
|
switch (s.k) {
|
|
308
317
|
case 'break':
|
|
318
|
+
return s.ofSwitch !== true;
|
|
309
319
|
case 'continue':
|
|
310
320
|
return true;
|
|
311
321
|
case 'if':
|
|
312
|
-
return
|
|
322
|
+
return hasShallowLoopExit(s.then) || hasShallowLoopExit(s.else);
|
|
313
323
|
case 'switch':
|
|
314
|
-
return s.cases.some((c) =>
|
|
324
|
+
return s.cases.some((c) => hasShallowLoopExit(c.body)) || hasShallowLoopExit(s.default ?? []);
|
|
315
325
|
default:
|
|
316
326
|
return false;
|
|
317
327
|
}
|
|
@@ -424,6 +434,11 @@ function reindexCond(cond: Expr, walk: WalkLoop, iv: string): Expr | null {
|
|
|
424
434
|
return null;
|
|
425
435
|
}
|
|
426
436
|
const i: Expr = { k: 'var', name: iv };
|
|
437
|
+
// A fresh node, so the compare's `unsignedCmp` evidence (l3/ast.ts) is dropped, and that is
|
|
438
|
+
// PROVED rather than lost: `p < base + n` compared addresses, which C compares unsigned, and the
|
|
439
|
+
// walk makes `p == base + i` (scaled), so `i < n` compares the same values. Kept, the evidence
|
|
440
|
+
// would have rank.ts withhold the index spelling over an `s32` counter that the pointer
|
|
441
|
+
// spelling, judged on the same rule, never owed.
|
|
427
442
|
return swap ? { k: 'bin', op: cond.op, l: n, r: i } : { k: 'bin', op: cond.op, l: i, r: n };
|
|
428
443
|
}
|
|
429
444
|
|
|
@@ -494,56 +509,93 @@ function reindexStmt(s: Stmt, walk: WalkLoop, iv: string): Stmt | null {
|
|
|
494
509
|
return {
|
|
495
510
|
...s,
|
|
496
511
|
scrutinee,
|
|
497
|
-
cases: cases as
|
|
512
|
+
cases: cases as SwitchCase[],
|
|
498
513
|
default: dflt ?? undefined,
|
|
499
514
|
};
|
|
500
515
|
}
|
|
501
516
|
case 'break':
|
|
502
517
|
case 'continue':
|
|
518
|
+
case 'goto':
|
|
519
|
+
case 'caseEndGoto':
|
|
503
520
|
return s;
|
|
504
521
|
}
|
|
505
522
|
}
|
|
506
523
|
|
|
507
|
-
|
|
524
|
+
/** Whether any statement here, at any depth, writes memory (`store`). */
|
|
525
|
+
function stmtsStore(stmts: Stmt[]): boolean {
|
|
526
|
+
return stmts.some((s) => s.k === 'store' || stmtsStore(stmtChildren(s)));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** `e + c`, spelled `e - |c|` for a negative c and `e` alone for 0. */
|
|
530
|
+
function plusConst(e: Expr, c: number): Expr {
|
|
531
|
+
return c === 0 ? e : { k: 'bin', op: c < 0 ? '-' : '+', l: e, r: { k: 'const', value: Math.abs(c) } };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── v3: the up-counting walk with an EXPRESSION base ─────────────────────────────────────────
|
|
508
535
|
//
|
|
509
|
-
// p = (
|
|
536
|
+
// i = S; p = (T *)(EXPR); do { … *p … ; p = p + 1; i = i + 1; } while (i < n);
|
|
510
537
|
//
|
|
511
538
|
// 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
|
-
|
|
539
|
+
// reduction: deleting its init and step and spelling the deref as BASE[i - S + REST], REST in
|
|
540
|
+
// elements — the sole base addend of EXPR as the index base, the counter FIRST in the index sum —
|
|
541
|
+
// is the indexed source the compiler reduces back. One counter may carry SEVERAL walk pointers
|
|
542
|
+
// (KMC reduces `for (i = 1; …) { gH[i] = 7; gW[i] = 8; }` to one pointer per array): each keeps
|
|
543
|
+
// its own base and unit, and each re-spells on its own merits — one out of scope keeps walking,
|
|
544
|
+
// whatever the order of the steps. v3 SCOPE (decline over approximate):
|
|
545
|
+
// • each `p` is a `T *` over an integer T of W bytes, and every deref of `p` reads W. EXPR's sum
|
|
546
|
+
// counts in its BASE's unit. Over a W-element pointer base that is already elements, and REST
|
|
547
|
+
// stays verbatim, symbolic addends included. Over a byte base (a non-pointer var, a byte
|
|
548
|
+
// pointer, `(u32)&g`) a wider walk needs a constant total W divides (it is rescaled to
|
|
549
|
+
// elements), and a symbolic addend, which would need dividing, stays in the BYTE sum instead:
|
|
550
|
+
// the index base is `(T *)(BASE + SYM)`, the spelling `p = b + k; … p[i]` reduces back to;
|
|
551
|
+
// • the inits `i = S` and `p_k = EXPR_k` are the run of assignments immediately ahead of the
|
|
552
|
+
// `dowhile` (any order); EXPR (its pointer and 32-bit integer casts stripped; a narrowing cast
|
|
553
|
+
// declines) flattens over `+` into exactly ONE base addend — a bare var declared a non-pointer,
|
|
554
|
+
// a byte pointer or a pointer to W-byte integers (any other pointer strides a unit no element
|
|
555
|
+
// index names), or an object's address, `(u32)&g` beside other addends or `&g` alone — plus
|
|
556
|
+
// pure invariant addends (var/const/mul/shl trees whose vars the body never assigns, and,
|
|
557
|
+
// beside a call or a store in the loop, only params and locals whose address never escapes);
|
|
558
|
+
// • the body's contiguous TAIL is exactly one `p_k = p_k + 1` per walk pointer and one
|
|
559
|
+
// `i = i + 1` (any order), with `i` read by the do-while condition; each `p_k` is mentioned
|
|
560
|
+
// nowhere else in the function beyond its init, its step, and derefs AT OFFSET 0 inside this
|
|
561
|
+
// body, and not by the condition;
|
|
562
|
+
// • S is a constant and `i` a declared integer: any S at 32 bits, only S = 0 when narrower and
|
|
563
|
+
// unsigned, never when narrower and signed. The index is `i - S + REST` in elements, which is
|
|
564
|
+
// plain `i` for the init KMC folds from `for (i = S; …) base[i]`: `p = (u32)&g + S*W` over an
|
|
565
|
+
// object, `p = a0 + S` over a W-element pointer.
|
|
566
|
+
|
|
567
|
+
/** One v3 walk pointer's re-spelling: its deref `*p` becomes `base[i - S + idx… + elems]`. */
|
|
568
|
+
interface ExprWalkSpelling {
|
|
569
|
+
readonly p: string;
|
|
570
|
+
readonly base: Expr;
|
|
571
|
+
readonly idx: readonly Expr[];
|
|
572
|
+
readonly elems: number;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** The per-pointer half of v3: `init` (`p = EXPR`) decomposed into the index base and REST in
|
|
576
|
+
* elements, or null when `p`'s walk is out of scope. The loop-wide half is `tryExprWalkRun`. */
|
|
577
|
+
function exprWalkSpelling(
|
|
578
|
+
init: Stmt & { k: 'assign' },
|
|
527
579
|
dw: Stmt & { k: 'dowhile' },
|
|
528
580
|
ptrVars: Map<string, IrType>,
|
|
529
|
-
fnBody: Stmt[],
|
|
530
581
|
declTypes: VarTypes,
|
|
531
582
|
volatileLocals: ReadonlySet<string>,
|
|
532
|
-
|
|
533
|
-
|
|
583
|
+
privateNames: ReadonlySet<string>,
|
|
584
|
+
): ExprWalkSpelling | null {
|
|
585
|
+
const p = init.name;
|
|
586
|
+
const pT = ptrVars.get(p);
|
|
587
|
+
if (pT?.kind !== 'ptr' || pT.to.kind !== 'int' || pT.to.width % 8 !== 0 || volatileLocals.has(p)) {
|
|
534
588
|
return null;
|
|
535
589
|
}
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
if (pT.kind !== 'ptr' || pT.to.kind !== 'int' || pT.to.width !== 8) {
|
|
590
|
+
const W = pT.to.width / 8;
|
|
591
|
+
if (!derefWidths([dw], p).every((w) => w === W)) {
|
|
539
592
|
return null;
|
|
540
593
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
while (expr.k === 'cast') {
|
|
594
|
+
// EXPR decomposition. Only casts that keep all 32 bits are stripped — a pointer cast, or an
|
|
595
|
+
// integer cast at 32 bits. A narrowing cast over EXPR (`(u16 *)(s16)((u32)&g + 2)`) truncates the
|
|
596
|
+
// address, which no index off the untruncated base reproduces, so it stays and the walk declines.
|
|
597
|
+
let expr: Expr = init.value;
|
|
598
|
+
while (expr.k === 'cast' && (expr.to.kind === 'ptr' || (expr.to.kind === 'int' && expr.to.width === 32))) {
|
|
547
599
|
expr = expr.e;
|
|
548
600
|
}
|
|
549
601
|
const addends: Expr[] = [];
|
|
@@ -556,21 +608,40 @@ function tryExprWalk(
|
|
|
556
608
|
}
|
|
557
609
|
};
|
|
558
610
|
flatten(expr);
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
611
|
+
// The base candidates: a bare var, or an object's address. Beside other addends the address
|
|
612
|
+
// must sit under a 32-bit integer cast — the cast is what makes EXPR's `+` byte arithmetic (a
|
|
613
|
+
// bare `&g + 2` strides g's own size). ALONE it needs no cast: there is no `+` to stride, and
|
|
614
|
+
// the zero-offset init structure prints is `p = (T *)&g`, whose casts the strip above removed.
|
|
615
|
+
const addrOf = (a: Expr): Extract<Expr, { k: 'addr' }> | null =>
|
|
616
|
+
a.k === 'addr' && addends.length === 1
|
|
617
|
+
? a
|
|
618
|
+
: a.k === 'cast' && a.to.kind === 'int' && a.to.width === 32 && a.e.k === 'addr'
|
|
619
|
+
? a.e
|
|
620
|
+
: null;
|
|
621
|
+
const bases = addends.filter((a) => a.k === 'var' || addrOf(a) !== null);
|
|
622
|
+
if (bases.length !== 1) {
|
|
570
623
|
return null;
|
|
571
624
|
}
|
|
572
|
-
|
|
573
|
-
|
|
625
|
+
const rest = addends.filter((a) => a !== bases[0]);
|
|
626
|
+
const base: Extract<Expr, { k: 'var' | 'addr' }> = bases[0].k === 'var' ? bases[0] : addrOf(bases[0])!;
|
|
627
|
+
// The UNIT EXPR's `+` counts in is the base's own stride (declTypes carries params, locals AND
|
|
628
|
+
// globals). KMC folds `for (i = S; …) a0[i]` over a `u16 *a0` to `p = a0 + S`, element
|
|
629
|
+
// arithmetic. A var declared nowhere has an unknowable stride (its project declaration decides).
|
|
630
|
+
// An address needs no declaration: `&g` is g's address whatever g's type, and the printer always
|
|
631
|
+
// casts it to the access width (cfamily.ts legalizedIndexBase: the type walk answers nothing for
|
|
632
|
+
// an `addr`, so even a g declared with W-byte elements prints the cast).
|
|
633
|
+
let unit = 1;
|
|
634
|
+
if (base.k === 'var') {
|
|
635
|
+
const baseT = declTypes(base.name);
|
|
636
|
+
if (baseT === undefined) {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
if (baseT.kind === 'ptr') {
|
|
640
|
+
if (baseT.to.kind !== 'int' || (baseT.to.width !== 8 && baseT.to.width !== W * 8)) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
unit = baseT.to.width / 8;
|
|
644
|
+
}
|
|
574
645
|
}
|
|
575
646
|
const pure = (e: Expr): boolean =>
|
|
576
647
|
e.k === 'var' || e.k === 'const'
|
|
@@ -580,7 +651,6 @@ function tryExprWalk(
|
|
|
580
651
|
: e.k === 'cast'
|
|
581
652
|
? pure(e.e)
|
|
582
653
|
: false;
|
|
583
|
-
const assignedIn = (stmts: Stmt[], name: string): boolean => stmts.some((st) => stmtAssigns(st, name));
|
|
584
654
|
const restVars = new Set<string>();
|
|
585
655
|
const collectVars = (e: Expr): void => {
|
|
586
656
|
if (e.k === 'var') {
|
|
@@ -592,69 +662,192 @@ function tryExprWalk(
|
|
|
592
662
|
});
|
|
593
663
|
};
|
|
594
664
|
rest.forEach(collectVars);
|
|
595
|
-
|
|
596
|
-
|
|
665
|
+
if (base.k === 'var') {
|
|
666
|
+
restVars.add(base.name);
|
|
667
|
+
}
|
|
668
|
+
// INVARIANT across the loop, because the indexed spelling re-reads every name on every pass
|
|
669
|
+
// where the walk read it once, at the init: no `assign` in the body writes it, and — unless it
|
|
670
|
+
// is a param or a local whose address is never taken — no call and no memory store in the loop
|
|
671
|
+
// can either. A global (`p = (u16 *)(b + gK * 2)`, `g()` in the body) or an escaped local is
|
|
672
|
+
// written by a callee or through an aliasing pointer with no `assign` in sight.
|
|
673
|
+
// (a `postincr` is an assign in expression clothing; a `marker` is an instruction nobody modelled)
|
|
674
|
+
const loopExprs = [...walkExprs([dw])];
|
|
675
|
+
const loopWritesMemory = loopExprs.some((e) => e.k === 'call' || e.k === 'marker') || stmtsStore(dw.body);
|
|
676
|
+
if (
|
|
677
|
+
!rest.every(pure) ||
|
|
678
|
+
[...restVars].some(
|
|
679
|
+
(n) =>
|
|
680
|
+
volatileLocals.has(n) ||
|
|
681
|
+
dw.body.some((st) => stmtAssigns(st, n)) ||
|
|
682
|
+
loopExprs.some((e) => e.k === 'postincr' && e.name === n) ||
|
|
683
|
+
(loopWritesMemory && !privateNames.has(n)),
|
|
684
|
+
)
|
|
685
|
+
) {
|
|
597
686
|
return null;
|
|
598
687
|
}
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
//
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
688
|
+
// REST in ELEMENTS. Counted in the walk's own unit (a byte walk, or a W-element pointer base)
|
|
689
|
+
// it is already elements: the symbolic addends stay verbatim and the constants fold into one.
|
|
690
|
+
// Counted in bytes under a wider walk, the constants must total a multiple of W (rescaled to
|
|
691
|
+
// elements), and the symbolic addends would need dividing — so they stay in the BYTE sum, which
|
|
692
|
+
// becomes the index base: `(T *)(a0 + (a1 << 1))`, every non-constant addend in EXPR's order,
|
|
693
|
+
// the address keeping its `(u32)` (the printer casts the sum to the access width).
|
|
694
|
+
const consts = rest.filter((a): a is Extract<Expr, { k: 'const' }> => a.k === 'const');
|
|
695
|
+
const symbolic = rest.filter((a) => a.k !== 'const');
|
|
696
|
+
const restUnits = consts.reduce((acc, c) => acc + c.value, 0) | 0; // the 32-bit sum, signed
|
|
697
|
+
if (unit === W) {
|
|
698
|
+
return { p, base: { ...base }, idx: symbolic, elems: restUnits };
|
|
699
|
+
}
|
|
700
|
+
if (restUnits % W !== 0) {
|
|
610
701
|
return null;
|
|
611
702
|
}
|
|
612
|
-
const
|
|
613
|
-
|
|
703
|
+
const byteSum = addends
|
|
704
|
+
.filter((a) => a.k !== 'const')
|
|
705
|
+
.reduce<Expr | null>((acc, a) => (acc === null ? a : { k: 'bin', op: '+', l: acc, r: a }), null)!;
|
|
706
|
+
return { p, base: symbolic.length === 0 ? { ...base } : byteSum, idx: [], elems: restUnits / W };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** v3 on one do-while: `out` is the statement list already walked ahead of it. Returns the
|
|
710
|
+
* re-spelled loop and the statements that replace `out`'s trailing init run (the counter's init
|
|
711
|
+
* kept, the walk pointers' deleted), or null. Tries the longest init run first. */
|
|
712
|
+
function tryExprWalk(
|
|
713
|
+
out: readonly Stmt[],
|
|
714
|
+
dw: Stmt & { k: 'dowhile' },
|
|
715
|
+
ptrVars: Map<string, IrType>,
|
|
716
|
+
fnBody: Stmt[],
|
|
717
|
+
declTypes: VarTypes,
|
|
718
|
+
volatileLocals: ReadonlySet<string>,
|
|
719
|
+
privateNames: ReadonlySet<string>,
|
|
720
|
+
): { loop: Stmt; run: number; kept: Stmt[] } | null {
|
|
721
|
+
let nTail = 0;
|
|
722
|
+
while (nTail < dw.body.length && isUnitIncrement(dw.body[dw.body.length - 1 - nTail]) !== null) {
|
|
723
|
+
nTail++;
|
|
724
|
+
}
|
|
725
|
+
let nInit = 0;
|
|
726
|
+
while (nInit < out.length && out[out.length - 1 - nInit].k === 'assign') {
|
|
727
|
+
nInit++;
|
|
728
|
+
}
|
|
729
|
+
for (let n = Math.min(nTail, nInit); n >= 2; n--) {
|
|
730
|
+
const r = tryExprWalkRun(
|
|
731
|
+
out.slice(-n) as (Stmt & { k: 'assign' })[],
|
|
732
|
+
n,
|
|
733
|
+
dw,
|
|
734
|
+
ptrVars,
|
|
735
|
+
fnBody,
|
|
736
|
+
declTypes,
|
|
737
|
+
volatileLocals,
|
|
738
|
+
privateNames,
|
|
739
|
+
);
|
|
740
|
+
if (r) {
|
|
741
|
+
return r;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** v3 with the last `n` statements of the body as the step tail and `inits` (the last `n` ahead
|
|
748
|
+
* of the loop) as their inits: one counter, n - 1 walk pointers. */
|
|
749
|
+
function tryExprWalkRun(
|
|
750
|
+
inits: (Stmt & { k: 'assign' })[],
|
|
751
|
+
n: number,
|
|
752
|
+
dw: Stmt & { k: 'dowhile' },
|
|
753
|
+
ptrVars: Map<string, IrType>,
|
|
754
|
+
fnBody: Stmt[],
|
|
755
|
+
declTypes: VarTypes,
|
|
756
|
+
volatileLocals: ReadonlySet<string>,
|
|
757
|
+
privateNames: ReadonlySet<string>,
|
|
758
|
+
): { loop: Stmt; run: number; kept: Stmt[] } | null {
|
|
759
|
+
// BOTH kinds of step as the body's contiguous TAIL (any order): every deref then runs with `i`
|
|
760
|
+
// completed steps equal to each `p`'s, which is what `BASE[i + REST]` states. A step anywhere
|
|
761
|
+
// earlier (the `*++p` walk, a deref straddling the step) reads a different address. The tail
|
|
762
|
+
// and the init run name the same variables, each once.
|
|
763
|
+
const tail = dw.body.slice(-n);
|
|
764
|
+
const tailNames = tail.map((st) => isUnitIncrement(st)!);
|
|
765
|
+
const initNames = inits.map((st) => st.name);
|
|
766
|
+
if (
|
|
767
|
+
new Set(tailNames).size !== n ||
|
|
768
|
+
new Set(initNames).size !== n ||
|
|
769
|
+
!initNames.every((x) => tailNames.includes(x))
|
|
770
|
+
) {
|
|
614
771
|
return null;
|
|
615
772
|
}
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
if (ivT !== undefined && ivT.kind !== 'int') {
|
|
773
|
+
const counters = inits.filter((st) => !ptrVars.has(st.name));
|
|
774
|
+
if (counters.length !== 1) {
|
|
619
775
|
return null;
|
|
620
776
|
}
|
|
621
|
-
|
|
777
|
+
const ivInit = counters[0];
|
|
778
|
+
const iv = ivInit.name;
|
|
779
|
+
if (!mentionsVar(dw.cond, iv)) {
|
|
622
780
|
return null;
|
|
623
781
|
}
|
|
624
|
-
// The counter
|
|
625
|
-
// steps
|
|
626
|
-
|
|
782
|
+
// The counter starts at a CONSTANT S and its ONLY write anywhere in the body is the tail step:
|
|
783
|
+
// k completed steps put each `p` at EXPR + k*W and `i` at S + k, so the deref is
|
|
784
|
+
// BASE[i - S + REST]. That holds while `i` takes the values S + k, which a DECLARED integer
|
|
785
|
+
// counter does: at 32 bits always (it wraps with the address), and narrower only from S = 0 and
|
|
786
|
+
// unsigned, where it names elements 0 … 2^w - 1 before it wraps. A signed narrow counter wraps
|
|
787
|
+
// at 2^(w-1) (an s8 goes 127 -> -128 and names an element 256 behind the walk), and from a
|
|
788
|
+
// nonzero S `i - S` wraps sooner still; an undeclared one has no width to argue from.
|
|
789
|
+
if (ivInit.value.k !== 'const') {
|
|
627
790
|
return null;
|
|
628
791
|
}
|
|
629
|
-
|
|
792
|
+
const start = ivInit.value.value | 0;
|
|
793
|
+
const ivT = declTypes(iv);
|
|
794
|
+
if (ivT?.kind !== 'int' || !(ivT.width === 32 || (start === 0 && !ivT.signed)) || volatileLocals.has(iv)) {
|
|
630
795
|
return null;
|
|
631
796
|
}
|
|
632
|
-
if (
|
|
797
|
+
if (dw.body.slice(0, -n).some((st) => stmtAssigns(st, iv))) {
|
|
633
798
|
return null;
|
|
634
799
|
}
|
|
635
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
800
|
+
// Each walk pointer re-spells on its OWN merits: one out of scope (its init, an off-0 deref, a
|
|
801
|
+
// mention outside the walk, a mention in the condition) keeps walking, init and step in place,
|
|
802
|
+
// beside the ones that re-spell — each `p` is its own address, so deleting one walk changes no
|
|
803
|
+
// other. Which pointers re-spell is therefore a property of each, not of the tail's step order.
|
|
804
|
+
// Dropping one can only remove rewrites, so the loop below shrinks the set to a fixpoint.
|
|
805
|
+
const spellings = new Map<string, ExprWalkSpelling>();
|
|
806
|
+
for (const st of inits) {
|
|
807
|
+
if (st === ivInit || mentionsVar(dw.cond, st.name)) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const s = exprWalkSpelling(st, dw, ptrVars, declTypes, volatileLocals, privateNames);
|
|
811
|
+
if (s !== null) {
|
|
812
|
+
spellings.set(s.p, s);
|
|
646
813
|
}
|
|
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
814
|
}
|
|
654
|
-
|
|
655
|
-
|
|
815
|
+
const idxOf = (s: ExprWalkSpelling): Expr =>
|
|
816
|
+
plusConst(
|
|
817
|
+
s.idx.reduce<Expr>((acc, r) => ({ k: 'bin', op: '+', l: acc, r }), { k: 'var', name: iv }),
|
|
818
|
+
(s.elems - start) | 0,
|
|
819
|
+
);
|
|
820
|
+
while (spellings.size > 0) {
|
|
821
|
+
// rewrite the derefs, then verify nothing of any re-spelled `p` survives
|
|
822
|
+
const offsetDerefs = new Set<string>();
|
|
823
|
+
const rewriteExpr = (e: Expr): Expr => {
|
|
824
|
+
const m = mapExprChildren(e, rewriteExpr);
|
|
825
|
+
const s = m.k === 'index' && m.base.k === 'var' ? spellings.get(m.base.name) : undefined;
|
|
826
|
+
if (m.k === 'index' && s !== undefined) {
|
|
827
|
+
if (m.idx.k !== 'const' || m.idx.value !== 0) {
|
|
828
|
+
offsetDerefs.add(s.p);
|
|
829
|
+
return m;
|
|
830
|
+
}
|
|
831
|
+
return { ...m, base: { ...s.base }, idx: idxOf(s) };
|
|
832
|
+
}
|
|
833
|
+
return m;
|
|
834
|
+
};
|
|
835
|
+
const steps = new Set(tail.filter((st) => spellings.has(isUnitIncrement(st)!)));
|
|
836
|
+
const body = dw.body.filter((st) => !steps.has(st)).map((st) => mapStmtExprs(st, rewriteExpr));
|
|
837
|
+
const dw2: Stmt = { ...dw, body };
|
|
838
|
+
const failed = inits.filter(
|
|
839
|
+
(st) =>
|
|
840
|
+
spellings.has(st.name) &&
|
|
841
|
+
(offsetDerefs.has(st.name) ||
|
|
842
|
+
!confinedToWalk(fnBody, st.name, countMentions([st], st.name), dw) ||
|
|
843
|
+
countMentions([dw2], st.name) !== 0),
|
|
844
|
+
);
|
|
845
|
+
if (failed.length === 0) {
|
|
846
|
+
return { loop: dw2, run: n, kept: inits.filter((st) => !spellings.has(st.name)) };
|
|
847
|
+
}
|
|
848
|
+
failed.forEach((st) => spellings.delete(st.name));
|
|
656
849
|
}
|
|
657
|
-
return
|
|
850
|
+
return null;
|
|
658
851
|
}
|
|
659
852
|
|
|
660
853
|
/** Try the walk→index re-spelling on one function. Returns the transformed COPY when at least
|
|
@@ -679,6 +872,10 @@ export function reindexWalks(
|
|
|
679
872
|
sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
|
|
680
873
|
);
|
|
681
874
|
const storage = nameStorage(sfn);
|
|
875
|
+
// v3's invariant-addend test: the names no callee and no aliasing store can write — a param or a
|
|
876
|
+
// local whose address the function never takes
|
|
877
|
+
const addressTaken = new Set([...walkExprs(sfn.body)].flatMap((e) => (e.k === 'addr' ? [e.name] : [])));
|
|
878
|
+
const privateNames = new Set([...storage].filter(([n, c]) => c !== 'global' && !addressTaken.has(n)).map(([n]) => n));
|
|
682
879
|
for (const v of [...sfn.params, ...sfn.locals]) {
|
|
683
880
|
if (v.type.kind === 'ptr') {
|
|
684
881
|
ptrVars.set(v.name, v.type);
|
|
@@ -781,15 +978,14 @@ export function reindexWalks(
|
|
|
781
978
|
}
|
|
782
979
|
}
|
|
783
980
|
}
|
|
784
|
-
// v3 — the up-counting
|
|
981
|
+
// v3 — the up-counting walk with an expression base: `p = (T *)(EXPR)` immediately
|
|
785
982
|
// ahead of a counter-carried do-while (see tryExprWalk)
|
|
786
983
|
if (s.k === 'dowhile') {
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
out.
|
|
792
|
-
out.push(recurse(dw2));
|
|
984
|
+
const v3 = tryExprWalk(out, s, ptrVars, sfn.body, declTypes, volatileLocals, privateNames);
|
|
985
|
+
if (v3) {
|
|
986
|
+
// the walk inits are subsumed by the indexed spelling; the counter's init stays
|
|
987
|
+
out.splice(out.length - v3.run, v3.run, ...v3.kept);
|
|
988
|
+
out.push(recurse(v3.loop));
|
|
793
989
|
fired++;
|
|
794
990
|
continue;
|
|
795
991
|
}
|
package/src/l3/scopebase.ts
CHANGED
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
|
+
}
|