@asmlift/core 0.6.0 → 0.8.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 +48 -24
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/contracts.ts +60 -11
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +127 -30
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +209 -232
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +70 -3
- package/src/ir/opcodes.ts +52 -7
- package/src/ir/parse.ts +7 -1
- 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 +6 -6
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +110 -22
- package/src/l3/basecse.ts +50 -30
- package/src/l3/coalesce.ts +118 -61
- 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 +32 -29
- 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/tailret.ts +70 -0
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +15 -14
- 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/mangle.ts +49 -0
- package/src/pattern/engine.ts +132 -17
- package/src/pipeline.ts +39 -16
- 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 +345 -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 +133 -3
- package/src/raise/pre-recovery.ts +100 -25
- package/src/raise/retsink.ts +389 -19
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +141 -0
- package/src/rank-declare.ts +21 -13
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1176 -805
- 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/retspell.ts +95 -0
- package/src/structure/structure.ts +1425 -201
- package/src/structure/switch-recover.ts +101 -8
- package/src/symbols.ts +127 -6
- package/src/target.ts +374 -44
- package/src/trace.ts +28 -19
- package/src/variation-definitions.ts +1590 -0
- package/src/variation-gates.ts +92 -0
- package/src/variation-tokens.ts +356 -0
package/src/ir/core.ts
CHANGED
|
@@ -53,8 +53,55 @@ export interface Fn {
|
|
|
53
53
|
* so at its own definition), and a fact the structurer reads but the score probe's clone drops
|
|
54
54
|
* makes that probe's delta a fact about a program asmlift does not emit. */
|
|
55
55
|
slotHomes: SlotHomes | undefined;
|
|
56
|
+
/** L1 SIDE DATA (see {@link ParamEvidence}); set by the SSA builder, `undefined` on parsed IR.
|
|
57
|
+
* REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` and `slotHomes` are. */
|
|
58
|
+
paramEvidence: ParamEvidence | undefined;
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
/** What the machine's own object shows about each ENTRY PARAMETER, beyond the value graph — two
|
|
62
|
+
* observations the lift destroys, recorded so raise/paramwidth.ts can read them. Every entry
|
|
63
|
+
* parameter of a lifted function has an entry; a `Fn` with no map is one nobody measured.
|
|
64
|
+
*
|
|
65
|
+
* BOTH ARE OBSERVATIONS, NOT VERDICTS. What a compiler's object shows for a narrow DECLARED
|
|
66
|
+
* parameter is a fact about a TARGET, and this builder holds none; `target.ts`
|
|
67
|
+
* `compilerBehaviors.narrowParamWitness` decides what the pair means, and the disassemblies for
|
|
68
|
+
* each value live there.
|
|
69
|
+
*
|
|
70
|
+
* `deadHome` — THE STORE DOES NOT REACH THE IR. Both slot-modelling frontends spell a word
|
|
71
|
+
* sp-relative store as a write to the SSA key `sp@k` instead of a `store` op (`stackSlotKey`), so
|
|
72
|
+
* a slot nothing reloads has no reader, no op and no value: it simply is not there by L1. A
|
|
73
|
+
* parameter stored to two slots, one of them reloaded, still counts — the dead store happened —
|
|
74
|
+
* and the reader's own gates decide what a parameter with that much traffic may become.
|
|
75
|
+
*
|
|
76
|
+
* It asks NO FRAME PARTITION, and that is the difference from {@link SlotHomes}. A slot home is a
|
|
77
|
+
* DECLARATION RANK, so `sp@40` had to be classified as this function's local before it could be
|
|
78
|
+
* stamped, and MIPS declares no partition and so stamps none. "Stored here and never read back"
|
|
79
|
+
* needs no such classification: it is a statement about the store, true at any offset.
|
|
80
|
+
*
|
|
81
|
+
* `selfRedefined` — THE REGISTER IDENTITY DOES NOT REACH THE IR EITHER. SSA renames, so nothing
|
|
82
|
+
* downstream can tell a value the machine put back in the argument's OWN register from one it put
|
|
83
|
+
* in a scratch. This records the first: the first write to the parameter's register, in the entry
|
|
84
|
+
* block, is a value that parameter itself feeds. First and not any — a later write is a reuse of a
|
|
85
|
+
* register the parameter is done with.
|
|
86
|
+
*
|
|
87
|
+
* ENTRY PARAMETERS ONLY, for both. A dead spill of an ordinary value is a dead spill, and an
|
|
88
|
+
* ordinary register's self-update is arithmetic; it is the incoming ARGUMENT that carries a
|
|
89
|
+
* declaration.
|
|
90
|
+
*
|
|
91
|
+
* AND SO NEITHER FOLLOWS `replaceAllUsesWith`, where a {@link SlotHomes} entry does. A frame
|
|
92
|
+
* coordinate belongs to whichever value the structurer will name, so it travels with the uses;
|
|
93
|
+
* these are facts about the ARGUMENT REGISTER the machine was handed, and no other value can come
|
|
94
|
+
* to have been that argument. The score probe's clone re-keys the map (`cli/src/report.ts`)
|
|
95
|
+
* because a clone mints new `Value` objects for the same parameters; nothing else moves it. */
|
|
96
|
+
export interface ParamObservation {
|
|
97
|
+
/** the machine stored this parameter to a stack slot no load reads back (the ABI argument home) */
|
|
98
|
+
deadHome: boolean;
|
|
99
|
+
/** the first entry-block write to this parameter's own register is a value the parameter feeds */
|
|
100
|
+
selfRedefined: boolean;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type ParamEvidence = ReadonlyMap<Value, ParamObservation>;
|
|
104
|
+
|
|
58
105
|
/** Which `[sp,#k]` the machine homed a value at — the frame coordinate, carried L1 → L3.
|
|
59
106
|
*
|
|
60
107
|
* The coordinate exists only in the frontends: they record a word sp-relative slot's value in SSA
|
|
@@ -69,9 +116,10 @@ export interface Fn {
|
|
|
69
116
|
*
|
|
70
117
|
* DECLARES, NOT OWNS, AND THE DIFFERENCE IS AN AGBCC FACT WITH A LIVE DEPENDENCY: `ownedLocals`,
|
|
71
118
|
* the partition a def-less READ asks, admits agbcc's outgoing stack-argument area, and an offset
|
|
72
|
-
* there is an ABI position rather than an `expand_decl` rank. Under Thumb the
|
|
73
|
-
*
|
|
74
|
-
* (frontend/ssa.ts) for the
|
|
119
|
+
* there is an ABI position rather than an `expand_decl` rank. Under Thumb the declared range
|
|
120
|
+
* therefore starts above that area, where the largest outgoing argument block the frontend could
|
|
121
|
+
* LICENSE ends — see `declaredLocals` (frontend/ssa.ts) for what the licence proves and for the
|
|
122
|
+
* frames it refuses outright.
|
|
75
123
|
*
|
|
76
124
|
* ONE consumer reads it for its content — the structurer, which turns it into
|
|
77
125
|
* `SFn.locals[i].slots`; everything else only carries it (`replaceAllUsesWith`, the report's
|
|
@@ -117,6 +165,14 @@ export type SlotHomes = Map<Value, Set<number>>;
|
|
|
117
165
|
* param have no common scale. LAST write, not first: a predecessor commonly writes one key several
|
|
118
166
|
* times (1,867 of 5,283 records over three checkouts), and the edge carries what the last left.
|
|
119
167
|
*
|
|
168
|
+
* A SECOND CONSUMER READS THE ABSENCE, not the order. `structure.ts`'s `enclosingCarrierName` takes
|
|
169
|
+
* a missing `lastWrite` entry on a measured block as the licence to give a nested loop's parameter
|
|
170
|
+
* the enclosing header's NAME: the key was not written, so the machine carried the value into the
|
|
171
|
+
* inner loop in the register it had — one variable, not a copy. There a stale or over-eager "not
|
|
172
|
+
* written" is not a mis-sorted copy but a copy the emitted C no longer makes, so the absence
|
|
173
|
+
* semantics below are load-bearing for naming, and a pass that drops a record it should have
|
|
174
|
+
* folded changes spellings, not just orders.
|
|
175
|
+
*
|
|
120
176
|
* Keyed by OBJECTS (the predecessor block, the destination param), never by arg position, so the
|
|
121
177
|
* param splices in `ir/simplify.ts` cannot leave it stale. A pass that moves one block's ops into
|
|
122
178
|
* another owes `foldWriteOrder`.
|
|
@@ -186,6 +242,17 @@ export function terminator(b: Block): Op | undefined {
|
|
|
186
242
|
return b.ops[b.ops.length - 1];
|
|
187
243
|
}
|
|
188
244
|
|
|
245
|
+
/** The layout fall-through stamp alone (`opcodes.ts` declares it on `br` and on `ret`). A pass that
|
|
246
|
+
* moves a `ret` ONTO an edge takes the edge's own stamp with it and nothing else: the rest of a
|
|
247
|
+
* terminator's attrs describe the branch, not the arrival.
|
|
248
|
+
*
|
|
249
|
+
* A GUARD rather than a fix — `fallthrough` is the only attr anything sets on a `br`, so copying
|
|
250
|
+
* the whole bag would behave identically — spelled so that the next attr a frontend invents cannot
|
|
251
|
+
* ride onto an edge it says nothing about. */
|
|
252
|
+
export function fallThroughOf(term: Op): Op['attrs'] {
|
|
253
|
+
return term.attrs.fallthrough === true ? { fallthrough: true } : {};
|
|
254
|
+
}
|
|
255
|
+
|
|
189
256
|
/** The successor blocks of `b`, read off its terminator. */
|
|
190
257
|
export function successorsOf(b: Block): Block[] {
|
|
191
258
|
return terminator(b)?.successors.map((s) => s.block) ?? [];
|
package/src/ir/opcodes.ts
CHANGED
|
@@ -177,7 +177,16 @@ export const OPCODES = {
|
|
|
177
177
|
// no more reapable than a dead `call`.
|
|
178
178
|
opaque: { operands: 'variadic', results: 1, effects: true },
|
|
179
179
|
// --- terminators ---
|
|
180
|
+
// `fallthrough: true` — OPTIONAL, the same fact as on `br`: a `ret` SUNK onto one edge
|
|
181
|
+
// (`raise/retsink.ts`, `raise/tailsink.ts`) replaces that edge's `br` and takes its stamp, so
|
|
182
|
+
// `structure/retspell.ts` can still read how the machine arrived. A `ret` a frontend built carries
|
|
183
|
+
// nothing, and is read by its block's in-edges instead.
|
|
180
184
|
ret: { operands: 'variadic', results: 0, terminator: true, successors: 0 },
|
|
185
|
+
// `fallthrough: true` — OPTIONAL, set by a frontend when the block carried NO control-transfer
|
|
186
|
+
// instruction and this `br` stands for the machine simply running into the next block. Its
|
|
187
|
+
// absence therefore means a real branch instruction. The one reader is `structure/retspell.ts`,
|
|
188
|
+
// which needs to tell a `b <label>` (a transfer the source asked for) from layout; a pass that
|
|
189
|
+
// builds a fresh `br` leaves it off and is read as a branch, which is the conservative answer.
|
|
181
190
|
br: { operands: 0, results: 0, terminator: true, successors: 1 },
|
|
182
191
|
cond_br: { operands: 1, results: 0, terminator: true, successors: 2 },
|
|
183
192
|
// Many-way switch dispatch (Regime B, jump table). The single operand is the scrutinee;
|
|
@@ -237,11 +246,11 @@ export const NEGATED_ICMP: Readonly<Record<string, Opcode>> = Object.fromEntries
|
|
|
237
246
|
/** Ops with an observable side effect: the flag on the signature, derived rather than re-listed.
|
|
238
247
|
* `isDceSafe` asks the same question of the FLAG through `opSig` rather than of this set, so the
|
|
239
248
|
* two cannot disagree. The SET's own consumers are `HOIST_UNSAFE_OPS` below, structure.ts's
|
|
240
|
-
*
|
|
249
|
+
* block-purity tests (may this block be folded into a loop header, is this exit owned),
|
|
241
250
|
* analysis.ts's memory-write barrier, divpow2's bias block (which is DELETED rather than moved),
|
|
242
|
-
* and the idiom layer's de-sequencing guard (pattern/engine.ts).
|
|
243
|
-
*
|
|
244
|
-
*
|
|
251
|
+
* and the idiom layer's de-sequencing guard (pattern/engine.ts). Derived rather than re-listed
|
|
252
|
+
* because three of those consumers each carried a hand-written copy of this membership, which is
|
|
253
|
+
* how the models drifted apart. */
|
|
245
254
|
export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
|
|
246
255
|
(Object.keys(OPCODES) as Opcode[]).filter((k) => (OPCODES[k] as OpSig).effects),
|
|
247
256
|
);
|
|
@@ -254,8 +263,9 @@ export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
|
|
|
254
263
|
* is raise/shortcircuit.ts, which hoists an arm's body into the block above, and the structurer
|
|
255
264
|
* inlines an unnamed value back into the `&&`/`||` right-hand side, where C's own short-circuit
|
|
256
265
|
* re-guards it. Adding the two reads
|
|
257
|
-
* here
|
|
258
|
-
* synthetic:strcmp1), so the argument
|
|
266
|
+
* here cost three byte-matches (kleod:UpdateHUDCounterDisplay, retired 2026-09-13 with its source,
|
|
267
|
+
* plus synthetic:breakloop and synthetic:strcmp1), so the argument was load-bearing rather than
|
|
268
|
+
* merely plausible. Not re-measured since the kleod row's retirement.
|
|
259
269
|
*
|
|
260
270
|
* KNOWN GAP: the trapping divides are absent too, and there the re-guard argument does NOT carry
|
|
261
271
|
* — a hoisted `sdiv` that the structurer NAMES becomes an unconditional statement. Left as it is
|
|
@@ -271,9 +281,20 @@ export const EFFECTFUL_OPS: ReadonlySet<string> = new Set(
|
|
|
271
281
|
* re-guard at the new point — actually holds at your call site. */
|
|
272
282
|
export const HOIST_UNSAFE_OPS: ReadonlySet<string> = EFFECTFUL_OPS;
|
|
273
283
|
|
|
284
|
+
/** The ops whose `operands[0]` is a memory-access BASE: `load base`, `store base, value`,
|
|
285
|
+
* `aload base, index`, `astore base, index, value` (the operand roles are in the registry above).
|
|
286
|
+
* Authored rather than derived — no registry field records the operand ROLE, and the only other
|
|
287
|
+
* memory-touching opcode is `call`, whose variadic operands are arguments and not a base.
|
|
288
|
+
*
|
|
289
|
+
* Two consumers ask two different questions of it and both need the same answer, which is why it
|
|
290
|
+
* is here and not next to either: `structure/analysis.ts` uses it for the address-home variation's slot
|
|
291
|
+
* model, and `raise/const.ts` to recognise a folded literal that IS an address. */
|
|
292
|
+
export const MEM_BASE_OPS: ReadonlySet<string> = new Set(['load', 'store', 'aload', 'astore']);
|
|
293
|
+
|
|
274
294
|
/** Ops whose answer depends on WHERE they run: an effect (its order against other effects is
|
|
275
295
|
* observable) or a memory read (it answers whichever stores ran before it). The question a pass
|
|
276
|
-
* asks before moving a computation to another point on the SAME path.
|
|
296
|
+
* asks before moving a computation to another point on the SAME path. `SPELLED_WHEN_DEAD_OPS`
|
|
297
|
+
* below asks a DIFFERENT question and derives, today, the same set. */
|
|
277
298
|
export const ORDER_SENSITIVE_OPS: ReadonlySet<string> = new Set(
|
|
278
299
|
(Object.keys(OPCODES) as Opcode[]).filter((k) => {
|
|
279
300
|
const sig = OPCODES[k] as OpSig;
|
|
@@ -281,6 +302,30 @@ export const ORDER_SENSITIVE_OPS: ReadonlySet<string> = new Set(
|
|
|
281
302
|
}),
|
|
282
303
|
);
|
|
283
304
|
|
|
305
|
+
/** Ops the structurer must still SPELL when nothing consumes their result — an effect, or a memory
|
|
306
|
+
* READ. Consumer: structure.ts's `sideEffects` walk.
|
|
307
|
+
*
|
|
308
|
+
* The read half is the entry worth arguing, because `reads` documents the OPPOSITE about C — a
|
|
309
|
+
* load nobody reads is deletable, nothing observes it. That is the C claim. The COMPILER claim
|
|
310
|
+
* points the other way: an optimizing compiler deletes every dead read it is ALLOWED to delete, so
|
|
311
|
+
* one still in the target is evidence the source's access was `volatile`. Membership here only
|
|
312
|
+
* says the structurer may not drop the op silently; whether a `volatile` can actually reach the
|
|
313
|
+
* access is a second, ADDRESS-level question the call site asks separately
|
|
314
|
+
* (structure.ts `volatileQualifiable`), and a read it answers no to is dropped as before.
|
|
315
|
+
*
|
|
316
|
+
* DERIVED FROM THE REGISTRY, not aliased to `ORDER_SENSITIVE_OPS`, even though the two are
|
|
317
|
+
* extensionally identical today and `HOIST_UNSAFE_OPS` above does alias `EFFECTFUL_OPS`. An alias
|
|
318
|
+
* makes two DIFFERENT questions incapable of ever differing, so the day one of them acquires an
|
|
319
|
+
* opcode the other should not have, the edit lands on both silently. The identity is pinned by a
|
|
320
|
+
* test instead (test/pattern.test.ts), where a future divergence surfaces as a decision to make
|
|
321
|
+
* rather than a coupling nobody sees. */
|
|
322
|
+
export const SPELLED_WHEN_DEAD_OPS: ReadonlySet<string> = new Set(
|
|
323
|
+
(Object.keys(OPCODES) as Opcode[]).filter((k) => {
|
|
324
|
+
const sig = OPCODES[k] as OpSig;
|
|
325
|
+
return sig.effects || sig.reads;
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
|
|
284
329
|
/** Ops that may not be RE-EVALUATED at another program point — order-sensitive, or trapping. The
|
|
285
330
|
* trap half is what separates this from `ORDER_SENSITIVE_OPS`: it only matters when the new point
|
|
286
331
|
* can be reached on a path the old one was not, so a consumer that merely re-orders on one path
|
package/src/ir/parse.ts
CHANGED
|
@@ -121,7 +121,13 @@ export function parse(text: string): Fn {
|
|
|
121
121
|
|
|
122
122
|
// No write order: the text form is the value graph, and the record is a measurement of the
|
|
123
123
|
// MACHINE. A parsed fn's edges are UNMEASURED, never written-nowhere (ir/core.ts `WriteOrder`).
|
|
124
|
-
return {
|
|
124
|
+
return {
|
|
125
|
+
name,
|
|
126
|
+
blocks: rawBlocks.map((r) => r.block),
|
|
127
|
+
writeOrder: undefined,
|
|
128
|
+
slotHomes: undefined,
|
|
129
|
+
paramEvidence: undefined,
|
|
130
|
+
};
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
/** Drop the two annotations `print(fn, { writeOrder: true })` appends, and NOTHING else.
|
package/src/ir/simplify.ts
CHANGED
|
@@ -105,7 +105,7 @@ function trivialPhiValue(incoming: readonly Successor[], i: number, param: Value
|
|
|
105
105
|
export function firstTrivialPhi(fn: Fn): { block: Block; param: Value } | null {
|
|
106
106
|
// ONE pass over the successor edges, indexed by target — `simplifyTrivialPhis` rescans the
|
|
107
107
|
// whole function per block, which is fine for a mutating fixpoint and not for a check on the
|
|
108
|
-
// raising tower's hot path (a candidate fan re-raises the same function once per lift
|
|
108
|
+
// raising tower's hot path (a candidate fan re-raises the same function once per lift).
|
|
109
109
|
const incomingOf = new Map<Block, Successor[]>();
|
|
110
110
|
for (const pb of fn.blocks) {
|
|
111
111
|
for (const op of pb.ops) {
|
package/src/l3/address.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// declares which one it means instead of restating four lines and drifting.
|
|
8
8
|
//
|
|
9
9
|
// • baseConst — a deref BASE, through SCALAR pointer casts only. A cast to a STRUCT pointer is
|
|
10
|
-
// the dot-form's base and is refused, because a
|
|
11
|
-
// stride (`((struct S *)K)[i].f` is not `((u8 *)K)[…]`). This is the reading a
|
|
10
|
+
// the dot-form's base and is refused, because a respell variation that re-spells THROUGH it collapses the
|
|
11
|
+
// stride (`((struct S *)K)[i].f` is not `((u8 *)K)[…]`). This is the reading a respell variation that
|
|
12
12
|
// REWRITES the base needs: l3/nearbase.ts's clusters, l3/volstore.ts's qualifier.
|
|
13
13
|
// • addrConst — the address an expression IS, through ANY pointer cast. Wider, and safe
|
|
14
14
|
// because nothing re-spells through it: l3/volatileptr.ts counts volatility claims with it.
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// L3 respell variation: a pointer local the source ADVANCED between two accesses, rather than two
|
|
2
|
+
// addresses the compiler derived from one.
|
|
3
|
+
//
|
|
4
|
+
// `ldr r3,=X; strh [r3]; adds r3,#2; strh [r3]` — the machine held an address in a register, used
|
|
5
|
+
// it, moved it, used it again. `raise/const.ts` folds the lift's `add(const X, const 2)` into the
|
|
6
|
+
// literal `X + 2`, because on Thumb that pair is also how a compiler materialises a 32-bit literal
|
|
7
|
+
// it cannot encode in one instruction, and it records the distinction it is erasing as
|
|
8
|
+
// `index.baseAdvanced` (l3/ast.ts's third evidence field). This pass reads it:
|
|
9
|
+
//
|
|
10
|
+
// *(u16 *)0x04000048 = a; *(u16 *)0x0400004A = b;
|
|
11
|
+
// → u16 *p = (u16 *)0x04000048; *p = a; p = p + 1; *p = b;
|
|
12
|
+
//
|
|
13
|
+
// WHY IT IS A CANDIDATE. Against the INDEXED spelling of the same minted local the advance buys
|
|
14
|
+
// nothing: agbcc folds `p = p + 1; *p` straight back into `strh [r3, #2]`, byte for byte the
|
|
15
|
+
// subscript's own object. What makes it visible is the CONJUNCTION with a `volatile` pointee,
|
|
16
|
+
// which bars that fold and leaves the `add` the target records. Both halves are compiled against
|
|
17
|
+
// `kleod:StreamCmd_SetWindowRegs`'s target object; the four corners are in test/advance.test.ts's
|
|
18
|
+
// header. So this pass emits a spelling and `compareScored` referees; nothing here claims the
|
|
19
|
+
// source wrote it.
|
|
20
|
+
//
|
|
21
|
+
// SOUNDNESS IS ADDRESS EQUALITY. `p` is freshly minted and assigned by nothing else, so at each
|
|
22
|
+
// member's access it holds `A0 + Σ steps so far` — that member's own absolute address — PROVIDED
|
|
23
|
+
// every advance sits between the accesses it separates on every path, and PROVIDED every node this
|
|
24
|
+
// pass re-spells as `*p` is one of those accesses. A top-level statement list has no back edge and
|
|
25
|
+
// runs its statements in order at most once each, so placing each advance at the top level
|
|
26
|
+
// immediately above its member's statement, with the members at STRICTLY INCREASING top-level
|
|
27
|
+
// indices, carries the first half.
|
|
28
|
+
//
|
|
29
|
+
// THE SECOND HALF IS `rewrite`, AND IT MATCHES BY ADDRESS, NOT BY IDENTITY (`:rewrite` below): it
|
|
30
|
+
// replaces EVERY `index` node whose `cellAddress` is a chain member's, wherever it sits. So a
|
|
31
|
+
// second access at a member's address — a twin at the top level, or one inside an arm or a loop
|
|
32
|
+
// body — is re-spelled `*p` at a point where `p` does not hold that address. The two rules that
|
|
33
|
+
// refuse those shapes (`member-second-site`, `member-nested-site`, and their head twins) are
|
|
34
|
+
// therefore SOUND, not narrowing, and the shape is pinned by test/advance.test.ts's `an access at
|
|
35
|
+
// a chain address inside a loop is not re-spelled`.
|
|
36
|
+
//
|
|
37
|
+
// SCOPE (decline over approximate) is `ADVANCE_HEAD_GATES` and `ADVANCE_MEMBER_GATES` below — as
|
|
38
|
+
// tables rather than an `||` chain, so `sound` costs a `guardedBy` and every rule is ablated
|
|
39
|
+
// against the real pass by test/advance.test.ts's battery, which records WHAT THE ABLATED PASS
|
|
40
|
+
// EMITS and checks `sound` against it. NOT by `bench gates`: `pnpm bench gates --pass advance`
|
|
41
|
+
// answers `no censusable pass "advance"`, and structurally must, because this pass is reached
|
|
42
|
+
// through a static import binding in rank.ts rather than through a mutable caller-side record
|
|
43
|
+
// (run/gate-census.ts's header, which measures the `TypeError` a module-namespace write raises).
|
|
44
|
+
// The firing census below was therefore taken by hand, with the recipe it states.
|
|
45
|
+
//
|
|
46
|
+
// FIVE OF THE TWELVE ARE NARROWING rather than soundness and each says which it is. Three are
|
|
47
|
+
// judgements about what the asm shows (`head-already-advanced`, `member-negative-step`,
|
|
48
|
+
// `member-no-evidence` — the last is what makes this a reading rather than a guess); one,
|
|
49
|
+
// `member-signedness`, buys the minted local ONE pointee type where the backend would otherwise
|
|
50
|
+
// spell a correct reinterpret cast; one, `member-element-grid`, is a LOUD refusal — ablated it
|
|
51
|
+
// emits `p0 = p0 + 0.5;`, which is not C. Dropping `member-no-evidence` alone leaves the emitted
|
|
52
|
+
// step `undefined / width` = `NaN`, refused downstream only by the two arithmetic rules'
|
|
53
|
+
// comparisons against it (`NaN % w !== 0`, `NaN !== addr`); the battery's `noncompile` verdicts
|
|
54
|
+
// for both are what hold that.
|
|
55
|
+
//
|
|
56
|
+
// HOW OFTEN EACH FIRES, over the whole corpus — `bench sweep --fan`, both arms, 2,126 records,
|
|
57
|
+
// instrumented on `firstRejection` (2026-09-12), which is HAND INSTRUMENTATION and reproduced by
|
|
58
|
+
// wrapping both tables in `tallying()` (l3/gates.ts) at this pass's one call site in rank.ts,
|
|
59
|
+
// passing `.gates` to `advancedBases`, and printing `.refusals()` when the sweep ends. The numbers
|
|
60
|
+
// count CALLS, and enumeration calls this pass about eleven times per record, once per structure setting's
|
|
61
|
+
// tree:
|
|
62
|
+
// 23,322 calls · 112 found a chain · 23,210 declined
|
|
63
|
+
// head-second-site 872 · member-no-evidence 664 · head-nested-site 256 · head-already-advanced 144
|
|
64
|
+
// every other member rule: 0
|
|
65
|
+
// So the eight remaining member rules are pinned by the battery and by NOTHING IN THE CORPUS —
|
|
66
|
+
// where the corpus refuses a chain, it refuses it at the head. Chain lengths found: 96 of two
|
|
67
|
+
// members and 16 of four, no others.
|
|
68
|
+
//
|
|
69
|
+
// WHAT THIS PASS DOES NOT DO, both measured rather than assumed:
|
|
70
|
+
// • A function with TWO disjoint chains gets one candidate, spelling the FIRST BY POSITION — not
|
|
71
|
+
// the longest, and the second chain is unreachable by any variation. ZERO of the 112 chain-bearing
|
|
72
|
+
// calls above held a second chain sharing no address with the first (the instrument kept
|
|
73
|
+
// scanning), so the second local this would need has no inhabitant to price it.
|
|
74
|
+
// • The init is `prepend`ed and there is no sunk twin; see the note at `placeBaseLocals` below.
|
|
75
|
+
// • IT IS MAP-LESS ONLY. Every member is reached through `cellAddress`, which answers null once
|
|
76
|
+
// a symbol map promotes the pool word to `®_WININ` — so with a map this pass enumerates
|
|
77
|
+
// nothing, and every `/advance` candidate the corpus carries is a `/raw-globals` one. That is
|
|
78
|
+
// what caps the variation at the six rows `bench sweep --fan --base origin/main` names
|
|
79
|
+
// (apps/benchmark/dataset/synthetic.ts, at `volwalk`), and it is the question to ask of it the
|
|
80
|
+
// day the symbol-map direction lands: this capability survives only if `cellAddress` learns
|
|
81
|
+
// the promoted form.
|
|
82
|
+
import { type IrType, scalarTypeForAccess } from '../ir/types';
|
|
83
|
+
import { cellAddress } from './address';
|
|
84
|
+
import { type Expr, type SFn, type Stmt, isLoop, mapExprChildren, mapStmtExprs, stmtChildren, stmtExprs } from './ast';
|
|
85
|
+
import { type Gate, firstRejection } from './gates';
|
|
86
|
+
import type { BaseInit } from './hoist';
|
|
87
|
+
import { nameAllocator, placeBaseLocals } from './hoist';
|
|
88
|
+
|
|
89
|
+
/** One const-addressed access, with the top-level statement it was reached at. */
|
|
90
|
+
export interface Site {
|
|
91
|
+
stmt: number;
|
|
92
|
+
addr: number;
|
|
93
|
+
width: number;
|
|
94
|
+
signed: boolean;
|
|
95
|
+
advanced?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One candidate member, judged against the chain so far. `twin`/`nested` are the two ways some
|
|
99
|
+
* OTHER node in the tree names this site's address — the facts `rewrite`'s by-address match makes
|
|
100
|
+
* load-bearing. */
|
|
101
|
+
export interface MemberCtx {
|
|
102
|
+
prev: Site;
|
|
103
|
+
site: Site;
|
|
104
|
+
twin: boolean;
|
|
105
|
+
nested: boolean;
|
|
106
|
+
}
|
|
107
|
+
export type HeadCtx = Omit<MemberCtx, 'prev'>;
|
|
108
|
+
|
|
109
|
+
/** The head's own admission. The two address rules are the same PREDICATE as the member table's
|
|
110
|
+
* and deliberately not the same rule objects (see gates.ts on why a second consumer owns its
|
|
111
|
+
* own): a head that is re-spelled at a second site is wrong for the same reason a member is. */
|
|
112
|
+
export const ADVANCE_HEAD_GATES: readonly Gate<HeadCtx>[] = [
|
|
113
|
+
{
|
|
114
|
+
id: 'head-second-site',
|
|
115
|
+
why: 'the rewrite finds accesses by address, so another access to the same address would read `p` before it is set',
|
|
116
|
+
sound: true,
|
|
117
|
+
guardedBy: 'advance.test.ts: a chain address reached at a second site declines',
|
|
118
|
+
rejects: (c) => c.twin,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'head-nested-site',
|
|
122
|
+
why: 'the same address inside an arm or a loop body is re-spelled at a point `p` may not hold',
|
|
123
|
+
sound: true,
|
|
124
|
+
guardedBy: 'advance.test.ts: an access at a chain address inside a loop is not re-spelled',
|
|
125
|
+
rejects: (c) => c.nested,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'head-already-advanced',
|
|
129
|
+
why: 'an access the machine reached by stepping from an earlier one continues that chain, and starting a chain there would load an address the machine never loaded',
|
|
130
|
+
sound: false,
|
|
131
|
+
guardedBy: 'advance.test.ts: a chain may not START at an advanced site',
|
|
132
|
+
rejects: (c) => c.site.advanced !== undefined,
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
/** Each successor, against the member before it. FIRST rejection wins, so a refusal is
|
|
137
|
+
* attributable to one rule. */
|
|
138
|
+
export const ADVANCE_MEMBER_GATES: readonly Gate<MemberCtx>[] = [
|
|
139
|
+
{
|
|
140
|
+
id: 'member-no-evidence',
|
|
141
|
+
why: 'where the lift recorded no step, the pair is the compiler deriving two addresses from one literal',
|
|
142
|
+
sound: false,
|
|
143
|
+
guardedBy: 'advance.test.ts: the same pair with no evidence declines',
|
|
144
|
+
rejects: (c) => c.site.advanced === undefined,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'member-second-site',
|
|
148
|
+
why: 'the rewrite finds accesses by address, so another access to the same address would read `p` at the wrong value',
|
|
149
|
+
sound: true,
|
|
150
|
+
guardedBy: 'advance.test.ts: a chain address reached at a second site declines',
|
|
151
|
+
rejects: (c) => c.twin,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'member-nested-site',
|
|
155
|
+
why: 'the same address inside an arm or a loop body is re-spelled at a point `p` may not hold',
|
|
156
|
+
sound: true,
|
|
157
|
+
guardedBy: 'advance.test.ts: an access at a chain address inside a loop is not re-spelled',
|
|
158
|
+
rejects: (c) => c.nested,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'member-statement-order',
|
|
162
|
+
why: 'the advance must sit between the two accesses it separates, so the indices must increase',
|
|
163
|
+
sound: true,
|
|
164
|
+
guardedBy: 'advance.test.ts: two accesses in ONE statement are not a chain',
|
|
165
|
+
rejects: (c) => c.site.stmt <= c.prev.stmt,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'member-width',
|
|
169
|
+
why: 'the new pointer has one pointee width, and `*p` at another width names other bytes',
|
|
170
|
+
sound: true,
|
|
171
|
+
guardedBy: 'advance.test.ts: members of different widths decline',
|
|
172
|
+
rejects: (c) => c.site.width !== c.prev.width,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'member-signedness',
|
|
176
|
+
why: 'the new pointer has one pointee type, and a second access of another type would need a cast the source did not write',
|
|
177
|
+
sound: false,
|
|
178
|
+
guardedBy: 'advance.test.ts: members of different signedness decline',
|
|
179
|
+
rejects: (c) => c.site.signed !== c.prev.signed,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'member-element-grid',
|
|
183
|
+
why: 'a step that is not a whole number of elements would be written `p = p + step / width` with a fraction, which is not C',
|
|
184
|
+
sound: false,
|
|
185
|
+
guardedBy: 'advance.test.ts: a step off the element grid declines',
|
|
186
|
+
rejects: (c) => c.site.advanced! % c.prev.width !== 0,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'member-step-lands',
|
|
190
|
+
why: 'a step that does not land on this access is evidence about some other pair of addresses',
|
|
191
|
+
sound: true,
|
|
192
|
+
guardedBy: 'advance.test.ts: a step that does not land on the next access declines',
|
|
193
|
+
rejects: (c) => c.prev.addr + c.site.advanced! !== c.site.addr,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'member-negative-step',
|
|
197
|
+
why: 'a backward step (`p = p + -1`) is correct C, but this variation writes only forward steps',
|
|
198
|
+
sound: false,
|
|
199
|
+
guardedBy: 'advance.test.ts: a NEGATIVE step declines',
|
|
200
|
+
rejects: (c) => c.site.advanced! <= 0,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
/** Every const-addressed `index` node in the body, split into the ones reached EXACTLY ONCE per
|
|
205
|
+
* execution of a top-level statement — the only places an advance statement can be put — and the
|
|
206
|
+
* ADDRESSES of every other one, which the chain rule refuses outright.
|
|
207
|
+
*
|
|
208
|
+
* A loop's OWN expression joins its body on the second side: a `while` condition runs once per
|
|
209
|
+
* iteration, so an advance above the loop and an access in its test are not the same count. A
|
|
210
|
+
* top-level `if`'s condition stays on the first side, because the `if` statement itself runs once
|
|
211
|
+
* whatever its arms do. */
|
|
212
|
+
function collectSites(body: readonly Stmt[]): { sites: Site[]; nestedAddrs: Set<number> } {
|
|
213
|
+
const sites: Site[] = [];
|
|
214
|
+
const nestedAddrs = new Set<number>();
|
|
215
|
+
const visit = (e: Expr, stmt: number, nested: boolean): void => {
|
|
216
|
+
if (e.k === 'index') {
|
|
217
|
+
const addr = cellAddress(e);
|
|
218
|
+
if (addr !== null) {
|
|
219
|
+
if (nested) {
|
|
220
|
+
nestedAddrs.add(addr);
|
|
221
|
+
} else {
|
|
222
|
+
sites.push({ stmt, addr, width: e.width, signed: e.signed, advanced: e.baseAdvanced });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
mapExprChildren(e, (c) => {
|
|
227
|
+
visit(c, stmt, nested);
|
|
228
|
+
return c;
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
const walk = (stmts: readonly Stmt[], stmt: number, nested: boolean): void => {
|
|
232
|
+
for (const s of stmts) {
|
|
233
|
+
const repeats = isLoop(s);
|
|
234
|
+
for (const e of stmtExprs(s)) {
|
|
235
|
+
visit(e, stmt, nested || repeats);
|
|
236
|
+
}
|
|
237
|
+
walk(stmtChildren(s), stmt, true);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
body.forEach((s, i) => walk([s], i, false));
|
|
241
|
+
return { sites, nestedAddrs };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The one chain this pass spells, or null.
|
|
245
|
+
*
|
|
246
|
+
* A NON-MEMBER SITE BETWEEN TWO MEMBERS DOES NOT END THE CHAIN. `p` is freshly minted, so an
|
|
247
|
+
* access that does not touch it cannot move it — and the clientele is MMIO setup code, where one
|
|
248
|
+
* `REG_BLDCNT = y;` between two window writes is the ordinary case. So the head is chosen by the
|
|
249
|
+
* head table rather than by position, and the walk scans every later site rather than stopping at
|
|
250
|
+
* the first one a gate refuses. Ambiguity is resolved greedily in statement order: where two later
|
|
251
|
+
* sites would both extend the chain, the earlier one does.
|
|
252
|
+
*
|
|
253
|
+
* THE TOLERANCE HAS NO CORPUS INHABITANT. MMIO writes with an unrelated const-addressed access
|
|
254
|
+
* between two members appear nowhere in the corpus, so this is a rule the file can state
|
|
255
|
+
* truthfully rather than reach a sweep can show. */
|
|
256
|
+
function chainOf(sites: readonly Site[], nestedAddrs: ReadonlySet<number>, gates: AdvanceGates): Site[] | null {
|
|
257
|
+
const head = gates.head ?? ADVANCE_HEAD_GATES;
|
|
258
|
+
const member = gates.member ?? ADVANCE_MEMBER_GATES;
|
|
259
|
+
const occurrences = new Map<number, number>();
|
|
260
|
+
for (const s of sites) {
|
|
261
|
+
occurrences.set(s.addr, (occurrences.get(s.addr) ?? 0) + 1);
|
|
262
|
+
}
|
|
263
|
+
const ctx = (site: Site): HeadCtx => ({
|
|
264
|
+
site,
|
|
265
|
+
twin: (occurrences.get(site.addr) ?? 0) > 1,
|
|
266
|
+
nested: nestedAddrs.has(site.addr),
|
|
267
|
+
});
|
|
268
|
+
for (let i = 0; i < sites.length; i++) {
|
|
269
|
+
if (firstRejection(head, ctx(sites[i])) !== null) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const chain = [sites[i]];
|
|
273
|
+
for (let j = i + 1; j < sites.length; j++) {
|
|
274
|
+
if (firstRejection(member, { prev: chain[chain.length - 1], ...ctx(sites[j]) }) === null) {
|
|
275
|
+
chain.push(sites[j]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (chain.length >= 2) {
|
|
279
|
+
return chain;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The two tables, ablatable — `gates.ts`'s reason: a test drops one entry and re-runs the REAL
|
|
286
|
+
* predicate on real input, with no test-only branch in the shipped path. Nothing in `src/` passes
|
|
287
|
+
* this; a shipped ablation of a `sound: true` rule emits wrong addresses, which is what
|
|
288
|
+
* `ablateHeuristic` refuses. */
|
|
289
|
+
export interface AdvanceGates {
|
|
290
|
+
head?: readonly Gate<HeadCtx>[];
|
|
291
|
+
member?: readonly Gate<MemberCtx>[];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Re-spell one advanced chain as a pointer local moved in place, or decline (null). */
|
|
295
|
+
export function advancedBases(sfn: SFn, gates: AdvanceGates = {}): SFn | null {
|
|
296
|
+
const { sites, nestedAddrs } = collectSites(sfn.body);
|
|
297
|
+
const chain = chainOf(sites, nestedAddrs, gates);
|
|
298
|
+
if (chain === null) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const name = nameAllocator(sfn)();
|
|
302
|
+
const elem: IrType = scalarTypeForAccess(chain[0].width, chain[0].signed);
|
|
303
|
+
const ptr: IrType = { kind: 'ptr', to: elem };
|
|
304
|
+
const members = new Set(chain.map((m) => m.addr));
|
|
305
|
+
// The access itself: every chain member reads `*p`, because `p` has been advanced to exactly its
|
|
306
|
+
// address. The evidence fields go with the old base — they described how the ADDRESS was
|
|
307
|
+
// computed, and this spelling is the answer to that question rather than another instance of it.
|
|
308
|
+
//
|
|
309
|
+
// BY ADDRESS, NOT BY IDENTITY, and the header's soundness argument turns on it: a node this
|
|
310
|
+
// finds at a member's address that is NOT the member — a twin, or one inside an arm or a loop —
|
|
311
|
+
// is re-spelled too, which is why the gates that refuse those shapes are `sound: true`.
|
|
312
|
+
const rewrite = (e: Expr): Expr => {
|
|
313
|
+
const m = mapExprChildren(e, rewrite);
|
|
314
|
+
const addr = m.k === 'index' ? cellAddress(m) : null;
|
|
315
|
+
if (m.k === 'index' && addr !== null && members.has(addr)) {
|
|
316
|
+
return { k: 'index', base: { k: 'var', name }, idx: { k: 'const', value: 0 }, width: m.width, signed: m.signed };
|
|
317
|
+
}
|
|
318
|
+
return m;
|
|
319
|
+
};
|
|
320
|
+
// The emitted distance is the GATED quantity — the step `member-step-lands` tied to this pair of
|
|
321
|
+
// addresses and `member-element-grid` divided — rather than the address difference, which is the
|
|
322
|
+
// same number only because those two rules hold. Deriving it separately is how a later ablation
|
|
323
|
+
// of one of them emits a fractional advance nothing checked.
|
|
324
|
+
//
|
|
325
|
+
// WHICH MAKES THE ARITHMETIC HERE TOTAL ONLY BECAUSE OF THE TABLE, and both ways out are LOUD
|
|
326
|
+
// rather than silent — measured, and pinned by the battery's `noncompile` verdicts rather than
|
|
327
|
+
// guarded here: with `member-element-grid` dropped this emits `p0 = p0 + 0.5;`, and with
|
|
328
|
+
// `member-no-evidence` dropped `advanced` is `undefined` and this emits `p0 = p0 + NaN;`.
|
|
329
|
+
// Neither is C, so a candidate carrying one is dropped at compile with its message rather than
|
|
330
|
+
// scored — which is why the two rules are `sound: false` and why no `Number.isInteger` refusal
|
|
331
|
+
// stands here: adding one would turn those two ablations into a DECLINE and delete the evidence
|
|
332
|
+
// the battery reads. `member-no-evidence` is ablatable by `ablateHeuristic`, so a round that
|
|
333
|
+
// ships that ablation as a ranked candidate ships noncompiling sources; that is its price.
|
|
334
|
+
const advanceAt = new Map<number, number>();
|
|
335
|
+
for (let i = 1; i < chain.length; i++) {
|
|
336
|
+
advanceAt.set(chain[i].stmt, chain[i].advanced! / chain[i].width);
|
|
337
|
+
}
|
|
338
|
+
const body: Stmt[] = [];
|
|
339
|
+
sfn.body.forEach((s, i) => {
|
|
340
|
+
const step = advanceAt.get(i);
|
|
341
|
+
if (step !== undefined) {
|
|
342
|
+
body.push({
|
|
343
|
+
k: 'assign',
|
|
344
|
+
name,
|
|
345
|
+
value: { k: 'bin', op: '+', l: { k: 'var', name }, r: { k: 'const', value: step } },
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
body.push(mapStmtExprs(s, rewrite));
|
|
349
|
+
});
|
|
350
|
+
const init: BaseInit = {
|
|
351
|
+
k: 'assign',
|
|
352
|
+
name,
|
|
353
|
+
value: { k: 'cast', to: ptr, e: { k: 'const', value: chain[0].addr } },
|
|
354
|
+
};
|
|
355
|
+
const locals = [...sfn.locals, { name, type: ptr as SFn['locals'][number]['type'] }];
|
|
356
|
+
// `prepend` for `l3/nearbase.ts`'s reason and a second one this pass owns: the init MATERIALISES
|
|
357
|
+
// the register the chain advances, and the target's own instruction order is what says where the
|
|
358
|
+
// pool word was loaded. Putting it in first-use order instead moves it below whatever else the
|
|
359
|
+
// function loads first, which on `kleod:StreamCmd_SetWindowRegs` swaps the two pool words and
|
|
360
|
+
// costs the match; test/advance.test.ts's `the base init leads` pins the emitted order.
|
|
361
|
+
//
|
|
362
|
+
// AND NO `/advance/sinkinit` ALTERNATIVE, unlike `/nearbase`, which ships one for exactly this decision —
|
|
363
|
+
// not because the decision is better determined here (the generator cannot see the target either
|
|
364
|
+
// way) but because the twin CANNOT EXIST. `sinkInitsToFirstUse` sinks an init only when
|
|
365
|
+
// `localMentions` counts ONE assignment to its local ("or the move would cross the other write",
|
|
366
|
+
// l3/hoist.ts), and an advance IS a second assignment to this one — so the sink declines on every
|
|
367
|
+
// tree this pass produces, by construction rather than by row: the sink returns null on the
|
|
368
|
+
// advanced tree, and registering `/advance/sinkinit` adds no candidate to
|
|
369
|
+
// `kleod:StreamCmd_SetWindowRegs:agbcc`'s fan. The `prepend` decision above is therefore the only
|
|
370
|
+
// placement this variation HAS, which is a stronger reason to record the compile behind it.
|
|
371
|
+
const { body: placed } = placeBaseLocals({ ...sfn, locals, body }, [init], 'prepend');
|
|
372
|
+
return { ...sfn, locals, body: placed };
|
|
373
|
+
}
|