@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/homesplit.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The `/livebase-block × /regionbase` PAIRING (rank.ts `/livebase-block/homesplit`): one base kept
|
|
2
2
|
// at the function head and a SECOND base split into one local per region, in the same function.
|
|
3
3
|
//
|
|
4
|
-
// WHY IT IS NOT REACHABLE FROM EITHER
|
|
4
|
+
// WHY IT IS NOT REACHABLE FROM EITHER VARIATION. Both are whole-FUNCTION policies over the bases they
|
|
5
5
|
// bind: `hoistBaseLocals` homes every key its table admits at one placement, and
|
|
6
6
|
// `hoistScopedBases` splits every key its region rule admits. A function whose two bases want
|
|
7
7
|
// OPPOSITE answers is spelled by neither. `synthetic:dmapoll` is that function and its endpoint is
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
// over ONE input is the shape that would collide, and nothing here does it.
|
|
20
20
|
//
|
|
21
21
|
// WHICH KEY IS WITHHELD IS NOT DERIVABLE, so every admitted key is offered as its own candidate,
|
|
22
|
-
//
|
|
23
|
-
// take toward each other. The
|
|
24
|
-
// IDENTITY: `bench diff` and docs/ranked-repro.md compare candidates by
|
|
22
|
+
// NAMED BY THAT KEY, and the differ referees — the same posture `/scopebase` and `/regionbase`
|
|
23
|
+
// take toward each other. The variation carries `homeSplitTag(key)` because a candidate's variations are its
|
|
24
|
+
// IDENTITY: `bench diff` and docs/ranked-repro.md compare candidates by them, so one name over two
|
|
25
25
|
// withholds would hide a program swap from both. The withhold itself is DATA: one rejection
|
|
26
26
|
// prepended to the caller's own admission table, in the `Gate<BaseKey>` type that table already
|
|
27
27
|
// has, so `firstRejection` attributes a refusal to it, `without` ablates it, and the composed table
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// EXACTLY ONE KEY IS WITHHELD, and the arity is a claim rather than an oversight. A two-key withhold
|
|
31
31
|
// exists only where the caller's table binds three — `homesplit-fan-cap` admits no more — and there
|
|
32
32
|
// it is the three further pairs, each carrying the three respells rank.ts derives from one pipe:
|
|
33
|
-
// NINE more candidates per
|
|
33
|
+
// NINE more candidates per structure setting on those functions alone, before any stacked variation, and no
|
|
34
34
|
// row asks for one. `l3/volatileptr.ts`'s `volatileSubsetCandidates` enumerates every proper subset
|
|
35
35
|
// under the same cap; it does that because a row demanded each of them. Widen this the same way,
|
|
36
36
|
// on a row.
|
|
@@ -59,7 +59,7 @@ export const withholdingKey = (gates: readonly Gate<BaseKey>[], key: string): re
|
|
|
59
59
|
...gates,
|
|
60
60
|
];
|
|
61
61
|
|
|
62
|
-
/** The withheld key as a
|
|
62
|
+
/** The withheld key as a variation's SUBJECT: `c:67109076 4 true` → `0x40000d4.4s`. Width and signedness
|
|
63
63
|
* ride because they are part of the key — two keys over one address are two different spellings.
|
|
64
64
|
*
|
|
65
65
|
* PARSED BY THE KEY'S PRODUCER (`l3/basecse.ts`'s `parseBaseKey`, beside `keyOf`), because a base
|
|
@@ -68,9 +68,9 @@ export const withholdingKey = (gates: readonly Gate<BaseKey>[], key: string): re
|
|
|
68
68
|
* separator rather than the type's. Split from the FRONT, the type reads as the width and the
|
|
69
69
|
* width as the signedness; split from the END, the `a:` form comes out right and the `c:` form
|
|
70
70
|
* runs `Number` over `67109076 <u16*>`, tagging every cast over a numeric base `0xNaN` and
|
|
71
|
-
* collapsing distinct keys onto one
|
|
71
|
+
* collapsing distinct keys onto one name. No shipped table admits a cast base outside
|
|
72
72
|
* `/orderbase`, and `/orderbase` carries `pairings: false`, so no such key reaches this function
|
|
73
|
-
* today — but a candidate
|
|
73
|
+
* today — but a candidate's variations are its identity (`bench diff` and docs/ranked-repro.md compare
|
|
74
74
|
* candidates by it), and one roster line is all that stands between the two. The sibling half of
|
|
75
75
|
* the same hazard is already guarded in `splitHomeBases`, which translates a cast base to no
|
|
76
76
|
* region key and declines. */
|
|
@@ -79,7 +79,7 @@ export function homeSplitTag(key: string): string {
|
|
|
79
79
|
const base = leaf.startsWith('c:') ? `0x${Number(leaf.slice(2)).toString(16)}` : leaf.slice(leaf.indexOf(':') + 1);
|
|
80
80
|
// The cast's element type stays in the token — it is part of the key's identity, since two casts
|
|
81
81
|
// over one symbol are two locals that stride differently. Whitespace is squeezed defensively
|
|
82
|
-
// rather than because any type spells one: a
|
|
82
|
+
// rather than because any type spells one: a variation is one whitespace-free word everywhere it is
|
|
83
83
|
// read, and that has to hold whatever `typeToString` grows.
|
|
84
84
|
const type = castType === null ? '' : `<${castType}>`;
|
|
85
85
|
return `${`${base}${type}`.replace(/\s+/g, '')}.${width}${signed ? 's' : 'u'}`;
|
|
@@ -98,13 +98,13 @@ export interface HomeSplitFanCtx {
|
|
|
98
98
|
export const HOMESPLIT_FAN_GATES: readonly Gate<HomeSplitFanCtx>[] = [
|
|
99
99
|
{
|
|
100
100
|
id: 'homesplit-degenerate',
|
|
101
|
-
why: '
|
|
101
|
+
why: 'with one hoistable base there is nothing new: withholding it is `regionbase`, and withholding none is `livebase-block`',
|
|
102
102
|
sound: false,
|
|
103
103
|
rejects: (c) => c.hoistableKeys < 2,
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
id: 'homesplit-fan-cap',
|
|
107
|
-
why: '
|
|
107
|
+
why: 'more than three hoistable bases would add a candidate for each, times the volatile pairings',
|
|
108
108
|
sound: false,
|
|
109
109
|
rejects: (c) => c.hoistableKeys > 3,
|
|
110
110
|
},
|
|
@@ -143,13 +143,13 @@ export interface HomeSplitCtx {
|
|
|
143
143
|
export const HOMESPLIT_GATES: readonly Gate<HomeSplitCtx>[] = [
|
|
144
144
|
{
|
|
145
145
|
id: 'homesplit-no-region',
|
|
146
|
-
why: 'a
|
|
146
|
+
why: 'a base held back from the function-top local that no per-region local takes instead leaves the spelling the default carries',
|
|
147
147
|
sound: false,
|
|
148
148
|
rejects: (c) => !c.withheldSplits,
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
id: 'homesplit-drops-device-volatile',
|
|
152
|
-
why: 'a device read left inline
|
|
152
|
+
why: 'a device read left inline would carry neither `volatile` nor `vol-store`',
|
|
153
153
|
sound: false,
|
|
154
154
|
rejects: (c) => c.inlineDeviceRead,
|
|
155
155
|
},
|
package/src/l3/initfirst.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: a loop INIT moves above the guard that encloses it, and the guard reads
|
|
2
2
|
// the initialized variable.
|
|
3
3
|
//
|
|
4
4
|
// `for (i = 0; i < n; i++)` compiles with the init BEFORE the zero-trip test (`mov r3,#0` then
|
|
5
5
|
// `cmp r3, r5`), while `if (0 < n) { i = 0; do … }` compiles with the init behind the branch.
|
|
6
6
|
// Both source forms lift to the SAME IR — a const has no position — so which the original spelled
|
|
7
|
-
// is not recoverable; this
|
|
7
|
+
// is not recoverable; this variation emits the init-first sibling and the differ referees:
|
|
8
8
|
//
|
|
9
9
|
// if (0 < n) { v = 0; … } → v = 0; if (v < n) { … }
|
|
10
10
|
//
|
|
@@ -61,7 +61,7 @@ const stripWideIntCast = (e: Expr): Expr =>
|
|
|
61
61
|
|
|
62
62
|
/** Every deref rooted at a var through casts only. The var-root rule is a TWO-WORLD argument, not a
|
|
63
63
|
* volatility proof: a deref through a plain-declared pointer local may still be MMIO, but the
|
|
64
|
-
* /volatile
|
|
64
|
+
* /volatile variation enumerates the qualified sibling — where this variation refuses — so both worlds reach
|
|
65
65
|
* the differ and collapsing reads here is the plain world's own premise. A raw `*(u16 *)CONST` deref
|
|
66
66
|
* has NO local for /volatile to qualify, so no sibling carries the volatile world and the collapse
|
|
67
67
|
* would silently discard it. */
|
package/src/l3/inlinebase.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: DELETE a pointer local holding a CONSTANT address and spell each access
|
|
2
2
|
// through it as the cast constant (`*(u16 *)0x4000208` rather than `p = (u16 *)0x4000208; *p`).
|
|
3
3
|
//
|
|
4
4
|
// The local is structure/analysis.ts's value-home spelling for a `const` with 2+ consumers that
|
|
@@ -25,34 +25,34 @@
|
|
|
25
25
|
// separate at 11 against 12 on their own, and are BYTE-IDENTICAL once the slot qualifier is
|
|
26
26
|
// there too — the shape that matches (agbcc 2.9-arm-000512, `-O2 -mthumb-interwork -Wimplicit
|
|
27
27
|
// -fhex-asm -fprologue-bugfix`; `.s` diff empty, `.o` identical under cmp). So rank.ts emits
|
|
28
|
-
// the qualified spelling as a second OUTPUT of this
|
|
28
|
+
// the qualified spelling as a second OUTPUT of this variation, `/volatile` narrowed to the locals
|
|
29
29
|
// it deletes. Each cast this mints carries the qualifier; a use whose width does not stride the
|
|
30
30
|
// declared pointee renders through the C-family printer's reinterpret cast instead, which
|
|
31
31
|
// carries it too (backend/cfamily.ts) — in C the access takes the OUTER type, so a plain cast
|
|
32
32
|
// there would spell exactly the silent drop this paragraph exists to prevent.
|
|
33
33
|
//
|
|
34
34
|
// GATE (INLINEBASE_GATES) — the local must be all of: pointer-typed; initialized by a bare
|
|
35
|
-
// NONZERO `const` (a `(T *)base` CAST initializer is l3/basecse.ts's reuse hoist, whose own
|
|
35
|
+
// NONZERO `const` (a `(T *)base` CAST initializer is l3/basecse.ts's reuse hoist, whose own variation
|
|
36
36
|
// family owns that question; `0` is NULL, never an address, which is also the sibling qualifier
|
|
37
|
-
//
|
|
37
|
+
// variation's rule); assigned exactly once, by a statement at the body's TOP LEVEL that no earlier
|
|
38
38
|
// statement's mention precedes; never address-taken; used only as the base of an `index`, at 2+
|
|
39
39
|
// sites (one use is not the reused address this exists for); not object-`volatile` (a
|
|
40
40
|
// `T *volatile p` has no inhabitant, and cfamily.ts prints that flag in the pointee's position);
|
|
41
41
|
// and not `frame` (a slot is an asm fact — see the SFn.locals doc). Anything else, and nothing
|
|
42
42
|
// qualifying at all, DECLINES (null) rather than approximating.
|
|
43
43
|
//
|
|
44
|
-
// A
|
|
45
|
-
// underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked
|
|
44
|
+
// A RESPELL VARIATION RATHER THAN A STRUCTURE VARIATION, which is a cost decision and not the
|
|
45
|
+
// underdetermination one (docs/level-tower.md, "a third fork sits inside the ranked variations").
|
|
46
46
|
// The question — does a `const` with 2+ consumers live across a call in a named local — is the one
|
|
47
47
|
// `/reread-globals`, `/addr-home`, `/expr-home` and `/derived-home` each answer as a
|
|
48
|
-
//
|
|
48
|
+
// STRUCTURE_VARIATIONS entry. A structure variation here would double the enumeration on every function it admits;
|
|
49
49
|
// substituting on the already-homed tree costs 766 candidates over 47058 (+1.6%) across the 33 of
|
|
50
50
|
// 69 klonoa functions that lift with no symbol map.
|
|
51
51
|
//
|
|
52
52
|
// KNOWN GAP, and it is the price of that choice rather than an oversight: only `index` bases are
|
|
53
53
|
// re-spelled, so the same L2 home passed to a callee or standing as a `field` base is out of
|
|
54
54
|
// reach — `otherUses` refuses it. Reaching those needs the un-homed tree, which is
|
|
55
|
-
// structure/analysis.ts's decision; the day a row demands one, this becomes
|
|
55
|
+
// structure/analysis.ts's decision; the day a row demands one, this becomes a structure variation.
|
|
56
56
|
//
|
|
57
57
|
// The idiom it recovers is every GBA project's register macro: `*(vu16 *)0x4000208` is what
|
|
58
58
|
// `REG_IME` expands to, so the deleted local is not merely an undone home.
|
|
@@ -71,20 +71,20 @@ interface BaseCtx {
|
|
|
71
71
|
export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
|
|
72
72
|
{
|
|
73
73
|
id: 'non-pointer',
|
|
74
|
-
why: '
|
|
74
|
+
why: 'only a local holding an address is substituted; a local holding a scalar value is a different question',
|
|
75
75
|
sound: false,
|
|
76
76
|
rejects: (c) => !c.isPointer,
|
|
77
77
|
},
|
|
78
78
|
{
|
|
79
79
|
id: 'object-volatile',
|
|
80
|
-
why: 'the substitution
|
|
80
|
+
why: 'the substitution keeps only the `volatile` of what the pointer points to, so a pointer that is itself volatile would lose its own',
|
|
81
81
|
sound: true,
|
|
82
82
|
guardedBy: 'inlinebase.test.ts: an object-volatile or frame local declines',
|
|
83
83
|
rejects: (c) => c.objectVolatile,
|
|
84
84
|
},
|
|
85
85
|
{
|
|
86
86
|
id: 'frame',
|
|
87
|
-
why: 'a slot the
|
|
87
|
+
why: 'a stack slot the assembly used is a fact about the function, not a spelling to undo',
|
|
88
88
|
sound: false,
|
|
89
89
|
rejects: (c) => c.hasFrame,
|
|
90
90
|
},
|
|
@@ -104,7 +104,7 @@ export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
|
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
id: 'null-base',
|
|
107
|
-
why: '`0` is
|
|
107
|
+
why: 'a base of `0` is a null pointer, never an address, and `volatile` refuses it too',
|
|
108
108
|
sound: false,
|
|
109
109
|
rejects: (c) => c.m.constValue === 0,
|
|
110
110
|
},
|
|
@@ -126,12 +126,12 @@ export const INLINEBASE_GATES: readonly Gate<BaseCtx>[] = [
|
|
|
126
126
|
id: 'other-uses',
|
|
127
127
|
why: 'a use the substitution cannot reach would name the deleted local',
|
|
128
128
|
sound: true,
|
|
129
|
-
guardedBy: 'inlinebase.test.ts: a use that is not an `index` base is outside what the
|
|
129
|
+
guardedBy: 'inlinebase.test.ts: a use that is not an `index` base is outside what the variation re-spells',
|
|
130
130
|
rejects: (c) => c.m.otherUses !== 0,
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
id: 'single-use',
|
|
134
|
-
why: 'one use is not the reused address this
|
|
134
|
+
why: 'one use is not the reused address this variation exists for',
|
|
135
135
|
sound: false,
|
|
136
136
|
rejects: (c) => c.m.baseUses < 2,
|
|
137
137
|
},
|
|
@@ -164,8 +164,8 @@ function plan(sfn: SFn): Map<string, Extract<Expr, { k: 'cast' }>> {
|
|
|
164
164
|
return out;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
/** Which locals this
|
|
168
|
-
* pairing, so the qualified output never qualifies a pointer the
|
|
167
|
+
/** Which locals this variation would delete — rank.ts narrows `/volatile` to exactly these before
|
|
168
|
+
* pairing, so the qualified output never qualifies a pointer the variation leaves standing. */
|
|
169
169
|
export function inlinableConstBases(sfn: SFn): string[] {
|
|
170
170
|
return [...plan(sfn).keys()];
|
|
171
171
|
}
|
package/src/l3/mentions.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// What one L3 tree does to each of its locals, counted once.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Respell variations ask overlapping versions of the question and would disagree if each walked the tree
|
|
4
4
|
// its own way: l3/inlinebase.ts needs the SHAPE of every use (only an `index` base is re-spellable,
|
|
5
5
|
// and the single assignment must be a top-level `const` nothing mentions earlier), while
|
|
6
6
|
// l3/volatileval.ts needs the COUNTS, to check the tree still performs every access the machine
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// oversight: a POSITION added to `index` reaches the generic vocabulary for free and this walk not
|
|
15
15
|
// at all. Every position is enumerated below and pinned by
|
|
16
16
|
// test/array-rank-guards.test.ts, beside the generic helpers it cannot speak for.
|
|
17
|
-
import { type Expr, type SFn, type Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
|
|
17
|
+
import { type Expr, type SFn, type Stmt, exprChildren, stmtChildren, stmtExprs, walkExprs } from './ast';
|
|
18
18
|
|
|
19
19
|
export interface Mentions {
|
|
20
20
|
/** assignments to the name, at any nesting */
|
|
@@ -24,7 +24,7 @@ export interface Mentions {
|
|
|
24
24
|
/** the bare-`const` value that assignment stores, or null if it stores anything else */
|
|
25
25
|
constValue: number | null;
|
|
26
26
|
addrTaken: number;
|
|
27
|
-
/** uses as the `base` of an `index` node — the only use shape a base
|
|
27
|
+
/** uses as the `base` of an `index` node — the only use shape a base-hoisting variation can re-spell */
|
|
28
28
|
baseUses: number;
|
|
29
29
|
/** every other read */
|
|
30
30
|
otherUses: number;
|
|
@@ -53,7 +53,7 @@ function walkExpr(e: Expr, visit: (x: Expr, isIndexBase: boolean) => void, isInd
|
|
|
53
53
|
if (e.k === 'index') {
|
|
54
54
|
walkExpr(e.base, visit, true);
|
|
55
55
|
// `lead` — a multidimensional global's LEADING subscripts — is an ordinary value position, so
|
|
56
|
-
// a name mentioned there is a real read. Missing it does not cost a candidate: it lets a
|
|
56
|
+
// a name mentioned there is a real read. Missing it does not cost a candidate: it lets a variation
|
|
57
57
|
// DELETE a local the body still names.
|
|
58
58
|
for (const l of e.lead ?? []) {
|
|
59
59
|
walkExpr(l, visit, false);
|
|
@@ -67,7 +67,7 @@ function walkExpr(e: Expr, visit: (x: Expr, isIndexBase: boolean) => void, isInd
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** Every mention of every local, keyed by name. Locals only — a param or a global name is not in
|
|
70
|
-
* the map, and a
|
|
70
|
+
* the map, and a pass asking about one gets `undefined` rather than a zeroed record. */
|
|
71
71
|
export function localMentions(sfn: SFn): Map<string, Mentions> {
|
|
72
72
|
const t = new Map<string, Mentions>(sfn.locals.map((l) => [l.name, blank()]));
|
|
73
73
|
const seen = (name: string, at: number): Mentions | undefined => {
|
|
@@ -111,3 +111,66 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
|
|
|
111
111
|
sfn.body.forEach((s, i) => stmt(s, i, true));
|
|
112
112
|
return t;
|
|
113
113
|
}
|
|
114
|
+
|
|
115
|
+
/** THE ONE WALK behind `mentionsAnyLocal` and `mentionedLocals` below: which of `names` anything
|
|
116
|
+
* under `stmts` still NAMES — as an assignment TARGET (which carries no expression, so no walk
|
|
117
|
+
* over values can see it), as a read, or as an address. `first` returns at the first hit it
|
|
118
|
+
* reaches, which is all the boolean caller needs.
|
|
119
|
+
*
|
|
120
|
+
* The question a pass that DELETES a declaration has to answer, and it lives here rather than in
|
|
121
|
+
* the deleting pass for the reason this file's header states about its own walk: a second walk
|
|
122
|
+
* over the node vocabulary is how a new node kind becomes a silent undercount, and beside
|
|
123
|
+
* `localMentions` a divergence is at least visible. This one is answered over a SUBTREE, so it
|
|
124
|
+
* cannot be derived from the counts above — `localMentions` is keyed to `sfn.locals` across the
|
|
125
|
+
* whole body, and l3/unmerge.ts's whole point is that those counts are sampled before any
|
|
126
|
+
* rewriting and go stale.
|
|
127
|
+
*
|
|
128
|
+
* TOTAL over the vocabulary by construction: `assign` is the only `Stmt` carrying a bare name and
|
|
129
|
+
* `var`/`addr` the only `Expr`s, and both walks are derived from `stmtChildren`/`stmtExprs` — so a
|
|
130
|
+
* `for`'s init and inc, a `switch`'s scrutinee, its cases and its default are all covered. */
|
|
131
|
+
function scanMentions(stmts: readonly Stmt[], names: ReadonlySet<string>, first: boolean): Set<string> {
|
|
132
|
+
// TWO FLAT SWEEPS, not one expression walk per nesting level. `walkExprs` already descends
|
|
133
|
+
// `stmtChildren` (ast.ts), so calling it per statement from inside a recursion that ALSO
|
|
134
|
+
// descends re-walks every nested expression once per enclosing level — quadratic in the nesting
|
|
135
|
+
// depth, 301 `has` calls at depth 24 where one pass needs 25. Small at today's call sites (one
|
|
136
|
+
// rewritten subtree per un-merge site, one dropped-locals set per respelled tree), but this is a
|
|
137
|
+
// SHARED helper and its cost belongs in its contract.
|
|
138
|
+
const found = new Set<string>();
|
|
139
|
+
const body = [...stmts] as Stmt[];
|
|
140
|
+
const stack: Stmt[] = [...body];
|
|
141
|
+
while (stack.length > 0) {
|
|
142
|
+
const s = stack.pop()!;
|
|
143
|
+
if (s.k === 'assign' && names.has(s.name)) {
|
|
144
|
+
found.add(s.name);
|
|
145
|
+
if (first) {
|
|
146
|
+
return found;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
stack.push(...stmtChildren(s));
|
|
150
|
+
}
|
|
151
|
+
for (const e of walkExprs(body)) {
|
|
152
|
+
if ((e.k === 'var' || e.k === 'addr') && names.has(e.name)) {
|
|
153
|
+
found.add(e.name);
|
|
154
|
+
if (first) {
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return found;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** true when anything under `stmts` still names one of `names`. See `scanMentions` above. */
|
|
163
|
+
export function mentionsAnyLocal(stmts: readonly Stmt[], names: ReadonlySet<string>): boolean {
|
|
164
|
+
return scanMentions(stmts, names, true).size > 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** WHICH of `names` the tree still mentions — the same walk `mentionsAnyLocal` answers "any" over,
|
|
168
|
+
* told to keep going, for the caller that has to NAME the survivors.
|
|
169
|
+
*
|
|
170
|
+
* It exists so that contracts.ts's `assertNoOrphanedLocals` — the loud backstop for exactly the
|
|
171
|
+
* mistake this predicate guards — does not carry a THIRD hand-rolled copy of the node vocabulary.
|
|
172
|
+
* The boolean cannot serve it (the diagnostic needs the set) and one call per dropped name would
|
|
173
|
+
* be a walk per name. */
|
|
174
|
+
export function mentionedLocals(stmts: readonly Stmt[], names: ReadonlySet<string>): Set<string> {
|
|
175
|
+
return scanMentions(stmts, names, false);
|
|
176
|
+
}
|
package/src/l3/mulfirst.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: put the PRODUCT operand first in a commutative `+`.
|
|
2
2
|
//
|
|
3
3
|
// structure.ts's def-order rule spells commutative operands in EVALUATION order, which recovers
|
|
4
4
|
// gcc's left-to-right source order. IDO and mwcc break the correspondence for exactly one shape:
|
|
5
5
|
// in `a*b + c` they SCHEDULE the independent load of `c` above the product's `mflo`/`mullw`, so
|
|
6
6
|
// the machine add reads (c, product) and def order re-spells the source's product-first sum as
|
|
7
|
-
// c-first. Which order the source used is not recoverable from positions there — so this
|
|
7
|
+
// c-first. Which order the source used is not recoverable from positions there — so this variation
|
|
8
8
|
// emits the product-first sibling and the differ referees (verified byte-identical against IDO
|
|
9
9
|
// on the bg_area row; the def-order spelling stays in the list for sources that really were
|
|
10
10
|
// c-first).
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// SCOPE (decline over approximate): a `+` is flipped only when exactly ONE side is a product
|
|
13
13
|
// (`bin('*')` at the root, casts looked through) — two products or none leave nothing to anchor
|
|
14
14
|
// the flip on. A side carrying an effect (a call, a marker) never moves — evaluation order of
|
|
15
|
-
// the operands is what the
|
|
15
|
+
// the operands is what the variation edits. Declines (null) when no `+` changes, so no duplicate
|
|
16
16
|
// candidate.
|
|
17
17
|
import type { Expr, SFn } from './ast';
|
|
18
18
|
import { exprHasEffect, mapExprChildren, mapStmtExprs } from './ast';
|
package/src/l3/nearbase.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: NEIGHBOR absolute addresses derive from one shared base local.
|
|
2
2
|
//
|
|
3
3
|
// A cluster of raw-address accesses a few bytes apart is one object's cells: the compiler holds
|
|
4
4
|
// the object's base in a register and derives each cell (`add #72` / `add #74` off one pool
|
|
5
5
|
// word, a halfword offset beyond the load range forcing the add, the in-range word staying
|
|
6
|
-
// `[rN, #112]`), where a per-cell spelling anchors one pool constant per address. This
|
|
6
|
+
// `[rN, #112]`), where a per-cell spelling anchors one pool constant per address. This variation
|
|
7
7
|
// re-spells every deref base in a cluster as an offset from a `u8 *` base local holding the
|
|
8
8
|
// cluster's lowest address, and the differ referees:
|
|
9
9
|
//
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// VALUE inside its window re-spells too, as `(s32)(b + off)` — the address of a cell handed to
|
|
22
22
|
// something (a DMA source register) is the same derived add in the original, and the two
|
|
23
23
|
// spellings are value-equal by construction, so the differ referees — including an integer that
|
|
24
|
-
// only coincidentally lands in the window, which is the stated cost of the
|
|
24
|
+
// only coincidentally lands in the window, which is the stated cost of the variation (the `s32` cast
|
|
25
25
|
// assumes addresses below 2^31, true of every target that declares nearBaseSpan today). Declines
|
|
26
26
|
// (null) when no cluster forms.
|
|
27
27
|
import { baseConst } from './address';
|
|
@@ -31,7 +31,7 @@ import { type BaseInit, nameAllocator, placeBaseLocals } from './hoist';
|
|
|
31
31
|
|
|
32
32
|
/** `span` is the target's single-add-immediate derivation reach
|
|
33
33
|
* (TargetDescription.compilerBehaviors.nearBaseSpan) — a target that declares none never runs
|
|
34
|
-
* this
|
|
34
|
+
* this variation. */
|
|
35
35
|
export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
|
|
36
36
|
if (!Number.isFinite(span) || span < 0) {
|
|
37
37
|
return null; // a hostile span stalls the cluster window instead of shrinking it
|
package/src/l3/offmember.ts
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// displacement. A window refusal removes ZERO further bases in either configuration.
|
|
39
39
|
//
|
|
40
40
|
// PREMISE. It would be refusing a dropped qualifier, and there is none to drop: `/volatile`
|
|
41
|
-
// wraps the base in a CAST that `non-leaf-base` refuses, so the two
|
|
41
|
+
// wraps the base in a CAST that `non-leaf-base` refuses, so the two variations cannot compose and
|
|
42
42
|
// the tree this pass is handed is unqualified in both configurations. Nor is a tie-break lost —
|
|
43
43
|
// `deviceVolatileClaims` counts only qualifiers a tree already asserts, so an unqualified tree
|
|
44
44
|
// scores zero whichever way it is spelled.
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
// "PROTECTS NO ROW" IS A CENSUS AND NOT A SAMPLE, and the population is small because this is an
|
|
97
97
|
// extra GATE: it only ever removes candidates, so only a row whose winner carries `/offmember` can
|
|
98
98
|
// move at all. THAT POPULATION IS A QUERY, not a count to keep in step by hand — the rows of
|
|
99
|
-
// `apps/benchmark/results/results.json` whose `asmlift.
|
|
99
|
+
// `apps/benchmark/results/results.json` whose `asmlift.winnerVariations` contains `offmember` — and
|
|
100
100
|
// it grows with the corpus, so re-run it before repeating the result. It held eight rows besides
|
|
101
101
|
// `ProcessInputAndUpdateEntities` when the census ran, all unmoved with the rule on:
|
|
102
102
|
// `synthetic:basecell`, `synthetic:bgfixed`, `synthetic:foldsink`, `sa3:sub_802DFC8` and
|
|
@@ -222,7 +222,7 @@ function membersOf(sites: readonly Site[]): Site[] {
|
|
|
222
222
|
* THE FOURTH IS NOT ABOUT PLACEMENT, and it is the easy one to leave out: two views of one
|
|
223
223
|
* offset at one width but
|
|
224
224
|
* DIFFERENT SIGNEDNESS (`ldrb` and `ldrsb` at the same address). One member has one type, so
|
|
225
|
-
*
|
|
225
|
+
* spelling both through it changes what one of the two READS — `scalarTypeForAccess` honours
|
|
226
226
|
* signedness at widths 1 and 2, so an unsigned read becomes sign-extending. That is a value
|
|
227
227
|
* change rather than a spelling change, and the differ can referee it only by luck: a masked or
|
|
228
228
|
* compared result compiles to the same bytes while the published C says something the asm does
|
|
@@ -328,7 +328,7 @@ function admit(
|
|
|
328
328
|
|
|
329
329
|
/** The gate table an ablation swaps out. Optional, so a caller gets the shipped table by default.
|
|
330
330
|
* The pass needs nothing else from the target: the fold this exists for is `foldsConstAddrOffset`
|
|
331
|
-
* and rank.ts asks that before offering the
|
|
331
|
+
* and rank.ts asks that before offering the variation at all. */
|
|
332
332
|
export interface OffmemberOpts {
|
|
333
333
|
readonly gates?: readonly Gate<OffmemberBase>[];
|
|
334
334
|
}
|
|
@@ -339,7 +339,7 @@ export function offmemberBases(sfn: SFn, opts: OffmemberOpts = {}): readonly str
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
/** Re-spell every admitted base's constant subscripts as members of a synthesized struct.
|
|
342
|
-
* `null` when nothing is admitted — the
|
|
342
|
+
* `null` when nothing is admitted — the variation then contributes no candidate. */
|
|
343
343
|
export function spellOperandMembers(sfn: SFn, opts: OffmemberOpts = {}): SFn | null {
|
|
344
344
|
// Past EVERY `Off<N>` the tree already carries, never the first free one (`ir/struct-names.ts`,
|
|
345
345
|
// shared with the other two minters, which also records that this scan returns 0 on every
|
package/src/l3/parkfirst.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: park incoming ARGUMENTS first in the entry straight-line prefix.
|
|
2
2
|
//
|
|
3
3
|
// A copy of an incoming parameter into a local (`v = a1`) reproduces the register park the
|
|
4
4
|
// compiler performed to free a caller-save register (`mov ip, r1`). The park instruction lifts
|
|
5
5
|
// to pure SSA aliasing — no op, no position — so the emitted order falls out of block emission
|
|
6
6
|
// (materialized statements first, edge copies last), while the compiler may have parked BEFORE
|
|
7
7
|
// any of those statements ran (hipress homes its counter in ip before loading a byte into the
|
|
8
|
-
// vacated r1). Both orders are legitimate C for the same asm; this
|
|
8
|
+
// vacated r1). Both orders are legitimate C for the same asm; this variation emits the park-first
|
|
9
9
|
// sibling and the differ referees.
|
|
10
10
|
//
|
|
11
11
|
// SCOPE (decline over approximate): only plain assigns in the ENTRY straight-line prefix (the
|
|
12
12
|
// leading run of assigns) move; a park's RHS must be pure over PARAMETERS AND CONSTANTS through
|
|
13
13
|
// scalar nodes only (var/const/un/bin/cast — a memory read or a call would be re-scheduled, not
|
|
14
14
|
// re-spelled), so a constant initializer qualifies as a park and a param leaf is NOT required —
|
|
15
|
-
// what this
|
|
15
|
+
// what this variation moves is the leading run's ORDER, and a constant is as free of position as a
|
|
16
16
|
// parked register is; and a park never crosses a statement that writes a name it reads, reads or
|
|
17
17
|
// writes its destination (`&v` counts as touching v), or carries an effect. Relative order — of the
|
|
18
18
|
// parks and of everything else — is preserved. Declines (null) when nothing moves.
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
// spells a store to a bare scalar GLOBAL as an `assign` like any other, so such a store parks too.
|
|
23
23
|
// The crossing checks are NAME-KEYED, so a crossed statement that reaches the destination (or a
|
|
24
24
|
// name the park reads) through an ALIAS rather than by name is invisible to them — the same
|
|
25
|
-
// name-keyed model every
|
|
25
|
+
// name-keyed model every respell variation at this level defers aliasing to.
|
|
26
26
|
//
|
|
27
|
-
// The kmc hipress residual is this
|
|
27
|
+
// The kmc hipress residual is this variation's OTHER projection — its keep-load renders first while
|
|
28
28
|
// gcc2.7.2 schedules it last — so a second inhabitant consolidates both into one entry-prefix
|
|
29
|
-
// ordering
|
|
29
|
+
// ordering variation rather than growing a sibling.
|
|
30
30
|
import type { Expr, SFn, Stmt } from './ast';
|
|
31
31
|
import { exprChildren, exprHasEffect } from './ast';
|
|
32
32
|
|
package/src/l3/pollguard.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3 poll-shape
|
|
1
|
+
// L3 poll-shape respell variations: `pollGuards` regrows an empty bottom-tested loop's guard;
|
|
2
2
|
// `pollReads` folds a materialized poll's re-read back into its while condition. Each carries
|
|
3
3
|
// its own trace argument below.
|
|
4
4
|
//
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// For an empty body the two forms compile to the SAME instructions — gcc collapses the guard
|
|
8
8
|
// into the bottom test late (jump optimization), AFTER flow has counted the guard's reads — so
|
|
9
|
-
// the
|
|
9
|
+
// the decision leaves no instruction trace, only a register-allocation ripple: the extra
|
|
10
10
|
// source-level read raises the condition operands' ref counts, which re-orders the allocator's
|
|
11
11
|
// priorities for the WHOLE function (the busy-wait's base landing in a low reg vs `ip`). Which
|
|
12
12
|
// form the source spelled is unrecoverable from the bytes; both are emitted and the differ
|
|
@@ -34,7 +34,7 @@ export function pollGuards(sfn: SFn): SFn | null {
|
|
|
34
34
|
return changed ? { ...sfn, body } : null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// L3
|
|
37
|
+
// L3 respell variation: a materialized POLL re-reads in its own condition.
|
|
38
38
|
//
|
|
39
39
|
// v = dma[2]; while ((v & BUSY) != 0) { v = dma[2]; } → while ((dma[2] & BUSY) != 0) {}
|
|
40
40
|
//
|
package/src/l3/ptrfield.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3
|
|
1
|
+
// L3 respell variation: declare a recovered WORD field a POINTER (`void *field_4;` rather than
|
|
2
2
|
// `s32 field_4;`), and cast at each read.
|
|
3
3
|
//
|
|
4
4
|
// raise/structs.ts recovers a field's type from the ACCESS WIDTH alone — a 4-byte load is `s32`,
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
// each use computes the same value it did. Nothing else moves. THE 32-BIT ASSUMPTION IS ASSERTED,
|
|
35
35
|
// NOT CHECKED — there is no pointer-width field on TargetDescription to check it against, and the
|
|
36
36
|
// assumption is already tower-wide (`l3/typing.ts`'s `ptrElemBytes` returns 4 for any pointee). On
|
|
37
|
-
// a 64-bit target this
|
|
37
|
+
// a 64-bit target this variation would change a struct's LAYOUT rather than only its spelling, so the
|
|
38
38
|
// width field is what that target's first row must add, and this note is where to start.
|
|
39
39
|
//
|
|
40
40
|
// IT FLIPS EVERY ADMITTED FIELD AT ONCE, and its own paragraph above says the knowledge is
|
|
41
41
|
// PER-FIELD — so the subset enumeration `l3/volatileptr.ts` does for exactly this reason
|
|
42
|
-
// (`volatileSubsetCandidates`, capped at three locals) is the shape this
|
|
43
|
-
// want. It is not built yet because nothing demands it: swept over 834 corpus trees, the
|
|
42
|
+
// (`volatileSubsetCandidates`, capped at three locals) is the shape this variation will eventually
|
|
43
|
+
// want. It is not built yet because nothing demands it: swept over 834 corpus trees, the variation
|
|
44
44
|
// fires on 42, and 34 of those have a single admitted field. The six 2-field trees and the two
|
|
45
45
|
// 4-field ones (`sa3:sa2__sub_8083504` flips Struct0.field_8/12 and Struct2.field_8/12 together)
|
|
46
46
|
// are where 1 of 3 and 1 of 15 non-empty subsets is reachable. A row that needs one of the missing
|
package/src/l3/regspell.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// asmlift L3 — the REGISTER-COPY
|
|
1
|
+
// asmlift L3 — the REGISTER-COPY respell variation, differ-ranked like the three before it (the fourth,
|
|
2
2
|
// after signedness / branch sense / walk-vs-index).
|
|
3
3
|
//
|
|
4
4
|
// A compiler's register allocation leaves SOURCE-visible footprints the coalescing structurer
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// a bin operand materializes into its own fresh local first (`v = 128 << 9; … v / x …`) —
|
|
27
27
|
// the register the compiler staged the constant in.
|
|
28
28
|
// R3 — return assign-back: a non-var return expression lands in a fresh local first
|
|
29
|
-
// (`r = E; return r`). Emitted as a SEPARATE
|
|
29
|
+
// (`r = E; return r`). Emitted as a SEPARATE result (with/without R3) when R1/R2 fired:
|
|
30
30
|
// which tail the source spelled is itself ambiguous.
|
|
31
31
|
import { IrType, T } from '../ir/types';
|
|
32
32
|
import { Expr, SFn, Stmt, exprEquals, mapExprChildren } from './ast';
|
|
@@ -124,8 +124,8 @@ function isConstExpr(e: Expr): boolean {
|
|
|
124
124
|
* var and only the fresh spelling exists — so the tails are a 1- OR 2-element list and the SECOND
|
|
125
125
|
* spelling is not at a fixed index.
|
|
126
126
|
*
|
|
127
|
-
* This field exists because the caller
|
|
128
|
-
* in this repo reads (
|
|
127
|
+
* This field exists because the caller names these, and a candidate's variations are the instrument
|
|
128
|
+
* every census in this repo reads (a `bench diff` FIELD). Index a `['/regcopy', '/regcopy-ret',
|
|
129
129
|
* '/regcopy-ret-fresh']` table by POSITION instead and an R1-less function publishes its fresh
|
|
130
130
|
* tail as `/regcopy-ret` — the dead-var-reuse name on the spelling that has no dead var to reuse
|
|
131
131
|
* — and a census over the token `/regcopy-ret-fresh` then censuses nothing wherever R1 declines,
|
|
@@ -133,7 +133,7 @@ function isConstExpr(e: Expr): boolean {
|
|
|
133
133
|
export type RegcopyTail = 'none' | 'reuse' | 'fresh';
|
|
134
134
|
|
|
135
135
|
export interface RegcopySpelling {
|
|
136
|
-
/** which R3 tail this
|
|
136
|
+
/** which R3 tail this result carries — `none` is the un-tailed base */
|
|
137
137
|
tail: RegcopyTail;
|
|
138
138
|
sfn: SFn;
|
|
139
139
|
}
|
|
@@ -213,7 +213,7 @@ function renameSubexpr(e: Expr, E: Expr, name: string): Expr {
|
|
|
213
213
|
return mapExprChildren(e, (c) => renameSubexpr(c, E, name));
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
/** Apply the register-copy re-spelling. Returns 0–3
|
|
216
|
+
/** Apply the register-copy re-spelling. Returns 0–3 results — the base, plus the R3 tail in each
|
|
217
217
|
* spelling that exists (reuse needs R1 to have fired, fresh always does) — and an EMPTY list when
|
|
218
218
|
* nothing fired. Pure — never mutates the input. */
|
|
219
219
|
export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
|
|
@@ -247,7 +247,7 @@ export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
|
|
|
247
247
|
if (m) {
|
|
248
248
|
const { v, E, updArm, upd, cond } = m;
|
|
249
249
|
// GUARDS BEFORE ALLOCATION (a declined shape must leave no residue — the leaked
|
|
250
|
-
// dead `w` even perturbed the live-name count this
|
|
250
|
+
// dead `w` even perturbed the live-name count this variation exists to reproduce):
|
|
251
251
|
// • E and the cond pure; the cond's non-E operand must NOT mention v (a clamp's
|
|
252
252
|
// `if (a < v)` would compare against the POST-assignment v — reproduced);
|
|
253
253
|
// • the copy w carries E's RENDERED type, not v's declared one (retyping a u32
|
|
@@ -327,7 +327,7 @@ export function registerishSpellings(sfn: SFn): RegcopySpelling[] {
|
|
|
327
327
|
}
|
|
328
328
|
const base: SFn = { ...sfn, locals: [...locals], body: afterR2 };
|
|
329
329
|
|
|
330
|
-
// R3
|
|
330
|
+
// R3 results: the tail assign-back — a non-var return lands in a local first. WHICH local is
|
|
331
331
|
// itself allocator-ambiguous (gcc 2.9 wanted R1's dead value var — live-name-count sensitive;
|
|
332
332
|
// another allocator may want the fresh one), so BOTH tails are emitted as candidates rather
|
|
333
333
|
// than asserting one compiler's preference; the source dedupe collapses them when identical.
|