@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/l3/reindex.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// asmlift L3 — the walk→index
|
|
1
|
+
// asmlift L3 — the walk→index RESPELL VARIATION, differ-ranked.
|
|
2
2
|
//
|
|
3
3
|
// A compiler strength-reduces a source-level `arr[i]` loop into a pointer WALK (`*p; p += 1`),
|
|
4
4
|
// so asmlift's faithful lift of the machine form emits the walk — but recompiling the walk
|
|
5
5
|
// rarely reproduces the bytes the INDEXED source produced (different induction variable,
|
|
6
6
|
// different regalloc). Which representation the source used is genuinely ambiguous from asm —
|
|
7
7
|
// exactly the class of ambiguity asmlift resolves by CANDIDATES, not guesses (rank.ts: "types
|
|
8
|
-
// are differ-ranked
|
|
8
|
+
// are differ-ranked variations"). This module produces the indexed re-spelling of a structured
|
|
9
9
|
// function; enumerateCandidates emits BOTH and the objdiff score referees.
|
|
10
10
|
//
|
|
11
11
|
// v1 SCOPE (decline over approximate): a loop is re-spelled only when ALL hold —
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// it (its statements are the loop-preceding subset of the else arm, now unconditionally hoisted).
|
|
30
30
|
// The walk pointers KEEP their init and lose their step — gcc folds a loop-invariant pointer
|
|
31
31
|
// local into addressing, so `p = B; … p[i]` and `B[i]` compile identically, and keeping the
|
|
32
|
-
// local is what lets the `/volatile`
|
|
32
|
+
// local is what lets the `/volatile` variation qualify a numeric B. Several walk pointers share the
|
|
33
33
|
// one counter (`dotprod`'s a/b pair). Its OWN rules, on top of the shared table below —
|
|
34
34
|
// • the guard tests THE SAME var the counter is initialised from, against 0, in the sense that
|
|
35
35
|
// skips the loop; the do-while exit is exactly `k != 0`;
|
|
@@ -227,7 +227,7 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
|
|
|
227
227
|
},
|
|
228
228
|
{
|
|
229
229
|
id: 'global-counter',
|
|
230
|
-
why: 'a global counter’s final value is observable to every other caller,
|
|
230
|
+
why: 'a global counter’s final value is observable to every other caller, interrupt handler and translation unit',
|
|
231
231
|
sound: true,
|
|
232
232
|
guardedBy: 'reindex.test.ts: the counter may not be a GLOBAL',
|
|
233
233
|
rejects: (c) => !c.kIsDeclared,
|
|
@@ -255,14 +255,14 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
|
|
|
255
255
|
},
|
|
256
256
|
{
|
|
257
257
|
id: 'body-exit',
|
|
258
|
-
why: 'the steps sat
|
|
258
|
+
why: 'the steps sat at the end of the body, which a `continue` skips and a `for`’s increment does not',
|
|
259
259
|
sound: true,
|
|
260
260
|
guardedBy: 'reindex.test.ts: a `break` in the body declines',
|
|
261
261
|
rejects: (c) => c.coreHasExit,
|
|
262
262
|
},
|
|
263
263
|
{
|
|
264
264
|
id: 'walk-base',
|
|
265
|
-
why: 'the kept init must be a value the rewrite can leave standing
|
|
265
|
+
why: 'the kept init must be a value the rewrite can leave standing: a variable, or an address that can be loaded again',
|
|
266
266
|
sound: true,
|
|
267
267
|
guardedBy: 'reindex.test.ts: a walk pointer with no init ahead of the loop declines',
|
|
268
268
|
rejects: (c) => c.badBases.length > 0,
|
|
@@ -290,7 +290,7 @@ export const COUNTDOWN_GATES: readonly Gate<CountdownCtx>[] = [
|
|
|
290
290
|
},
|
|
291
291
|
{
|
|
292
292
|
id: 'leftover-walk',
|
|
293
|
-
why: 'a
|
|
293
|
+
why: 'a statement left outside the deleted step still reads the walk pointer, which a path that skipped the step never set',
|
|
294
294
|
sound: true,
|
|
295
295
|
guardedBy: 'reindex.test.ts: a leftover mentioning a walk pointer declines',
|
|
296
296
|
rejects: (c) => c.leakyLeftovers > 0,
|
|
@@ -367,13 +367,18 @@ function reindexExpr(e: Expr, walk: WalkLoop, iv: string): Expr | null {
|
|
|
367
367
|
? { k: 'var', name: iv }
|
|
368
368
|
: { k: 'bin', op: '+', l: { k: 'var', name: iv }, r: e.idx };
|
|
369
369
|
// NOTE: this rebuilds the node from parts, so any field not named here is DROPPED — and the
|
|
370
|
-
// `index` node has exactly
|
|
371
|
-
// `lead` is declined above (the deref side); it cannot arrive on
|
|
372
|
-
// `walk.base` is a local pointer and structuring only ever puts
|
|
373
|
-
// own name. Dropping the other
|
|
374
|
-
//
|
|
375
|
-
// `
|
|
376
|
-
//
|
|
370
|
+
// `index` node has exactly five optional ones (ast.ts): `lead`, `baseElem`, `operandOff`,
|
|
371
|
+
// `baseOrdered`, `baseAdvanced`. `lead` is declined above (the deref side); it cannot arrive on
|
|
372
|
+
// the base side either, since `walk.base` is a local pointer and structuring only ever puts
|
|
373
|
+
// `lead` on an array GLOBAL's own name. Dropping the other four is right rather than merely
|
|
374
|
+
// harmless: `baseElem` states the element type of a base the C type walk cannot type, and the
|
|
375
|
+
// rebuilt base is `{k:'var', name: walk.base}` — a LOCAL pointer, whose declared type the walk
|
|
376
|
+
// reads straight out of the print env, so a statement about the base it replaced would describe
|
|
377
|
+
// a different base and the walk answers without one; `baseOrdered` is stamped per SYMBOL on an
|
|
378
|
+
// order-licensed GLOBAL, which a local var base never is; `operandOff`'s readers
|
|
379
|
+
// (l3/basecse.ts, l3/offmember.ts) ask about a CONSTANT subscript, which the rewritten
|
|
380
|
+
// `i`/`i + k` index never is; and `baseAdvanced` describes an address the machine reached from
|
|
381
|
+
// a CONSTANT one, which a walk pointer's is not.
|
|
377
382
|
return { k: 'index', base: { k: 'var', name: walk.base }, idx, width: e.width, signed: e.signed };
|
|
378
383
|
}
|
|
379
384
|
let failed = false;
|
|
@@ -653,8 +658,8 @@ function tryExprWalk(
|
|
|
653
658
|
* one loop re-spelled, or null (no candidate) when nothing fired — callers emit the extra
|
|
654
659
|
* candidate only on non-null. Pure: never mutates the input SFn. `keptWalks` collects the names
|
|
655
660
|
* of the pointers each fired loop kept as its base — v1 the walk's base (a param lands in the
|
|
656
|
-
* set too, inertly: the volatile
|
|
657
|
-
* themselves — the locals the /indexed/volatile
|
|
661
|
+
* set too, inertly: the volatile variation marks only declared locals), v2 the walk pointers
|
|
662
|
+
* themselves — the locals the /indexed/volatile composition (rank.ts) narrows the volatile variation
|
|
658
663
|
* to. A v3 loop contributes nothing: it DELETES its pointer, and its base is qualified through
|
|
659
664
|
* the /livebase pairings instead. `gates` is the shared countdown admission table — a parameter
|
|
660
665
|
* so a test can ablate one entry and re-run the real pass. */
|
|
@@ -666,7 +671,7 @@ export function reindexWalks(
|
|
|
666
671
|
const ptrVars = new Map<string, IrType>();
|
|
667
672
|
const declTypes = declaredTypes(sfn);
|
|
668
673
|
// BOTH volatility facts (ast.ts SFn.locals): the object-volatile counter, and the pointer whose
|
|
669
|
-
// POINTEE is volatile — which is the one the `/volatile`
|
|
674
|
+
// POINTEE is volatile — which is the one the `/volatile` variation mints, and the one a walk carries.
|
|
670
675
|
const volatileLocals = new Set(
|
|
671
676
|
sfn.locals.filter((l) => l.volatile === true || l.pointeeVolatile === true).map((l) => l.name),
|
|
672
677
|
);
|
package/src/l3/scopebase.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: name a reused global base in a pointer local placed by SCOPE rather than at
|
|
2
2
|
// the function top. Two region rules ship, one pass and one collected index behind them:
|
|
3
3
|
//
|
|
4
4
|
// `/scopebase` ONE local for a key, at the innermost list holding all of its uses (else the
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
// and the three-block-scoped one assemble byte-identically. So there is no nested
|
|
10
10
|
// declaration block here and none is needed — the locals are declared at function
|
|
11
11
|
// top and only their ASSIGNMENTS are placed per region. That compiler fact is
|
|
12
|
-
// PINNED rather than asserted: packages/cli/test/matching/decl-scope-
|
|
12
|
+
// PINNED rather than asserted: packages/cli/test/matching/decl-scope-variation.test.ts
|
|
13
13
|
// compiles both spellings through the project's own agbcc and compares the object
|
|
14
14
|
// bytes, and compiles a count-collapsed third spelling to show the COUNT is not
|
|
15
15
|
// free either.
|
|
16
16
|
//
|
|
17
|
-
// The
|
|
18
|
-
// kleod:UpdateHUDCounterDisplay its match
|
|
17
|
+
// The variation earned its place on kleod's kl-eod-decomp rows: returning `null` from `hoistScopedBases`
|
|
18
|
+
// cost kleod:UpdateHUDCounterDisplay its match. That row was retired on 2026-09-13 with its source,
|
|
19
|
+
// so no current benchmark row is known to guard this file; re-measure by ablation on the current
|
|
20
|
+
// rows before naming one.
|
|
19
21
|
//
|
|
20
22
|
// `l3/basecse.ts` already hoists a reused leaf base — at three positions now, of which two are in
|
|
21
23
|
// the TOP-LEVEL statement list (the function top, or an init's first use where a roster row asks
|
|
@@ -28,8 +30,8 @@
|
|
|
28
30
|
// narrows that range and does not close it: the init still lands ABOVE the `if`. `l3/hoist.ts`'s
|
|
29
31
|
// third placement, `scope`, now does close it for the run basecse places, so "into a nested list"
|
|
30
32
|
// is no longer this file's alone; what stays here is the base this file can SEE (below) and the
|
|
31
|
-
// COUNT question (`REGION_RULES`), which no placement answers. That argument is why the
|
|
32
|
-
// scope-aware; it is NOT a claim about what the
|
|
33
|
+
// COUNT question (`REGION_RULES`), which no placement answers. That argument is why the variation is
|
|
34
|
+
// scope-aware; it is NOT a claim about what the variation achieves, and no committed measurement
|
|
33
35
|
// separates basecse's two flat placements (the one that did edited a reference source by hand and
|
|
34
36
|
// cannot be re-run). On kleod:UpdateHUDCounterDisplay the primary path declines outright (a later
|
|
35
37
|
// pass retired the phi it keyed on, so the base's uses span the function body), and the cluster
|
|
@@ -47,7 +49,7 @@
|
|
|
47
49
|
// shape. The decomp author's alternative is a no-op read-modify-write (`g[0][K] += 0;`) purely to
|
|
48
50
|
// force that materialization; naming the base is the same codegen without the quirk.
|
|
49
51
|
//
|
|
50
|
-
// A
|
|
52
|
+
// A VARIATION, not a committed rewrite: both region rules are emitted as ADDITIONAL candidates (rank.ts
|
|
51
53
|
// `/scopebase`, `/regionbase`) with the differ refereeing, so the un-hoisted spelling is always
|
|
52
54
|
// still in the list and neither can cost a match.
|
|
53
55
|
//
|
|
@@ -60,19 +62,19 @@
|
|
|
60
62
|
// sees it (they check resolution, deref typing, and whether a local is written ANYWHERE).
|
|
61
63
|
//
|
|
62
64
|
// ORDERING: `hoistBaseLocals` (basecse) runs unconditionally in `structureChecked`, BEFORE
|
|
63
|
-
// rank's
|
|
65
|
+
// rank's variations see the tree. So this pass's `addr`/`const` input is what basecse's DEFAULT table
|
|
64
66
|
// refused — EVERY gate in it, single-use bases as much as loop and repeated-constant-offset ones —
|
|
65
67
|
// which is why `SCOPEBASE_GATES` re-states basecse's rules rather than assuming those bases never
|
|
66
68
|
// arrive.
|
|
67
69
|
import { assertHoistsDominate } from '../contracts';
|
|
68
70
|
import { type IrType, T, scalarTypeForAccess } from '../ir/types';
|
|
69
71
|
import type { Expr, SFn, Stmt } from './ast';
|
|
70
|
-
import { mapExprChildren, stmtExprs, stmtLists } from './ast';
|
|
72
|
+
import { isLoop, mapExprChildren, stmtExprs, stmtLists } from './ast';
|
|
71
73
|
import { type Gate, ablateHeuristic, firstRejection } from './gates';
|
|
72
74
|
import { nameAllocator, takenNames } from './hoist';
|
|
73
75
|
import { addressableGlobals } from './storage';
|
|
74
76
|
|
|
75
|
-
/** A base this
|
|
77
|
+
/** A base this variation may name: a leaf whose value is a fixed address. */
|
|
76
78
|
type LeafBase = Extract<Expr, { k: 'addr' } | { k: 'const' } | { k: 'var' }>;
|
|
77
79
|
|
|
78
80
|
/** THE identity of a base — what makes two accesses "the same address".
|
|
@@ -89,7 +91,7 @@ export interface AccessCtx {
|
|
|
89
91
|
readonly addressable: ReadonlySet<string>;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
/** Which accesses this
|
|
94
|
+
/** Which accesses this variation may re-point. BOTH rules are SOUND: each one, removed, makes the
|
|
93
95
|
* rewrite name DIFFERENT BYTES — C that compiles, type-checks and scores, which is the failure
|
|
94
96
|
* mode nothing downstream catches.
|
|
95
97
|
*
|
|
@@ -117,14 +119,14 @@ export const SCOPEBASE_ELIGIBILITY: readonly Gate<AccessCtx>[] = [
|
|
|
117
119
|
},
|
|
118
120
|
{
|
|
119
121
|
id: 'shadowed-or-nonarray-base',
|
|
120
|
-
why: '`&name`
|
|
122
|
+
why: 'taking `&name` of a local or a pointer-shaped global names a different object',
|
|
121
123
|
sound: true,
|
|
122
124
|
guardedBy: 'addr-placement.test.ts: scopebase declines the shadowed name rather than take its address',
|
|
123
125
|
rejects: (c) => c.base.k === 'var' && !c.addressable.has(c.base.name),
|
|
124
126
|
},
|
|
125
127
|
];
|
|
126
128
|
|
|
127
|
-
/** An access this
|
|
129
|
+
/** An access this variation may re-point, or null. */
|
|
128
130
|
function eligible(
|
|
129
131
|
e: Expr,
|
|
130
132
|
globals: ReadonlySet<string>,
|
|
@@ -225,7 +227,7 @@ function collect(body: Stmt[], path: Stmt[][], loop: boolean[], idxPath: number[
|
|
|
225
227
|
};
|
|
226
228
|
for (const [i, s] of body.entries()) {
|
|
227
229
|
at = i;
|
|
228
|
-
const
|
|
230
|
+
const repeats = isLoop(s);
|
|
229
231
|
// A loop's OWN condition runs every iteration — a base there is loop-invariant exactly as a
|
|
230
232
|
// body use is, and it lives at THIS list, which does not. basecse.ts and argbase.ts treat the
|
|
231
233
|
// CONDITION the same way. They do NOT agree about a `for`'s `init`: basecse counts it in-loop
|
|
@@ -233,7 +235,7 @@ function collect(body: Stmt[], path: Stmt[][], loop: boolean[], idxPath: number[
|
|
|
233
235
|
// it at the enclosing cadence, which is the truthful reading — it runs once. Recorded because
|
|
234
236
|
// the divergence is real and an extraction has to pick one; both readings are pinned in
|
|
235
237
|
// test/addr-placement.test.ts so the pick is deliberate rather than whichever survives.
|
|
236
|
-
stmtExprs(s).forEach((e) => visit(e,
|
|
238
|
+
stmtExprs(s).forEach((e) => visit(e, repeats));
|
|
237
239
|
if (s.k === 'for') {
|
|
238
240
|
// `init`/`inc` are typed as the full Stmt union, so a COMPOUND one is type-legal. `stmtExprs`
|
|
239
241
|
// reaches only its own expressions while `rewriteStmt` descends into any nested list — the
|
|
@@ -255,7 +257,7 @@ function collect(body: Stmt[], path: Stmt[][], loop: boolean[], idxPath: number[
|
|
|
255
257
|
stmtExprs(s.inc).forEach((e) => visit(e, true));
|
|
256
258
|
}
|
|
257
259
|
for (const child of stmtLists(s)) {
|
|
258
|
-
collect(child, [...path, child], [...loop,
|
|
260
|
+
collect(child, [...path, child], [...loop, repeats], [...idxPath, i], st);
|
|
259
261
|
}
|
|
260
262
|
}
|
|
261
263
|
}
|
|
@@ -320,7 +322,7 @@ function deepestCluster(all: Site[]): { scope: Stmt[]; depth: number; uses: Site
|
|
|
320
322
|
return best;
|
|
321
323
|
}
|
|
322
324
|
|
|
323
|
-
/** How a key's uses are cut into REGIONS — the one
|
|
325
|
+
/** How a key's uses are cut into REGIONS — the one dimension this pass varies. Names a `REGION_RULES`
|
|
324
326
|
* entry; it is the only field a production caller passes. */
|
|
325
327
|
export type RegionSelector = 'whole' | 'per-region';
|
|
326
328
|
|
|
@@ -414,13 +416,13 @@ export interface RegionCtx {
|
|
|
414
416
|
const COUNTING_RULES: readonly Gate<RegionCtx>[] = [
|
|
415
417
|
{
|
|
416
418
|
id: 'single-use',
|
|
417
|
-
why: '
|
|
419
|
+
why: 'a base accessed once is as cheap to load again as to hold in a named local',
|
|
418
420
|
sound: false,
|
|
419
421
|
rejects: (c) => c.uses < 2,
|
|
420
422
|
},
|
|
421
423
|
{
|
|
422
424
|
id: 'repeated-const-offset',
|
|
423
|
-
why: 'a fixed offset
|
|
425
|
+
why: 'a fixed offset read and then written is one scalar update, and the compiler loads its address again for it',
|
|
424
426
|
sound: false,
|
|
425
427
|
rejects: (c) => c.repeatedConstOffset,
|
|
426
428
|
},
|
|
@@ -431,7 +433,7 @@ const COUNTING_RULES: readonly Gate<RegionCtx>[] = [
|
|
|
431
433
|
const LOOP_RULES: readonly Gate<RegionCtx>[] = [
|
|
432
434
|
{
|
|
433
435
|
id: 'per-iteration-use',
|
|
434
|
-
why: 'no scope reachable from the use runs
|
|
436
|
+
why: 'no scope reachable from the use runs as often as a loop condition or a `for` increment',
|
|
435
437
|
sound: false,
|
|
436
438
|
rejects: (c) => c.perIteration,
|
|
437
439
|
},
|
|
@@ -443,7 +445,7 @@ const LOOP_RULES: readonly Gate<RegionCtx>[] = [
|
|
|
443
445
|
},
|
|
444
446
|
];
|
|
445
447
|
|
|
446
|
-
/** The admission rules. NONE is sound: a wrong
|
|
448
|
+
/** The admission rules. NONE is sound: a wrong decision here names the same address in a different
|
|
447
449
|
* place, so it costs bytes and a match, never meaning — the eligibility table above is where
|
|
448
450
|
* meaning is at stake, and `rank.ts` keeps the un-hoisted spelling beside every candidate.
|
|
449
451
|
*
|
|
@@ -504,7 +506,7 @@ export const SCOPEBASE_GATES: readonly Gate<RegionCtx>[] = [...COUNTING_RULES, .
|
|
|
504
506
|
const perRegionReading = (g: Gate<RegionCtx>): Gate<RegionCtx> => ({
|
|
505
507
|
...g,
|
|
506
508
|
id: `region-${g.id}`,
|
|
507
|
-
why:
|
|
509
|
+
why: `counted over one region’s own uses: ${g.why}`,
|
|
508
510
|
});
|
|
509
511
|
|
|
510
512
|
/** `/regionbase`'s admission (rank.ts): the per-region readings of `SCOPEBASE_GATES`, MINUS the one
|
|
@@ -531,7 +533,7 @@ const perRegionReading = (g: Gate<RegionCtx>): Gate<RegionCtx> => ({
|
|
|
531
533
|
* admits.
|
|
532
534
|
*
|
|
533
535
|
* ONE RULE HERE IS PRICED BY A ROW; three are not. Ablating `region-single-use` moves
|
|
534
|
-
* `synthetic:dmascope` — the
|
|
536
|
+
* `synthetic:dmascope` — the variation's own row — while `region-repeated-const-offset`,
|
|
535
537
|
* `per-iteration-use` and `regions-degenerate` each leave all five gating rows exactly where they
|
|
536
538
|
* stand. NO SCORE PAIR IS QUOTED for that move: `dmascope` is MATCH in the committed artifact, so
|
|
537
539
|
* a pair whose unablated endpoint is a nonmatch score describes a corpus state that no longer
|
|
@@ -546,7 +548,7 @@ export const REGIONBASE_GATES: readonly Gate<RegionCtx>[] = [
|
|
|
546
548
|
...ablateHeuristic(LOOP_RULES, 'nested-loop-use'),
|
|
547
549
|
{
|
|
548
550
|
id: 'regions-degenerate',
|
|
549
|
-
why: '
|
|
551
|
+
why: 'a single region is the function-top local that the default hoist and `livebase` already offer',
|
|
550
552
|
sound: false,
|
|
551
553
|
rejects: (c) => c.siblingRegions < 2,
|
|
552
554
|
},
|
|
@@ -554,7 +556,7 @@ export const REGIONBASE_GATES: readonly Gate<RegionCtx>[] = [
|
|
|
554
556
|
|
|
555
557
|
/** THE REGION RULE, as a value. A third rule is one entry here — a partition, a gate table, and
|
|
556
558
|
* the population its counting rules are judged over — rather than three hand-edited branches in
|
|
557
|
-
* three functions, which is the same doctrine `rank.ts` states for its
|
|
559
|
+
* three functions, which is the same doctrine `rank-variations.ts` states for its hoist roster ("one entry
|
|
558
560
|
* here, one gate table, and that table's line in the gate-contract roster — not nine hand-edited
|
|
559
561
|
* sites that can drift"). */
|
|
560
562
|
export interface RegionRule {
|
|
@@ -675,9 +677,10 @@ export function planScopedBases(sfn: SFn, opts: ScopeBaseOpts = {}): ScopedBaseP
|
|
|
675
677
|
* PER KEY, not per function, and the difference is not hypothetical. Nothing in the L3 contract
|
|
676
678
|
* forbids the sharing — `l3/pollguard.ts` already emits it (`{ k: 'if', cond: s.cond, then: [s] }`
|
|
677
679
|
* puts one `cond` object at two tree positions), and it is harmless today only because the shapes
|
|
678
|
-
* are derived AFTER this
|
|
680
|
+
* are derived AFTER this variation in `rank.ts`, an ordering nothing pins. A whole-function decline
|
|
679
681
|
* would make a future producer that shares one node silently delete every base this pass names —
|
|
680
|
-
* including `kleod:UpdateHUDCounterDisplay`'s match,
|
|
682
|
+
* including, until that row was retired (2026-09-13), `kleod:UpdateHUDCounterDisplay`'s match,
|
|
683
|
+
* which returning `null` cost. Refusing the
|
|
681
684
|
* key that actually shares costs that key's spelling and nothing else, and the differ still has
|
|
682
685
|
* every other spelling in the list.
|
|
683
686
|
*
|
|
@@ -757,14 +760,14 @@ export function planScopedBases(sfn: SFn, opts: ScopeBaseOpts = {}): ScopedBaseP
|
|
|
757
760
|
|
|
758
761
|
/**
|
|
759
762
|
* The re-spelling `regions` asks for — `/scopebase` or `/regionbase` — or null when nothing
|
|
760
|
-
* qualifies (the caller then adds no candidate rather than a duplicate of the
|
|
763
|
+
* qualifies (the caller then adds no candidate rather than a duplicate of the default).
|
|
761
764
|
*/
|
|
762
765
|
export const hoistScopedBases = (sfn: SFn, opts: ScopeBaseOpts = {}): SFn | null =>
|
|
763
766
|
applyScopedBasePlan(sfn, planScopedBases(sfn, opts));
|
|
764
767
|
|
|
765
768
|
/**
|
|
766
769
|
* `plan` applied to the tree it was planned over — null when it decided nothing (the caller then
|
|
767
|
-
* adds no candidate rather than a duplicate of the
|
|
770
|
+
* adds no candidate rather than a duplicate of the default).
|
|
768
771
|
*
|
|
769
772
|
* IDENTITY-BOUND to that tree, and not by the type: `scope` is matched against statement LISTS and
|
|
770
773
|
* `repoint` against access NODES, both by reference. A plan from a DIFFERENT tree therefore splices
|
package/src/l3/sinkinit.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: sink each leading pointer-base INIT to the statement that first uses it.
|
|
2
2
|
//
|
|
3
3
|
// `l3/basecse.ts`'s COMMITTED call emits every base hoist at the head of `sfn.body`, so a base
|
|
4
4
|
// first touched halfway down the function is live across everything above it — a live range the
|
|
5
|
-
// original never had, and agbcc pays for it with a callee-saved register. (Its roster
|
|
6
|
-
// ask for this placement directly, through the same `l3/hoist.ts` mechanism this
|
|
7
|
-
// the
|
|
5
|
+
// original never had, and agbcc pays for it with a callee-saved register. (Its roster's hoists
|
|
6
|
+
// ask for this placement directly, through the same `l3/hoist.ts` mechanism this variation uses; what
|
|
7
|
+
// the variation adds is reaching the run on a tree the roster did not build — one NOTHING re-hoisted,
|
|
8
8
|
// or one `l3/nearbase.ts` prepended into, which is the `/nearbase/sinkinit` pairing.)
|
|
9
9
|
// Compiled pair on `synthetic:basehome`:
|
|
10
10
|
// assigning at the top adds `push {r4, lr}` / `pop {r4}` / `pop {r0}` / `bx r0` where assigning at
|
|
11
|
-
// the first use keeps a plain `bx lr`. The ladder that row records is the argument for the
|
|
11
|
+
// the first use keeps a plain `bx lr`. The ladder that row records is the argument for the variation
|
|
12
12
|
// being a PLACEMENT rather than a wider hoist — top-placed 11, not hoisted at all 9, placed at
|
|
13
13
|
// first use 0. A hoist at the wrong place is worse than no hoist.
|
|
14
14
|
//
|
|
15
|
-
// A
|
|
15
|
+
// A VARIATION, NOT THE DEFAULT. Which placement the source used is per-function knowledge the asm does
|
|
16
16
|
// not carry: moving basecse's own head placement to first use moves 8 benchmark rows, 4 better and
|
|
17
17
|
// 4 worse, two of the losses being matches. The UNSUNK spelling always rides beside this one —
|
|
18
18
|
// head-placed wherever basecse built the run, prepend-placed under `/nearbase` — and the differ
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// immediately before the first TOP-LEVEL statement mentioning its name and never INTO a nested
|
|
26
26
|
// scope, so it still dominates every use — planning a hoist inside a scope is `l3/scopebase.ts`'s
|
|
27
27
|
// job and it does the domination work. `placeBaseLocals` carries the refusals; a run where none of
|
|
28
|
-
// them moves is this
|
|
28
|
+
// them moves is this variation declining.
|
|
29
29
|
//
|
|
30
30
|
// SEMANTICS BY CONSTRUCTION: the moved value is a pure address leaf — it reads nothing, writes its
|
|
31
31
|
// own plain cell and cannot fault — and every statement it crosses mentions the name nowhere, an
|
package/src/l3/slotorder.ts
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
// LOWER `[sp,#k]`, and a source whose two spilled locals are declared the other way round compiles
|
|
8
8
|
// to the same object with those two operands swapped and nothing else moved.
|
|
9
9
|
//
|
|
10
|
-
// A
|
|
10
|
+
// A COMPILER BEHAVIOR, NOT A RANKED VARIATION. The asm does not underdetermine the answer: a
|
|
11
11
|
// `[sp,#k]` operand NAMES the slot, and slot → declaration rank is a FUNCTION once the compiler is
|
|
12
|
-
// fixed.
|
|
12
|
+
// fixed. A variation exists where two source spellings collapse to the same object and only the differ
|
|
13
13
|
// can choose between them; here the object chooses. The fan cost is zero — this rewrites the one
|
|
14
14
|
// tree every candidate already carries, adding no candidate, no `structure()` call and no compile.
|
|
15
15
|
//
|
|
16
|
-
// WHY `emit` OWNS IT. Two reasons, and they point the same way. It must run AFTER every L3
|
|
16
|
+
// WHY `emit` OWNS IT. Two reasons, and they point the same way. It must run AFTER every L3 respell variation,
|
|
17
17
|
// because each of those rebuilds the declaration list (appends: basecse, scopebase, argbase,
|
|
18
18
|
// nearbase, regspell, reindex; filters: unmerge, coalesce, dce, inlinebase, pollguard, unreduce),
|
|
19
19
|
// and `emit` is last by construction. And it must NOT run at a `.emit(` call site: there are seven
|
package/src/l3/storage.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// SHADOWING is what the copies disagreed on, and the disagreement is REAL rather than drift: a
|
|
10
10
|
// name can be declared both a global and a local, because `SFn.globals` records what the code
|
|
11
11
|
// REFERENCES (structure.ts `noteGlobal`) independently of what it declares. Which answer is right
|
|
12
|
-
// depends on how the asking
|
|
12
|
+
// depends on how the asking pass will SPELL the name, so the two questions are two functions
|
|
13
13
|
// here instead of one set each caller filters its own way:
|
|
14
14
|
// • it keeps the reference verbatim (`(u8 *)g`) — the shadow is harmless, the spelling denotes
|
|
15
15
|
// whatever the original access denoted: `declaredGlobals`;
|
package/src/l3/tailmerge.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// above covers only below-vs-in-arms.
|
|
26
26
|
//
|
|
27
27
|
// KNOWN INTERACTIONS, both byte-level rather than soundness. This pass is unconditional like
|
|
28
|
-
// `dce.ts` and `basecse.ts` rather than a differ-refereed
|
|
28
|
+
// `dce.ts` and `basecse.ts` rather than a differ-refereed variation, and the argument those files each
|
|
29
29
|
// state for themselves applies here too and was missing: a wrong merge changes recompiled bytes and
|
|
30
30
|
// surfaces as a LOST match under the zero-lost gate, never as wrong C.
|
|
31
31
|
//
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
// ways in `test/tailmerge.test.ts`.
|
|
50
50
|
// A ranked row is not stuck with either order: `/copy-defpos` (rank.ts) enumerates the
|
|
51
51
|
// def-position spelling beside the record's, so the merged form is a candidate the differ can
|
|
52
|
-
// pick on bytes — on `CountCollectedGems` its gate admits the
|
|
52
|
+
// pick on bytes — on `CountCollectedGems` its gate admits the variation on the map-ful and map-less
|
|
53
53
|
// lifts alike. What no arm of the fan spells is the third form, the hidden statement merged
|
|
54
54
|
// while KEEPING the record's order.
|
|
55
55
|
//
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// L3 spelling pass: drop a void `return;` the assembly says the source never wrote.
|
|
2
|
+
//
|
|
3
|
+
// `structure/retspell.ts` marks a return `unspelled` when the machine reached that epilogue without
|
|
4
|
+
// the `b <epilogue>` a source `return;` compiles to — the reading, and what makes it decidable, is
|
|
5
|
+
// stated there. This pass is the other half: a mark alone is not a licence to delete, because a
|
|
6
|
+
// `return` is still a control transfer in the STATEMENT tree. Deleting one that something follows
|
|
7
|
+
// lets control run on into it, and that is a semantic change, not a spelling one.
|
|
8
|
+
//
|
|
9
|
+
// So the licence is TAIL POSITION and nothing weaker: nothing executes after the statement on any
|
|
10
|
+
// path, all the way up to the end of the function. A return ending a loop body, a `switch` arm, or
|
|
11
|
+
// an `if` arm the function continues past is left exactly where it is, whatever the mark says.
|
|
12
|
+
//
|
|
13
|
+
// TWO SHAPES REFUSE EVEN IN TAIL POSITION, both because removing the statement would leave a
|
|
14
|
+
// statement list that has to be re-spelled rather than shortened:
|
|
15
|
+
//
|
|
16
|
+
// - the sole statement of a `then` arm. `if (c) { }` is not the answer — `if (!c) { … }` is, and
|
|
17
|
+
// which sense a compiler emits is a per-SITE question this pass holds nothing to decide.
|
|
18
|
+
// - the whole function body. A body is not a place a statement can vanish from.
|
|
19
|
+
//
|
|
20
|
+
// An `else` arm IS allowed to empty: an `if` with no else is the same statement, and both the
|
|
21
|
+
// printer (`backend/cfamily.ts`) and `l3/dce.ts` already spell `else: []` that way.
|
|
22
|
+
//
|
|
23
|
+
// THE REFUSALS ARE PROSE, NOT A `Gate` TABLE, and that is the second of the three answers
|
|
24
|
+
// `docs/level-tower.md` sanctions rather than an omission. A table buys the ablation — drop this
|
|
25
|
+
// rule and something breaks — as a test instead of a claim, and it is worth building for a refusal a
|
|
26
|
+
// round has HAD to instrument. None of these was: each is a property of one candidate's own
|
|
27
|
+
// position, and `test/tailret.test.ts` holds a test per refusal, which is the ablation the table
|
|
28
|
+
// would have bought. Convert them when a round has to argue about one.
|
|
29
|
+
//
|
|
30
|
+
// Ordering: after `l3/tailmerge.ts`, whose peel moves `assign`/`store`/`exprstmt` only, so a
|
|
31
|
+
// `return` ending an arm blocks it — run first and this pass hands tailmerge arms it could not
|
|
32
|
+
// otherwise peel, changing rows that have nothing to do with returns. Before `l3/dce.ts`, so its
|
|
33
|
+
// branch peephole sees the `else` this pass empties. `pipeline.ts` commits that order.
|
|
34
|
+
import type { SFn, Stmt } from './ast';
|
|
35
|
+
|
|
36
|
+
/** Can this statement be deleted outright — a void return the asm did not spell? */
|
|
37
|
+
const isDroppable = (s: Stmt): boolean => s.k === 'return' && s.value === undefined && s.unspelled === true;
|
|
38
|
+
|
|
39
|
+
/** `stmts` rewritten. `isTail` — control falls off the END of the function after this list, so its
|
|
40
|
+
* last statement is in tail position. `mayEmpty` — the list is allowed to come back empty. */
|
|
41
|
+
function walk(stmts: Stmt[], isTail: boolean, mayEmpty: boolean): Stmt[] {
|
|
42
|
+
const out = stmts.map((s, i) => {
|
|
43
|
+
const tail = isTail && i === stmts.length - 1;
|
|
44
|
+
switch (s.k) {
|
|
45
|
+
case 'if':
|
|
46
|
+
return { ...s, then: walk(s.then, tail, false), else: walk(s.else, tail, true) };
|
|
47
|
+
case 'while':
|
|
48
|
+
case 'dowhile':
|
|
49
|
+
case 'for':
|
|
50
|
+
// A return inside a loop is never in tail position: the statement after it is the next
|
|
51
|
+
// iteration.
|
|
52
|
+
return { ...s, body: walk(s.body, false, false) };
|
|
53
|
+
case 'switch':
|
|
54
|
+
// Nor inside a `switch`: dropping an arm's return diverts it into the arm below.
|
|
55
|
+
return {
|
|
56
|
+
...s,
|
|
57
|
+
cases: s.cases.map((c) => ({ ...c, body: walk(c.body, false, false) })),
|
|
58
|
+
...(s.default ? { default: walk(s.default, false, false) } : {}),
|
|
59
|
+
};
|
|
60
|
+
default:
|
|
61
|
+
return s;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const last = out[out.length - 1];
|
|
65
|
+
return isTail && last !== undefined && isDroppable(last) && (out.length > 1 || mayEmpty) ? out.slice(0, -1) : out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function dropUnspelledReturns(sfn: SFn): SFn {
|
|
69
|
+
return { ...sfn, body: walk(sfn.body, true, false) };
|
|
70
|
+
}
|
package/src/l3/typing.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { IrType, T, scalarTypeForAccess } from '../ir/types';
|
|
|
21
21
|
import { type Expr, type SFn, exprChildren } from './ast';
|
|
22
22
|
|
|
23
23
|
/** The declared type of a printed variable — the env `exprCType` judges rendered C against.
|
|
24
|
-
* THE one copy of the SFn→env derivation — printers, contracts and L3
|
|
24
|
+
* THE one copy of the SFn→env derivation — printers, contracts and L3 respell variations alike: each
|
|
25
25
|
* consumer judging against anything but the declarations it emits would let them disagree. */
|
|
26
26
|
export type VarTypes = (name: string) => IrType | undefined;
|
|
27
27
|
|
|
@@ -133,7 +133,7 @@ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes):
|
|
|
133
133
|
*
|
|
134
134
|
* THE one rendered-signedness judgment, and it lives beside the declarations it judges against
|
|
135
135
|
* because every consumer shares it: the C-family backend's operand pin, structure.ts's
|
|
136
|
-
* unsigned-compare gate (the /uns-cmp
|
|
136
|
+
* unsigned-compare gate (the /uns-cmp variation) and its signed-compare pin, and — through
|
|
137
137
|
* `arithConversionSignedness` above — initfirst's compare-meaning gate. Two of those models
|
|
138
138
|
* disagreeing about one expression is the drift this placement prevents.
|
|
139
139
|
*
|
|
@@ -149,7 +149,7 @@ export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes):
|
|
|
149
149
|
* instruction-for-instruction the same where `f` really returns `int`); a missing one is a
|
|
150
150
|
* miscompile — with `u32 f(void);` in scope `f() / a` calls `__udivsi3` where the machine called
|
|
151
151
|
* `__divsi3`, and `if (f() >= 0)` compiles to `bl f; mov r0, #0`, comparison and both arms gone.
|
|
152
|
-
* The one consumer that reads `undefined` the other way is the /uns-cmp
|
|
152
|
+
* The one consumer that reads `undefined` the other way is the /uns-cmp variation's own gate, which is
|
|
153
153
|
* asking whether an operand is ALREADY unsigned.
|
|
154
154
|
*
|
|
155
155
|
* Anything narrower than 32 bits promotes to `int` and is therefore SIGNED, whatever it was
|