@asmlift/core 0.5.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +176 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/src/l3/gates.ts
CHANGED
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
//
|
|
8
8
|
// `why` is a LABEL, one line. The argument for why the rule is correct belongs in the file header,
|
|
9
9
|
// which has room; duplicating it here is how a table stops paying for itself.
|
|
10
|
+
//
|
|
11
|
+
// A table a variation's definition names (`variation-gates.ts`) shows its `why` to a reader, in the
|
|
12
|
+
// webapp's variation drawer, so there the label is plain prose: no tag, file or function name, or
|
|
13
|
+
// shouted word. `variation-offers.test.ts` holds it to that.
|
|
10
14
|
export interface Gate<Ctx> {
|
|
11
15
|
/** stable, kebab-case; appears in test names and in the contract report */
|
|
12
16
|
readonly id: string;
|
|
13
|
-
/** one line: the reason the rule exists */
|
|
17
|
+
/** one line: the reason the rule exists, readable without the code */
|
|
14
18
|
readonly why: string;
|
|
15
19
|
/** Remove it and some candidate is WRONG, not merely worse. Everything else is a codegen
|
|
16
20
|
* heuristic the differ still referees. This flag is what makes `guardedBy` mandatory. */
|
|
@@ -42,6 +46,27 @@ export function without<Ctx>(gates: readonly Gate<Ctx>[], id: string): readonly
|
|
|
42
46
|
return gates.filter((g) => g.id !== id);
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
// NO `just(table, ids)` SELECTOR, deliberately. Selecting rule OBJECTS by id shares the predicate
|
|
50
|
+
// AND the `sound` claim AND the `guardedBy` guard, and a second consumer of a rule wants only the
|
|
51
|
+
// first of the three: a rule that is sound for a declaration is a heuristic for a generated
|
|
52
|
+
// candidate, and the guard the contract test then checks ablates the rule against the OTHER
|
|
53
|
+
// consumer. What a second consumer shares is a PREDICATE — an ordinary function — and what it owns
|
|
54
|
+
// is its own rule objects. `ORDER_SHAPE_GATES` (raise/globalshape.ts) is the worked example, with
|
|
55
|
+
// the over-admission id-selection would carry.
|
|
56
|
+
|
|
57
|
+
/** `without` for SHIPPED code. A test may ablate any gate — that is how `guardedBy` differential
|
|
58
|
+
* tests work — but a pass that re-runs itself with an ablated table as a ranked candidate may
|
|
59
|
+
* only drop a HEURISTIC: ablating a `sound: true` gate would ship semantically wrong candidates,
|
|
60
|
+
* and on a nonmatch row the best-scoring source is shown to the user. A derived table is a
|
|
61
|
+
* top-level const, so the throw fires at import — the mistake cannot ship. */
|
|
62
|
+
export function ablateHeuristic<Ctx>(gates: readonly Gate<Ctx>[], id: string): readonly Gate<Ctx>[] {
|
|
63
|
+
const g = gates.find((x) => x.id === id);
|
|
64
|
+
if (g?.sound) {
|
|
65
|
+
throw new Error(`gate '${id}' is sound — a shipped ablation of it emits wrong candidates`);
|
|
66
|
+
}
|
|
67
|
+
return without(gates, id);
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
/** Structural defects in a gate table — the part checkable without running the pass. Returns
|
|
46
71
|
* findings rather than throwing, so core stays free of a test-framework import. */
|
|
47
72
|
export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
|
|
@@ -65,3 +90,73 @@ export function gateTableDefects<Ctx>(gates: readonly Gate<Ctx>[]): string[] {
|
|
|
65
90
|
}
|
|
66
91
|
return out;
|
|
67
92
|
}
|
|
93
|
+
|
|
94
|
+
/** A gate table that counts its own refusals — {@link tallying}'s return. */
|
|
95
|
+
export interface Tallied<Ctx> {
|
|
96
|
+
/** Hand this to the pass, in place of the table it wraps. */
|
|
97
|
+
readonly gates: readonly Gate<Ctx>[];
|
|
98
|
+
/** The census so far, most-refused first, ties in table order. A snapshot: counts keep
|
|
99
|
+
* accumulating across every later call, which is what a corpus-wide census wants.
|
|
100
|
+
*
|
|
101
|
+
* THERE IS NO RESET, deliberately — a per-row census is two snapshots DIFFED, not a fresh
|
|
102
|
+
* wrapper per row, because a wrapper is a new table IDENTITY and a reader that keys on one
|
|
103
|
+
* (`rank.ts`'s `censuses` memo) sees a fresh key every row. Counting is keyed by `g.id`, so a
|
|
104
|
+
* table COMPOSED from several should be run past `gateTableDefects` first: two rules sharing an
|
|
105
|
+
* id sum into one number, and the contract test only checks the tables on its own roster. */
|
|
106
|
+
readonly refusals: () => readonly (readonly [string, number])[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The same table, wrapping each `rejects` in a counter — so a caller OUTSIDE core can obtain the
|
|
110
|
+
* per-id census that `l3/coalesce.ts` and `structure/namecoalesce.ts` hand-roll into their return
|
|
111
|
+
* type (`l3/scopebase.ts` reports the same attribution per KEY), from any pass that takes its
|
|
112
|
+
* table as a parameter:
|
|
113
|
+
*
|
|
114
|
+
* const t = tallying(UNMERGE_SITE_GATES);
|
|
115
|
+
* unmergeJoins(sfn, { site: t.gates });
|
|
116
|
+
* console.log(t.refusals()); // [['empty-arm', 168], ['no-merge-name', 80]]
|
|
117
|
+
*
|
|
118
|
+
* THAT IS THE API AND NOT YET A CENSUS: nothing exports a corpus of trees to loop over, and a
|
|
119
|
+
* tabled pass's only shipped caller is normally inside core. Taking the census off a REAL
|
|
120
|
+
* enumeration is `pnpm bench gates --pass <id>`, whose header
|
|
121
|
+
* (`apps/benchmark/src/run/gate-census.ts`) holds the measured reasons it is a subcommand rather
|
|
122
|
+
* than a script to copy, and what a SECOND censusable pass costs.
|
|
123
|
+
*
|
|
124
|
+
* WHAT IT COUNTS IS AN EVALUATION THAT ANSWERED TRUE, not a site. Under `firstRejection` — which
|
|
125
|
+
* short-circuits — that is the FIRST rejecter, so this produces the same census the hand-rolled
|
|
126
|
+
* maps do, with the same reading: an id absent from it is starved OR REDUNDANT WITH an earlier
|
|
127
|
+
* rule, and telling the two apart takes the same rule run with the rest of the table empty
|
|
128
|
+
* (`grep -n "ON ITS OWN" packages/core/src/raise/globalshape.ts`, whose dated table ships three
|
|
129
|
+
* rules of the second kind). A consumer that asks the table something else — `.some`, `.filter` —
|
|
130
|
+
* gets one count per evaluation instead, which is a different question and rarely the one wanted.
|
|
131
|
+
*
|
|
132
|
+
* AND IT COUNTS REFUSALS, WHICH IS NOT REACH. A rule can refuse hundreds of times and still change
|
|
133
|
+
* no output, because a later rule or a narrowing outside the table would have refused the same
|
|
134
|
+
* sites: that is the MOVED column, it costs an ablation rather than a census, and `l3/unmerge.ts`'s
|
|
135
|
+
* header carries the worked example of the two disagreeing.
|
|
136
|
+
*
|
|
137
|
+
* IT CHANGES NO BEHAVIOUR: each wrapper's predicate IS the original's, `id`/`why`/`sound`/
|
|
138
|
+
* `guardedBy` are carried, so `without`, `ablateHeuristic` and `gateTableDefects` all still hold
|
|
139
|
+
* over the result. What it does change is the table's IDENTITY, and some readers key on that —
|
|
140
|
+
* `rank.ts`'s `censuses` memo is a `Map` over `Gate<BaseKey>[]` instances — so wrap once and reuse
|
|
141
|
+
* `gates`, rather than per call. */
|
|
142
|
+
export function tallying<Ctx>(gates: readonly Gate<Ctx>[]): Tallied<Ctx> {
|
|
143
|
+
const counts = new Map<string, number>();
|
|
144
|
+
const order = new Map(gates.map((g, i) => [g.id, i]));
|
|
145
|
+
return {
|
|
146
|
+
gates: gates.map((g) => ({
|
|
147
|
+
...g,
|
|
148
|
+
rejects: (c: Ctx) => {
|
|
149
|
+
const r = g.rejects(c);
|
|
150
|
+
if (r) {
|
|
151
|
+
counts.set(g.id, (counts.get(g.id) ?? 0) + 1);
|
|
152
|
+
}
|
|
153
|
+
return r;
|
|
154
|
+
},
|
|
155
|
+
})),
|
|
156
|
+
refusals: () =>
|
|
157
|
+
[...counts].sort((a, b) => b[1] - a[1] || (order.get(a[0]) ?? 0) - (order.get(b[0]) ?? 0)) as readonly (readonly [
|
|
158
|
+
string,
|
|
159
|
+
number,
|
|
160
|
+
])[],
|
|
161
|
+
};
|
|
162
|
+
}
|
package/src/l3/hoist.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
// L3 — the
|
|
1
|
+
// L3 — the MECHANISMS a pass needs to place a hoisted local: how a fresh name is chosen, where the
|
|
2
|
+
// leading run of base inits starts and ends, and where in the body that run goes.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Their users differ. Every pass that mints a local takes `nameAllocator` (or `takenNames`, to
|
|
5
|
+
// number its own); the three that touch basecse's leading init run read the rest — basecse and
|
|
6
|
+
// nearbase mint into it, sinkinit moves statements out of it, and all three have to agree on where
|
|
7
|
+
// it stops and how a body carrying it is rebuilt. What stays with each caller is ELIGIBILITY:
|
|
8
|
+
// which values become a local at all, and when it is worth doing. WHERE the run goes is
|
|
9
|
+
// `placeBaseLocals`'s `placement` argument, and that argument is two questions rather than one —
|
|
10
|
+
// see `HoistPlacement` and `BaseInitPlacement`. Everything lives in one file because each half was
|
|
11
|
+
// a per-caller copy once and every copy drifted from its original.
|
|
10
12
|
import type { Expr, SFn, Stmt } from './ast';
|
|
11
|
-
import {
|
|
13
|
+
import { exprChildren, mapStmtLists, stmtChildren, stmtExprs, stmtLists } from './ast';
|
|
14
|
+
import { localMentions } from './mentions';
|
|
12
15
|
|
|
13
|
-
/** Every identifier a
|
|
16
|
+
/** Every identifier a MINTED name must not collide with, anywhere in `sfn` — the hoists below,
|
|
17
|
+
* and reindex's induction names.
|
|
14
18
|
*
|
|
15
19
|
* Wider than "the declared locals" on purpose, and each addition is a real collision:
|
|
16
20
|
* - params and locals, obviously;
|
|
@@ -18,7 +22,7 @@ import { mapExprChildren, stmtChildren, stmtExprs } from './ast';
|
|
|
18
22
|
* silently redirects every later mention of it;
|
|
19
23
|
* - every CALL TARGET — a local named like a callee shadows the function;
|
|
20
24
|
* - every assignment target, which includes names no declaration list carries. */
|
|
21
|
-
function takenNames(sfn: SFn): Set<string> {
|
|
25
|
+
export function takenNames(sfn: SFn): Set<string> {
|
|
22
26
|
const taken = new Set<string>([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
|
|
23
27
|
const visit = (e: Expr): void => {
|
|
24
28
|
if (e.k === 'var' || e.k === 'addr') {
|
|
@@ -27,10 +31,9 @@ function takenNames(sfn: SFn): Set<string> {
|
|
|
27
31
|
if (e.k === 'call') {
|
|
28
32
|
taken.add(e.fn);
|
|
29
33
|
}
|
|
30
|
-
|
|
34
|
+
for (const c of exprChildren(e)) {
|
|
31
35
|
visit(c);
|
|
32
|
-
|
|
33
|
-
});
|
|
36
|
+
}
|
|
34
37
|
};
|
|
35
38
|
const walk = (stmts: Stmt[]): void => {
|
|
36
39
|
for (const s of stmts) {
|
|
@@ -63,3 +66,279 @@ export function nameAllocator(sfn: SFn): () => string {
|
|
|
63
66
|
return nm;
|
|
64
67
|
};
|
|
65
68
|
}
|
|
69
|
+
|
|
70
|
+
export type BaseInit = Extract<Stmt, { k: 'assign' }>;
|
|
71
|
+
|
|
72
|
+
/** Whether `s` is a BASE INIT: a ptr-cast of an `addr`/`const` leaf assigned into a declared
|
|
73
|
+
* NON-VOLATILE local. It reads nothing and writes its own plain cell, which is what makes the run
|
|
74
|
+
* of them re-orderable and each of them movable. The volatile exclusion is load-bearing — two
|
|
75
|
+
* writes to `volatile` locals are observably ordered, so one at the head simply ends the run. */
|
|
76
|
+
function isBaseInit(s: Stmt, plainLocals: ReadonlySet<string>): s is BaseInit {
|
|
77
|
+
return (
|
|
78
|
+
s.k === 'assign' &&
|
|
79
|
+
plainLocals.has(s.name) &&
|
|
80
|
+
s.value.k === 'cast' &&
|
|
81
|
+
s.value.to.kind === 'ptr' &&
|
|
82
|
+
(s.value.e.k === 'addr' || s.value.e.k === 'const')
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** `body` split at the end of its LEADING run of base inits — the run `placeBaseLocals` places. */
|
|
87
|
+
function splitLeadingBaseInits(sfn: SFn, body: readonly Stmt[]): { inits: BaseInit[]; rest: Stmt[] } {
|
|
88
|
+
const plain = new Set(sfn.locals.filter((l) => !l.volatile).map((l) => l.name));
|
|
89
|
+
let n = 0;
|
|
90
|
+
while (n < body.length && isBaseInit(body[n], plain)) {
|
|
91
|
+
n++;
|
|
92
|
+
}
|
|
93
|
+
return { inits: body.slice(0, n) as BaseInit[], rest: body.slice(n) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** For each local, the index of the first TOP-LEVEL statement of `rest` that mentions it, absent
|
|
97
|
+
* when none does. `mentions.ts`'s notion of a mention, so `&p` counts: an init must precede the
|
|
98
|
+
* address being taken as surely as it must precede a read. */
|
|
99
|
+
function firstUseIn(sfn: SFn, rest: readonly Stmt[]): Map<string, number> {
|
|
100
|
+
const out = new Map<string, number>();
|
|
101
|
+
for (const [name, m] of localMentions({ ...sfn, body: [...rest] })) {
|
|
102
|
+
if (m.firstAt !== null) {
|
|
103
|
+
out.set(name, m.firstAt);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Does `s` mention `name` in its OWN expressions or as its assignment target — nothing nested?
|
|
110
|
+
* `mentions.ts`'s notion of a mention (`&p` counts), asked of one statement rather than of a
|
|
111
|
+
* top-level index. */
|
|
112
|
+
function mentionsHere(s: Stmt, name: string): boolean {
|
|
113
|
+
if (s.k === 'assign' && s.name === name) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
let found = false;
|
|
117
|
+
const visit = (e: Expr): void => {
|
|
118
|
+
if ((e.k === 'var' || e.k === 'addr') && e.name === name) {
|
|
119
|
+
found = true;
|
|
120
|
+
}
|
|
121
|
+
for (const c of exprChildren(e)) {
|
|
122
|
+
visit(c);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
stmtExprs(s).forEach(visit);
|
|
126
|
+
return found;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** …and the same question of the whole subtree. */
|
|
130
|
+
function mentionsStmt(s: Stmt, name: string): boolean {
|
|
131
|
+
return mentionsHere(s, name) || stmtChildren(s).some((c) => mentionsStmt(c, name));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `{ list, at }` for a top-level first-use index, or null when there is none — the `first-use`
|
|
135
|
+
* answer in the shape the `scope` one comes back in. */
|
|
136
|
+
function mapTo(list: Stmt[], at: number | undefined): { list: Stmt[]; at: number } | null {
|
|
137
|
+
return at === undefined ? null : { list, at };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** How many times each statement appears in `list`. */
|
|
141
|
+
function tally(list: readonly Stmt[]): Map<Stmt, number> {
|
|
142
|
+
const out = new Map<Stmt, number>();
|
|
143
|
+
for (const s of list) {
|
|
144
|
+
out.set(s, (out.get(s) ?? 0) + 1);
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The innermost statement list holding EVERY mention of `name`, and the index in it of the first
|
|
150
|
+
* statement that mentions it. This is `firstUseIn` continued downward, and it descends only on
|
|
151
|
+
* three facts: exactly one statement of this list mentions the name, that statement mentions it
|
|
152
|
+
* nowhere OUTSIDE the lists it opens (its own condition, or a `for`'s `init`/`inc` — which are
|
|
153
|
+
* statements no list holds), and exactly one of those lists holds it. Any of the three failing
|
|
154
|
+
* means a nested list does not hold every mention, so this list is as deep as the init may go.
|
|
155
|
+
* Stopping at the top level reproduces `first-use` exactly, which is what lets `scope` be a
|
|
156
|
+
* placement rather than a second policy.
|
|
157
|
+
*
|
|
158
|
+
* DOMINATION IS THE DESCENT'S OWN INVARIANT: every mention is at or after the returned index
|
|
159
|
+
* within the returned list, so the init reaches all of them — including from inside a loop body,
|
|
160
|
+
* where it simply re-assigns the same link-time constant. `hoistBaseLocals` still CHECKS it
|
|
161
|
+
* (`assertHoistsDominate`), because an argument is not a check.
|
|
162
|
+
*
|
|
163
|
+
* A LOOP BODY IS ALSO A LIST NO SHIPPED CANDIDATE CAN REACH, which is a stronger statement than
|
|
164
|
+
* the safety argument above and the one that keeps re-assigning a base per iteration out of
|
|
165
|
+
* published C. Every gate table any caller pairs with `scope` keeps `BASECSE_GATES`' `loop` rule —
|
|
166
|
+
* `ORDERBASE_GATES`, the only roster table at this placement, ablates `cast-base` and `single-use`
|
|
167
|
+
* and nothing else — so a base with ANY use inside a loop is refused before a placement is
|
|
168
|
+
* consulted. Both halves are pinned in test/sinkinit.test.ts: neither table admits one, and where
|
|
169
|
+
* the mechanism is handed such a base directly the tree it emits still dominates. Loop-body bases
|
|
170
|
+
* are `l3/scopebase.ts`'s, which plans its own local rather than moving this run. */
|
|
171
|
+
function scopeSite(list: Stmt[], name: string): { list: Stmt[]; at: number } | null {
|
|
172
|
+
const idxs = list.flatMap((s, i) => (mentionsStmt(s, name) ? [i] : []));
|
|
173
|
+
if (idxs.length === 0) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const here = { list, at: idxs[0] };
|
|
177
|
+
if (idxs.length > 1) {
|
|
178
|
+
return here;
|
|
179
|
+
}
|
|
180
|
+
const s = list[idxs[0]];
|
|
181
|
+
const lists = stmtLists(s);
|
|
182
|
+
const inner = lists.filter((l) => l.some((x) => mentionsStmt(x, name)));
|
|
183
|
+
if (inner.length !== 1) {
|
|
184
|
+
return here;
|
|
185
|
+
}
|
|
186
|
+
// A MULTISET difference, not a set one: `stmtChildren` yields a `for`'s `init` and `inc` beside
|
|
187
|
+
// its body, and one `Stmt` object may sit at two tree positions. Subtracting by identity would
|
|
188
|
+
// read a shared `init` as opened and miss the mention it makes before the body ever runs.
|
|
189
|
+
const opened = tally(lists.flat());
|
|
190
|
+
const outside =
|
|
191
|
+
mentionsHere(s, name) ||
|
|
192
|
+
[...tally(stmtChildren(s))].some(([c, n]) => n > (opened.get(c) ?? 0) && mentionsStmt(c, name));
|
|
193
|
+
return outside ? here : (scopeSite(inner[0], name) ?? here);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** WHERE a run of base inits sits, once this file has put it in FIRST-USE order — the order the
|
|
197
|
+
* compiler loads the pool words in (`l3/basecse.ts`'s `collect`), so it is the order a reference
|
|
198
|
+
* spelling that named these bases would have.
|
|
199
|
+
*
|
|
200
|
+
* `head` keeps the whole ordered run at the top of the body.
|
|
201
|
+
* `first-use` then moves each init down to immediately before the statement that first mentions
|
|
202
|
+
* it, which is where a base reached ONCE was loaded and which keeps a base first touched halfway
|
|
203
|
+
* down the body out of the live range above it.
|
|
204
|
+
* `scope` reads that same query one nesting level at a time: an init whose every mention lives
|
|
205
|
+
* inside ONE nested list goes inside that list, at the first mention there. `first-use` stops at
|
|
206
|
+
* the top-level statement — a base used only inside an `if` arm still has its pool word loaded
|
|
207
|
+
* above the branch — and on agbcc, whose statement order survives into the object, the two are
|
|
208
|
+
* different bytes. Where no nested list holds every mention this IS `first-use`, which is what
|
|
209
|
+
* makes it a placement rather than a second policy.
|
|
210
|
+
*
|
|
211
|
+
* These three are the positions a hoist may state (rank.ts) and the only values
|
|
212
|
+
* `hoistBaseLocals` accepts. */
|
|
213
|
+
export type HoistPlacement = 'head' | 'first-use' | 'scope';
|
|
214
|
+
|
|
215
|
+
/** `HoistPlacement` plus the ABSTENTION: `prepend` puts the minted inits above a run that keeps
|
|
216
|
+
* the order it arrived in, consulting neither the first-use query nor the sort. A third VALUE and
|
|
217
|
+
* not a third position, passed only by `l3/nearbase.ts`, whose header carries the argument for it.
|
|
218
|
+
*
|
|
219
|
+
* `hoistBaseLocals` may NOT be handed this: prepending there spells a newly minted base's pool
|
|
220
|
+
* load above locals the compiler loads first (`l3/basecse.ts`'s own header), so the two passes
|
|
221
|
+
* take different types rather than the same type and a comment. */
|
|
222
|
+
export type BaseInitPlacement = HoistPlacement | 'prepend';
|
|
223
|
+
|
|
224
|
+
/** `sfn.body` rebuilt with `minted` added to its leading base-init run and the whole run placed
|
|
225
|
+
* per `placement`, plus which inits ended up away from the head.
|
|
226
|
+
*
|
|
227
|
+
* `sfn` is both the statements and the DECLARATION ENVIRONMENT the first-use and mention queries
|
|
228
|
+
* resolve against, so a caller that mints must pass a shell that already declares the new names
|
|
229
|
+
* AND carries the rewritten body. One argument rather than two is the point: a shell whose
|
|
230
|
+
* declarations and statements disagree is not expressible here.
|
|
231
|
+
*
|
|
232
|
+
* ONE ORDER, THEN THE POLICY. Under both `HoistPlacement` values the run is put in FIRST-USE
|
|
233
|
+
* order before `placement` is consulted — pool-load order, and what makes the two COMPOSABLE
|
|
234
|
+
* rather than merely adjacent: `first-use` applied to a `head` result is `first-use` applied to
|
|
235
|
+
* the input, so `/livebase/sinkinit` (a hoist at the head that a second pass then sinks) and
|
|
236
|
+
* `/basefold/sinkinit` (one hoist placed at first use) are the same transform and the `/sinkinit`
|
|
237
|
+
* suffix names one thing wherever it appears. Order the run only on the `head` branch and they
|
|
238
|
+
* part company on the inits that CANNOT move, which is the half of the run whose order the
|
|
239
|
+
* compiler still reads. Pinned in test/sinkinit.test.ts. `prepend` opts out of all of it.
|
|
240
|
+
*
|
|
241
|
+
* Ties keep list order — existing inits before minted ones, and two inits assigning the SAME
|
|
242
|
+
* local in their original sequence, which a stable sort is what guarantees: they write one cell,
|
|
243
|
+
* so their order is the only thing that says which value it ends up holding. Two that SINK to the
|
|
244
|
+
* same statement keep it too, which is what the splice loop's second sort key is for.
|
|
245
|
+
*
|
|
246
|
+
* Under `first-use`, an init then moves down if the function assigns its local exactly ONCE (the
|
|
247
|
+
* move would otherwise cross that other write), something in the remaining body mentions it, and
|
|
248
|
+
* it is not already sitting at the first such statement.
|
|
249
|
+
*
|
|
250
|
+
* IT REPORTS THE MOTION rather than its size, and its callers judge those lists instead of arguing
|
|
251
|
+
* about them. `moved` names every init that left the leading run; `nested` is the subset that
|
|
252
|
+
* landed in a list OTHER than the top-level one, which only `scope` can produce.
|
|
253
|
+
*
|
|
254
|
+
* `nested` empty under `scope` means the placement DEGENERATED — every init went exactly where
|
|
255
|
+
* `first-use` would have put it, so the emitted tree is that placement's spelling under a second
|
|
256
|
+
* name. A caller offering placements as candidates has to know, or it enumerates one spelling
|
|
257
|
+
* twice (l3/basecse.ts's `hoistBaseLocals`). */
|
|
258
|
+
export function placeBaseLocals(
|
|
259
|
+
sfn: SFn,
|
|
260
|
+
minted: readonly BaseInit[],
|
|
261
|
+
placement: BaseInitPlacement,
|
|
262
|
+
): { body: Stmt[]; moved: readonly string[]; nested: readonly string[] } {
|
|
263
|
+
const still = { moved: [], nested: [] };
|
|
264
|
+
const body = sfn.body;
|
|
265
|
+
const { inits: head, rest } = splitLeadingBaseInits(sfn, body);
|
|
266
|
+
if (head.length + minted.length === 0) {
|
|
267
|
+
return { body: [...body], ...still };
|
|
268
|
+
}
|
|
269
|
+
if (placement === 'prepend') {
|
|
270
|
+
return { body: [...minted, ...head, ...rest], ...still };
|
|
271
|
+
}
|
|
272
|
+
const firstUse = firstUseIn(sfn, rest);
|
|
273
|
+
const at = (s: BaseInit): number => firstUse.get(s.name) ?? rest.length;
|
|
274
|
+
const all = [...head, ...minted].sort((a, b) => at(a) - at(b));
|
|
275
|
+
if (placement === 'head') {
|
|
276
|
+
return { body: [...all, ...rest], ...still };
|
|
277
|
+
}
|
|
278
|
+
const whole = localMentions({ ...sfn, body: [...all, ...rest] });
|
|
279
|
+
const stay: BaseInit[] = [];
|
|
280
|
+
const sunk: { site: Stmt[]; at: number; init: BaseInit; i: number }[] = [];
|
|
281
|
+
for (const [i, init] of all.entries()) {
|
|
282
|
+
// Assigned exactly once, or the move would cross the other write.
|
|
283
|
+
const site =
|
|
284
|
+
whole.get(init.name)?.assigns === 1
|
|
285
|
+
? placement === 'scope'
|
|
286
|
+
? scopeSite(rest, init.name)
|
|
287
|
+
: mapTo(rest, firstUse.get(init.name))
|
|
288
|
+
: null;
|
|
289
|
+
// Nothing mentions it, or it is already sitting at the first statement of the top-level list:
|
|
290
|
+
// there is no move to make and the init stays in the leading run.
|
|
291
|
+
if (site === null || (site.list === rest && site.at === 0)) {
|
|
292
|
+
stay.push(init);
|
|
293
|
+
} else {
|
|
294
|
+
sunk.push({ site: site.list, at: site.at, init, i });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Rebuild `rest` around the sink sites, matching each list by IDENTITY against the tree the sites
|
|
298
|
+
// were computed on — so the walk emits a fresh list only along the path to a site and hands every
|
|
299
|
+
// other statement back unchanged.
|
|
300
|
+
const bySite = new Map<Stmt[], typeof sunk>();
|
|
301
|
+
for (const s of sunk) {
|
|
302
|
+
bySite.set(s.site, [...(bySite.get(s.site) ?? []), s]);
|
|
303
|
+
}
|
|
304
|
+
const rebuild = (list: Stmt[]): Stmt[] => {
|
|
305
|
+
const here = bySite.get(list);
|
|
306
|
+
// CONSUMED: one `Stmt[]` object sitting at two tree positions takes the init at the FIRST of
|
|
307
|
+
// them, never at both, where a second splice would write the same local twice. `scopeSite`
|
|
308
|
+
// cannot return such a list — sharing means two statements mention the local, which stops the
|
|
309
|
+
// descent at their common list — so this restates that invariant where breaking it would be
|
|
310
|
+
// silent. Nothing in the L3 contract forbids the sharing itself (l3/scopebase.ts records a
|
|
311
|
+
// producer that shares an expression node).
|
|
312
|
+
bySite.delete(list);
|
|
313
|
+
let changed = here !== undefined;
|
|
314
|
+
const mapped = list.map((s) => {
|
|
315
|
+
let inner = false;
|
|
316
|
+
const out = mapStmtLists(s, (l) => {
|
|
317
|
+
const r = rebuild(l);
|
|
318
|
+
inner ||= r !== l;
|
|
319
|
+
return r;
|
|
320
|
+
});
|
|
321
|
+
changed ||= inner;
|
|
322
|
+
return inner ? out : s;
|
|
323
|
+
});
|
|
324
|
+
if (!changed) {
|
|
325
|
+
return list;
|
|
326
|
+
}
|
|
327
|
+
// Descending by target index, so an earlier insertion does not shift the position a later one
|
|
328
|
+
// was computed against — and descending among the inits SHARING a target too, because splicing
|
|
329
|
+
// each at the same index puts the last one spliced on top. Without that second key a run that
|
|
330
|
+
// sinks together comes out REVERSED, which is the one order this function exists to avoid: it
|
|
331
|
+
// is pool-load order the sort above is spelling, and `head` would have kept it.
|
|
332
|
+
for (const { at: to, init } of [...(here ?? [])].sort((a, b) => b.at - a.at || b.i - a.i)) {
|
|
333
|
+
mapped.splice(to, 0, init);
|
|
334
|
+
}
|
|
335
|
+
return mapped;
|
|
336
|
+
};
|
|
337
|
+
// `site !== rest` is the whole nesting question: every site is a list of the tree the sites were
|
|
338
|
+
// computed on, and the top-level one is `rest` by identity.
|
|
339
|
+
return {
|
|
340
|
+
body: [...stay, ...rebuild(rest)],
|
|
341
|
+
moved: sunk.map((s) => s.init.name),
|
|
342
|
+
nested: sunk.filter((s) => s.site !== rest).map((s) => s.init.name),
|
|
343
|
+
};
|
|
344
|
+
}
|