@asmlift/core 0.6.0 → 0.7.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 +2 -2
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/contracts.ts +60 -11
- package/src/frontend/ssa.ts +1 -1
- package/src/frontend/thumb.ts +2 -2
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +8 -0
- package/src/ir/opcodes.ts +43 -7
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +4 -4
- package/src/l3/ast.ts +65 -21
- package/src/l3/basecse.ts +48 -28
- package/src/l3/coalesce.ts +9 -9
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +28 -25
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +13 -13
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/pattern/engine.ts +4 -4
- package/src/pipeline.ts +17 -5
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +24 -2
- package/src/raise/pre-recovery.ts +90 -25
- package/src/raise/retsink.ts +381 -15
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +4 -4
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1148 -803
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/structure.ts +1415 -200
- package/src/structure/switch-recover.ts +100 -7
- package/src/symbols.ts +127 -6
- package/src/target.ts +155 -35
- package/src/trace.ts +1 -1
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/src/l3/mentions.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// What one L3 tree does to each of its locals, counted once.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Respell variations ask overlapping versions of the question and would disagree if each walked the tree
|
|
4
4
|
// its own way: l3/inlinebase.ts needs the SHAPE of every use (only an `index` base is re-spellable,
|
|
5
5
|
// and the single assignment must be a top-level `const` nothing mentions earlier), while
|
|
6
6
|
// l3/volatileval.ts needs the COUNTS, to check the tree still performs every access the machine
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// oversight: a POSITION added to `index` reaches the generic vocabulary for free and this walk not
|
|
15
15
|
// at all. Every position is enumerated below and pinned by
|
|
16
16
|
// test/array-rank-guards.test.ts, beside the generic helpers it cannot speak for.
|
|
17
|
-
import { type Expr, type SFn, type Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
|
|
17
|
+
import { type Expr, type SFn, type Stmt, exprChildren, stmtChildren, stmtExprs, walkExprs } from './ast';
|
|
18
18
|
|
|
19
19
|
export interface Mentions {
|
|
20
20
|
/** assignments to the name, at any nesting */
|
|
@@ -24,7 +24,7 @@ export interface Mentions {
|
|
|
24
24
|
/** the bare-`const` value that assignment stores, or null if it stores anything else */
|
|
25
25
|
constValue: number | null;
|
|
26
26
|
addrTaken: number;
|
|
27
|
-
/** uses as the `base` of an `index` node — the only use shape a base
|
|
27
|
+
/** uses as the `base` of an `index` node — the only use shape a base-hoisting variation can re-spell */
|
|
28
28
|
baseUses: number;
|
|
29
29
|
/** every other read */
|
|
30
30
|
otherUses: number;
|
|
@@ -53,7 +53,7 @@ function walkExpr(e: Expr, visit: (x: Expr, isIndexBase: boolean) => void, isInd
|
|
|
53
53
|
if (e.k === 'index') {
|
|
54
54
|
walkExpr(e.base, visit, true);
|
|
55
55
|
// `lead` — a multidimensional global's LEADING subscripts — is an ordinary value position, so
|
|
56
|
-
// a name mentioned there is a real read. Missing it does not cost a candidate: it lets a
|
|
56
|
+
// a name mentioned there is a real read. Missing it does not cost a candidate: it lets a variation
|
|
57
57
|
// DELETE a local the body still names.
|
|
58
58
|
for (const l of e.lead ?? []) {
|
|
59
59
|
walkExpr(l, visit, false);
|
|
@@ -67,7 +67,7 @@ function walkExpr(e: Expr, visit: (x: Expr, isIndexBase: boolean) => void, isInd
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** Every mention of every local, keyed by name. Locals only — a param or a global name is not in
|
|
70
|
-
* the map, and a
|
|
70
|
+
* the map, and a pass asking about one gets `undefined` rather than a zeroed record. */
|
|
71
71
|
export function localMentions(sfn: SFn): Map<string, Mentions> {
|
|
72
72
|
const t = new Map<string, Mentions>(sfn.locals.map((l) => [l.name, blank()]));
|
|
73
73
|
const seen = (name: string, at: number): Mentions | undefined => {
|
|
@@ -111,3 +111,66 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
|
|
|
111
111
|
sfn.body.forEach((s, i) => stmt(s, i, true));
|
|
112
112
|
return t;
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/** THE ONE WALK behind `mentionsAnyLocal` and `mentionedLocals` below: which of `names` anything
|
|
116
|
+
* under `stmts` still NAMES — as an assignment TARGET (which carries no expression, so no walk
|
|
117
|
+
* over values can see it), as a read, or as an address. `first` returns at the first hit it
|
|
118
|
+
* reaches, which is all the boolean caller needs.
|
|
119
|
+
*
|
|
120
|
+
* The question a pass that DELETES a declaration has to answer, and it lives here rather than in
|
|
121
|
+
* the deleting pass for the reason this file's header states about its own walk: a second walk
|
|
122
|
+
* over the node vocabulary is how a new node kind becomes a silent undercount, and beside
|
|
123
|
+
* `localMentions` a divergence is at least visible. This one is answered over a SUBTREE, so it
|
|
124
|
+
* cannot be derived from the counts above — `localMentions` is keyed to `sfn.locals` across the
|
|
125
|
+
* whole body, and l3/unmerge.ts's whole point is that those counts are sampled before any
|
|
126
|
+
* rewriting and go stale.
|
|
127
|
+
*
|
|
128
|
+
* TOTAL over the vocabulary by construction: `assign` is the only `Stmt` carrying a bare name and
|
|
129
|
+
* `var`/`addr` the only `Expr`s, and both walks are derived from `stmtChildren`/`stmtExprs` — so a
|
|
130
|
+
* `for`'s init and inc, a `switch`'s scrutinee, its cases and its default are all covered. */
|
|
131
|
+
function scanMentions(stmts: readonly Stmt[], names: ReadonlySet<string>, first: boolean): Set<string> {
|
|
132
|
+
// TWO FLAT SWEEPS, not one expression walk per nesting level. `walkExprs` already descends
|
|
133
|
+
// `stmtChildren` (ast.ts), so calling it per statement from inside a recursion that ALSO
|
|
134
|
+
// descends re-walks every nested expression once per enclosing level — quadratic in the nesting
|
|
135
|
+
// depth, 301 `has` calls at depth 24 where one pass needs 25. Small at today's call sites (one
|
|
136
|
+
// rewritten subtree per un-merge site, one dropped-locals set per respelled tree), but this is a
|
|
137
|
+
// SHARED helper and its cost belongs in its contract.
|
|
138
|
+
const found = new Set<string>();
|
|
139
|
+
const body = [...stmts] as Stmt[];
|
|
140
|
+
const stack: Stmt[] = [...body];
|
|
141
|
+
while (stack.length > 0) {
|
|
142
|
+
const s = stack.pop()!;
|
|
143
|
+
if (s.k === 'assign' && names.has(s.name)) {
|
|
144
|
+
found.add(s.name);
|
|
145
|
+
if (first) {
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
stack.push(...stmtChildren(s));
|
|
150
|
+
}
|
|
151
|
+
for (const e of walkExprs(body)) {
|
|
152
|
+
if ((e.k === 'var' || e.k === 'addr') && names.has(e.name)) {
|
|
153
|
+
found.add(e.name);
|
|
154
|
+
if (first) {
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return found;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** true when anything under `stmts` still names one of `names`. See `scanMentions` above. */
|
|
163
|
+
export function mentionsAnyLocal(stmts: readonly Stmt[], names: ReadonlySet<string>): boolean {
|
|
164
|
+
return scanMentions(stmts, names, true).size > 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** WHICH of `names` the tree still mentions — the same walk `mentionsAnyLocal` answers "any" over,
|
|
168
|
+
* told to keep going, for the caller that has to NAME the survivors.
|
|
169
|
+
*
|
|
170
|
+
* It exists so that contracts.ts's `assertNoOrphanedLocals` — the loud backstop for exactly the
|
|
171
|
+
* mistake this predicate guards — does not carry a THIRD hand-rolled copy of the node vocabulary.
|
|
172
|
+
* The boolean cannot serve it (the diagnostic needs the set) and one call per dropped name would
|
|
173
|
+
* be a walk per name. */
|
|
174
|
+
export function mentionedLocals(stmts: readonly Stmt[], names: ReadonlySet<string>): Set<string> {
|
|
175
|
+
return scanMentions(stmts, names, false);
|
|
176
|
+
}
|
package/src/l3/mulfirst.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: put the PRODUCT operand first in a commutative `+`.
|
|
2
2
|
//
|
|
3
3
|
// structure.ts's def-order rule spells commutative operands in EVALUATION order, which recovers
|
|
4
4
|
// gcc's left-to-right source order. IDO and mwcc break the correspondence for exactly one shape:
|
|
5
5
|
// in `a*b + c` they SCHEDULE the independent load of `c` above the product's `mflo`/`mullw`, so
|
|
6
6
|
// the machine add reads (c, product) and def order re-spells the source's product-first sum as
|
|
7
|
-
// c-first. Which order the source used is not recoverable from positions there — so this
|
|
7
|
+
// c-first. Which order the source used is not recoverable from positions there — so this variation
|
|
8
8
|
// emits the product-first sibling and the differ referees (verified byte-identical against IDO
|
|
9
9
|
// on the bg_area row; the def-order spelling stays in the list for sources that really were
|
|
10
10
|
// c-first).
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// SCOPE (decline over approximate): a `+` is flipped only when exactly ONE side is a product
|
|
13
13
|
// (`bin('*')` at the root, casts looked through) — two products or none leave nothing to anchor
|
|
14
14
|
// the flip on. A side carrying an effect (a call, a marker) never moves — evaluation order of
|
|
15
|
-
// the operands is what the
|
|
15
|
+
// the operands is what the variation edits. Declines (null) when no `+` changes, so no duplicate
|
|
16
16
|
// candidate.
|
|
17
17
|
import type { Expr, SFn } from './ast';
|
|
18
18
|
import { exprHasEffect, mapExprChildren, mapStmtExprs } from './ast';
|
package/src/l3/nearbase.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: NEIGHBOR absolute addresses derive from one shared base local.
|
|
2
2
|
//
|
|
3
3
|
// A cluster of raw-address accesses a few bytes apart is one object's cells: the compiler holds
|
|
4
4
|
// the object's base in a register and derives each cell (`add #72` / `add #74` off one pool
|
|
5
5
|
// word, a halfword offset beyond the load range forcing the add, the in-range word staying
|
|
6
|
-
// `[rN, #112]`), where a per-cell spelling anchors one pool constant per address. This
|
|
6
|
+
// `[rN, #112]`), where a per-cell spelling anchors one pool constant per address. This variation
|
|
7
7
|
// re-spells every deref base in a cluster as an offset from a `u8 *` base local holding the
|
|
8
8
|
// cluster's lowest address, and the differ referees:
|
|
9
9
|
//
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// VALUE inside its window re-spells too, as `(s32)(b + off)` — the address of a cell handed to
|
|
22
22
|
// something (a DMA source register) is the same derived add in the original, and the two
|
|
23
23
|
// spellings are value-equal by construction, so the differ referees — including an integer that
|
|
24
|
-
// only coincidentally lands in the window, which is the stated cost of the
|
|
24
|
+
// only coincidentally lands in the window, which is the stated cost of the variation (the `s32` cast
|
|
25
25
|
// assumes addresses below 2^31, true of every target that declares nearBaseSpan today). Declines
|
|
26
26
|
// (null) when no cluster forms.
|
|
27
27
|
import { baseConst } from './address';
|
|
@@ -31,7 +31,7 @@ import { type BaseInit, nameAllocator, placeBaseLocals } from './hoist';
|
|
|
31
31
|
|
|
32
32
|
/** `span` is the target's single-add-immediate derivation reach
|
|
33
33
|
* (TargetDescription.compilerBehaviors.nearBaseSpan) — a target that declares none never runs
|
|
34
|
-
* this
|
|
34
|
+
* this variation. */
|
|
35
35
|
export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
|
|
36
36
|
if (!Number.isFinite(span) || span < 0) {
|
|
37
37
|
return null; // a hostile span stalls the cluster window instead of shrinking it
|
package/src/l3/offmember.ts
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// displacement. A window refusal removes ZERO further bases in either configuration.
|
|
39
39
|
//
|
|
40
40
|
// PREMISE. It would be refusing a dropped qualifier, and there is none to drop: `/volatile`
|
|
41
|
-
// wraps the base in a CAST that `non-leaf-base` refuses, so the two
|
|
41
|
+
// wraps the base in a CAST that `non-leaf-base` refuses, so the two variations cannot compose and
|
|
42
42
|
// the tree this pass is handed is unqualified in both configurations. Nor is a tie-break lost —
|
|
43
43
|
// `deviceVolatileClaims` counts only qualifiers a tree already asserts, so an unqualified tree
|
|
44
44
|
// scores zero whichever way it is spelled.
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
// "PROTECTS NO ROW" IS A CENSUS AND NOT A SAMPLE, and the population is small because this is an
|
|
97
97
|
// extra GATE: it only ever removes candidates, so only a row whose winner carries `/offmember` can
|
|
98
98
|
// move at all. THAT POPULATION IS A QUERY, not a count to keep in step by hand — the rows of
|
|
99
|
-
// `apps/benchmark/results/results.json` whose `asmlift.
|
|
99
|
+
// `apps/benchmark/results/results.json` whose `asmlift.winnerVariations` contains `offmember` — and
|
|
100
100
|
// it grows with the corpus, so re-run it before repeating the result. It held eight rows besides
|
|
101
101
|
// `ProcessInputAndUpdateEntities` when the census ran, all unmoved with the rule on:
|
|
102
102
|
// `synthetic:basecell`, `synthetic:bgfixed`, `synthetic:foldsink`, `sa3:sub_802DFC8` and
|
|
@@ -222,7 +222,7 @@ function membersOf(sites: readonly Site[]): Site[] {
|
|
|
222
222
|
* THE FOURTH IS NOT ABOUT PLACEMENT, and it is the easy one to leave out: two views of one
|
|
223
223
|
* offset at one width but
|
|
224
224
|
* DIFFERENT SIGNEDNESS (`ldrb` and `ldrsb` at the same address). One member has one type, so
|
|
225
|
-
*
|
|
225
|
+
* spelling both through it changes what one of the two READS — `scalarTypeForAccess` honours
|
|
226
226
|
* signedness at widths 1 and 2, so an unsigned read becomes sign-extending. That is a value
|
|
227
227
|
* change rather than a spelling change, and the differ can referee it only by luck: a masked or
|
|
228
228
|
* compared result compiles to the same bytes while the published C says something the asm does
|
|
@@ -328,7 +328,7 @@ function admit(
|
|
|
328
328
|
|
|
329
329
|
/** The gate table an ablation swaps out. Optional, so a caller gets the shipped table by default.
|
|
330
330
|
* The pass needs nothing else from the target: the fold this exists for is `foldsConstAddrOffset`
|
|
331
|
-
* and rank.ts asks that before offering the
|
|
331
|
+
* and rank.ts asks that before offering the variation at all. */
|
|
332
332
|
export interface OffmemberOpts {
|
|
333
333
|
readonly gates?: readonly Gate<OffmemberBase>[];
|
|
334
334
|
}
|
|
@@ -339,7 +339,7 @@ export function offmemberBases(sfn: SFn, opts: OffmemberOpts = {}): readonly str
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
/** Re-spell every admitted base's constant subscripts as members of a synthesized struct.
|
|
342
|
-
* `null` when nothing is admitted — the
|
|
342
|
+
* `null` when nothing is admitted — the variation then contributes no candidate. */
|
|
343
343
|
export function spellOperandMembers(sfn: SFn, opts: OffmemberOpts = {}): SFn | null {
|
|
344
344
|
// Past EVERY `Off<N>` the tree already carries, never the first free one (`ir/struct-names.ts`,
|
|
345
345
|
// shared with the other two minters, which also records that this scan returns 0 on every
|
package/src/l3/parkfirst.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: park incoming ARGUMENTS first in the entry straight-line prefix.
|
|
2
2
|
//
|
|
3
3
|
// A copy of an incoming parameter into a local (`v = a1`) reproduces the register park the
|
|
4
4
|
// compiler performed to free a caller-save register (`mov ip, r1`). The park instruction lifts
|
|
5
5
|
// to pure SSA aliasing — no op, no position — so the emitted order falls out of block emission
|
|
6
6
|
// (materialized statements first, edge copies last), while the compiler may have parked BEFORE
|
|
7
7
|
// any of those statements ran (hipress homes its counter in ip before loading a byte into the
|
|
8
|
-
// vacated r1). Both orders are legitimate C for the same asm; this
|
|
8
|
+
// vacated r1). Both orders are legitimate C for the same asm; this variation emits the park-first
|
|
9
9
|
// sibling and the differ referees.
|
|
10
10
|
//
|
|
11
11
|
// SCOPE (decline over approximate): only plain assigns in the ENTRY straight-line prefix (the
|
|
12
12
|
// leading run of assigns) move; a park's RHS must be pure over PARAMETERS AND CONSTANTS through
|
|
13
13
|
// scalar nodes only (var/const/un/bin/cast — a memory read or a call would be re-scheduled, not
|
|
14
14
|
// re-spelled), so a constant initializer qualifies as a park and a param leaf is NOT required —
|
|
15
|
-
// what this
|
|
15
|
+
// what this variation moves is the leading run's ORDER, and a constant is as free of position as a
|
|
16
16
|
// parked register is; and a park never crosses a statement that writes a name it reads, reads or
|
|
17
17
|
// writes its destination (`&v` counts as touching v), or carries an effect. Relative order — of the
|
|
18
18
|
// parks and of everything else — is preserved. Declines (null) when nothing moves.
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
// spells a store to a bare scalar GLOBAL as an `assign` like any other, so such a store parks too.
|
|
23
23
|
// The crossing checks are NAME-KEYED, so a crossed statement that reaches the destination (or a
|
|
24
24
|
// name the park reads) through an ALIAS rather than by name is invisible to them — the same
|
|
25
|
-
// name-keyed model every
|
|
25
|
+
// name-keyed model every respell variation at this level defers aliasing to.
|
|
26
26
|
//
|
|
27
|
-
// The kmc hipress residual is this
|
|
27
|
+
// The kmc hipress residual is this variation's OTHER projection — its keep-load renders first while
|
|
28
28
|
// gcc2.7.2 schedules it last — so a second inhabitant consolidates both into one entry-prefix
|
|
29
|
-
// ordering
|
|
29
|
+
// ordering variation rather than growing a sibling.
|
|
30
30
|
import type { Expr, SFn, Stmt } from './ast';
|
|
31
31
|
import { exprChildren, exprHasEffect } from './ast';
|
|
32
32
|
|
package/src/l3/pollguard.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3 poll-shape
|
|
1
|
+
// L3 poll-shape respell variations: `pollGuards` regrows an empty bottom-tested loop's guard;
|
|
2
2
|
// `pollReads` folds a materialized poll's re-read back into its while condition. Each carries
|
|
3
3
|
// its own trace argument below.
|
|
4
4
|
//
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// For an empty body the two forms compile to the SAME instructions — gcc collapses the guard
|
|
8
8
|
// into the bottom test late (jump optimization), AFTER flow has counted the guard's reads — so
|
|
9
|
-
// the
|
|
9
|
+
// the decision leaves no instruction trace, only a register-allocation ripple: the extra
|
|
10
10
|
// source-level read raises the condition operands' ref counts, which re-orders the allocator's
|
|
11
11
|
// priorities for the WHOLE function (the busy-wait's base landing in a low reg vs `ip`). Which
|
|
12
12
|
// form the source spelled is unrecoverable from the bytes; both are emitted and the differ
|
|
@@ -34,7 +34,7 @@ export function pollGuards(sfn: SFn): SFn | null {
|
|
|
34
34
|
return changed ? { ...sfn, body } : null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// L3
|
|
37
|
+
// L3 respell variation: a materialized POLL re-reads in its own condition.
|
|
38
38
|
//
|
|
39
39
|
// v = dma[2]; while ((v & BUSY) != 0) { v = dma[2]; } → while ((dma[2] & BUSY) != 0) {}
|
|
40
40
|
//
|
package/src/l3/ptrfield.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: declare a recovered WORD field a POINTER (`void *field_4;` rather than
|
|
2
2
|
// `s32 field_4;`), and cast at each read.
|
|
3
3
|
//
|
|
4
4
|
// raise/structs.ts recovers a field's type from the ACCESS WIDTH alone — a 4-byte load is `s32`,
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
// each use computes the same value it did. Nothing else moves. THE 32-BIT ASSUMPTION IS ASSERTED,
|
|
35
35
|
// NOT CHECKED — there is no pointer-width field on TargetDescription to check it against, and the
|
|
36
36
|
// assumption is already tower-wide (`l3/typing.ts`'s `ptrElemBytes` returns 4 for any pointee). On
|
|
37
|
-
// a 64-bit target this
|
|
37
|
+
// a 64-bit target this variation would change a struct's LAYOUT rather than only its spelling, so the
|
|
38
38
|
// width field is what that target's first row must add, and this note is where to start.
|
|
39
39
|
//
|
|
40
40
|
// IT FLIPS EVERY ADMITTED FIELD AT ONCE, and its own paragraph above says the knowledge is
|
|
41
41
|
// PER-FIELD — so the subset enumeration `l3/volatileptr.ts` does for exactly this reason
|
|
42
|
-
// (`volatileSubsetCandidates`, capped at three locals) is the shape this
|
|
43
|
-
// want. It is not built yet because nothing demands it: swept over 834 corpus trees, the
|
|
42
|
+
// (`volatileSubsetCandidates`, capped at three locals) is the shape this variation will eventually
|
|
43
|
+
// want. It is not built yet because nothing demands it: swept over 834 corpus trees, the variation
|
|
44
44
|
// fires on 42, and 34 of those have a single admitted field. The six 2-field trees and the two
|
|
45
45
|
// 4-field ones (`sa3:sa2__sub_8083504` flips Struct0.field_8/12 and Struct2.field_8/12 together)
|
|
46
46
|
// are where 1 of 3 and 1 of 15 non-empty subsets is reachable. A row that needs one of the missing
|
package/src/l3/regspell.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// asmlift L3 — the REGISTER-COPY
|
|
1
|
+
// asmlift L3 — the REGISTER-COPY respell variation, differ-ranked like the three before it (the fourth,
|
|
2
2
|
// after signedness / branch sense / walk-vs-index).
|
|
3
3
|
//
|
|
4
4
|
// A compiler's register allocation leaves SOURCE-visible footprints the coalescing structurer
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// a bin operand materializes into its own fresh local first (`v = 128 << 9; … v / x …`) —
|
|
27
27
|
// the register the compiler staged the constant in.
|
|
28
28
|
// R3 — return assign-back: a non-var return expression lands in a fresh local first
|
|
29
|
-
// (`r = E; return r`). Emitted as a SEPARATE
|
|
29
|
+
// (`r = E; return r`). Emitted as a SEPARATE result (with/without R3) when R1/R2 fired:
|
|
30
30
|
// which tail the source spelled is itself ambiguous.
|
|
31
31
|
import { IrType, T } from '../ir/types';
|
|
32
32
|
import { Expr, SFn, Stmt, exprEquals, mapExprChildren } from './ast';
|
|
@@ -124,8 +124,8 @@ function isConstExpr(e: Expr): boolean {
|
|
|
124
124
|
* var and only the fresh spelling exists — so the tails are a 1- OR 2-element list and the SECOND
|
|
125
125
|
* spelling is not at a fixed index.
|
|
126
126
|
*
|
|
127
|
-
* This field exists because the caller
|
|
128
|
-
* in this repo reads (
|
|
127
|
+
* This field exists because the caller names these, and a candidate's variations are the instrument
|
|
128
|
+
* every census in this repo reads (a `bench diff` FIELD). Index a `['/regcopy', '/regcopy-ret',
|
|
129
129
|
* '/regcopy-ret-fresh']` table by POSITION instead and an R1-less function publishes its fresh
|
|
130
130
|
* tail as `/regcopy-ret` — the dead-var-reuse name on the spelling that has no dead var to reuse
|
|
131
131
|
* — and a census over the token `/regcopy-ret-fresh` then censuses nothing wherever R1 declines,
|
|
@@ -133,7 +133,7 @@ function isConstExpr(e: Expr): boolean {
|
|
|
133
133
|
export type RegcopyTail = 'none' | 'reuse' | 'fresh';
|
|
134
134
|
|
|
135
135
|
export interface RegcopySpelling {
|
|
136
|
-
/** which R3 tail this
|
|
136
|
+
/** which R3 tail this result carries — `none` is the un-tailed base */
|
|
137
137
|
tail: RegcopyTail;
|
|
138
138
|
sfn: SFn;
|
|
139
139
|
}
|
|
@@ -213,7 +213,7 @@ function renameSubexpr(e: Expr, E: Expr, name: string): Expr {
|
|
|
213
213
|
return mapExprChildren(e, (c) => renameSubexpr(c, E, name));
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
/** Apply the register-copy re-spelling. Returns 0–3
|
|
216
|
+
/** Apply the register-copy re-spelling. Returns 0–3 results — the base, plus the R3 tail in each
|
|
217
217
|
* spelling that exists (reuse needs R1 to have fired, fresh always does) — and an EMPTY list when
|
|
218
218
|
* nothing fired. Pure — never mutates the input. */
|
|
219
219
|
export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
|
|
@@ -247,7 +247,7 @@ export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
|
|
|
247
247
|
if (m) {
|
|
248
248
|
const { v, E, updArm, upd, cond } = m;
|
|
249
249
|
// GUARDS BEFORE ALLOCATION (a declined shape must leave no residue — the leaked
|
|
250
|
-
// dead `w` even perturbed the live-name count this
|
|
250
|
+
// dead `w` even perturbed the live-name count this variation exists to reproduce):
|
|
251
251
|
// • E and the cond pure; the cond's non-E operand must NOT mention v (a clamp's
|
|
252
252
|
// `if (a < v)` would compare against the POST-assignment v — reproduced);
|
|
253
253
|
// • the copy w carries E's RENDERED type, not v's declared one (retyping a u32
|
|
@@ -327,7 +327,7 @@ export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
|
|
|
327
327
|
}
|
|
328
328
|
const base: SFn = { ...sfn, locals: [...locals], body: afterR2 };
|
|
329
329
|
|
|
330
|
-
// R3
|
|
330
|
+
// R3 results: the tail assign-back — a non-var return lands in a local first. WHICH local is
|
|
331
331
|
// itself allocator-ambiguous (gcc 2.9 wanted R1's dead value var — live-name-count sensitive;
|
|
332
332
|
// another allocator may want the fresh one), so BOTH tails are emitted as candidates rather
|
|
333
333
|
// than asserting one compiler's preference; the source dedupe collapses them when identical.
|
package/src/l3/reindex.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// asmlift L3 — the walk→index
|
|
1
|
+
// asmlift L3 — the walk→index RESPELL VARIATION, differ-ranked.
|
|
2
2
|
//
|
|
3
3
|
// A compiler strength-reduces a source-level `arr[i]` loop into a pointer WALK (`*p; p += 1`),
|
|
4
4
|
// so asmlift's faithful lift of the machine form emits the walk — but recompiling the walk
|
|
5
5
|
// rarely reproduces the bytes the INDEXED source produced (different induction variable,
|
|
6
6
|
// different regalloc). Which representation the source used is genuinely ambiguous from asm —
|
|
7
7
|
// exactly the class of ambiguity asmlift resolves by CANDIDATES, not guesses (rank.ts: "types
|
|
8
|
-
// are differ-ranked
|
|
8
|
+
// are differ-ranked variations"). This module produces the indexed re-spelling of a structured
|
|
9
9
|
// function; enumerateCandidates emits BOTH and the objdiff score referees.
|
|
10
10
|
//
|
|
11
11
|
// v1 SCOPE (decline over approximate): a loop is re-spelled only when ALL hold —
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// it (its statements are the loop-preceding subset of the else arm, now unconditionally hoisted).
|
|
30
30
|
// The walk pointers KEEP their init and lose their step — gcc folds a loop-invariant pointer
|
|
31
31
|
// local into addressing, so `p = B; … p[i]` and `B[i]` compile identically, and keeping the
|
|
32
|
-
// local is what lets the `/volatile`
|
|
32
|
+
// local is what lets the `/volatile` variation qualify a numeric B. Several walk pointers share the
|
|
33
33
|
// one counter (`dotprod`'s a/b pair). Its OWN rules, on top of the shared table below —
|
|
34
34
|
// • the guard tests THE SAME var the counter is initialised from, against 0, in the sense that
|
|
35
35
|
// skips the loop; the do-while exit is exactly `k != 0`;
|
|
@@ -227,7 +227,7 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
|
|
|
227
227
|
},
|
|
228
228
|
{
|
|
229
229
|
id: 'global-counter',
|
|
230
|
-
why: 'a global counter’s final value is observable to every other caller,
|
|
230
|
+
why: 'a global counter’s final value is observable to every other caller, interrupt handler and translation unit',
|
|
231
231
|
sound: true,
|
|
232
232
|
guardedBy: 'reindex.test.ts: the counter may not be a GLOBAL',
|
|
233
233
|
rejects: (c) => !c.kIsDeclared,
|
|
@@ -255,14 +255,14 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
|
|
|
255
255
|
},
|
|
256
256
|
{
|
|
257
257
|
id: 'body-exit',
|
|
258
|
-
why: 'the steps sat
|
|
258
|
+
why: 'the steps sat at the end of the body, which a `continue` skips and a `for`’s increment does not',
|
|
259
259
|
sound: true,
|
|
260
260
|
guardedBy: 'reindex.test.ts: a `break` in the body declines',
|
|
261
261
|
rejects: (c) => c.coreHasExit,
|
|
262
262
|
},
|
|
263
263
|
{
|
|
264
264
|
id: 'walk-base',
|
|
265
|
-
why: 'the kept init must be a value the rewrite can leave standing
|
|
265
|
+
why: 'the kept init must be a value the rewrite can leave standing: a variable, or an address that can be loaded again',
|
|
266
266
|
sound: true,
|
|
267
267
|
guardedBy: 'reindex.test.ts: a walk pointer with no init ahead of the loop declines',
|
|
268
268
|
rejects: (c) => c.badBases.length > 0,
|
|
@@ -290,7 +290,7 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
|
|
|
290
290
|
},
|
|
291
291
|
{
|
|
292
292
|
id: 'leftover-walk',
|
|
293
|
-
why: 'a
|
|
293
|
+
why: 'a statement left outside the deleted step still reads the walk pointer, which a path that skipped the step never set',
|
|
294
294
|
sound: true,
|
|
295
295
|
guardedBy: 'reindex.test.ts: a leftover mentioning a walk pointer declines',
|
|
296
296
|
rejects: (c) => c.leakyLeftovers > 0,
|
|
@@ -367,13 +367,18 @@ function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
|
|
|
367
367
|
? { k: 'var', name: iv }
|
|
368
368
|
: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: e.idx };
|
|
369
369
|
// NOTE: this rebuilds the node from parts, so any field not named here is DROPPED — and the
|
|
370
|
-
// `index` node has exactly
|
|
371
|
-
// `lead` is declined above (the deref side); it cannot arrive on
|
|
372
|
-
// `walk.base` is a local pointer and structuring only ever puts
|
|
373
|
-
// own name. Dropping the other
|
|
374
|
-
//
|
|
375
|
-
// `
|
|
376
|
-
//
|
|
370
|
+
// `index` node has exactly five optional ones (ast.ts): `lead`, `baseElem`, `operandOff`,
|
|
371
|
+
// `baseOrdered`, `baseAdvanced`. `lead` is declined above (the deref side); it cannot arrive on
|
|
372
|
+
// the base side either, since `walk.base` is a local pointer and structuring only ever puts
|
|
373
|
+
// `lead` on an array GLOBAL's own name. Dropping the other four is right rather than merely
|
|
374
|
+
// harmless: `baseElem` states the element type of a base the C type walk cannot type, and the
|
|
375
|
+
// rebuilt base is `{k:'var', name: walk.base}` — a LOCAL pointer, whose declared type the walk
|
|
376
|
+
// reads straight out of the print env, so a statement about the base it replaced would describe
|
|
377
|
+
// a different base and the walk answers without one; `baseOrdered` is stamped per SYMBOL on an
|
|
378
|
+
// order-licensed GLOBAL, which a local var base never is; `operandOff`'s readers
|
|
379
|
+
// (l3/basecse.ts, l3/offmember.ts) ask about a CONSTANT subscript, which the rewritten
|
|
380
|
+
// `i`/`i + k` index never is; and `baseAdvanced` describes an address the machine reached from
|
|
381
|
+
// a CONSTANT one, which a walk pointer's is not.
|
|
377
382
|
return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
|
|
378
383
|
}
|
|
379
384
|
let failed = false;
|
|
@@ -653,8 +658,8 @@ function tryExprWalk(
|
|
|
653
658
|
* one loop re-spelled, or null (no candidate) when nothing fired — callers emit the extra
|
|
654
659
|
* candidate only on non-null. Pure: never mutates the input SFn. `keptWalks` collects the names
|
|
655
660
|
* of the pointers each fired loop kept as its base — v1 the walk's base (a param lands in the
|
|
656
|
-
* set too, inertly: the volatile
|
|
657
|
-
* themselves — the locals the /indexed/volatile
|
|
661
|
+
* set too, inertly: the volatile variation marks only declared locals), v2 the walk pointers
|
|
662
|
+
* themselves — the locals the /indexed/volatile composition (rank.ts) narrows the volatile variation
|
|
658
663
|
* to. A v3 loop contributes nothing: it DELETES its pointer, and its base is qualified through
|
|
659
664
|
* the /livebase pairings instead. `gates` is the shared countdown admission table — a parameter
|
|
660
665
|
* so a test can ablate one entry and re-run the real pass. */
|
|
@@ -666,7 +671,7 @@ export function reindexWalks(
|
|
|
666
671
|
const ptrVars = new Map<string, IrType>();
|
|
667
672
|
const declTypes = declaredTypes(sfn);
|
|
668
673
|
// BOTH volatility facts (ast.ts SFn.locals): the object-volatile counter, and the pointer whose
|
|
669
|
-
// POINTEE is volatile — which is the one the `/volatile`
|
|
674
|
+
// POINTEE is volatile — which is the one the `/volatile` variation mints, and the one a walk carries.
|
|
670
675
|
const volatileLocals = new Set(
|
|
671
676
|
sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
|
|
672
677
|
);
|