@asmlift/core 0.4.0 → 0.6.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 +238 -164
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +341 -22
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +24 -6
- package/src/frontend/opaque.ts +31 -18
- package/src/frontend/ppc.ts +54 -7
- package/src/frontend/ssa.ts +632 -13
- package/src/frontend/thumb.ts +2786 -286
- package/src/ir/alias.ts +129 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +156 -27
- 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/argbase.ts +8 -2
- package/src/l3/ast.ts +464 -49
- package/src/l3/basecse.ts +709 -88
- package/src/l3/coalesce.ts +521 -66
- package/src/l3/dce.ts +54 -19
- package/src/l3/gates.ts +88 -0
- 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 +113 -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 +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +649 -219
- 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 +23 -4
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -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 +236 -13
- package/src/pipeline.ts +206 -49
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +4 -3
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +44 -19
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +101 -16
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +215 -14
- package/src/raise/shortcircuit.ts +477 -79
- package/src/raise/struct-arrays.ts +21 -3
- package/src/raise/structs.ts +61 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1726 -251
- package/src/structure/analysis.ts +1516 -220
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2850 -533
- package/src/structure/switch-recover.ts +688 -147
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// The `/livebase-block × /regionbase` PAIRING (rank.ts `/livebase-block/homesplit`): one base kept
|
|
2
|
+
// at the function head and a SECOND base split into one local per region, in the same function.
|
|
3
|
+
//
|
|
4
|
+
// WHY IT IS NOT REACHABLE FROM EITHER LEVER. Both are whole-FUNCTION policies over the bases they
|
|
5
|
+
// bind: `hoistBaseLocals` homes every key its table admits at one placement, and
|
|
6
|
+
// `hoistScopedBases` splits every key its region rule admits. A function whose two bases want
|
|
7
|
+
// OPPOSITE answers is spelled by neither. `synthetic:dmapoll` is that function and its endpoint is
|
|
8
|
+
// compiled rather than argued: with both bases at function scope agbcc scores 11, with both split
|
|
9
|
+
// per region 18, with neither hoisted 69 — and 0 only where the two policies apply to DIFFERENT
|
|
10
|
+
// bases (the lattice in apps/benchmark/dataset/synthetic.ts).
|
|
11
|
+
//
|
|
12
|
+
// A PIPE, NEVER A MERGE, and that is the whole safety argument for the names. `hoistBaseLocals`
|
|
13
|
+
// runs FIRST with one key withheld; every key it homes now reads through a pointer LOCAL, which
|
|
14
|
+
// `SCOPEBASE_ELIGIBILITY`'s `shadowed-or-nonarray-base` refuses outright, so no key the head hoist
|
|
15
|
+
// claimed reaches the second pass at all. What that pass DOES see is the withheld key together with
|
|
16
|
+
// every base the caller's table never bound, and it splits those too — `splitHomeBases` counts the
|
|
17
|
+
// withheld key's own entries and nothing else. `nameAllocator` (l3/hoist.ts) re-derives its taken
|
|
18
|
+
// names from the tree it is handed, so the second pass cannot re-mint the first's. Merging two runs
|
|
19
|
+
// over ONE input is the shape that would collide, and nothing here does it.
|
|
20
|
+
//
|
|
21
|
+
// WHICH KEY IS WITHHELD IS NOT DERIVABLE, so every admitted key is offered as its own candidate,
|
|
22
|
+
// LABELLED WITH THAT KEY, and the differ referees — the same posture `/scopebase` and `/regionbase`
|
|
23
|
+
// take toward each other. The label carries `homeSplitTag(key)` because a candidate label is an
|
|
24
|
+
// IDENTITY: `bench diff` and docs/ranked-repro.md compare candidates by it, so one label over two
|
|
25
|
+
// withholds would hide a program swap from both. The withhold itself is DATA: one rejection
|
|
26
|
+
// prepended to the caller's own admission table, in the `Gate<BaseKey>` type that table already
|
|
27
|
+
// has, so `firstRejection` attributes a refusal to it, `without` ablates it, and the composed table
|
|
28
|
+
// is on `gate-contract.test.ts`'s roster like any other.
|
|
29
|
+
//
|
|
30
|
+
// EXACTLY ONE KEY IS WITHHELD, and the arity is a claim rather than an oversight. A two-key withhold
|
|
31
|
+
// exists only where the caller's table binds three — `homesplit-fan-cap` admits no more — and there
|
|
32
|
+
// it is the three further pairs, each carrying the three respells rank.ts derives from one pipe:
|
|
33
|
+
// NINE more candidates per axis point on those functions alone, before any shape product, and no
|
|
34
|
+
// row asks for one. `l3/volatileptr.ts`'s `volatileSubsetCandidates` enumerates every proper subset
|
|
35
|
+
// under the same cap; it does that because a row demanded each of them. Widen this the same way,
|
|
36
|
+
// on a row.
|
|
37
|
+
//
|
|
38
|
+
// SEMANTICS ARE PRESERVED BY CONSTRUCTION — both halves only re-spell where the address of a global
|
|
39
|
+
// is materialized, and each half's own contracts (`placeBaseLocals`' ordering, `assertHoistsDominate`
|
|
40
|
+
// on the region plan) still run. What the pairing can get WRONG is bytes, and one qualifier.
|
|
41
|
+
import { addrConst, inRange } from './address';
|
|
42
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
43
|
+
import { exprChildren, stmtExprs, stmtLists } from './ast';
|
|
44
|
+
import { type BaseKey, baseSites, hoistBaseLocals, leafId, parseBaseKey } from './basecse';
|
|
45
|
+
import { type Gate, firstRejection } from './gates';
|
|
46
|
+
import type { HoistPlacement } from './hoist';
|
|
47
|
+
import { applyScopedBasePlan, planScopedBases, scopedBaseKey } from './scopebase';
|
|
48
|
+
import { volatileEligibleValue } from './volatileptr';
|
|
49
|
+
|
|
50
|
+
/** `gates` with `key` refused. The withhold goes FIRST so `firstRejection` attributes a refusal to
|
|
51
|
+
* it rather than to whichever inherited rule would also have fired. */
|
|
52
|
+
export const withholdingKey = (gates: readonly Gate<BaseKey>[], key: string): readonly Gate<BaseKey>[] => [
|
|
53
|
+
{
|
|
54
|
+
id: 'withheld-key',
|
|
55
|
+
why: 'this key is the one the region rule is to split, so the head hoist must not claim it',
|
|
56
|
+
sound: false,
|
|
57
|
+
rejects: (c) => c.key === key,
|
|
58
|
+
},
|
|
59
|
+
...gates,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/** The withheld key as a LABEL token: `c:67109076 4 true` → `0x40000d4.4s`. Width and signedness
|
|
63
|
+
* ride because they are part of the key — two keys over one address are two different spellings.
|
|
64
|
+
*
|
|
65
|
+
* PARSED BY THE KEY'S PRODUCER (`l3/basecse.ts`'s `parseBaseKey`, beside `keyOf`), because a base
|
|
66
|
+
* id is not one space-free word and a local split here gets it wrong in both directions. `baseId`
|
|
67
|
+
* spells a CAST base as `a:gEnigmaBerries <Elem5*>` — one space, and it is the grammar's own
|
|
68
|
+
* separator rather than the type's. Split from the FRONT, the type reads as the width and the
|
|
69
|
+
* width as the signedness; split from the END, the `a:` form comes out right and the `c:` form
|
|
70
|
+
* runs `Number` over `67109076 <u16*>`, tagging every cast over a numeric base `0xNaN` and
|
|
71
|
+
* collapsing distinct keys onto one label. No shipped table admits a cast base outside
|
|
72
|
+
* `/orderbase`, and `/orderbase` carries `pairings: false`, so no such key reaches this function
|
|
73
|
+
* today — but a candidate LABEL is an identity (`bench diff` and docs/ranked-repro.md compare
|
|
74
|
+
* candidates by it), and one roster line is all that stands between the two. The sibling half of
|
|
75
|
+
* the same hazard is already guarded in `splitHomeBases`, which translates a cast base to no
|
|
76
|
+
* region key and declines. */
|
|
77
|
+
export function homeSplitTag(key: string): string {
|
|
78
|
+
const { leaf, castType, width, signed } = parseBaseKey(key);
|
|
79
|
+
const base = leaf.startsWith('c:') ? `0x${Number(leaf.slice(2)).toString(16)}` : leaf.slice(leaf.indexOf(':') + 1);
|
|
80
|
+
// The cast's element type stays in the token — it is part of the key's identity, since two casts
|
|
81
|
+
// over one symbol are two locals that stride differently. Whitespace is squeezed defensively
|
|
82
|
+
// rather than because any type spells one: a label is one whitespace-free word everywhere it is
|
|
83
|
+
// read, and that has to hold whatever `typeToString` grows.
|
|
84
|
+
const type = castType === null ? '' : `<${castType}>`;
|
|
85
|
+
return `${`${base}${type}`.replace(/\s+/g, '')}.${width}${signed ? 's' : 'u'}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The FUNCTION-level half of the admission: how many keys the caller's table binds on this tree.
|
|
89
|
+
* Nothing about one withheld key, which is why it is decided once (see `homeSplitWithholds`). */
|
|
90
|
+
export interface HomeSplitFanCtx {
|
|
91
|
+
/** how many keys the caller's admission table binds — the pairing needs at least two */
|
|
92
|
+
readonly hoistableKeys: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The two rules that read the tree's KEY COUNT and nothing else, which is why they are asked once
|
|
96
|
+
* per (tree, table) rather than per candidate: inside the pipe they would cost a head hoist, a
|
|
97
|
+
* region plan, a rewrite and a census to report a fact the caller holds before any of it. */
|
|
98
|
+
export const HOMESPLIT_FAN_GATES: readonly Gate<HomeSplitFanCtx>[] = [
|
|
99
|
+
{
|
|
100
|
+
id: 'homesplit-degenerate',
|
|
101
|
+
why: 'withholding the only hoistable key is `/regionbase`, and withholding none is `/livebase-block`',
|
|
102
|
+
sound: false,
|
|
103
|
+
rejects: (c) => c.hoistableKeys < 2,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'homesplit-fan-cap',
|
|
107
|
+
why: 'one candidate per hoistable key, times the volatile products — the whole cost of the axis',
|
|
108
|
+
sound: false,
|
|
109
|
+
rejects: (c) => c.hoistableKeys > 3,
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
/** One candidate PAIRING, as the per-key admission rules see it. */
|
|
114
|
+
export interface HomeSplitCtx {
|
|
115
|
+
/** the region rule gives the withheld key two or more locals */
|
|
116
|
+
readonly withheldSplits: boolean;
|
|
117
|
+
/** the withheld key's head home would have been a `/volatile`-qualified local at a DEVICE
|
|
118
|
+
* address, and the piped tree leaves a READ of that base inline */
|
|
119
|
+
readonly inlineDeviceRead: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The pairing's PER-KEY admission. NONE is sound, and none is owed a `sound: true`: withholding a
|
|
123
|
+
* key from the head hoist and splitting it per region names the SAME ADDRESS in a different place
|
|
124
|
+
* (`keyOf` folds width and signedness into the key), the two rules that decide MEANING already
|
|
125
|
+
* live in `SCOPEBASE_ELIGIBILITY`, and a plan over a SUBSET of the keys admits nothing the
|
|
126
|
+
* all-keys plan did not. What these rules decide is bytes, fan, and one qualifier.
|
|
127
|
+
*
|
|
128
|
+
* `homesplit-drops-device-volatile` is the one that protects a row rather than the fan.
|
|
129
|
+
* `/volatile` (l3/volatileptr.ts) qualifies MINTED POINTER LOCALS and `/vol-store` (l3/volstore.ts)
|
|
130
|
+
* a STORE at a fixed device address; a device READ left inline is reached by neither. Withholding
|
|
131
|
+
* a key is exactly what can leave one there — the head hoist would have homed it into a local
|
|
132
|
+
* `/volatile` covers — so the pairing declines rather than publish a spelling that silently drops
|
|
133
|
+
* a qualifier the spelling it replaces CARRIED.
|
|
134
|
+
*
|
|
135
|
+
* WHETHER IT CARRIED ONE IS `/volatile`'s OWN QUESTION, so it is asked of that model
|
|
136
|
+
* (`volatileEligibleValue`) rather than re-derived here from the base's node kind or its address.
|
|
137
|
+
* The difference is the arm the benchmark runs on: under a symbol map an absolute pool constant
|
|
138
|
+
* lifts to a `gaddr`, and `/volatile` VETOES a local fed `&gSym` because the symbol map owns a
|
|
139
|
+
* declared global's volatility. On a named base the head home this rule protects is therefore
|
|
140
|
+
* unqualified too — nothing is dropped, and refusing there would delete a spelling to protect a
|
|
141
|
+
* qualifier NEITHER side carries. The address is a second question, asked only of a base the
|
|
142
|
+
* qualifier could have reached. */
|
|
143
|
+
export const HOMESPLIT_GATES: readonly Gate<HomeSplitCtx>[] = [
|
|
144
|
+
{
|
|
145
|
+
id: 'homesplit-no-region',
|
|
146
|
+
why: 'a withheld key the region rule declines to split leaves the spelling the primary carries',
|
|
147
|
+
sound: false,
|
|
148
|
+
rejects: (c) => !c.withheldSplits,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'homesplit-drops-device-volatile',
|
|
152
|
+
why: 'a device read left inline is qualified by neither /volatile nor /vol-store',
|
|
153
|
+
sound: false,
|
|
154
|
+
rejects: (c) => c.inlineDeviceRead,
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
/** The keys this tree offers as withholds — one candidate each, or none at all. The caller asks
|
|
159
|
+
* ONCE per (tree, admission table) and loops over the answer.
|
|
160
|
+
*
|
|
161
|
+
* IT IS A KEY-COUNT FACT AND NEVER A SYMBOL-MAP ONE. The keys are not spelled here: they arrive
|
|
162
|
+
* from `l3/basecse.ts`, whose `leafId`/`keyOf` render a leaf base as `c:<const>` wherever it is
|
|
163
|
+
* not a named `a:<sym>`, and `homeSplitTag` above prints that half as `0x…`. (This module's own
|
|
164
|
+
* `leafBaseId`, BELOW, is a different reader — it identifies bases inside
|
|
165
|
+
* `baseReads`/`splitHomeBases` and never produces the argument.) So a tree lifted with NO map
|
|
166
|
+
* reaches this door on plain addresses: enumerated map-less, `EntityHitReaction` in the klonoa
|
|
167
|
+
* `code_1` checkout yields 512 `/homesplit` candidates over 512 distinct sources, keyed
|
|
168
|
+
* `0x3004c20.1u` and `0x40000d4.4s` — TWO numeric keys. "No map, so this product cannot fire" is
|
|
169
|
+
* therefore a fact about whichever function was censused, not about the arm, and a skip built on
|
|
170
|
+
* it deletes live candidates. */
|
|
171
|
+
export function homeSplitWithholds(
|
|
172
|
+
keys: readonly string[],
|
|
173
|
+
admission: readonly Gate<HomeSplitFanCtx>[] = HOMESPLIT_FAN_GATES,
|
|
174
|
+
): readonly string[] {
|
|
175
|
+
return firstRejection(admission, { hoistableKeys: keys.length }) === null ? keys : [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface HomeSplitOpts {
|
|
179
|
+
/** the caller's own admission table — `/livebase-block`'s on the roster row that pairs */
|
|
180
|
+
readonly gates: readonly Gate<BaseKey>[];
|
|
181
|
+
readonly placement: HoistPlacement;
|
|
182
|
+
/** the key withheld from the head hoist, in `l3/basecse.ts` vocabulary */
|
|
183
|
+
readonly key: string;
|
|
184
|
+
readonly deviceRegisters?: readonly [number, number];
|
|
185
|
+
readonly admission?: readonly Gate<HomeSplitCtx>[];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A leaf deref base's identity, ignoring the access width — `l3/basecse.ts`'s spelling of it,
|
|
189
|
+
* taken from that module rather than re-spelled here, and `null` for every other base kind. */
|
|
190
|
+
const leafBaseId = (b: Expr): string | null => (b.k === 'const' || b.k === 'addr' ? leafId(b) : null);
|
|
191
|
+
|
|
192
|
+
/** Every leaf-based READ in the tree, by base identity — an access reached anywhere but as a
|
|
193
|
+
* store's own lvalue. A store's lvalue is what `/vol-store` can still qualify; nothing else is. */
|
|
194
|
+
function baseReads(body: Stmt[], out: Set<string>): void {
|
|
195
|
+
const visit = (e: Expr): void => {
|
|
196
|
+
if (e.k === 'index') {
|
|
197
|
+
const id = leafBaseId(e.base);
|
|
198
|
+
if (id !== null) {
|
|
199
|
+
out.add(id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
exprChildren(e).forEach(visit);
|
|
203
|
+
};
|
|
204
|
+
const walk = (s: Stmt): void => {
|
|
205
|
+
for (const e of stmtExprs(s)) {
|
|
206
|
+
// a store's LVALUE is the one position `/vol-store` reaches, so it is not a read
|
|
207
|
+
if (s.k === 'store' && e === s.lval) {
|
|
208
|
+
exprChildren(e).forEach(visit);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
visit(e);
|
|
212
|
+
}
|
|
213
|
+
if (s.k === 'for') {
|
|
214
|
+
// `init` and `inc` are STATEMENTS, reached by neither `stmtExprs` nor `stmtLists` — the same
|
|
215
|
+
// walker asymmetry `l3/scopebase.ts`'s `collect` calls out. A read missed here is a device
|
|
216
|
+
// read the rule below cannot see.
|
|
217
|
+
walk(s.init);
|
|
218
|
+
walk(s.inc);
|
|
219
|
+
}
|
|
220
|
+
for (const child of stmtLists(s)) {
|
|
221
|
+
child.forEach(walk);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
body.forEach(walk);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The pairing for ONE withheld key: the tree with every OTHER admitted base homed at `placement`,
|
|
229
|
+
* and the withheld one split per region. `homed` is the intermediate — the caller re-checks the
|
|
230
|
+
* placement differential across the pipe with it. Null when a rule refuses, or when either half
|
|
231
|
+
* declines.
|
|
232
|
+
*/
|
|
233
|
+
export function splitHomeBases(sfn: SFn, opts: HomeSplitOpts): { homed: SFn; split: SFn } | null {
|
|
234
|
+
const gates = opts.admission ?? HOMESPLIT_GATES;
|
|
235
|
+
// `null` only at `placement: 'scope'` (l3/basecse.ts), and no PAIRING roster row sits at that
|
|
236
|
+
// placement today — so this is a guard rather than a path.
|
|
237
|
+
const homed = hoistBaseLocals(sfn, withholdingKey(opts.gates, opts.key), opts.placement);
|
|
238
|
+
if (homed === null) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
// The withheld key in the REGION pass's vocabulary. The two passes key on the same address but
|
|
242
|
+
// spell an `addr` base's identity differently, so the translation is explicit — a string compare
|
|
243
|
+
// across them would silently never match for an `addr` base. A CAST base (an array-of-struct
|
|
244
|
+
// element's `(struct S *)&gSym`) has no spelling in the region pass at all — its `LeafBase` is
|
|
245
|
+
// the leaf kinds only — so it translates to nothing and the pairing declines, which is the same
|
|
246
|
+
// answer the region planner would have given.
|
|
247
|
+
const meta = baseSites(sfn).get(opts.key);
|
|
248
|
+
const scoped = meta && meta.base.k !== 'cast' ? scopedBaseKey(meta.base, meta.width, meta.signed) : null;
|
|
249
|
+
// ONE plan, then its applier — the count below and the rewrite read the same decision rather than
|
|
250
|
+
// two runs of the planner that a future rule could make disagree.
|
|
251
|
+
const plan = planScopedBases(homed, { regions: 'per-region' });
|
|
252
|
+
const split = plan.entries.filter((e) => e.key === scoped).length;
|
|
253
|
+
const out = applyScopedBasePlan(homed, plan);
|
|
254
|
+
// Judged on the SPLIT tree, which is the only one that answers the question: an access the region
|
|
255
|
+
// rule repoints into a minted local is one `/volatile` covers, and on `homed` it is still inline.
|
|
256
|
+
const reads = new Set<string>();
|
|
257
|
+
baseReads((out ?? homed).body, reads);
|
|
258
|
+
const base = meta?.base ?? null;
|
|
259
|
+
const id = base ? leafBaseId(base) : null;
|
|
260
|
+
if (
|
|
261
|
+
firstRejection(gates, {
|
|
262
|
+
withheldSplits: split >= 2,
|
|
263
|
+
inlineDeviceRead:
|
|
264
|
+
id !== null &&
|
|
265
|
+
base !== null &&
|
|
266
|
+
volatileEligibleValue(base) &&
|
|
267
|
+
inRange(addrConst(base), opts.deviceRegisters) &&
|
|
268
|
+
reads.has(id),
|
|
269
|
+
}) !== null
|
|
270
|
+
) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
if (out === null) {
|
|
274
|
+
if (split >= 2) {
|
|
275
|
+
// `homesplit-no-region` admitted two or more entries for the withheld key, so the plan is
|
|
276
|
+
// neither empty nor compound and the applier cannot decline. Loud rather than silent.
|
|
277
|
+
throw new Error(`homesplit: the region plan splits ${opts.key} ${split} ways and the applier declined`);
|
|
278
|
+
}
|
|
279
|
+
// With that rule ABLATED (`without(HOMESPLIT_GATES, …)`) its premise is gone, and an ablation
|
|
280
|
+
// that converts a decline into a throw prices the rule at every dropped candidate instead of
|
|
281
|
+
// at the spelling it keeps out. A decline is the honest answer on that path.
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
return { homed, split: out };
|
|
285
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// L3 re-spelling lever: a loop INIT moves above the guard that encloses it, and the guard reads
|
|
2
|
+
// the initialized variable.
|
|
3
|
+
//
|
|
4
|
+
// `for (i = 0; i < n; i++)` compiles with the init BEFORE the zero-trip test (`mov r3,#0` then
|
|
5
|
+
// `cmp r3, r5`), while `if (0 < n) { i = 0; do … }` compiles with the init behind the branch.
|
|
6
|
+
// Both source forms lift to the SAME IR — a const has no position — so which the original spelled
|
|
7
|
+
// is not recoverable; this lever emits the init-first sibling and the differ referees:
|
|
8
|
+
//
|
|
9
|
+
// if (0 < n) { v = 0; … } → v = 0; if (v < n) { … }
|
|
10
|
+
//
|
|
11
|
+
// Two rewrites (the guard re-spelling gated on the moved write being dead — see SCOPE):
|
|
12
|
+
// • common-arm hoist — both arms of an `if` begin with the SAME pure-const assign
|
|
13
|
+
// (`if (c) { v = 0; } else { v = 0; … }`, gcc's inverted-guard shape): the assign moves above
|
|
14
|
+
// the `if`, and an emptied then-arm flips into its negated else form.
|
|
15
|
+
// • guard re-spelling — an else-less `if` whose then-arm begins with `v = X` and whose
|
|
16
|
+
// condition carries X ITSELF as a comparison side: the assign moves above and that side
|
|
17
|
+
// becomes `v`. X is a const (`for (j = 0; j < n;…)`'s shape) or a pure NON-VOLATILE read
|
|
18
|
+
// (`for (j = *p; j < size;…)` — guard and init read the same cell back to back, which the
|
|
19
|
+
// local-spelling source reads ONCE; collapsing the adjacent pair is what the compiler saw).
|
|
20
|
+
//
|
|
21
|
+
// SCOPE (decline over approximate): both rewrites touch PRIVATE locals only — a bare-global
|
|
22
|
+
// assign stores memory other code observes, a VOLATILE local's store is itself observable (the
|
|
23
|
+
// escaped DMA scratch), and a local whose ADDRESS is taken anywhere can be read through the
|
|
24
|
+
// pointer where no name betrays it — all three stay put. The common-arm hoist carries pure
|
|
25
|
+
// CONST values only. A guard re-spell's X must be call-free and marker-free, may mention only
|
|
26
|
+
// the function's own non-volatile params/locals (a global or `&gSym` could be project-declared
|
|
27
|
+
// volatile, and a volatile read may not be deduplicated), and the substitution must PRESERVE
|
|
28
|
+
// THE COMPARE'S MEANING: the variable's declared type can differ from X's rendered type, so the
|
|
29
|
+
// swap is admitted only when both sides were provably non-negative (every signedness reading
|
|
30
|
+
// agrees there) or the compare's rendered signedness is unchanged — anything indeterminate
|
|
31
|
+
// refuses. The condition must not read the variable (its pre-assign value dies in the move)
|
|
32
|
+
// and, for a READ X, must be effect-free as a whole (the hoist moves X's read above it). The
|
|
33
|
+
// guard re-spelling
|
|
34
|
+
// mints a write on a previously write-free path, so it needs that write to be DEAD there: the
|
|
35
|
+
// variable must be untouched after the `if` in its own list and in the tail of every ancestor
|
|
36
|
+
// list (a sibling arm of an ancestor `if` is not "after" — it never runs in the same entry).
|
|
37
|
+
// Under a loop or switch ancestor the tails stop describing what runs next (a back edge
|
|
38
|
+
// re-enters everything, a case can fall through), so there the variable must appear nowhere
|
|
39
|
+
// outside the rewritten `if` at all. Declines (null) when nothing changes.
|
|
40
|
+
import type { Expr, SFn, Stmt } from './ast';
|
|
41
|
+
import { NEGATE_REL, exprChildren, exprEquals, exprHasEffect, stmtChildren, stmtExprs, walkExprs } from './ast';
|
|
42
|
+
import { arithConversionSignedness, declaredTypes, provablyNonNegative } from './typing';
|
|
43
|
+
|
|
44
|
+
const readsVar = (e: Expr, name: string): boolean =>
|
|
45
|
+
((e.k === 'var' || e.k === 'addr') && e.name === name) || exprChildren(e).some((c) => readsVar(c, name));
|
|
46
|
+
|
|
47
|
+
// READS only — a pure write in a tail is benign (it overwrites the minted value on every path,
|
|
48
|
+
// and any read after it is that write's business); touchesOutside below is TOTAL because strong
|
|
49
|
+
// mode must know the name is absent, presence of any kind included.
|
|
50
|
+
const stmtTouches = (s: Stmt, name: string): boolean =>
|
|
51
|
+
stmtExprs(s).some((e) => readsVar(e, name)) || stmtChildren(s).some((x) => stmtTouches(x, name));
|
|
52
|
+
|
|
53
|
+
const isConstAssign = (s: Stmt): s is Extract<Stmt, { k: 'assign' }> & { value: { k: 'const'; value: number } } =>
|
|
54
|
+
s.k === 'assign' && s.value.k === 'const';
|
|
55
|
+
|
|
56
|
+
/** Peel value-preserving `(s32)`/`(u32)` casts. Width 32 only: a narrower target truncates. A
|
|
57
|
+
* `volatile` cast is never peeled — the qualifier is the access's meaning, and the differ cannot
|
|
58
|
+
* referee its loss. */
|
|
59
|
+
const stripWideIntCast = (e: Expr): Expr =>
|
|
60
|
+
e.k === 'cast' && e.to.kind === 'int' && e.to.width === 32 && e.volatile !== true ? stripWideIntCast(e.e) : e;
|
|
61
|
+
|
|
62
|
+
/** Every deref rooted at a var through casts only. The var-root rule is a TWO-WORLD argument, not a
|
|
63
|
+
* volatility proof: a deref through a plain-declared pointer local may still be MMIO, but the
|
|
64
|
+
* /volatile axis enumerates the qualified sibling — where this lever refuses — so both worlds reach
|
|
65
|
+
* the differ and collapsing reads here is the plain world's own premise. A raw `*(u16 *)CONST` deref
|
|
66
|
+
* has NO local for /volatile to qualify, so no sibling carries the volatile world and the collapse
|
|
67
|
+
* would silently discard it. */
|
|
68
|
+
const varRooted = (e: Expr): boolean => (e.k === 'var' ? true : e.k === 'cast' ? varRooted(e.e) : false);
|
|
69
|
+
|
|
70
|
+
/** A guard re-spell's X: call/marker-free, every named leaf a non-volatile param/local of THIS
|
|
71
|
+
* function (a global or `&gSym` could be project-declared volatile), and every deref `varRooted`. */
|
|
72
|
+
const hoistableRead = (e: Expr, ownNames: ReadonlySet<string>, volatileLocals: ReadonlySet<string>): boolean => {
|
|
73
|
+
if (e.k === 'call' || e.k === 'marker' || e.k === 'addr') {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (e.k === 'var' && (!ownNames.has(e.name) || volatileLocals.has(e.name))) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if ((e.k === 'index' || e.k === 'field') && !varRooted(e.base)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return exprChildren(e).every((c) => hoistableRead(c, ownNames, volatileLocals));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** A compare operand and the init's value denote the same 32-bit value under different SPELLINGS
|
|
86
|
+
* when a width-32 cast is all that separates them: `/uns-cmp` wraps one side in `(u32)` to make
|
|
87
|
+
* the branch unsigned, and on a zero-trip guard that side is the very const the init assigns.
|
|
88
|
+
* The swap is still exact — `v = X` stores X's 32 bits and `v` is 32-bit-declared
|
|
89
|
+
* (meaningPreserved refuses otherwise), so `v` and `(u32)X` carry the same bit pattern and only
|
|
90
|
+
* the compare's rendered signedness can differ, which meaningPreserved checks separately. A
|
|
91
|
+
* NARROWING cast changes the value and never matches, and a `volatile` one is not peeled at all. */
|
|
92
|
+
const sameValue = (a: Expr, b: Expr): boolean => exprEquals(stripWideIntCast(a), stripWideIntCast(b));
|
|
93
|
+
|
|
94
|
+
/** The compare-meaning gate (see SCOPE): substituting `v` for X may change the compare's
|
|
95
|
+
* rendered signedness through v's declared type. Sufficiency: v's declared width must be 32
|
|
96
|
+
* (the assignment `v = X` then represents any 32-bit-or-narrower X exactly, so v's runtime
|
|
97
|
+
* value EQUALS X's — a narrow-declared v would truncate and no signedness reasoning survives
|
|
98
|
+
* that), and then (a) both original sides provably in [0, 2^31) ⇒ signed and unsigned
|
|
99
|
+
* compares agree on the actual values whatever the swap does to rendered signedness; (b)
|
|
100
|
+
* otherwise a defined, UNCHANGED rendered signedness over equal values gives the identical
|
|
101
|
+
* result. Anything indeterminate refuses. */
|
|
102
|
+
const meaningPreserved = (
|
|
103
|
+
l: Expr,
|
|
104
|
+
r: Expr,
|
|
105
|
+
side: 'l' | 'r',
|
|
106
|
+
v: string,
|
|
107
|
+
env: ReturnType<typeof declaredTypes>,
|
|
108
|
+
): boolean => {
|
|
109
|
+
const vt = env(v);
|
|
110
|
+
if (vt?.kind !== 'int' || vt.width !== 32) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (provablyNonNegative(l, env) && provablyNonNegative(r, env)) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
const before = arithConversionSignedness(l, r, env);
|
|
117
|
+
const vv: Expr = { k: 'var', name: v };
|
|
118
|
+
const after = side === 'l' ? arithConversionSignedness(vv, r, env) : arithConversionSignedness(l, vv, env);
|
|
119
|
+
return before !== undefined && before === after;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** TOTAL (reads and pure writes) — see the note on stmtTouches. Runs against the pre-rewrite
|
|
123
|
+
* tree (`skip` is an original-tree statement, found by identity); the rewrites never change a
|
|
124
|
+
* name's presence in a subtree, so the verdict carries over to the rewritten one. */
|
|
125
|
+
const touchesOutside = (list: Stmt[], skip: Stmt, name: string): boolean =>
|
|
126
|
+
list.some(
|
|
127
|
+
(st) =>
|
|
128
|
+
st !== skip &&
|
|
129
|
+
(stmtExprs(st).some((e) => readsVar(e, name)) ||
|
|
130
|
+
(st.k === 'assign' && st.name === name) ||
|
|
131
|
+
touchesOutside(stmtChildren(st), skip, name)),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
/** Every name whose ADDRESS this function takes anywhere — a local read through the captured
|
|
135
|
+
* pointer with no name in sight. */
|
|
136
|
+
function addressTakenNames(sfn: SFn): Set<string> {
|
|
137
|
+
const taken = new Set<string>();
|
|
138
|
+
for (const e of walkExprs(sfn.body)) {
|
|
139
|
+
if (e.k === 'addr') {
|
|
140
|
+
taken.add(e.name);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return taken;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function initFirstGuards(sfn: SFn): SFn | null {
|
|
147
|
+
let changed = false;
|
|
148
|
+
// the names both rewrites may move a write of: this function's params and its non-volatile
|
|
149
|
+
// locals, minus everything whose address escapes
|
|
150
|
+
const addressTaken = addressTakenNames(sfn);
|
|
151
|
+
const fnLocal = new Set(
|
|
152
|
+
[...sfn.params.map((d) => d.name), ...sfn.locals.filter((l) => l.volatile !== true).map((l) => l.name)].filter(
|
|
153
|
+
(n) => !addressTaken.has(n),
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
const volatileLocals = new Set(
|
|
157
|
+
sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
|
|
158
|
+
);
|
|
159
|
+
const ownNames = new Set([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
|
|
160
|
+
const env = declaredTypes(sfn);
|
|
161
|
+
|
|
162
|
+
// `tails`: for each ancestor list, the statements after the ancestor on the path here.
|
|
163
|
+
// `strong`: a loop or switch ancestor exists, so tails stop bounding what runs after.
|
|
164
|
+
interface Ctx {
|
|
165
|
+
tails: Stmt[][];
|
|
166
|
+
strong: boolean;
|
|
167
|
+
}
|
|
168
|
+
// Assigns this pass itself hoisted to an arm head's parent list. An ancestor `if` whose arm now
|
|
169
|
+
// BEGINS with one would otherwise re-spell it again — rewriting its own condition's accidental
|
|
170
|
+
// matching const into the variable and stealing the arrangement the inner guard needed.
|
|
171
|
+
const moved = new Set<Stmt>();
|
|
172
|
+
const rewriteList = (list: Stmt[], ctx: Ctx): Stmt[] => {
|
|
173
|
+
const out: Stmt[] = [];
|
|
174
|
+
for (let i = 0; i < list.length; i++) {
|
|
175
|
+
const s0 = list[i];
|
|
176
|
+
const s = recurse(s0, { tails: [...ctx.tails, list.slice(i + 1)], strong: ctx.strong });
|
|
177
|
+
if (s.k !== 'if') {
|
|
178
|
+
out.push(s);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
let { cond, then, else: els } = s;
|
|
182
|
+
// common-arm hoist
|
|
183
|
+
const hoisted: { name: string; value: number }[] = [];
|
|
184
|
+
for (;;) {
|
|
185
|
+
const t0 = then[0];
|
|
186
|
+
const e0 = els[0];
|
|
187
|
+
if (
|
|
188
|
+
t0 === undefined ||
|
|
189
|
+
e0 === undefined ||
|
|
190
|
+
moved.has(t0) ||
|
|
191
|
+
moved.has(e0) ||
|
|
192
|
+
!isConstAssign(t0) ||
|
|
193
|
+
!isConstAssign(e0) ||
|
|
194
|
+
!fnLocal.has(t0.name) ||
|
|
195
|
+
t0.name !== e0.name ||
|
|
196
|
+
t0.value.value !== e0.value.value ||
|
|
197
|
+
readsVar(cond, t0.name)
|
|
198
|
+
) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
out.push(t0);
|
|
202
|
+
moved.add(t0);
|
|
203
|
+
hoisted.push({ name: t0.name, value: t0.value.value });
|
|
204
|
+
changed = true;
|
|
205
|
+
then = then.slice(1);
|
|
206
|
+
els = els.slice(1);
|
|
207
|
+
}
|
|
208
|
+
if (then.length === 0 && els.length > 0) {
|
|
209
|
+
const neg = cond.k === 'bin' ? NEGATE_REL[cond.op] : undefined;
|
|
210
|
+
if (neg !== undefined && cond.k === 'bin') {
|
|
211
|
+
cond = { ...cond, op: neg };
|
|
212
|
+
[then, els] = [els, then];
|
|
213
|
+
changed = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// a COMMON-hoisted variable holds its const on both paths, so the condition's matching
|
|
217
|
+
// const operand reads through it unconditionally — no tail gate needed.
|
|
218
|
+
// A BARE const only, where the guard re-spelling below uses the cast-tolerant `sameValue`:
|
|
219
|
+
// this site does not run `meaningPreserved`, so it may not swap in a name whose declared
|
|
220
|
+
// type could re-render the compare's signedness — which is exactly what peeling a `(u32)`
|
|
221
|
+
// would put on the table. A row that needs `/uns-cmp`'s spelling hoisted here brings the
|
|
222
|
+
// gate with it.
|
|
223
|
+
for (const hv of hoisted) {
|
|
224
|
+
if (cond.k === 'bin' && NEGATE_REL[cond.op]) {
|
|
225
|
+
if (cond.l.k === 'const' && cond.l.value === hv.value) {
|
|
226
|
+
cond = { ...cond, l: { k: 'var', name: hv.name } };
|
|
227
|
+
} else if (cond.r.k === 'const' && cond.r.value === hv.value) {
|
|
228
|
+
cond = { ...cond, r: { k: 'var', name: hv.name } };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// guard re-spelling — X a const or a hoistable read, matched as a whole comparison side
|
|
233
|
+
if (
|
|
234
|
+
els.length === 0 &&
|
|
235
|
+
then.length > 0 &&
|
|
236
|
+
then[0].k === 'assign' &&
|
|
237
|
+
!moved.has(then[0]) &&
|
|
238
|
+
fnLocal.has(then[0].name) &&
|
|
239
|
+
cond.k === 'bin' &&
|
|
240
|
+
NEGATE_REL[cond.op]
|
|
241
|
+
) {
|
|
242
|
+
const init = then[0];
|
|
243
|
+
const rest = list.slice(i + 1);
|
|
244
|
+
const side =
|
|
245
|
+
isConstAssign(init) || hoistableRead(init.value, ownNames, volatileLocals)
|
|
246
|
+
? sameValue(cond.l, init.value)
|
|
247
|
+
? ('l' as const)
|
|
248
|
+
: sameValue(cond.r, init.value)
|
|
249
|
+
? ('r' as const)
|
|
250
|
+
: null
|
|
251
|
+
: null;
|
|
252
|
+
const deadAfter = ctx.strong
|
|
253
|
+
? !touchesOutside(sfn.body, s0, init.name)
|
|
254
|
+
: !rest.some((t) => stmtTouches(t, init.name)) &&
|
|
255
|
+
ctx.tails.every((tail) => !tail.some((t) => stmtTouches(t, init.name)));
|
|
256
|
+
if (
|
|
257
|
+
side !== null &&
|
|
258
|
+
!readsVar(cond, init.name) &&
|
|
259
|
+
deadAfter &&
|
|
260
|
+
// A READ X's hoist moves its evaluation ABOVE the whole condition, so the condition must
|
|
261
|
+
// carry no effect it could cross (a call there could write the cell X reads); a CONST
|
|
262
|
+
// init crosses nothing and keeps the wider admission.
|
|
263
|
+
(isConstAssign(init) || !exprHasEffect(cond)) &&
|
|
264
|
+
meaningPreserved(cond.l, cond.r, side, init.name, env)
|
|
265
|
+
) {
|
|
266
|
+
out.push(init);
|
|
267
|
+
moved.add(init);
|
|
268
|
+
cond = { ...cond, [side]: { k: 'var', name: init.name } };
|
|
269
|
+
then = then.slice(1);
|
|
270
|
+
changed = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
out.push({ ...s, cond, then, else: els });
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const recurse = (s: Stmt, ctx: Ctx): Stmt => {
|
|
279
|
+
const strong = { ...ctx, strong: true };
|
|
280
|
+
switch (s.k) {
|
|
281
|
+
case 'if':
|
|
282
|
+
return { ...s, then: rewriteList(s.then, ctx), else: rewriteList(s.else, ctx) };
|
|
283
|
+
case 'while':
|
|
284
|
+
case 'dowhile':
|
|
285
|
+
return { ...s, body: rewriteList(s.body, strong) };
|
|
286
|
+
case 'for':
|
|
287
|
+
return { ...s, body: rewriteList(s.body, strong) };
|
|
288
|
+
case 'switch':
|
|
289
|
+
return {
|
|
290
|
+
...s,
|
|
291
|
+
cases: s.cases.map((c) => ({ ...c, body: rewriteList(c.body, strong) })),
|
|
292
|
+
...(s.default ? { default: rewriteList(s.default, strong) } : {}),
|
|
293
|
+
};
|
|
294
|
+
default:
|
|
295
|
+
return s;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const body = rewriteList(sfn.body, { tails: [], strong: false });
|
|
300
|
+
return changed ? { ...sfn, body } : null;
|
|
301
|
+
}
|