@asmlift/core 0.8.1 → 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 +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/l3/basecse.ts
CHANGED
|
@@ -248,15 +248,35 @@ function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
/** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local.
|
|
252
|
-
|
|
251
|
+
/** Rewrite every `index`-of-hoistable-base whose key is hoisted so its base becomes the hoist local.
|
|
252
|
+
*
|
|
253
|
+
* A `volatile` cast over a hoisted LEAF keeps its cast and takes the local under it:
|
|
254
|
+
* `((volatile s32 *)0x40000D4)[2]` beside a hoisted `p0 = (s32 *)0x40000D4` becomes
|
|
255
|
+
* `((volatile s32 *)p0)[2]`. The cast is never hoisted itself (`isHoistableBase`), since the local
|
|
256
|
+
* would drop its qualifier; under it the local is the same address, so the access keeps its
|
|
257
|
+
* qualifier and its cell, and reads the register the compiler already holds the base in. Inline, a
|
|
258
|
+
* qualified access at a nonzero offset makes agbcc materialize a second literal with the offset
|
|
259
|
+
* folded in (`.word 0x40000dc` + `ldr r0, [r0]`) where the source read `[r4, #0x8]` off the base
|
|
260
|
+
* it had loaded — the `dmaRegs[2];` that ends a DMA macro. `leafLocal` maps a hoisted leaf to its
|
|
261
|
+
* local, whichever key hoisted it. */
|
|
262
|
+
function rewrite(e: Expr, localFor: Map<string, string>, leafLocal: ReadonlyMap<string, string>): Expr {
|
|
253
263
|
if (e.k === 'index' && isHoistableBase(e.base)) {
|
|
254
264
|
const nm = localFor.get(keyOf(e.base, e.width, e.signed));
|
|
255
265
|
if (nm) {
|
|
256
|
-
return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor) };
|
|
266
|
+
return { ...e, base: { k: 'var', name: nm }, idx: rewrite(e.idx, localFor, leafLocal) };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (e.k === 'index' && e.base.k === 'cast' && e.base.volatile === true && isHoistableLeaf(e.base.e)) {
|
|
270
|
+
const nm = leafLocal.get(leafId(e.base.e));
|
|
271
|
+
if (nm) {
|
|
272
|
+
return {
|
|
273
|
+
...e,
|
|
274
|
+
base: { ...e.base, e: { k: 'var', name: nm } },
|
|
275
|
+
idx: rewrite(e.idx, localFor, leafLocal),
|
|
276
|
+
};
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
|
-
return mapExprChildren(e, (c) => rewrite(c, localFor));
|
|
279
|
+
return mapExprChildren(e, (c) => rewrite(c, localFor, leafLocal));
|
|
260
280
|
}
|
|
261
281
|
|
|
262
282
|
/** One base under consideration, keyed as `(base, width, signedness)`. */
|
|
@@ -784,7 +804,15 @@ export function hoistBaseLocals(
|
|
|
784
804
|
});
|
|
785
805
|
}
|
|
786
806
|
|
|
787
|
-
const
|
|
807
|
+
const leafLocal = new Map<string, string>();
|
|
808
|
+
for (const k of hoisted) {
|
|
809
|
+
const b = meta.get(k)!.base;
|
|
810
|
+
const leaf = leafId(b.k === 'cast' ? b.e : b);
|
|
811
|
+
if (!leafLocal.has(leaf)) {
|
|
812
|
+
leafLocal.set(leaf, localFor.get(k)!);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const rewritten = sfn.body.map((s) => mapStmtExprs(s, (e) => rewrite(e, localFor, leafLocal)));
|
|
788
816
|
// The new inits join the tree's LEADING run of base inits rather than being prepended above it:
|
|
789
817
|
// when rank's /livebase re-runs this pass the head already carries the default run's, and
|
|
790
818
|
// blindly prepending would spell the new base's pool load above locals the compiler loads first.
|
package/src/l3/coalesce.ts
CHANGED
|
@@ -12,23 +12,24 @@
|
|
|
12
12
|
// and whether fall-through joins the two arms onto one path. `coalesceCandidates` offers both.
|
|
13
13
|
import { typeToString } from '../ir/types';
|
|
14
14
|
import type { Expr, SFn, Stmt } from './ast';
|
|
15
|
-
import { exprChildren, isLoop, mapExprChildren, stmtChildren, stmtExprs } from './ast';
|
|
15
|
+
import { exprChildren, isLoop, mapExprChildren, mentionedName, stmtChildren, stmtExprs } from './ast';
|
|
16
16
|
import { type Gate, firstRejection } from './gates';
|
|
17
17
|
|
|
18
18
|
function namesIn(e: Expr, out: Set<string>): void {
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// ever built for it.
|
|
19
|
+
// EVERY mention, through `mentionedName`'s vocabulary rather than this file's own: a span that
|
|
20
|
+
// misses one is SHORT, and a short span is a clobber where a long one is only a missed merge.
|
|
21
|
+
// `&sp0` is a LOCAL's mention — the structurer renders an `laddr` frame object as an addr node
|
|
22
|
+
// over a name that IS in `sfn.locals` — and `n++` in a bottom test is a counter's only mention
|
|
23
|
+
// inside its own loop. A global name comes along for the ride and costs nothing — it is not in
|
|
24
|
+
// `sfn.locals`, so no pair is ever built for it.
|
|
25
25
|
//
|
|
26
|
-
// `rename` DISAGREES WITH THIS, and knowingly: it rewrites
|
|
27
|
-
// absorbed while mentioned through `&` leaves that mention standing against a declaration
|
|
28
|
-
// merge deleted. Reconciling the two changes which candidates compile — a measured change, not a
|
|
26
|
+
// `rename` DISAGREES WITH THIS, and knowingly: it rewrites `var` and `postincr` leaves, so a
|
|
27
|
+
// local absorbed while mentioned through `&` leaves that mention standing against a declaration
|
|
28
|
+
// the merge deleted. Reconciling the two changes which candidates compile — a measured change, not a
|
|
29
29
|
// cleanup — so today's behaviour is pinned exactly in coalesce.test.ts ('rename') rather than
|
|
30
30
|
// repaired here.
|
|
31
|
-
|
|
31
|
+
const n = mentionedName(e);
|
|
32
|
+
if (n !== undefined) out.add(n);
|
|
32
33
|
for (const c of exprChildren(e)) namesIn(c, out);
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -62,7 +63,14 @@ export interface Span {
|
|
|
62
63
|
* arithmetic over the variable is its own history. The read must be a `var`: `mentions` counts an
|
|
63
64
|
* `addr` too, and `&a` is not a read of `a`. */
|
|
64
65
|
const readsVarArithmetically = (e: Expr, n: string): boolean => {
|
|
65
|
-
if (
|
|
66
|
+
if (
|
|
67
|
+
e.k === 'index' ||
|
|
68
|
+
e.k === 'field' ||
|
|
69
|
+
e.k === 'call' ||
|
|
70
|
+
e.k === 'marker' ||
|
|
71
|
+
e.k === 'postincr' ||
|
|
72
|
+
e.k === 'addr'
|
|
73
|
+
) {
|
|
66
74
|
return false;
|
|
67
75
|
}
|
|
68
76
|
return (e.k === 'var' && e.name === n) || exprChildren(e).some((c) => readsVarArithmetically(c, n));
|
|
@@ -155,7 +163,7 @@ function spans(body: Stmt[]): Map<string, Span> {
|
|
|
155
163
|
}
|
|
156
164
|
function rename(body: Stmt[], from: string, to: string): Stmt[] {
|
|
157
165
|
const inExpr = (e: Expr): Expr =>
|
|
158
|
-
e.k === 'var' && e.name === from ? { ...e, name: to } : mapExprChildren(e, inExpr);
|
|
166
|
+
(e.k === 'var' || e.k === 'postincr') && e.name === from ? { ...e, name: to } : mapExprChildren(e, inExpr);
|
|
159
167
|
const inStmt = (s: Stmt): Stmt => {
|
|
160
168
|
const r = { ...s } as Record<string, unknown>;
|
|
161
169
|
if (s.k === 'assign' && s.name === from) r.name = to;
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// asmlift — a local aggregate's constant INITIALIZER, recovered from the copy that fills it.
|
|
2
|
+
//
|
|
3
|
+
// `u8 a[16] = { 0, 1, 2, … };` is not a statement in the machine. A compiler that keeps the
|
|
4
|
+
// aggregate in memory places the initial image as a constant of its own, under a label the source
|
|
5
|
+
// never wrote, and copies it into the frame at entry: agbcc 2.9 writes `.LC0:` in .rodata and
|
|
6
|
+
// `ldr r1,=.LC0; mov r0,sp; mov r2,#16; bl memcpy` (varasm.c `output_constant_def`, expr.c
|
|
7
|
+
// `store_constructor` → `emit_block_move`). The lift reads that image as an ANONYMOUS object
|
|
8
|
+
// (ir/core.ts `LocalObject.anonymous`), so the tree holds the copy as written —
|
|
9
|
+
// `memcpy(sp0, .LC0, 16)` — and the image in `SFn.images`, which no declaration spells. This pass
|
|
10
|
+
// spells the pair back as the declaration it came from: the local's initializer, and no copy.
|
|
11
|
+
//
|
|
12
|
+
// IT RUNS AT EMISSION, NOT IN STRUCTURE. Between the two the tree belongs to the contracts and the
|
|
13
|
+
// respell variations, and every one of them is right about the copy as it stands: it is a call the
|
|
14
|
+
// asm makes (contracts.ts `assertEffectsPreserved` counts it), and it takes the local's address,
|
|
15
|
+
// which is what keeps dead-store elimination and the coalescers off the object. A folded tree
|
|
16
|
+
// would hold a local nothing mentions, which l3/dce.ts deletes — and with it the initializer the
|
|
17
|
+
// compiler keeps (agbcc copies `u8 a[16] = {…}` even when nothing reads `a`). So structure()
|
|
18
|
+
// asks this pass whether the fold is possible and refuses the function loudly where it is not,
|
|
19
|
+
// and the C-family emitter folds (backend/cfamily.ts), the same seam l3/slotorder.ts uses.
|
|
20
|
+
//
|
|
21
|
+
// THE FOLD IS SOUND exactly when the declaration's initializer and the copy are one program:
|
|
22
|
+
// • the copy is a block copy the C standard fixes (proto.ts `blockCopyRoles`), a TOP-LEVEL
|
|
23
|
+
// statement of the body, so it runs once and on every path that reaches it;
|
|
24
|
+
// • its destination is a whole local integer array of the copied size — or a reservation the
|
|
25
|
+
// frame rounded up by less than a word, reached by nothing but elements inside the copy and its
|
|
26
|
+
// whole address — not volatile, and nothing before the copy mentions it but a local assigned
|
|
27
|
+
// its address alone, so moving the copy to the declaration changes no value anything reads (an
|
|
28
|
+
// initializer runs at entry, the copy at its own position);
|
|
29
|
+
// • its source is the whole image, which nothing but such copies mentions, and whose size is the
|
|
30
|
+
// count — one image may fill several locals, since the compiler places one per identical
|
|
31
|
+
// constant (varasm.c `output_constant_def`), each into its own local. Several arrays in one
|
|
32
|
+
// frame are each their own local where the frame audit proves the boundaries between them
|
|
33
|
+
// (frontend/frame-objects.ts `provenParts`). KNOWN GAP: where it keeps the frame's
|
|
34
|
+
// address-taken bytes as one local instead — a neighbour handed to a callee nothing bounds, or
|
|
35
|
+
// a compiler whose placement of such arrays is not known (target.ts `addressTakenArrayOrder`)
|
|
36
|
+
// — the image refuses at the size check;
|
|
37
|
+
// • the image writes one element width (a struct's mixed widths, or padding alone, has no
|
|
38
|
+
// element type this pass could declare). An element it relocates holds an address, spelled as
|
|
39
|
+
// the address constant it is (`u32 a[16] = { (u32)&h1, … }`, which agbcc copies from the same
|
|
40
|
+
// `.word h1` image as the pointer array the source declared, compiled).
|
|
41
|
+
// The local keeps its own element type where the body reaches it by an offset or a cast — those
|
|
42
|
+
// were spelled against it, so its width must be the image's — and takes the image's where only
|
|
43
|
+
// its whole address and element accesses, which each print their own width, reach it.
|
|
44
|
+
//
|
|
45
|
+
// Thumb is the only frontend that reads an anonymous image today. The rule is asked of the tree,
|
|
46
|
+
// for any compiler, but the only copy it reads is a call (proto.ts `blockCopyRoles`). KNOWN GAP: an
|
|
47
|
+
// INLINE copy — agbcc's `ldmia`/`stmia` for a word-aligned image below its memcpy sizes, lw/sw or
|
|
48
|
+
// lwl/lwr+swl/swr (IDO, gcc 2.7.2, KMC), lwz/stw (mwcc) — declines, naming the copy it does not
|
|
49
|
+
// read; no benchmark row has one, and the MIPS and PPC frontends refuse the address-taken frame
|
|
50
|
+
// local before the copy is reached.
|
|
51
|
+
import { type IrType, typeToString } from '../ir/types';
|
|
52
|
+
import { blockCopyRoles } from '../proto';
|
|
53
|
+
import {
|
|
54
|
+
type Expr,
|
|
55
|
+
type InitAddress,
|
|
56
|
+
type SFn,
|
|
57
|
+
type SImage,
|
|
58
|
+
type Stmt,
|
|
59
|
+
mapExprChildren,
|
|
60
|
+
mapStmtExprs,
|
|
61
|
+
mentionedName,
|
|
62
|
+
walkExprs,
|
|
63
|
+
} from './ast';
|
|
64
|
+
import { localMentions, mentionsAnyLocal } from './mentions';
|
|
65
|
+
|
|
66
|
+
/** `e` without the casts around it. */
|
|
67
|
+
function bare(e: Expr): Expr {
|
|
68
|
+
return e.k === 'cast' ? bare(e.e) : e;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The value of `e` where it is integer arithmetic on constants alone, or null. A copy's count is
|
|
72
|
+
* a constant the machine may build in several instructions — agbcc spells 256 and up as an 8-bit
|
|
73
|
+
* immediate shifted (`mov r2,#0x80; lsl r2,r2,#1`), which the tree keeps as `128 << 1`. */
|
|
74
|
+
function constantValue(e: Expr): number | null {
|
|
75
|
+
const b = bare(e);
|
|
76
|
+
if (b.k === 'const') {
|
|
77
|
+
return b.value;
|
|
78
|
+
}
|
|
79
|
+
if (b.k !== 'bin') {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const l = constantValue(b.l);
|
|
83
|
+
const r = constantValue(b.r);
|
|
84
|
+
if (l === null || r === null) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
switch (b.op) {
|
|
88
|
+
case '+':
|
|
89
|
+
return l + r;
|
|
90
|
+
case '-':
|
|
91
|
+
return l - r;
|
|
92
|
+
case '*':
|
|
93
|
+
return l * r;
|
|
94
|
+
case '<<':
|
|
95
|
+
return r >= 0 && r < 32 ? l * 2 ** r : null;
|
|
96
|
+
case '|':
|
|
97
|
+
return l >= 0 && r >= 0 && l < 2 ** 31 && r < 2 ** 31 ? l | r : null;
|
|
98
|
+
default:
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The object `e` names whole — `x` or `&x` — or null. */
|
|
104
|
+
function wholeObject(e: Expr): string | null {
|
|
105
|
+
const b = bare(e);
|
|
106
|
+
return b.k === 'postincr' ? null : (mentionedName(b) ?? null);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Every mention of `name` as an expression in `body`. */
|
|
110
|
+
function mentions(body: readonly Stmt[], name: string): number {
|
|
111
|
+
let n = 0;
|
|
112
|
+
for (const e of walkExprs(body as Stmt[])) {
|
|
113
|
+
if (mentionedName(e) === name) {
|
|
114
|
+
n++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return n;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const byteSize = (t: IrType): number | null =>
|
|
121
|
+
t.kind === 'int'
|
|
122
|
+
? t.width / 8
|
|
123
|
+
: t.kind === 'array'
|
|
124
|
+
? (() => {
|
|
125
|
+
const e = byteSize(t.elem);
|
|
126
|
+
return e === null ? null : e * t.count;
|
|
127
|
+
})()
|
|
128
|
+
: null;
|
|
129
|
+
|
|
130
|
+
/** `v`, an element of `width` bytes, read at the given signedness. */
|
|
131
|
+
function resign(v: number, width: number, signed: boolean): number {
|
|
132
|
+
const bits = 8 * width;
|
|
133
|
+
const u = ((v % 2 ** bits) + 2 ** bits) % 2 ** bits;
|
|
134
|
+
return signed && u >= 2 ** (bits - 1) ? u - 2 ** bits : u;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** One copy's fold: the copy statement's index, the local and its declaration. */
|
|
138
|
+
interface Fold {
|
|
139
|
+
at: number;
|
|
140
|
+
local: string;
|
|
141
|
+
type: IrType;
|
|
142
|
+
init: (number | InitAddress | null)[];
|
|
143
|
+
string: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Why `image` cannot fold, as the tail of a sentence naming it, or the fold of each copy of it. */
|
|
147
|
+
function planFolds(sfn: SFn, image: SImage): Fold[] | string {
|
|
148
|
+
if (image.type.kind !== 'array' || image.type.elem.kind !== 'int') {
|
|
149
|
+
return 'whose image writes no one element width — a struct, or padding alone';
|
|
150
|
+
}
|
|
151
|
+
const copies: number[] = [];
|
|
152
|
+
sfn.body.forEach((s, i) => {
|
|
153
|
+
if (
|
|
154
|
+
s.k === 'exprstmt' &&
|
|
155
|
+
s.value.k === 'call' &&
|
|
156
|
+
wholeObject(s.value.args[blockCopyRoles(s.value.fn)?.src ?? -1] ?? { k: 'const', value: 0 }) === image.name
|
|
157
|
+
) {
|
|
158
|
+
copies.push(i);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
if (copies.length === 0) {
|
|
162
|
+
return (
|
|
163
|
+
'which no top-level block-copy call (memcpy) takes as its source — the one copy folded into an ' +
|
|
164
|
+
'initializer; an inline, element-by-element copy of the image, and any other read of it, is not'
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (mentions(sfn.body, image.name) !== copies.length) {
|
|
168
|
+
return 'which the body reads other than as the source of a block copy at the top level';
|
|
169
|
+
}
|
|
170
|
+
const folds: Fold[] = [];
|
|
171
|
+
for (const at of copies) {
|
|
172
|
+
const fold = planCopy(sfn, image, at);
|
|
173
|
+
if (typeof fold === 'string') {
|
|
174
|
+
return fold;
|
|
175
|
+
}
|
|
176
|
+
folds.push(fold);
|
|
177
|
+
}
|
|
178
|
+
return folds;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Why the block copy of `image` at body index `at` cannot fold, or its fold. */
|
|
182
|
+
function planCopy(sfn: SFn, image: SImage, at: number): Fold | string {
|
|
183
|
+
const elem = (image.type as Extract<IrType, { kind: 'array' }>).elem as Extract<IrType, { kind: 'int' }>;
|
|
184
|
+
const size = byteSize(image.type)!;
|
|
185
|
+
const call = (sfn.body[at] as Extract<Stmt, { k: 'exprstmt' }>).value as Extract<Expr, { k: 'call' }>;
|
|
186
|
+
const roles = blockCopyRoles(call.fn)!;
|
|
187
|
+
if (constantValue(call.args[roles.n] ?? { k: 'var', name: '' }) !== size) {
|
|
188
|
+
return `whose copy does not move its whole ${size} bytes`;
|
|
189
|
+
}
|
|
190
|
+
const local = wholeObject(call.args[roles.dst] ?? { k: 'const', value: 0 });
|
|
191
|
+
const decl = sfn.locals.find((l) => l.name === local);
|
|
192
|
+
if (local === null || decl === undefined) {
|
|
193
|
+
return 'whose copy does not fill a local of this function';
|
|
194
|
+
}
|
|
195
|
+
if (decl.init !== undefined) {
|
|
196
|
+
return `whose copy fills '${local}', which another image already initializes`;
|
|
197
|
+
}
|
|
198
|
+
if (decl.volatile || decl.pointeeVolatile) {
|
|
199
|
+
return `whose copy fills '${local}', which is volatile`;
|
|
200
|
+
}
|
|
201
|
+
const declElem = decl.type.kind === 'array' ? decl.type.elem : null;
|
|
202
|
+
const declSize = byteSize(decl.type);
|
|
203
|
+
// The copy moves the whole object — a brace initializer's image carries its zero tail — so the
|
|
204
|
+
// count is the object's size, and a reservation the frame audit measured wider by less than a
|
|
205
|
+
// word is the frame's own rounding (agbcc rounds a 6-byte array's frame to 8).
|
|
206
|
+
if (declElem === null || declElem.kind !== 'int' || declSize === null || declSize < size || declSize - size >= 4) {
|
|
207
|
+
return `whose copy fills '${local}', a ${typeToString(decl.type)}, which is not an integer array of its ${size} bytes`;
|
|
208
|
+
}
|
|
209
|
+
// A local that holds nothing but the local's address — `p0 = (u8 *)&sp0;`, the base the
|
|
210
|
+
// readability hoist (l3/basecse.ts) places at the head of the body, ahead of the copy — reads no
|
|
211
|
+
// element where it is assigned, so the copy may move past it; its uses are the local's.
|
|
212
|
+
const aliasAt = aliasesOf(sfn, local);
|
|
213
|
+
const reach = new Set([local, ...aliasAt.keys()]);
|
|
214
|
+
const aliasing = new Set(aliasAt.values());
|
|
215
|
+
const kept = sfn.body.filter((_, i) => i !== at && !aliasing.has(i));
|
|
216
|
+
if (
|
|
217
|
+
mentionsAnyLocal(
|
|
218
|
+
sfn.body.slice(0, at).filter((_, i) => !aliasing.has(i)),
|
|
219
|
+
reach,
|
|
220
|
+
)
|
|
221
|
+
) {
|
|
222
|
+
return `whose copy into '${local}' is not the first thing the body does with it`;
|
|
223
|
+
}
|
|
224
|
+
const use = uses(kept, reach);
|
|
225
|
+
const count = size / (elem.width / 8);
|
|
226
|
+
// Reached as anything but elements, the local may be reached past the bytes copied — the tail a
|
|
227
|
+
// fill writes after a short string's copy — so the reservation must be the copy's exactly.
|
|
228
|
+
if (use.other && byteSize(decl.type) !== size) {
|
|
229
|
+
return `whose copy fills '${local}', a ${typeToString(decl.type)} the body reaches past the ${size} bytes copied`;
|
|
230
|
+
}
|
|
231
|
+
if (use.extents.some(([from, to]) => from < 0 || to > size)) {
|
|
232
|
+
return `whose copy fills '${local}', which the body indexes past the ${size} bytes copied`;
|
|
233
|
+
}
|
|
234
|
+
// An offset or a cast was spelled against the local's own element, so the image's must match and
|
|
235
|
+
// the local keeps its type. Element accesses print their own width, so where only they and the
|
|
236
|
+
// whole address reach it, the declaration (the frame audit may type it by bytes alone) witnesses
|
|
237
|
+
// nothing: the image says the element, and its signedness where it writes a negative (agbcc does
|
|
238
|
+
// only for a signed element), else the reads of that width do.
|
|
239
|
+
let own: Extract<IrType, { kind: 'int' }>;
|
|
240
|
+
if (use.other) {
|
|
241
|
+
if (declElem.width !== elem.width) {
|
|
242
|
+
return `whose ${elem.width / 8}-byte elements are not the ${declElem.width / 8}-byte elements the body reaches '${local}' by`;
|
|
243
|
+
}
|
|
244
|
+
own = declElem;
|
|
245
|
+
} else {
|
|
246
|
+
const signs = new Set(use.reads.filter((r) => r.width * 8 === elem.width).map((r) => r.signed));
|
|
247
|
+
const signed =
|
|
248
|
+
elem.signed ||
|
|
249
|
+
(signs.size === 1 ? [...signs][0] : declElem.width === elem.width && use.indexed > 0 && declElem.signed);
|
|
250
|
+
own = { ...elem, signed };
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
at,
|
|
254
|
+
local,
|
|
255
|
+
type: { kind: 'array', elem: own, count },
|
|
256
|
+
// an address is the address whatever the element's signedness: its cast spells that
|
|
257
|
+
init: image.init.map((v) => (typeof v === 'number' ? resign(v, elem.width / 8, own.signed) : v)),
|
|
258
|
+
string: image.string === true && elem.width === 8,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The locals of `sfn` that hold `local`'s address and nothing else — assigned it once, at the top
|
|
263
|
+
* level of the body, and never written through `&` — each with the index of that assignment. */
|
|
264
|
+
function aliasesOf(sfn: SFn, local: string): Map<string, number> {
|
|
265
|
+
const counts = localMentions(sfn);
|
|
266
|
+
const out = new Map<string, number>();
|
|
267
|
+
sfn.body.forEach((s, i) => {
|
|
268
|
+
const m = s.k === 'assign' ? counts.get(s.name) : undefined;
|
|
269
|
+
if (
|
|
270
|
+
s.k === 'assign' &&
|
|
271
|
+
s.name !== local &&
|
|
272
|
+
m?.assigns === 1 &&
|
|
273
|
+
m.addrTaken === 0 &&
|
|
274
|
+
wholeObject(s.value) === local
|
|
275
|
+
) {
|
|
276
|
+
out.set(s.name, i);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** How `body` reaches the object the names in `reach` all denote whole: the byte extent of each
|
|
283
|
+
* direct element access with a constant index (`sp0[k]`, `((u8 *)sp0)[k]`, `p0[k]` — the index
|
|
284
|
+
* node carries its own width), how many of those there are in all, and whether anything reaches
|
|
285
|
+
* it other than as a whole call argument or a direct element access. */
|
|
286
|
+
function uses(
|
|
287
|
+
body: readonly Stmt[],
|
|
288
|
+
reach: ReadonlySet<string>,
|
|
289
|
+
): { indexed: number; extents: [number, number][]; reads: { width: number; signed: boolean }[]; other: boolean } {
|
|
290
|
+
let total = 0;
|
|
291
|
+
let accounted = 0;
|
|
292
|
+
let indexed = 0;
|
|
293
|
+
const extents: [number, number][] = [];
|
|
294
|
+
const reads: { width: number; signed: boolean }[] = [];
|
|
295
|
+
const reaches = (e: Expr): boolean => {
|
|
296
|
+
const n = wholeObject(e);
|
|
297
|
+
return n !== null && reach.has(n);
|
|
298
|
+
};
|
|
299
|
+
for (const e of walkExprs(body as Stmt[])) {
|
|
300
|
+
const n = mentionedName(e);
|
|
301
|
+
if (n !== undefined && reach.has(n)) {
|
|
302
|
+
total++;
|
|
303
|
+
}
|
|
304
|
+
if (e.k === 'call') {
|
|
305
|
+
accounted += e.args.filter(reaches).length;
|
|
306
|
+
}
|
|
307
|
+
if (e.k === 'index' && reaches(e.base)) {
|
|
308
|
+
accounted++;
|
|
309
|
+
indexed++;
|
|
310
|
+
reads.push({ width: e.width, signed: e.signed });
|
|
311
|
+
const k = bare(e.idx);
|
|
312
|
+
if (k.k === 'const') {
|
|
313
|
+
extents.push([k.value * e.width, (k.value + 1) * e.width]);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { indexed, extents, reads, other: total !== accounted };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Whether every anonymous image `sfn` holds folds into a local's initializer: null when it does,
|
|
321
|
+
* else the image's name and the tail of the sentence that refuses it. */
|
|
322
|
+
export function constantImageRefusal(sfn: SFn): { image: string; refused: string } | null {
|
|
323
|
+
for (const image of sfn.images ?? []) {
|
|
324
|
+
const plan = planFolds(sfn, image);
|
|
325
|
+
if (typeof plan === 'string') {
|
|
326
|
+
return { image: image.name, refused: plan };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** `e` with every access to a folded local that reads one of its elements AS that element spelled
|
|
333
|
+
* `a[k]` on the local itself. The structurer spelled the access against the type the frame audit
|
|
334
|
+
* gave the local — bytes, unsigned — so a read of the element type this pass gives it (`s8`, from
|
|
335
|
+
* the image) came out through the local's address and a cast, `((s8 *)a)[5]`. The two are one C
|
|
336
|
+
* value and not one compile: agbcc re-forms `sp + 0x15` for the cast where `b[5]` reads through
|
|
337
|
+
* the base it already holds for `b`'s copy (`ldrsb r0, [r5, r0]`). An access of another width or
|
|
338
|
+
* signedness keeps its cast, which is the element type it spells. */
|
|
339
|
+
function asElement(e: Expr, by: ReadonlyMap<string, Fold>): Expr {
|
|
340
|
+
const inner = mapExprChildren(e, (c) => asElement(c, by));
|
|
341
|
+
if (inner.k !== 'index' || (inner.lead?.length ?? 0) > 0 || inner.baseElem !== undefined) {
|
|
342
|
+
return inner;
|
|
343
|
+
}
|
|
344
|
+
// the object the base names whole, as the fold's own census of element accesses reads it (`uses`)
|
|
345
|
+
const name = wholeObject(inner.base);
|
|
346
|
+
const fold = name === null ? undefined : by.get(name);
|
|
347
|
+
const elem = fold?.type.kind === 'array' ? fold.type.elem : undefined;
|
|
348
|
+
if (elem?.kind !== 'int' || elem.width !== inner.width * 8 || elem.signed !== inner.signed) {
|
|
349
|
+
return inner;
|
|
350
|
+
}
|
|
351
|
+
return { ...inner, base: { k: 'var', name: name! }, subscript: true };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** `sfn` with every anonymous image folded into the initializer of each local its copies fill,
|
|
355
|
+
* the copies gone and the image with them. Throws where one does not fold: structure() refused
|
|
356
|
+
* that function already, so a tree that reaches here unfoldable was made so by a later pass. */
|
|
357
|
+
export function foldConstantImages(sfn: SFn): SFn {
|
|
358
|
+
if (sfn.images === undefined) {
|
|
359
|
+
return sfn;
|
|
360
|
+
}
|
|
361
|
+
let out: SFn = { ...sfn };
|
|
362
|
+
delete out.images;
|
|
363
|
+
for (const image of sfn.images) {
|
|
364
|
+
const plan = planFolds(out, image);
|
|
365
|
+
if (typeof plan === 'string') {
|
|
366
|
+
throw new Error(`cannot spell '${sfn.name}': the compiler's anonymous constant '${image.name}' ${plan}`);
|
|
367
|
+
}
|
|
368
|
+
const copies = new Set(plan.map((f) => f.at));
|
|
369
|
+
const by = new Map(plan.map((f) => [f.local, f]));
|
|
370
|
+
out = {
|
|
371
|
+
...out,
|
|
372
|
+
body: out.body.filter((_, i) => !copies.has(i)).map((st) => mapStmtExprs(st, (e) => asElement(e, by))),
|
|
373
|
+
locals: out.locals.map((l) => {
|
|
374
|
+
const f = by.get(l.name);
|
|
375
|
+
return f === undefined
|
|
376
|
+
? l
|
|
377
|
+
: { ...l, type: f.type, init: f.init, ...(f.string ? { initString: true as const } : {}) };
|
|
378
|
+
}),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
package/src/l3/dce.ts
CHANGED
|
@@ -25,13 +25,15 @@
|
|
|
25
25
|
// unresolved `?` value trips the contract first and never reaches DCE; `mustKeep` treating `?` as
|
|
26
26
|
// keep is defense-in-depth for any future caller that skips that check.
|
|
27
27
|
import type { Expr, SFn, Stmt } from './ast';
|
|
28
|
-
import { exprChildren, negateCond, stmtChildren, stmtExprs } from './ast';
|
|
28
|
+
import { exprChildren, keepsHelperRel, negateCond, stmtChildren, stmtExprs } from './ast';
|
|
29
29
|
|
|
30
30
|
/** Accumulate every LOCAL-eligible `var` name read anywhere in `e` (recurses all sub-exprs). An
|
|
31
31
|
* `addr` name counts too: taking a local's address makes every store to it observable through the
|
|
32
|
-
* escaped pointer, so an address-taken local is never dead here.
|
|
32
|
+
* escaped pointer, so an address-taken local is never dead here. A `postincr` reads its name
|
|
33
|
+
* before it writes it, so it pins the store that reaches it — `n++` in a loop test is the only
|
|
34
|
+
* reader of the `n = 0` above the loop. */
|
|
33
35
|
function readsInto(e: Expr, out: Set<string>): void {
|
|
34
|
-
if (e.k === 'var' || e.k === 'addr') {
|
|
36
|
+
if (e.k === 'var' || e.k === 'addr' || e.k === 'postincr') {
|
|
35
37
|
out.add(e.name);
|
|
36
38
|
}
|
|
37
39
|
for (const c of exprChildren(e)) {
|
|
@@ -48,6 +50,7 @@ function reads(e: Expr): Set<string> {
|
|
|
48
50
|
/** True if `e` carries any reason NOT to speculatively delete its assignment/condition:
|
|
49
51
|
* - `call` — may write memory/globals (a side effect);
|
|
50
52
|
* - `marker` — the annotate-mode ASMLIFT_ERROR gap signal, which must survive so the gap stays loud;
|
|
53
|
+
* - `postincr` — its write is the loop update; deleting the assignment would delete the update;
|
|
51
54
|
* - the strict-mode `?` unresolved sentinel (`{k:'var', name:'?'}`) — dropping it would let a
|
|
52
55
|
* value asmlift could NOT lift slip past `assertResolved`, silently downgrading a loud gap;
|
|
53
56
|
* - a memory load (`index`/`field`) — a deref's volatility is unknowable here, so a possibly-effectful read
|
|
@@ -56,7 +59,7 @@ function reads(e: Expr): Set<string> {
|
|
|
56
59
|
* observable access the machine performed; deleting the dead assignment would delete the read.
|
|
57
60
|
* A dead assignment whose value contains any of these is kept. */
|
|
58
61
|
function mustKeep(e: Expr, volatiles: ReadonlySet<string>): boolean {
|
|
59
|
-
if (e.k === 'call' || e.k === 'marker' || e.k === 'index' || e.k === 'field') {
|
|
62
|
+
if (e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || e.k === 'index' || e.k === 'field') {
|
|
60
63
|
return true;
|
|
61
64
|
}
|
|
62
65
|
if (e.k === 'var' && (e.name === '?' || volatiles.has(e.name))) {
|
|
@@ -95,6 +98,16 @@ function allAddrNamesInto(stmts: Stmt[], out: Set<string>): void {
|
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
/** Where a `break` and a `continue` at this depth land, as the set live there. A jump does not fall
|
|
102
|
+
* through to the statement after it: the live set at a jump is its TARGET's, not what the rest of
|
|
103
|
+
* the enclosing block reads. `break` targets the innermost loop or switch, `continue` the innermost
|
|
104
|
+
* loop; each is that statement's conservative in-scope set (everything read anywhere in it plus
|
|
105
|
+
* what is live after it), which also covers the loop's own next test and update. */
|
|
106
|
+
interface JumpLive {
|
|
107
|
+
brk: ReadonlySet<string>;
|
|
108
|
+
cont?: ReadonlySet<string>; // absent in a switch no loop encloses
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
/** Backward live-variable walk over one block. `liveOut` is the set of locals live on exit;
|
|
99
112
|
* returns the rewritten block and the set live on entry. */
|
|
100
113
|
function dceBlock(
|
|
@@ -102,6 +115,7 @@ function dceBlock(
|
|
|
102
115
|
liveOut: ReadonlySet<string>,
|
|
103
116
|
locals: ReadonlySet<string>,
|
|
104
117
|
volatiles: ReadonlySet<string>,
|
|
118
|
+
jumps: JumpLive | null,
|
|
105
119
|
): { out: Stmt[]; liveIn: Set<string> } {
|
|
106
120
|
const live = new Set(liveOut);
|
|
107
121
|
const rev: Stmt[] = [];
|
|
@@ -153,12 +167,19 @@ function dceBlock(
|
|
|
153
167
|
}
|
|
154
168
|
case 'break':
|
|
155
169
|
case 'continue': {
|
|
170
|
+
// A mid-body `if (c) { v = x; break; }` carries the loop's exit value in `v`, while the
|
|
171
|
+
// statements after the `if` overwrite `v` for the next iteration. Leaving `live` as the
|
|
172
|
+
// fall-through set would judge the carried copy dead and drop it.
|
|
173
|
+
const target = s.k === 'break' ? jumps?.brk : jumps?.cont;
|
|
174
|
+
if (target) {
|
|
175
|
+
setLive(new Set(target));
|
|
176
|
+
}
|
|
156
177
|
rev.push(s);
|
|
157
178
|
break;
|
|
158
179
|
}
|
|
159
180
|
case 'if': {
|
|
160
|
-
const t = dceBlock(s.then, live, locals, volatiles);
|
|
161
|
-
const e = dceBlock(s.else, live, locals, volatiles);
|
|
181
|
+
const t = dceBlock(s.then, live, locals, volatiles, jumps);
|
|
182
|
+
const e = dceBlock(s.else, live, locals, volatiles, jumps);
|
|
162
183
|
const nlive = new Set<string>();
|
|
163
184
|
for (const r of reads(s.cond)) {
|
|
164
185
|
nlive.add(r);
|
|
@@ -175,7 +196,7 @@ function dceBlock(
|
|
|
175
196
|
if (mustKeep(s.cond, volatiles)) {
|
|
176
197
|
rev.push({ k: 'exprstmt', value: s.cond });
|
|
177
198
|
}
|
|
178
|
-
} else if (t.out.length === 0) {
|
|
199
|
+
} else if (t.out.length === 0 && !keepsHelperRel(s.cond)) {
|
|
179
200
|
rev.push({ k: 'if', cond: negateCond(s.cond), then: e.out, else: [] });
|
|
180
201
|
} else {
|
|
181
202
|
rev.push({ k: 'if', cond: s.cond, then: t.out, else: e.out });
|
|
@@ -188,7 +209,7 @@ function dceBlock(
|
|
|
188
209
|
// loop-carried store is never cut. Body DCE removes only what is dead on EVERY path.
|
|
189
210
|
const loopLive = new Set(live);
|
|
190
211
|
allReadsInto([s], loopLive);
|
|
191
|
-
const b = dceBlock(s.body, loopLive, locals, volatiles);
|
|
212
|
+
const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
|
|
192
213
|
const nlive = new Set(loopLive);
|
|
193
214
|
for (const r of b.liveIn) {
|
|
194
215
|
nlive.add(r);
|
|
@@ -200,7 +221,7 @@ function dceBlock(
|
|
|
200
221
|
case 'for': {
|
|
201
222
|
const loopLive = new Set(live);
|
|
202
223
|
allReadsInto([s], loopLive);
|
|
203
|
-
const b = dceBlock(s.body, loopLive, locals, volatiles);
|
|
224
|
+
const b = dceBlock(s.body, loopLive, locals, volatiles, { brk: loopLive, cont: loopLive });
|
|
204
225
|
const nlive = new Set(loopLive);
|
|
205
226
|
for (const r of b.liveIn) {
|
|
206
227
|
nlive.add(r);
|
|
@@ -214,8 +235,9 @@ function dceBlock(
|
|
|
214
235
|
// read anywhere in the switch as live throughout — no case-body store is ever cut.
|
|
215
236
|
const swLive = new Set(live);
|
|
216
237
|
allReadsInto([s], swLive);
|
|
217
|
-
const
|
|
218
|
-
const
|
|
238
|
+
const inSwitch: JumpLive = { brk: swLive, cont: jumps?.cont };
|
|
239
|
+
const cases = s.cases.map((c) => ({ ...c, body: dceBlock(c.body, swLive, locals, volatiles, inSwitch).out }));
|
|
240
|
+
const def = s.default ? dceBlock(s.default, swLive, locals, volatiles, inSwitch).out : s.default;
|
|
219
241
|
const nlive = new Set(swLive);
|
|
220
242
|
for (const r of reads(s.scrutinee)) {
|
|
221
243
|
nlive.add(r);
|
|
@@ -263,7 +285,7 @@ export function eliminateDeadStores(sfn: SFn): SFn {
|
|
|
263
285
|
allAddrNamesInto(sfn.body, addressTaken);
|
|
264
286
|
const volatiles = new Set(sfn.locals.filter((l) => l.volatile).map((l) => l.name));
|
|
265
287
|
const locals = new Set(sfn.locals.filter((l) => !l.volatile && !addressTaken.has(l.name)).map((l) => l.name));
|
|
266
|
-
const body = dceBlock(sfn.body, new Set<string>(), locals, volatiles).out;
|
|
288
|
+
const body = dceBlock(sfn.body, new Set<string>(), locals, volatiles, null).out;
|
|
267
289
|
const used = new Set<string>();
|
|
268
290
|
referencedNames(body, used);
|
|
269
291
|
return { ...sfn, body, locals: sfn.locals.filter((l) => used.has(l.name) || l.volatile) };
|
package/src/l3/gates.ts
CHANGED
|
@@ -19,8 +19,9 @@ export interface Gate<Ctx> {
|
|
|
19
19
|
/** Remove it and some candidate is WRONG, not merely worse. Everything else is a codegen
|
|
20
20
|
* heuristic the differ still referees. This flag is what makes `guardedBy` mandatory. */
|
|
21
21
|
readonly sound: boolean;
|
|
22
|
-
/** the test that fails when this gate is removed — required for a sound gate
|
|
23
|
-
|
|
22
|
+
/** the test that fails when this gate is removed — required for a sound gate; a rule with two
|
|
23
|
+
* halves names one test per half */
|
|
24
|
+
readonly guardedBy?: string | readonly string[];
|
|
24
25
|
/** true ⇒ REJECT this candidate */
|
|
25
26
|
readonly rejects: (c: Ctx) => boolean;
|
|
26
27
|
}
|
|
@@ -67,6 +68,11 @@ export function ablateHeuristic<Ctx>(gates: readonly Gate<Ctx>[], id: string): r
|
|
|
67
68
|
return without(gates, id);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** Every test a gate names as its guard, one per half of the rule. */
|
|
72
|
+
export function guardsOf<Ctx>(g: Gate<Ctx>): readonly string[] {
|
|
73
|
+
return typeof g.guardedBy === 'string' ? [g.guardedBy] : (g.guardedBy ?? []);
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
/** Structural defects in a gate table — the part checkable without running the pass. Returns
|
|
71
77
|
* findings rather than throwing, so core stays free of a test-framework import. */
|
|
72
78
|
export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
|
|
@@ -84,7 +90,7 @@ export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
|
|
|
84
90
|
out.push(`gate '${g.id}' has no usable \`why\``);
|
|
85
91
|
}
|
|
86
92
|
// the one rule that costs something to declare
|
|
87
|
-
if (g.sound && !g.
|
|
93
|
+
if (g.sound && !guardsOf(g).some((t) => t.trim())) {
|
|
88
94
|
out.push(`gate '${g.id}' is marked sound but names no guard`);
|
|
89
95
|
}
|
|
90
96
|
}
|