@asmlift/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +238 -167
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +194 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +54 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +337 -2
- package/src/ir/opcodes.ts +140 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/argbase.ts +2 -1
- package/src/l3/ast.ts +464 -57
- package/src/l3/basecse.ts +664 -76
- package/src/l3/coalesce.ts +429 -43
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +21 -0
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +113 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +110 -85
- package/src/l3/reindex.ts +715 -78
- package/src/l3/scopebase.ts +644 -218
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +15 -0
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +263 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +236 -13
- package/src/pipeline.ts +157 -56
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/globalshape.ts +1038 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +556 -0
- package/src/raise/paramwidth.ts +179 -0
- package/src/raise/pre-recovery.ts +97 -14
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +210 -10
- package/src/raise/shortcircuit.ts +474 -74
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +33 -3
- package/src/rank-axes.ts +630 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank.ts +1723 -272
- package/src/structure/analysis.ts +1392 -141
- package/src/structure/bitfields.ts +332 -0
- package/src/structure/globalaccess.ts +274 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +435 -0
- package/src/structure/structure.ts +2678 -526
- package/src/structure/switch-recover.ts +616 -144
- package/src/symbols.ts +62 -1
- package/src/target.ts +367 -24
- package/src/trace.ts +111 -32
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
// 1. SSA destruction WITH COALESCING — a merge block-argument that already carries a
|
|
5
5
|
// variable's value on one path is coalesced to that variable, so only the
|
|
6
6
|
// non-identity paths emit an assignment (reproducing agbcc's register allocation:
|
|
7
|
+
// the clamp0 diamond becomes `if (x < 0) x = 0; return x;` rather than a temp copy).
|
|
7
8
|
// NOTE the coupled INVERSE: l3/regspell.ts re-derives the UN-coalesced copy-carrying
|
|
8
9
|
// spelling as a ranked candidate — its R1 template matches THIS pass's diamond output
|
|
9
10
|
// shape, so a change to coalescing here can silently stop that lever firing (the
|
|
10
11
|
// matching-suite regspell gate is what makes the coupling loud).
|
|
11
|
-
// the clamp0 diamond becomes `if (x < 0) x = 0; return x;` rather than a temp copy).
|
|
12
12
|
// Coalescing is INTERFERENCE-CHECKED against per-block value liveness, and
|
|
13
13
|
// inline-at-use rendering carries an effect-ordering model: a call/load that cannot
|
|
14
14
|
// soundly render at its use is MATERIALIZED as a named temp at its own program
|
|
@@ -32,34 +32,63 @@
|
|
|
32
32
|
// Scope: reducible single-latch natural loops — GUARDED self-loop `while` (the guard-fusion
|
|
33
33
|
// un-rotation), UNGUARDED self-loop `do-while` (single block, header === latch), test-at-top
|
|
34
34
|
// `while`, bottom-test `do-while`, PROPERLY-nested loops, in-body `break`/early-`return`,
|
|
35
|
-
// comparison-tree and jump-table `switch
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
// comparison-tree and jump-table `switch`, and a switch arm that FALLS THROUGH into the next one
|
|
36
|
+
// (both regimes — see `ArmExit` in switch-recover.ts). Still DECLINED (loud StructureError, never
|
|
37
|
+
// wrong code): multi-latch headers, irreducible/overlapping loops, conditional `continue`, a
|
|
38
|
+
// `break` whose exit copies would clobber, and mixed-entry self-loops (a guarded header also
|
|
39
|
+
// entered by a plain br). Fall-through carries two REFUSALS of its own rather than a decline: a
|
|
40
|
+
// target language whose `case` cannot fall through (`spellSwitchFallthrough` false) sends Regime A
|
|
41
|
+
// back to if-recovery, and arms that do not linearize into one chain — two arms falling into the
|
|
42
|
+
// same sibling, or a fall into the `default:` — refuse in `chainArms`, which answers null.
|
|
43
|
+
import { Block, Fn, Op, Successor, Value, defOpMap, dominators, mergeClasses, successorsOf } from '../ir/core';
|
|
44
|
+
import { CAST_WIDTHS, EFFECTFUL_OPS } from '../ir/opcodes';
|
|
42
45
|
import { type IrType, T, scalarTypeForAccess, typeEquals } from '../ir/types';
|
|
43
|
-
import {
|
|
44
|
-
|
|
46
|
+
import {
|
|
47
|
+
BinOp,
|
|
48
|
+
Expr,
|
|
49
|
+
SFn,
|
|
50
|
+
Stmt,
|
|
51
|
+
SwitchCase,
|
|
52
|
+
exprChildren,
|
|
53
|
+
exprHasEffect,
|
|
54
|
+
gapReasonFor,
|
|
55
|
+
mapExprChildren,
|
|
56
|
+
mapStmtExprs,
|
|
57
|
+
negateCond,
|
|
58
|
+
stmtChildren,
|
|
59
|
+
walkExprs,
|
|
60
|
+
} from '../l3/ast';
|
|
61
|
+
import { type Gate, firstRejection } from '../l3/gates';
|
|
62
|
+
import { exprCType, provablyNonNegative, ptrElemBytes, renderedIntSignedness } from '../l3/typing';
|
|
45
63
|
import { returnType } from '../raise/recover';
|
|
46
64
|
import { collectStructs } from '../raise/structs';
|
|
47
65
|
import {
|
|
48
66
|
type DeclaredField,
|
|
49
67
|
type SymbolInfo,
|
|
50
68
|
type SymbolStructField,
|
|
51
|
-
arrayInnerExtents,
|
|
52
69
|
declaredFields,
|
|
53
70
|
isArrayField,
|
|
54
71
|
isBitfieldField,
|
|
72
|
+
isPtrField,
|
|
55
73
|
isScalarCellSize,
|
|
56
74
|
pointeeFields,
|
|
57
75
|
scalarCellType,
|
|
58
76
|
} from '../symbols';
|
|
59
77
|
import { analyze } from './analysis';
|
|
60
|
-
import {
|
|
61
|
-
import {
|
|
62
|
-
|
|
78
|
+
import { makeBitfieldSpelling } from './bitfields';
|
|
79
|
+
import {
|
|
80
|
+
addOffset,
|
|
81
|
+
addrIn,
|
|
82
|
+
bareArrayLead,
|
|
83
|
+
declaredSubscripts,
|
|
84
|
+
elementIndex,
|
|
85
|
+
globalByteBase,
|
|
86
|
+
globalOf,
|
|
87
|
+
} from './globalaccess';
|
|
88
|
+
import { makeLoopHazards, sunkCopyOverDroppedUndef, updateWriteSet } from './hazards';
|
|
89
|
+
import { type NaturalLoop, analyzeLoops } from './loops';
|
|
90
|
+
import { type NameMerge, coalesceNames } from './namecoalesce';
|
|
91
|
+
import { type ArmExit, makeSwitchRecovery } from './switch-recover';
|
|
63
92
|
|
|
64
93
|
// Lower a constant-offset memory access to its lvalue/rvalue Expr. If the base was recovered as a
|
|
65
94
|
// struct pointer (raise/structs.ts), the byte offset resolves to a NAMED field (`base->field_<off>`);
|
|
@@ -73,101 +102,14 @@ import { makeSwitchRecovery } from './switch-recover';
|
|
|
73
102
|
// TODAY — carrying the struct name (resolved against SFn.structs) is the same move as width and
|
|
74
103
|
// the named follow-up; until then no backend pays a tax for the tree cast (Pascal loud-fails
|
|
75
104
|
// `field` regardless, C++ falls through its leaf hook to the shared C spelling).
|
|
76
|
-
// `&gSym`, possibly wearing the value-context integer cast the additive lowering adds
|
|
77
|
-
// (`(u32)&gSym` — see lowerDef's addr-intify): both spell the same link-time constant, so the
|
|
78
|
-
// fold rules match through the cast and every access that CAN spell a named element still does.
|
|
79
|
-
// WIDTH 32 ONLY — a NARROWING cast (`(u8)&gSym`, from a zext/sext lowering) is a different
|
|
80
|
-
// VALUE (`addr & 0xFF`), and folding through it would read the named global at a wrong address
|
|
81
|
-
// (the adversarial round's probe: `*(u8*)(u8)&gSym` must keep its truncation, never become
|
|
82
|
-
// `*(u8*)&gSym` — let alone a confidently-named `gSym.field`).
|
|
83
|
-
function addrIn(e: Expr): Extract<Expr, { k: 'addr' }> | null {
|
|
84
|
-
if (e.k === 'addr') {
|
|
85
|
-
return e;
|
|
86
|
-
}
|
|
87
|
-
if (e.k === 'cast' && e.to.kind === 'int' && e.to.width === 32 && e.e.k === 'addr') {
|
|
88
|
-
return e.e;
|
|
89
|
-
}
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return { name: top.name, idx: { k: 'const', value: 0 } };
|
|
101
|
-
}
|
|
102
|
-
if (e.k === 'bin' && e.op === '+') {
|
|
103
|
-
for (const [side, other] of [
|
|
104
|
-
[e.l, e.r],
|
|
105
|
-
[e.r, e.l],
|
|
106
|
-
] as const) {
|
|
107
|
-
const addrSide = addrIn(side);
|
|
108
|
-
if (addrSide) {
|
|
109
|
-
const idx = elementIndex(other, width);
|
|
110
|
-
return idx ? { name: addrSide.name, idx } : null;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// THE one gate on the BARE-NAME array-global spelling (`gSym[i]` rather than `((T *)&gSym)[i]`),
|
|
118
|
-
// shared by the constant-offset and variable-index access paths so the two cannot disagree.
|
|
119
|
-
// Returns the `index` node's `lead` fragment when the bare form is spellable, or null to fall
|
|
120
|
-
// through to the always-valid `&gSym` cast form.
|
|
121
|
-
//
|
|
122
|
-
// Two facts are required, not one. The element WIDTH must match, as it always has. And the RANK
|
|
123
|
-
// must be SPELLABLE, because one subscript reaches an element only on a rank-1 array: on `u16
|
|
124
|
-
// g[4][0x400]`, `g[i]` is a ROW. Against the project's own header that is usually a type error,
|
|
125
|
-
// but where the row address flows into an integer context it is merely a warning and the emitted C
|
|
126
|
-
// then addresses a different object than the asm did — silently.
|
|
127
|
-
//
|
|
128
|
-
// A rank > 1 pins the leading dimensions at 0 and puts the whole flat element index in the last
|
|
129
|
-
// subscript (`g[0][i]`) — the same address arithmetic, and the idiom decomp sources themselves use
|
|
130
|
-
// when the split is not observable in the asm either (`gBgTilemapBufs[0][…]` in kleod,
|
|
131
|
-
// `gNatureStatTable[nature][…]` in pokeemerald). A rank the map states but cannot spell (an unknown
|
|
132
|
-
// inner extent) gets no bare form at all; `((T *)&gSym)[i]` is byte-identical and valid under ANY
|
|
133
|
-
// declaration, which is why it is the safe fallback. See symbols.ts arrayInnerExtents for why an
|
|
134
|
-
// ABSENT rank is read as 1 rather than as unknown.
|
|
135
|
-
function bareArrayLead(si: SymbolInfo, width: number): { lead?: number[] } | null {
|
|
136
|
-
if (si.shape !== 'array' || si.elemSize !== width) {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
const inner = arrayInnerExtents(si);
|
|
140
|
-
return inner === null ? null : inner.length === 0 ? {} : { lead: new Array<number>(inner.length).fill(0) };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// A BYTE residual read as an ELEMENT index of `elemSize`-wide elements, or null when it is not one
|
|
144
|
-
// — the residual then addresses mid-element and no whole-element spelling can express it, so the
|
|
145
|
-
// caller falls through to the honest cast forms. THE one copy of the rule, indexing the
|
|
146
|
-
// `&gSym`-based array spelling: width 1 → the byte residual IS the index; wider → a constant
|
|
147
|
-
// residual must divide exactly, and a non-constant one must already be element-scaled
|
|
148
|
-
// (`i * elemSize` / `i << log2(elemSize)`), which is exactly what the asm's own index scaling
|
|
149
|
-
// produced.
|
|
150
|
-
function elementIndex(residual: Expr, elemSize: number): Expr | null {
|
|
151
|
-
if (elemSize === 1) {
|
|
152
|
-
return residual;
|
|
153
|
-
}
|
|
154
|
-
if (residual.k === 'const') {
|
|
155
|
-
return residual.value % elemSize === 0 ? { k: 'const', value: residual.value / elemSize } : null;
|
|
156
|
-
}
|
|
157
|
-
if (residual.k === 'bin' && (residual.op === '*' || residual.op === '<<')) {
|
|
158
|
-
const factor =
|
|
159
|
-
residual.op === '<<'
|
|
160
|
-
? residual.r.k === 'const'
|
|
161
|
-
? 1 << residual.r.value
|
|
162
|
-
: 0
|
|
163
|
-
: residual.r.k === 'const'
|
|
164
|
-
? residual.r.value
|
|
165
|
-
: 0;
|
|
166
|
-
if (factor === elemSize) {
|
|
167
|
-
return residual.l;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
return null;
|
|
106
|
+
/** "What does this project know about the global named `n`" — the union of the project's own
|
|
107
|
+
* symbol map and the array shapes derived from this function's assembly, asked map-first. Only
|
|
108
|
+
* ever probed by name (never iterated or copied), which is what lets the union be a lookup rather
|
|
109
|
+
* than a merged Map. */
|
|
110
|
+
interface SymbolLookup {
|
|
111
|
+
get(name: string): SymbolInfo | undefined;
|
|
112
|
+
has(name: string): boolean;
|
|
171
113
|
}
|
|
172
114
|
|
|
173
115
|
/** The symbol-map rendering context threaded into memAccess/arrayAccess: shape facts per
|
|
@@ -176,6 +118,19 @@ function elementIndex(residual: Expr, elemSize: number): Expr | null {
|
|
|
176
118
|
interface SymRenderCtx {
|
|
177
119
|
info(name: string): SymbolInfo | undefined;
|
|
178
120
|
noteGlobal(name: string, type: IrType): void;
|
|
121
|
+
/** may {@link ptrMemberElement} spell a whole-element subscript through a pointer member — the
|
|
122
|
+
* `/no-ptr-elem` arm's OFF switch, off the `spellPtrMemberElements` structure option. */
|
|
123
|
+
ptrElements: boolean;
|
|
124
|
+
/** may {@link declaredSubscripts} recover a multidimensional global's declared subscripts out of
|
|
125
|
+
* the byte residual — the `/flat-rank` arm's OFF switch, off the `spellDeclaredSubscripts`
|
|
126
|
+
* structure option. */
|
|
127
|
+
declRank: boolean;
|
|
128
|
+
/** The members a symbol's declaration seats — a struct global's own, or a pointer global's
|
|
129
|
+
* pointee's — MEMOIZED per symbol. `declaredFields` validates every member and returns a fresh
|
|
130
|
+
* sorted copy on every call, and `isPtrValue` asks it for both operands of every binary node
|
|
131
|
+
* lowered, so an uncached lookup is an O(n log n) allocation on a hot path — inside a
|
|
132
|
+
* `structure()` a ranked run repeats once per candidate, 17,856 times on the largest fan. */
|
|
133
|
+
fieldsOf(name: string): DeclaredField[] | null;
|
|
179
134
|
}
|
|
180
135
|
|
|
181
136
|
// ── interior spelling through a POINTER-shaped global ────────────────────────────────────────
|
|
@@ -280,6 +235,161 @@ function memberQualsAllow(f: SymbolStructField, containerConst: boolean | undefi
|
|
|
280
235
|
return !(isStore && (f.const || containerConst));
|
|
281
236
|
}
|
|
282
237
|
|
|
238
|
+
/** The map member a `field` node NAMES, or null when it names none — THE one resolver for "what
|
|
239
|
+
* does the declaration say about this member", for rules that must reason about a member's type
|
|
240
|
+
* after the access rules have already spelled it. Both named spellings resolve, through the same
|
|
241
|
+
* shared gate their spelling passed: `gSym.member` off a struct global's {@link declaredFields},
|
|
242
|
+
* `gPtr->member` off the pointee's ({@link pointeeFields}). A synthesized `field_K` — the
|
|
243
|
+
* recovered-struct spelling, which no map declares — resolves to null, and so does any base that
|
|
244
|
+
* is not a map-shaped global, which is what makes every caller refuse rather than guess.
|
|
245
|
+
*
|
|
246
|
+
* The pointee arm reaches only what the MAP lets it: `pointeeAccess` gates every `gPtr->member`
|
|
247
|
+
* spelling on `spellsAccessType(f.signed, …)`, so a pointer field declaring no signedness — which
|
|
248
|
+
* is every one in the corpus's vendored maps — is never named, and a pointer read one indirection
|
|
249
|
+
* down spells `((s32 *)gQ)[1]`. That is a fact about those maps, not about this code: `SymbolMap`
|
|
250
|
+
* is a caller-supplied input, and one field flips it (`signed: true` on a 4-byte pointer member
|
|
251
|
+
* of a pointee yields `(u8 *)gQ->pInner`, cast and all, pinned in pointer-members.test.ts). So
|
|
252
|
+
* the arm is live and tested, and the byte-arithmetic rule that reads this answer is correct for
|
|
253
|
+
* it — where resolving a pointee member to null would reopen the double-scaling hole silently. */
|
|
254
|
+
function declaredMemberOf(x: Expr, sym: SymRenderCtx | undefined): DeclaredField | null {
|
|
255
|
+
if (x.k !== 'field' || x.base.k !== 'var' || sym === undefined) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
return sym.fieldsOf(x.base.name)?.find((f) => f.name === x.name) ?? null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The map member a `field` node names when the declaration makes it a POINTER — {@link
|
|
262
|
+
* isPtrField} being the shared two-fact test, so this and the synthesized declaration cannot
|
|
263
|
+
* disagree about what a member is. */
|
|
264
|
+
function ptrMemberDecl(x: Expr, sym: SymRenderCtx | undefined): DeclaredField | null {
|
|
265
|
+
const f = declaredMemberOf(x, sym);
|
|
266
|
+
return f !== null && isPtrField(f) ? f : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** A map-declared POINTER MEMBER as the additive lowering renders its VALUE: the bare `gSym.pBuf`,
|
|
270
|
+
* or that member wearing the byte-pointer / u32 cast the arithmetic guard adds. Both denote the
|
|
271
|
+
* same address and add BYTES to it, so both fold here; a cast to any OTHER pointer type is not
|
|
272
|
+
* looked through — it would re-scale everything added after it. */
|
|
273
|
+
function ptrMemberValue(x: Expr, sym: SymRenderCtx): { expr: Expr; field: DeclaredField } | null {
|
|
274
|
+
let bare = x;
|
|
275
|
+
if (x.k === 'cast') {
|
|
276
|
+
const t = x.to;
|
|
277
|
+
const isBytePtr = t.kind === 'ptr' && t.to.kind === 'int' && t.to.width === 8;
|
|
278
|
+
if (!isBytePtr && !(t.kind === 'int' && t.width === 32)) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
bare = x.e;
|
|
282
|
+
}
|
|
283
|
+
const f = ptrMemberDecl(bare, sym);
|
|
284
|
+
return f !== null ? { expr: bare, field: f } : null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Decompose an access base into "the VALUE of a map-declared POINTER MEMBER + a constant byte
|
|
288
|
+
* offset + at most ONE variable term" — the same decomposition {@link ptrGlobalBase} makes one
|
|
289
|
+
* indirection up, declining on the same conditions (two variable terms, no such member, a
|
|
290
|
+
* non-`+` operator), because only a single residual can be read as one element index. */
|
|
291
|
+
interface PtrMemberBase {
|
|
292
|
+
value: Expr;
|
|
293
|
+
field: DeclaredField;
|
|
294
|
+
byte: number;
|
|
295
|
+
idx: Expr | null;
|
|
296
|
+
}
|
|
297
|
+
function ptrMemberBase(e: Expr, sym: SymRenderCtx): PtrMemberBase | null {
|
|
298
|
+
const hits: { expr: Expr; field: DeclaredField }[] = [];
|
|
299
|
+
let byte = 0;
|
|
300
|
+
let idx: Expr | null = null;
|
|
301
|
+
let ok = true;
|
|
302
|
+
const visit = (x: Expr): void => {
|
|
303
|
+
if (!ok) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (x.k === 'bin' && x.op === '+') {
|
|
307
|
+
visit(x.l);
|
|
308
|
+
visit(x.r);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const m = hits.length === 0 ? ptrMemberValue(x, sym) : null;
|
|
312
|
+
if (m !== null) {
|
|
313
|
+
hits.push(m);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (x.k === 'const') {
|
|
317
|
+
byte += x.value;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (idx !== null) {
|
|
321
|
+
ok = false;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
idx = x;
|
|
325
|
+
};
|
|
326
|
+
visit(e);
|
|
327
|
+
return ok && hits.length === 1 ? { value: hits[0].expr, field: hits[0].field, byte, idx } : null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** `gSym.pBuf[i]` for an access through a POINTER MEMBER's VALUE — the spelling the project's own
|
|
331
|
+
* header makes available, where the byte arithmetic it replaces is the same address written the
|
|
332
|
+
* machine's way. The DECLARATION is what licenses it: `pointeeSize` says how wide an element is,
|
|
333
|
+
* so an access of exactly that width at a whole multiple of it IS the i-th element, and the
|
|
334
|
+
* reinterpret cast the backend adds strides by the same amount under any header.
|
|
335
|
+
*
|
|
336
|
+
* `carried` is an ELEMENT index the access already holds (an `aload`'s own index); everything
|
|
337
|
+
* else — a byte residual inside the base, the instruction's displacement `off` — is converted and
|
|
338
|
+
* summed into the one subscript, which is where the source put it.
|
|
339
|
+
*
|
|
340
|
+
* REFUSALS, and they are of TWO kinds.
|
|
341
|
+
*
|
|
342
|
+
* ADDRESS refusals — no whole-element spelling expresses the address at all: a variable residual
|
|
343
|
+
* that is not element-scaled, and a constant the element width does not divide. Both address
|
|
344
|
+
* MID-ELEMENT, so they fall through to the honest byte forms and must.
|
|
345
|
+
*
|
|
346
|
+
* REACH refusals — the address IS expressible and this rule declines anyway: an access of a
|
|
347
|
+
* DIFFERENT width than the declared element, and a sub-word access whose signedness the declared
|
|
348
|
+
* pointee does not carry. Neither is soundness. `exprCType` types a `field` node `undefined` (it
|
|
349
|
+
* types params and locals), so `derefStrideOk` is false for every base this rule produces and the
|
|
350
|
+
* backend ALWAYS emits the reinterpret cast — `((u16 *)gB.pMap)[i + 157]`, cast included, is what
|
|
351
|
+
* the accepted case spells. `((s32 *)gB.pMap)[i + K]` would be the same address and the same fill
|
|
352
|
+
* as the byte form those two clauses fall back to, and the byte form the width clause produces
|
|
353
|
+
* already spells `((s32 *)…)[157]` itself. What these two clauses encode is a REACH judgement — the
|
|
354
|
+
* map says this pointer addresses an array of THESE, so the source probably wrote a subscript of
|
|
355
|
+
* them — and a declaration-shaped access is where that judgement is most likely right. On a
|
|
356
|
+
* STORE the signedness clause has no premise at all (a store extends nothing), so it withholds
|
|
357
|
+
* the spelling from every `s8 *` / `s16 *` member store; that is left in place deliberately
|
|
358
|
+
* rather than widened, because this rule is not byte-neutral (see `spellPtrMemberElements`) and
|
|
359
|
+
* widening a non-neutral spelling's reach is a separate question a row has to ask.
|
|
360
|
+
*
|
|
361
|
+
* And the whole rule refuses when `/no-ptr-elem` turns it off — the axis, not a preference. */
|
|
362
|
+
function ptrMemberElement(
|
|
363
|
+
baseExpr: Expr,
|
|
364
|
+
carried: Expr | null,
|
|
365
|
+
off: number,
|
|
366
|
+
width: number,
|
|
367
|
+
signed: boolean,
|
|
368
|
+
sym: SymRenderCtx,
|
|
369
|
+
): Extract<Expr, { k: 'index' }> | null {
|
|
370
|
+
if (!sym.ptrElements) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const pm = ptrMemberBase(baseExpr, sym);
|
|
374
|
+
if (!pm || pm.field.pointeeSize !== width || (width < 4 && (pm.field.pointeeSigned ?? false) !== signed)) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const varIdx = pm.idx === null ? null : elementIndex(pm.idx, width); // THE one copy of that rule
|
|
378
|
+
const total = pm.byte + off;
|
|
379
|
+
if ((pm.idx !== null && varIdx === null) || total % width !== 0) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
// Variable terms first, constant last — the `idxVal + off / width` order every other indexed
|
|
383
|
+
// spelling in this file uses, and the one the source's own subscript is written in.
|
|
384
|
+
const terms = [varIdx, carried].filter((t): t is Expr => t !== null);
|
|
385
|
+
const k = total / width;
|
|
386
|
+
if (k !== 0 || terms.length === 0) {
|
|
387
|
+
terms.push({ k: 'const', value: k });
|
|
388
|
+
}
|
|
389
|
+
const idx = terms.reduce((l, r) => ({ k: 'bin', op: '+', l, r }));
|
|
390
|
+
return { k: 'index', base: pm.value, idx, width, signed };
|
|
391
|
+
}
|
|
392
|
+
|
|
283
393
|
// WHY THERE IS NO INDEXED `gPtr->arr[i]` SPELLING.
|
|
284
394
|
//
|
|
285
395
|
// Naming a member is only allowed where it is byte-identical to the cast form it replaces, and
|
|
@@ -389,6 +499,12 @@ function memAccess(
|
|
|
389
499
|
// named field (`gSym.field` — the source spelling a folded literal can never match); an ARRAY
|
|
390
500
|
// global indexes its BARE name (`gSym[i]`, see below). Exact field match only (offset AND
|
|
391
501
|
// width) — anything else falls through to the honest cast forms, never a guessed field.
|
|
502
|
+
// The constant offset this access reached through the instruction's MEMORY OPERAND. The two
|
|
503
|
+
// facts are separate at L2 — `off` is the load/store's own immediate, any addend the address
|
|
504
|
+
// carried is already inside `baseExpr` — and folding them into one subscript below
|
|
505
|
+
// (`idxVal + off / width`) is what makes the two indistinguishable at L3, so the displacement
|
|
506
|
+
// is recorded before the fold destroys it (see the `operandOff` note in l3/ast.ts).
|
|
507
|
+
const fromOperand = off !== 0 ? ({ operandOff: off } as const) : {};
|
|
392
508
|
if (sym) {
|
|
393
509
|
const gb = globalConstByte(baseExpr, off);
|
|
394
510
|
const si = gb ? sym.info(gb.name) : undefined;
|
|
@@ -417,6 +533,31 @@ function memAccess(
|
|
|
417
533
|
return spelled;
|
|
418
534
|
}
|
|
419
535
|
}
|
|
536
|
+
// …and one step further out: an access through a POINTER MEMBER's value is an ELEMENT of the
|
|
537
|
+
// buffer that member points AT (see ptrMemberElement).
|
|
538
|
+
const elem = ptrMemberElement(baseExpr, null, off, width, signed, sym);
|
|
539
|
+
if (elem) {
|
|
540
|
+
return { ...elem, ...fromOperand };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// …and the MULTIDIMENSIONAL bare-name spelling, which needs the byte terms globalOf's division
|
|
544
|
+
// into elements has already merged: `g[r][i]`, where a term at the declared ROW stride is `r`.
|
|
545
|
+
// Tried before the flat spellings because those cannot express a recovered row at all — and off
|
|
546
|
+
// under `/flat-rank`, which is what puts those flat spellings back in the fan for the differ.
|
|
547
|
+
const gbb = sym?.declRank ? globalByteBase(baseExpr) : null;
|
|
548
|
+
const siMulti = gbb ? sym!.info(gbb.name) : undefined;
|
|
549
|
+
const multi = siMulti ? declaredSubscripts(siMulti, gbb!.residual, width, signed) : null;
|
|
550
|
+
if (multi) {
|
|
551
|
+
sym!.noteGlobal(gbb!.name, T.ptr(T.int(width * 8, siMulti!.elemSigned ?? false)));
|
|
552
|
+
return {
|
|
553
|
+
k: 'index',
|
|
554
|
+
base: { k: 'var', name: gbb!.name },
|
|
555
|
+
idx: addOffset(multi.idx, off / width),
|
|
556
|
+
width,
|
|
557
|
+
signed,
|
|
558
|
+
lead: multi.lead,
|
|
559
|
+
...fromOperand,
|
|
560
|
+
};
|
|
420
561
|
}
|
|
421
562
|
const g = globalOf(baseExpr, width);
|
|
422
563
|
if (g) {
|
|
@@ -430,22 +571,17 @@ function memAccess(
|
|
|
430
571
|
if (off === 0 && idxVal.k === 'const' && idxVal.value === 0 && scalarGlobals.has(g.name)) {
|
|
431
572
|
return { k: 'var', name: g.name };
|
|
432
573
|
}
|
|
433
|
-
const idx
|
|
434
|
-
off === 0
|
|
435
|
-
? idxVal
|
|
436
|
-
: idxVal.k === 'const'
|
|
437
|
-
? { k: 'const', value: idxVal.value + off / width }
|
|
438
|
-
: { k: 'bin', op: '+', l: idxVal, r: { k: 'const', value: off / width } };
|
|
574
|
+
const idx = addOffset(idxVal, off / width);
|
|
439
575
|
// ARRAY-declared global (symbol map): index the bare name — `gSym[i]`, the spelling the
|
|
440
576
|
// dogfood proved agbcc needs for ROM tables — with the element type registered in the env
|
|
441
577
|
// so the stride check passes and no cast is added. Element-width match only.
|
|
442
578
|
const siArr = sym?.info(g.name);
|
|
443
|
-
const lead = siArr === undefined ? null : bareArrayLead(siArr, width);
|
|
579
|
+
const lead = siArr === undefined ? null : bareArrayLead(siArr, width, signed);
|
|
444
580
|
if (lead !== null) {
|
|
445
581
|
sym!.noteGlobal(g.name, T.ptr(T.int(width * 8, siArr!.elemSigned ?? false)));
|
|
446
|
-
return { k: 'index', base: { k: 'var', name: g.name }, idx, width, signed, ...lead };
|
|
582
|
+
return { k: 'index', base: { k: 'var', name: g.name }, idx, width, signed, ...lead, ...fromOperand };
|
|
447
583
|
}
|
|
448
|
-
return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed };
|
|
584
|
+
return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed, ...fromOperand };
|
|
449
585
|
}
|
|
450
586
|
const bt = base.type;
|
|
451
587
|
if (bt.kind === 'ptr' && bt.to.kind === 'struct') {
|
|
@@ -457,12 +593,14 @@ function memAccess(
|
|
|
457
593
|
const ok = rt?.kind === 'ptr' && rt.to.kind === 'struct' && rt.to.name === bt.to.name && baseExpr.k !== 'index';
|
|
458
594
|
return { k: 'field', base: ok ? baseExpr : { k: 'cast', to: bt, e: baseExpr }, name: `field_${off}` };
|
|
459
595
|
}
|
|
460
|
-
return { k: 'index', base: baseExpr, idx: { k: 'const', value: off / width }, width, signed };
|
|
596
|
+
return { k: 'index', base: baseExpr, idx: { k: 'const', value: off / width }, width, signed, ...fromOperand };
|
|
461
597
|
}
|
|
462
598
|
|
|
463
|
-
// A variable-index array access `base[index]
|
|
464
|
-
// array-of-STRUCT element (raise/struct-arrays.ts)
|
|
465
|
-
//
|
|
599
|
+
// A variable-index array access `base[index]`; `base[index].field_K` when a `fieldOff` marks an
|
|
600
|
+
// array-of-STRUCT element (raise/struct-arrays.ts); `base->field_K[index]` when a `memberOff` marks
|
|
601
|
+
// an array MEMBER of a struct (raise/memberarrays.ts). The `.field` on an array element prints
|
|
602
|
+
// with `.` (the printer decides dot-vs-arrow from the base being an `index` node); a member array's
|
|
603
|
+
// own `field` base is a pointer, so it prints with `->`.
|
|
466
604
|
//
|
|
467
605
|
// Scalar path: a width-carrying `index` node, no cast — the backend legalizes (see memAccess).
|
|
468
606
|
// Struct-array path: like memAccess's struct path, the recovered struct pointer type is an L2
|
|
@@ -475,18 +613,49 @@ function arrayAccess(
|
|
|
475
613
|
baseExpr: Expr,
|
|
476
614
|
idxExpr: Expr,
|
|
477
615
|
fieldOff: number | undefined,
|
|
616
|
+
memberOff: number | undefined,
|
|
478
617
|
elemSize: number,
|
|
479
618
|
signed: boolean,
|
|
480
619
|
ctype: (e: Expr) => IrType | undefined,
|
|
481
620
|
sym?: SymRenderCtx,
|
|
482
621
|
): Expr {
|
|
622
|
+
// An indexed access through a POINTER MEMBER's value is an ELEMENT of what it points at, exactly
|
|
623
|
+
// as in memAccess — the index the aload already carries is the subscript, and any byte residual
|
|
624
|
+
// in the base is converted and summed into it. A fieldOff/memberOff access selects an interior
|
|
625
|
+
// of a STRUCT element instead, which this spelling has no place to put, so it is left alone.
|
|
626
|
+
if (sym && fieldOff === undefined && memberOff === undefined) {
|
|
627
|
+
const elem = ptrMemberElement(baseExpr, idxExpr, 0, elemSize, signed, sym);
|
|
628
|
+
if (elem) {
|
|
629
|
+
return elem;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
483
632
|
// A variable-index access off a global's address indexes the ADDRESS `&gSym` (the cast form
|
|
484
633
|
// `((T *)&gSym)[i]` — valid for a struct global too, unlike casting the bare value). A
|
|
485
|
-
// struct-array-of-globals (fieldOff)
|
|
486
|
-
|
|
634
|
+
// struct-array-of-globals (fieldOff) or a member array (memberOff) through `&gSym` falls through:
|
|
635
|
+
// this spelling has no place to put the constant offset, so taking it would DROP the member and
|
|
636
|
+
// address the wrong bytes.
|
|
637
|
+
if (baseExpr.k === 'addr' && fieldOff === undefined && memberOff === undefined) {
|
|
487
638
|
// ARRAY-declared global (symbol map): the bare-name spelling, same rule as memAccess.
|
|
488
639
|
const si = sym?.info(baseExpr.name);
|
|
489
|
-
|
|
640
|
+
// NO declared-subscript recovery here, and the reason is the evidence, not the shape. The
|
|
641
|
+
// index reaching this spelling is already in ELEMENTS — the asm scaled the whole sum ONCE, at
|
|
642
|
+
// the end — and that single scale is what both candidate sources reduce to, so the term at the
|
|
643
|
+
// row's stride in elements is no longer evidence that the source named the row. Compiled, with
|
|
644
|
+
// the klonoa checkout's own agbcc command line:
|
|
645
|
+
//
|
|
646
|
+
// u16 g[4][0x400] — a POWER-OF-TWO row stride: `g[a][b]` is `lsl #0xb` + `lsl #0x1` added
|
|
647
|
+
// (the SEPARATE terms memAccess reads), `g[0][(a<<10)+b]` is `lsl #0xa; add; lsl #0x1`.
|
|
648
|
+
// DIFFERENT bytes — so the single-scale shape that arrives here is the FLAT spelling's own
|
|
649
|
+
// codegen, and recovering `g[a][b]` out of it emits a source that does not reproduce the
|
|
650
|
+
// input asm.
|
|
651
|
+
// u32 g[6][9] — a NON-power-of-two row stride: `g[a][b]` and `g[0][a*9+b]` are BYTE-
|
|
652
|
+
// IDENTICAL, because agbcc reassociates `(a*36)+(b*4)` into `((a*9)+b)*4` itself. Nothing
|
|
653
|
+
// referees the choice, which is the same reason `scaledBy` refuses a CONSTANT row term.
|
|
654
|
+
//
|
|
655
|
+
// Both cases say decline: in the first the evidence points the other way, in the second there
|
|
656
|
+
// is none. The recovery therefore lives on the byte residual alone (memAccess), where the two
|
|
657
|
+
// spellings are still distinguishable. Pinned by matching/array-rank-axis.test.ts.
|
|
658
|
+
const lead = si === undefined ? null : bareArrayLead(si, elemSize, signed);
|
|
490
659
|
if (lead !== null) {
|
|
491
660
|
sym!.noteGlobal(baseExpr.name, T.ptr(T.int(elemSize * 8, si!.elemSigned ?? false)));
|
|
492
661
|
return { k: 'index', base: { k: 'var', name: baseExpr.name }, idx: idxExpr, width: elemSize, signed, ...lead };
|
|
@@ -494,6 +663,31 @@ function arrayAccess(
|
|
|
494
663
|
return { k: 'index', base: baseExpr, idx: idxExpr, width: elemSize, signed };
|
|
495
664
|
}
|
|
496
665
|
const bt = base.type;
|
|
666
|
+
// The member-array spelling: the constant offset selects a MEMBER and the index strides inside
|
|
667
|
+
// it. Same cast rule as memAccess's struct path — the recovered struct-pointer type is an L2 fact
|
|
668
|
+
// the AST cannot carry, so a base that does not render as THAT struct pointer is cast here.
|
|
669
|
+
if (memberOff !== undefined) {
|
|
670
|
+
// Both offsets on one access is `b->field_K[i].field_J` — an array of STRUCTS at a member
|
|
671
|
+
// offset, which raise/memberarrays.ts declares as an array of scalars and its `claimed-access`
|
|
672
|
+
// gate refuses. Spelling it through either offset alone addresses the wrong bytes, so the
|
|
673
|
+
// unreachable case declines loudly rather than dropping one.
|
|
674
|
+
if (fieldOff !== undefined) {
|
|
675
|
+
throw new StructureError(
|
|
676
|
+
`an array access carries both a member offset (${memberOff}) and a field offset (${fieldOff})`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
const structTo = bt.kind === 'ptr' && bt.to.kind === 'struct' ? bt.to : null;
|
|
680
|
+
const rt = ctype(baseExpr);
|
|
681
|
+
const ok = rt?.kind === 'ptr' && rt.to.kind === 'struct' && rt.to.name === structTo?.name && baseExpr.k !== 'index';
|
|
682
|
+
const b = ok || structTo === null ? baseExpr : { k: 'cast' as const, to: T.ptr(structTo), e: baseExpr };
|
|
683
|
+
return {
|
|
684
|
+
k: 'index',
|
|
685
|
+
base: { k: 'field', base: b, name: `field_${memberOff}` },
|
|
686
|
+
idx: idxExpr,
|
|
687
|
+
width: elemSize,
|
|
688
|
+
signed,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
497
691
|
if (fieldOff !== undefined) {
|
|
498
692
|
const structTo = bt.kind === 'ptr' && bt.to.kind === 'struct' ? bt.to : null;
|
|
499
693
|
const rt = ctype(baseExpr);
|
|
@@ -553,9 +747,11 @@ const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
|
553
747
|
sub: '-',
|
|
554
748
|
mul: '*',
|
|
555
749
|
sdiv: '/',
|
|
556
|
-
|
|
750
|
+
// the UNSIGNED quotient/remainder — the C backend spells them `/`/`%` over an operand it casts
|
|
751
|
+
// unsigned (l3/ast.ts BinOp, backend/cfamily.ts C_SPELLING)
|
|
752
|
+
udiv: '/u',
|
|
557
753
|
smod: '%',
|
|
558
|
-
umod: '%',
|
|
754
|
+
umod: '%u',
|
|
559
755
|
or: '|',
|
|
560
756
|
and: '&',
|
|
561
757
|
xor: '^',
|
|
@@ -566,6 +762,10 @@ const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
|
566
762
|
logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
|
|
567
763
|
};
|
|
568
764
|
|
|
765
|
+
// The operators whose operand order the machine does not fix — candidates for the def-order
|
|
766
|
+
// re-spelling in lowerDef. `&&`/`||` are excluded: short-circuit order IS semantics.
|
|
767
|
+
const COMMUTATIVE_BIN: ReadonlySet<BinOp> = new Set(['+', '*', '&', '|', '^']);
|
|
768
|
+
|
|
569
769
|
// Recovered info for a self-loop header: its exit block and the per-parameter back-edge
|
|
570
770
|
// arg it feeds (the value on the header→header edge). The back-edge arg is the "next"
|
|
571
771
|
// value of the phi; mapping it back to the phi turns the latch test into the while test.
|
|
@@ -573,6 +773,21 @@ interface LoopInfo {
|
|
|
573
773
|
header: Block;
|
|
574
774
|
exit: Block;
|
|
575
775
|
backArgOfParam: Value[]; // index-aligned with header.params
|
|
776
|
+
/** The PURE forwarding block between the guard and the header, when the compiler's own
|
|
777
|
+
* loop-invariant motion parked computations there (`mov r3,#0x80; lsl r3,#24` feeding the
|
|
778
|
+
* latch test). Its defs render inline wherever the loop reads them; the block itself is never
|
|
779
|
+
* structured — the guard's inits come from ITS edge into the header instead. */
|
|
780
|
+
preheader?: Block;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// One early-`return` exit out of a loop body, and the blocks of it the loop emits inside its body
|
|
784
|
+
// (`earlyReturnArm` decides which). Per-EDGE rather than flattened into per-loop sets: ownership is
|
|
785
|
+
// decided against `from`, so a second edge into the same target is a separate question. `owned` has
|
|
786
|
+
// one reader, `emitDoWhile` — a `while` hoists no update, so it has no pre-update reads to exempt.
|
|
787
|
+
interface LoopArm {
|
|
788
|
+
from: Block;
|
|
789
|
+
to: Block;
|
|
790
|
+
owned: Set<Block>;
|
|
576
791
|
}
|
|
577
792
|
|
|
578
793
|
// A test-at-top multi-block `while`. The header is a pure test whose cond_br enters `bodyEntry`
|
|
@@ -585,6 +800,7 @@ interface WhileLoopInfo {
|
|
|
585
800
|
latch: Block; // the single block with the back-edge to header (its args = the update)
|
|
586
801
|
forwardPreds: Block[]; // header preds outside the loop body (the entry/init side)
|
|
587
802
|
body: Set<Block>; // the pure natural-loop body (for in-body vs exit classification)
|
|
803
|
+
arms: LoopArm[]; // the early-`return` exits out of the body (`earlyReturnArm`)
|
|
588
804
|
}
|
|
589
805
|
|
|
590
806
|
// Opcodes whose NUMBER OF EXECUTIONS is observable. Moving one of these out of a loop changes what
|
|
@@ -594,6 +810,74 @@ interface WhileLoopInfo {
|
|
|
594
810
|
// (structure/analysis.ts) already proves before it lets one inline at all.
|
|
595
811
|
const REPEATED_EFFECT = new Set(['call', 'opaque']);
|
|
596
812
|
|
|
813
|
+
/** No evidence about where this copy goes — NOT "written before everything". A destination the
|
|
814
|
+
* predecessor never wrote is an argument passing through, and both orders put those first, so the
|
|
815
|
+
* record answers no question the assembly put to it. Priced where the sort runs. */
|
|
816
|
+
const NO_RECORD = -1;
|
|
817
|
+
/** A MEASURED predecessor that recorded no destination of this edge — it wrote registers, none of
|
|
818
|
+
* them a key this edge copies. Distinct from an UNMEASURED pred, which has no record at all and
|
|
819
|
+
* falls back to the def-position proxy. */
|
|
820
|
+
const NO_WRITTEN_DESTINATIONS: ReadonlyMap<Value, number> = new Map<Value, number>();
|
|
821
|
+
|
|
822
|
+
/** Does the write-order record order some edge's copies differently from the def-position proxy?
|
|
823
|
+
* rank.ts's enumeration gate for `/copy-defpos`: where the two orders agree, the sibling is the
|
|
824
|
+
* same tree and the fan does not grow.
|
|
825
|
+
*
|
|
826
|
+
* A SUPERSET of the real sort, on purpose and in the safe direction — and the direction only
|
|
827
|
+
* holds because rank asks this of the SAME fn it then structures (a `variantGate`, evaluated on
|
|
828
|
+
* the variant's own fully-raised fn). Two gaps remain, both of which only say YES where the sort
|
|
829
|
+
* says nothing: `keepSlot`/`suppressedArgs` drop copies this still counts, and this asks of every
|
|
830
|
+
* edge where `preferDefPosCopyOrder` reorders only the acyclic ones (`copySetIsCyclic` needs the
|
|
831
|
+
* built copy list, which is not available here). So it can answer true for a pair that later
|
|
832
|
+
* collapses — the tree dedup then eats it — but never false for one that does not.
|
|
833
|
+
*
|
|
834
|
+
* ASKED ANY EARLIER THE CLAIM FAILS, in both directions. The answer moves with the SYMBOL MAP
|
|
835
|
+
* (klonoa's `UpdateHUDCollectibleCount` answers false under the map and true without it, off one
|
|
836
|
+
* asm — test/corpus/agbcc-hudcount.s) and with the STAGE, because `raise/latch.ts` rewrites the
|
|
837
|
+
* record this reads (klonoa's `EntityGravityAndFloorCheck`: false after `recoverTypes`, true
|
|
838
|
+
* after `foldEmptyLatches` — a RECORD of a measurement, not a live check: that function is in no
|
|
839
|
+
* corpus row and no fixture here, so the claim cannot be re-run from this repo).
|
|
840
|
+
*
|
|
841
|
+
* A SECOND, HAND-WRITTEN SPELLING of `edgeCopyRecords`' two comparators — including the stability
|
|
842
|
+
* that decides ties — not a call into them, and nothing forces the two to agree. A change to that
|
|
843
|
+
* sort has to be mirrored here BY HAND, or this gate keeps answering about an ordering the pass no
|
|
844
|
+
* longer produces. Unified deliberately not: this is `/copy-defpos`'s variantGate, so it decides
|
|
845
|
+
* which candidates are ENUMERATED and its predicate cannot move without moving rows. */
|
|
846
|
+
export function edgeCopyOrdersDiffer(fn: Fn): boolean {
|
|
847
|
+
const order = fn.writeOrder;
|
|
848
|
+
if (order === undefined) {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
const defs = defOpMap(fn);
|
|
852
|
+
for (const pred of fn.blocks) {
|
|
853
|
+
if (!order.writes.has(pred)) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const rec = order.lastWrite.get(pred) ?? NO_WRITTEN_DESTINATIONS;
|
|
857
|
+
const opAt = new Map(pred.ops.map((o, i) => [o, i] as const));
|
|
858
|
+
for (const op of pred.ops) {
|
|
859
|
+
for (const succ of op.successors) {
|
|
860
|
+
if (succ.args.length < 2) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const slots = succ.args.map((_, i) => i);
|
|
864
|
+
const proxyPos = (i: number) => {
|
|
865
|
+
const d = defs.get(succ.args[i]);
|
|
866
|
+
return d !== undefined && opAt.has(d) ? opAt.get(d)! : NO_RECORD;
|
|
867
|
+
};
|
|
868
|
+
const byRecord = [...slots].sort(
|
|
869
|
+
(a, b) => (rec.get(succ.block.params[a]) ?? NO_RECORD) - (rec.get(succ.block.params[b]) ?? NO_RECORD),
|
|
870
|
+
);
|
|
871
|
+
const byProxy = [...slots].sort((a, b) => proxyPos(a) - proxyPos(b));
|
|
872
|
+
if (byRecord.some((v, i) => v !== byProxy[i])) {
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
|
|
597
881
|
// A bottom-tested `do { body } while(cond)`. The header is the body entry (entered before any
|
|
598
882
|
// test); the LATCH holds the loop condition and the single exit. Body = header..latch structured, then
|
|
599
883
|
// the latch's own ops + the loop-update; the latch test is the do-while condition. The condition is
|
|
@@ -604,6 +888,86 @@ interface DoWhileInfo {
|
|
|
604
888
|
exit: Block;
|
|
605
889
|
forwardPreds: Block[];
|
|
606
890
|
body: Set<Block>; // the pure natural-loop body (for in-body vs exit classification)
|
|
891
|
+
arms: LoopArm[];
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** One carrier offered to one merge slot, as `FRESH_MERGE_GATES` judges it. */
|
|
895
|
+
export interface FreshMergeCarrier {
|
|
896
|
+
/** every in-edge passes the SAME value, so the merge is a pure alias of the carrier */
|
|
897
|
+
readonly allSame: boolean;
|
|
898
|
+
/** the carrier is an entry parameter, or a merge home this rule itself minted */
|
|
899
|
+
readonly paramRooted: boolean;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/** `freshParamMerge`'s admission: may this merge take its OWN home instead of the carrier's name?
|
|
903
|
+
* Nothing here is `sound` — both spellings are ordinary C over the same values, so `rank.ts`
|
|
904
|
+
* enumerates the ON one as `/fresh-merge` and the differ referees it. What keeps them two
|
|
905
|
+
* SPELLINGS rather than two PROGRAMS is `canTakeName`'s carrier width/sign check, which runs on
|
|
906
|
+
* the same carrier one step earlier.
|
|
907
|
+
*
|
|
908
|
+
* WHAT THE SPELLING BUYS: a parameter is live from entry, so its name pins the value to the
|
|
909
|
+
* register the ABI handed it; a fresh local is dead until its first arm, so the compiler may place
|
|
910
|
+
* the copy where the asm has it. Which merges actually needed a register of their own is not
|
|
911
|
+
* recoverable from the C, and the rule is ALL-OR-NOTHING over a function's param-rooted slots. On
|
|
912
|
+
* `max3` (agbcc -O2, scored against its own target) only the SECOND of the two chained merges is
|
|
913
|
+
* load-bearing: re-homing just the first is byte-identical to re-homing neither (score 5), and
|
|
914
|
+
* re-homing just the second is byte-identical to re-homing both (score 0, MATCH). Splitting the
|
|
915
|
+
* choice per slot would be a fan of 2^slots, so the axis offers the whole-function spelling.
|
|
916
|
+
*
|
|
917
|
+
* `param-rooted` is a SCOPE, not a derivation. A chain rooted in an ordinary merge home is left
|
|
918
|
+
* alone, and widening to one is a different, unmeasured axis: 293 of 721 map-less corpus rows
|
|
919
|
+
* carry at least one conditional merge slot (925 slots), of which the param rooting admits 109 —
|
|
920
|
+
* `LoadBGTilemapData` is one of the other 184, which is why the axis has no reach there — a RECORD
|
|
921
|
+
* of a measurement, not a live check: that function is in no corpus row and no fixture here (its
|
|
922
|
+
* attribution evidence is docs/lbg-attribution.md), so it cannot be re-run from this repo.
|
|
923
|
+
*
|
|
924
|
+
* The rooting is over carrier VALUES, and two shapes carry a parameter's value past it under
|
|
925
|
+
* another name, both reach-only: a redundant phi keeps the parameter's own name (`redundant-phi`)
|
|
926
|
+
* and its result is then an ordinary carrier, which fires on 0 of 721 lifted corpus rows; and the
|
|
927
|
+
* minted-home set records only merges that ended up FRESH, so a merge that refuses a parameter and
|
|
928
|
+
* then adopts an ordinary local is not recorded — 336 firings over 6 rows. */
|
|
929
|
+
export const FRESH_MERGE_GATES: readonly Gate<FreshMergeCarrier>[] = [
|
|
930
|
+
{
|
|
931
|
+
id: 'redundant-phi',
|
|
932
|
+
why: 'a merge every edge feeds the same value overwrites nothing, so its own home buys a copy',
|
|
933
|
+
sound: false,
|
|
934
|
+
guardedBy: 'fresh-merge.test.ts: a redundant phi over one parameter keeps the parameter',
|
|
935
|
+
rejects: (c) => c.allSame,
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
id: 'param-rooted',
|
|
939
|
+
why: "the rule's scope — a chain rooted in an ordinary merge home is a separate, unmeasured axis",
|
|
940
|
+
sound: false,
|
|
941
|
+
guardedBy: 'fresh-merge.test.ts: a merge over ordinary locals is untouched',
|
|
942
|
+
rejects: (c) => !c.paramRooted,
|
|
943
|
+
},
|
|
944
|
+
];
|
|
945
|
+
|
|
946
|
+
/** Whether any merge slot could reach `FRESH_MERGE_GATES` at all: an in-edge argument list that
|
|
947
|
+
* differs and includes an entry parameter — what `rank.ts` gates enumeration of `/fresh-merge` on.
|
|
948
|
+
* An OVER-approximation, which is the safe direction for a gate that only decides whether to
|
|
949
|
+
* enumerate: the walk can still refuse
|
|
950
|
+
* the carrier (`carriesPreUpdate`, `canTakeName`), and a slot whose carrier is a home this rule
|
|
951
|
+
* mints is not visible here at all. */
|
|
952
|
+
export function hasParamRootedMerge(fn: Fn): boolean {
|
|
953
|
+
const entryParams = new Set<Value>(fn.blocks[0].params);
|
|
954
|
+
return fn.blocks.slice(1).some((b) =>
|
|
955
|
+
b.params.some((_, i) => {
|
|
956
|
+
const args = fn.blocks.flatMap((pr) =>
|
|
957
|
+
pr.ops[pr.ops.length - 1].successors.filter((sx) => sx.block === b).map((sx) => sx.args[i]),
|
|
958
|
+
);
|
|
959
|
+
return args.some((a) => a !== args[0]) && args.some((a) => entryParams.has(a));
|
|
960
|
+
}),
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** True when the merge takes its own home. The table is a parameter so a test can drop one gate and
|
|
965
|
+
* re-run the real pass (`StructureHooks.freshMergeGates`). */
|
|
966
|
+
function reHomesParamMerge(
|
|
967
|
+
c: FreshMergeCarrier,
|
|
968
|
+
gates: readonly Gate<FreshMergeCarrier>[] = FRESH_MERGE_GATES,
|
|
969
|
+
): boolean {
|
|
970
|
+
return firstRejection(gates, c) === null;
|
|
607
971
|
}
|
|
608
972
|
|
|
609
973
|
// Structuring levers, threaded as DATA so a new one is a field here + its consumer, not a new
|
|
@@ -611,23 +975,119 @@ interface DoWhileInfo {
|
|
|
611
975
|
// returnsVoid — from the function's own prototype (suppress phantom r0 return);
|
|
612
976
|
// coalesceLoopInit — keep the induction var in its arg register;
|
|
613
977
|
// preserveDivergentBranchSense — reproduce source branch direction on divergent ifs;
|
|
614
|
-
//
|
|
978
|
+
// orderArgCopiesByWriteOrder — order edge copies by the order the pred WROTE their
|
|
979
|
+
// destinations.
|
|
615
980
|
// The last three are `compilerBehaviors` (target.ts) — this pass stays target-AGNOSTIC: it reads
|
|
616
981
|
// booleans, never a compiler name.
|
|
982
|
+
//
|
|
983
|
+
// WHAT A FIELD DOC BELOW HOLDS, narrowly: what the option MEANS to `structure()`, and the suffix of
|
|
984
|
+
// the axis that enumerates it. An AXIS's rationale, and any figure pricing its marginal value, live
|
|
985
|
+
// ONCE at its `STRUCTURING_AXES` entry in rank.ts — restated here the two copies rot separately,
|
|
986
|
+
// and only the rank.ts one sits next to the enumeration that could refute it. Figures pricing a
|
|
987
|
+
// DEFAULT this pass owns (the edge-copy ordering, `spellDeclaredSubscripts`) do belong here.
|
|
617
988
|
export interface StructureOptions {
|
|
618
989
|
returnsVoid?: boolean;
|
|
619
990
|
coalesceLoopInit?: boolean;
|
|
620
991
|
preserveDivergentBranchSense?: boolean;
|
|
621
|
-
|
|
992
|
+
// Spell a JOINED two-armed if with the negated condition and swapped arms: the asm branched
|
|
993
|
+
// forward to the taken block and fell through to the other, so a compiler that preserves
|
|
994
|
+
// source branch direction saw the FALL-THROUGH arm as `then` — the same layout evidence the
|
|
995
|
+
// divergent case reads (preserveDivergentBranchSense), which post-dominance hides here because
|
|
996
|
+
// both arms reconverge. Defaults to preserveDivergentBranchSense rather than to a constant, so a
|
|
997
|
+
// target that opts out of the divergent claim opts out of this one; target.ts says how.
|
|
998
|
+
//
|
|
999
|
+
// This is the ZERO POINT of rank.ts's `/flip-join` axis, not a per-compiler fact that closes
|
|
1000
|
+
// the question — docs/level-tower.md wants a default only where the mapping is a FUNCTION, and
|
|
1001
|
+
// benchmark rows still reach their winning spelling through the axis rather than through this
|
|
1002
|
+
// default. Read it forward only: it says which sense to emit ABSENT evidence of an inversion,
|
|
1003
|
+
// never that the asm's layout WAS the source's sense. What agbcc contributes is the refusals —
|
|
1004
|
+
// its gcc Makefile SRCS compiles neither sched.c nor reorg.c and toplev.c never sets
|
|
1005
|
+
// flag_schedule_insns, and gcse.c runs one_code_hoisting_pass only `if (optimize_size)`, which
|
|
1006
|
+
// toplev.c sets for -Os alone — so no scheduler and no hoister moves an arm's body across the
|
|
1007
|
+
// branch after stmt.c laid the arms out in source order.
|
|
1008
|
+
//
|
|
1009
|
+
// Three mechanisms DO invert the sense, and each is per-SITE where this lever is per-function,
|
|
1010
|
+
// so no value here is right in every `if` of a function that holds several: a short-circuit
|
|
1011
|
+
// fold picks which successor is `taken` from the asm's branch polarity, which on Thumb the
|
|
1012
|
+
// branch RANGE decides (raise/shortcircuit.ts); a relay past a branch's reach inverts to jump
|
|
1013
|
+
// around the long form; and a rotated loop's zero-trip guard is an `if` no source wrote at all
|
|
1014
|
+
// (`synthetic:fib`, `for(i=0;i<n;i++)`, emits `if (0 >= a0) … else do{…}while`), so there no
|
|
1015
|
+
// spelling is the faithful one and only the differ can choose.
|
|
1016
|
+
negateJoinedBranchSense?: boolean;
|
|
1017
|
+
orderArgCopiesByWriteOrder?: boolean;
|
|
1018
|
+
/** Order a measured edge's ACYCLIC copy set by the def-position proxy instead — the
|
|
1019
|
+
* `/copy-defpos` ranked sibling of the write-order spelling (rank.ts). A CYCLIC set keeps the
|
|
1020
|
+
* record either way: there an instruction names the compiler's temp, and no arm of the fan may
|
|
1021
|
+
* contradict it (`copySetIsCyclic`). Not a target field and deliberately not one: which order a
|
|
1022
|
+
* compiler laid out is two-sided inside one compiler, so the differ referees it per row. Inert
|
|
1023
|
+
* on an unmeasured pred, where the proxy is already what runs. */
|
|
1024
|
+
preferDefPosCopyOrder?: boolean;
|
|
622
1025
|
// Comparison-tree switch recovery: treat an `x != K` test as a case (the EQUAL side is a case
|
|
623
1026
|
// body). GCC freely uses `!=`; IDO prefers `==`/`<`. A per-compiler DATA lever, not an `arch ==`
|
|
624
1027
|
// branch — default true (permissive; the decline path keeps it sound either way).
|
|
625
1028
|
switchAllowsNeqCase?: boolean;
|
|
1029
|
+
// Comparison-tree switch recovery: treat a relational test whose BRANCH admits exactly one
|
|
1030
|
+
// scrutinee value as that case rather than as navigation. A per-compiler DATA lever declared in
|
|
1031
|
+
// TargetDescription.compilerBehaviors — a compiler opts in on evidence that its dispatch jumps
|
|
1032
|
+
// straight to a bounded subtree's body. Default false: absent, every relational edge navigates.
|
|
1033
|
+
switchAllowsBoundCase?: boolean;
|
|
1034
|
+
// Comparison-tree switch recovery: emit the case arms in the order the ASSEMBLY lays their
|
|
1035
|
+
// bodies out, rather than sorted by ascending case value. A per-compiler DATA lever declared in
|
|
1036
|
+
// TargetDescription.compilerBehaviors — a compiler opts in on evidence that it neither reorders
|
|
1037
|
+
// basic blocks nor schedules across them, so the layout it produced IS the order the source
|
|
1038
|
+
// wrote. Default false: absent, the arms keep the ascending spelling.
|
|
1039
|
+
switchArmsFollowLayout?: boolean;
|
|
1040
|
+
// Does the TARGET LANGUAGE spell a `switch` arm that runs on into the next one? Set from the
|
|
1041
|
+
// caller's LanguageBackend (`spellsSwitchFallthrough`), not from the compiler target: it is a
|
|
1042
|
+
// property of what the emitted source may say, and the only reason the structurer needs it is
|
|
1043
|
+
// that a fall-through switch has a SECOND, behaviourally identical recovery. Regime A declines
|
|
1044
|
+
// to if-recovery when it is false; Regime B, having no fallback, fails loud. Default true.
|
|
1045
|
+
spellSwitchFallthrough?: boolean;
|
|
1046
|
+
// Which way this compiler hands out frame slots against DECLARATION RANK: `ascending` = the
|
|
1047
|
+
// earlier-declared spilled local takes the LOWER `[sp,#k]`. A per-compiler DATA lever declared
|
|
1048
|
+
// in TargetDescription.compilerBehaviors, carried to the backend on `SFn.slotOrder` and applied
|
|
1049
|
+
// by `l3/slotorder.ts` at emit time. ABSENT means the ordering refuses — there is no default
|
|
1050
|
+
// direction, because a wrong one reorders declarations for no reason.
|
|
1051
|
+
//
|
|
1052
|
+
// TWO STATES, NOT THREE. `TargetDescription` spells a third, `'unknown'`, and it earns its place
|
|
1053
|
+
// there: it separates a direction that was measured and deliberately not shipped from one nobody
|
|
1054
|
+
// ever asked about, which is a fact about the target worth writing down. Nobody reads a
|
|
1055
|
+
// STRUCTURER option to learn what was measured, so carrying it here would be a second way to
|
|
1056
|
+
// spell "no" on a public option type. The translation happens once, in `structureOptionsFor`
|
|
1057
|
+
// (target.ts), which is where the target-to-structurer mapping lives.
|
|
1058
|
+
spillSlotOrder?: 'ascending' | 'descending';
|
|
1059
|
+
// Commutative load pairs re-spell in def (evaluation) order — see the swap in lowerDef. Default
|
|
1060
|
+
// true; verified byte-exact on agbcc and IDO. A per-compiler DATA lever declared in
|
|
1061
|
+
// TargetDescription.compilerBehaviors: the first compiler whose scheduler is shown re-ordering
|
|
1062
|
+
// independent loads flips it there, not in a code branch. A per-FUNCTION machine-order fallback
|
|
1063
|
+
// candidate is deliberately deferred until a row demands it.
|
|
1064
|
+
defOrderLoadPairs?: boolean;
|
|
626
1065
|
// Anchor a constant merge copy at its const op's ORIGINAL position instead of at the CFG edge:
|
|
627
1066
|
// `movs r9, #0` at entry ahead of a single-armed overwrite emits as a pre-initialization above
|
|
628
1067
|
// the `if`, not as its else-arm. A differ-refereed candidate axis (rank.ts `/defsite`), never a
|
|
629
1068
|
// default — see the refusal conditions where it is computed.
|
|
630
1069
|
anchorConstCopies?: boolean;
|
|
1070
|
+
// WIDEN `anchorConstCopies` to a LOOP HEADER's entry constant — `int s = 0;` hoisted above the
|
|
1071
|
+
// `if` that guards the loop, rather than written on the edge into it. A second placement
|
|
1072
|
+
// decision, so a second axis point (rank.ts `/defsite/loop-entry`) rather than a widening of
|
|
1073
|
+
// the first: on a function carrying both kinds of anchorable const, folding them into one flag
|
|
1074
|
+
// would make "anchor the plain ones, leave the loop's at its edge" — a spelling `/defsite`
|
|
1075
|
+
// emits today — unreachable. Inert unless `anchorConstCopies` is also on.
|
|
1076
|
+
//
|
|
1077
|
+
// AN AXIS RATHER THAN AN EXTENSION OF `l3/initfirst.ts`, whose header opens on the same rewrite
|
|
1078
|
+
// (`if (0 < n) { v = 0; … }` → `v = 0; if (v < n) { … }`) at a fraction of the price: a
|
|
1079
|
+
// re-spelling adds candidates only where it fires, while this multiplies every candidate below
|
|
1080
|
+
// it. The fork is REACH, and it is a hard one. `initfirst` MOVES A STATEMENT, and an edge copy
|
|
1081
|
+
// is not a statement — structuring mints it, choosing between the edge and the const op's own
|
|
1082
|
+
// position, and the IR that holds those positions is gone by the time L3 runs. So the shapes
|
|
1083
|
+
// where the two coincide are initfirst's for free — except that they never do: its guard
|
|
1084
|
+
// re-spelling wants an ELSE-LESS `if` and rewrites the condition to read the hoisted variable
|
|
1085
|
+
// (`if (v < n)`), where anchoring leaves the condition alone, so the two emit different sources.
|
|
1086
|
+
// Measured, not argued: `/initfirst` rides every spelling rank.ts enumerates, so it is scored on
|
|
1087
|
+
// every benchmark row already, and on each row this axis wins its `/initfirst`-only sibling is
|
|
1088
|
+
// either not enumerated at all or not byte-identical to the anchored source — the substitution
|
|
1089
|
+
// reaches none of them. Take the axis only while rows demand that; the price is in rank.ts.
|
|
1090
|
+
anchorLoopEntryConsts?: boolean;
|
|
631
1091
|
// HARDWARE fact from TargetDescription.capabilities.endianness, threaded by structureOptionsFor:
|
|
632
1092
|
// the bitfield extract recognizer solves an LSB-first equation, so it only runs on little-endian
|
|
633
1093
|
// data. The provider already refuses to EMIT bitfield facts for a big-endian ELF; this is the
|
|
@@ -636,13 +1096,104 @@ export interface StructureOptions {
|
|
|
636
1096
|
// Spell `(x << a) >> b` extracts of a struct global as the map's named bitfield member. On by
|
|
637
1097
|
// default; rank.ts enumerates the OFF spelling as the `/no-bitfield` axis, because the named
|
|
638
1098
|
// read recompiles at the DECLARATION's access width — where that diverges from the asm's load
|
|
639
|
-
// width the honest shift spelling is the one that matches, and the differ referees.
|
|
1099
|
+
// width the honest shift spelling is the one that matches, and the differ referees. Only the map
|
|
1100
|
+
// carries the names, so with no `symbols` this is normalized to false whatever a caller passes.
|
|
640
1101
|
spellBitfieldMembers?: boolean;
|
|
1102
|
+
// Spell an element-scaled offset through a map-declared POINTER MEMBER as a whole-element
|
|
1103
|
+
// subscript of it (`gBg.pMap[i + 157]`) rather than as the byte arithmetic it replaces. On by
|
|
1104
|
+
// default; rank.ts enumerates the OFF spelling as the `/no-ptr-elem` axis.
|
|
1105
|
+
//
|
|
1106
|
+
// IT IS AN AXIS AND NOT A DEFAULT BECAUSE IT IS NOT BYTE-NEUTRAL — the bar the block comment
|
|
1107
|
+
// above `spellablePointee` sets for a member spelling. Compiled against agbcc (`-mthumb-interwork -Wimplicit -O2 -fhex-asm
|
|
1108
|
+
// -fprologue-bugfix`), `((u16 *)gB.pMap)[i + K]` and `*(u16 *)((i << 1) + (u8 *)gB.pMap + 2K)`
|
|
1109
|
+
// are the same address and the same instruction COUNT at K = 0, 1 and 157 — and different
|
|
1110
|
+
// objects at all three, differing in which register the `add` targets. Which side matches is
|
|
1111
|
+
// per-function knowledge the asm does not carry, so both are emitted and the differ referees.
|
|
1112
|
+
// Only the map declares a pointee width, so with no `symbols` this is normalized to false.
|
|
1113
|
+
spellPtrMemberElements?: boolean;
|
|
1114
|
+
// Recover a multidimensional array global's DECLARED subscripts (`g[r][i]`) from a byte residual
|
|
1115
|
+
// carrying a term at the declared ROW stride, rather than spelling the whole residual as the
|
|
1116
|
+
// `*(T *)(… + (u32)&g)` cast it replaces. On by default; rank.ts enumerates the OFF spelling as
|
|
1117
|
+
// the `/flat-rank` axis.
|
|
1118
|
+
//
|
|
1119
|
+
// IT IS AN AXIS AND NOT A DEFAULT BECAUSE THE ASM DOES NOT DETERMINE IT, and the evidence that
|
|
1120
|
+
// it does not is the same compile the recovery's own premise rests on, read against the spelling
|
|
1121
|
+
// the recovery DISPLACES rather than against the flat one it refuses. For `u16 g[4][0x400]`:
|
|
1122
|
+
//
|
|
1123
|
+
// agbcc g[a][b] lsl #0xb ; lsl #0x1 ; add ; add | *(u16 *)((a<<11)+(b<<1)+(u32)&g)
|
|
1124
|
+
// md5 f58a694f… | md5 051bf506… DIFFERENT, and the
|
|
1125
|
+
// difference is WHERE THE POOL LOAD SITS — the shift structure the recovery
|
|
1126
|
+
// reads is IDENTICAL in both, so the residual is evidence about the row and
|
|
1127
|
+
// NOT about which of these two spellings wrote it.
|
|
1128
|
+
// kmc / mwcc the same: separate scales both sides, differing only in scheduling.
|
|
1129
|
+
// IDO the two are BYTE-IDENTICAL (md5 2b55b493…) — and IDO also distributes the
|
|
1130
|
+
// FLAT sum into the same separate scales, so on that compiler the recovery's
|
|
1131
|
+
// own premise is false and the flat spelling reaches it too.
|
|
1132
|
+
//
|
|
1133
|
+
// WHICH ROW OF THAT TABLE IS PINNED: the agbcc pair, by
|
|
1134
|
+
// packages/cli/test/matching/array-rank-axis.test.ts, which compiles both spellings through the
|
|
1135
|
+
// klonoa checkout's own template. The other three were measured by hand and nothing re-runs
|
|
1136
|
+
// them, so treat them as the record of a measurement rather than as a live check.
|
|
1137
|
+
//
|
|
1138
|
+
// So both are emitted and the differ referees, exactly as for `/no-ptr-elem`. Only the map
|
|
1139
|
+
// declares a rank, so with no `symbols` this is normalized to false.
|
|
1140
|
+
spellDeclaredSubscripts?: boolean;
|
|
641
1141
|
// Let a read of a named global render at its use across writes that PROVABLY cannot reach it
|
|
642
1142
|
// (a store to a different named global), instead of caching it in a local. Off by default;
|
|
643
1143
|
// rank.ts enumerates the ON spelling as the `/reread-globals` axis — see analysis.ts
|
|
644
1144
|
// AnalyzeOptions for why this is a differ-refereed lever and not a fix.
|
|
645
1145
|
rereadGlobals?: boolean;
|
|
1146
|
+
// Materialize a load that feeds a `cond_br` join arg, so the naming walk can home the join in
|
|
1147
|
+
// it and the identity arm elides to a one-sided in-place `if`. Off by default; rank.ts
|
|
1148
|
+
// enumerates the ON spelling as the `/inplace` axis — see analysis.ts AnalyzeOptions.
|
|
1149
|
+
materializeJoinFeeds?: boolean;
|
|
1150
|
+
// Materialize a pure computed address shared by 2+ memory accesses, and the multi-render loads
|
|
1151
|
+
// through it, reproducing the source's pointer-local + scalar-temp spelling. Off by default;
|
|
1152
|
+
// rank.ts enumerates the ON spelling as the `/addr-home` axis — see analysis.ts AnalyzeOptions.
|
|
1153
|
+
homeSharedAddresses?: boolean;
|
|
1154
|
+
// Materialize a pure value with 2+ distinct consumers, at least one of them inside a loop the
|
|
1155
|
+
// def sits outside — the register the compiler holds across the iterations. Off by default;
|
|
1156
|
+
// rank.ts enumerates the ON spelling as the `/expr-home` axis — see analysis.ts AnalyzeOptions.
|
|
1157
|
+
homeLoopExprs?: boolean;
|
|
1158
|
+
// Materialize a pure value with 2+ consumers standing on a memory read — the register the asm
|
|
1159
|
+
// carried the DERIVED value in, where the read's own home is a register that died at the
|
|
1160
|
+
// computation. Off by default; rank.ts enumerates the ON spelling as the `/derived-home` axis —
|
|
1161
|
+
// see analysis.ts AnalyzeOptions.
|
|
1162
|
+
homeDerivedReads?: boolean;
|
|
1163
|
+
// Materialize a pure value that one join's incoming edges render into the SAME parameter slot
|
|
1164
|
+
// from 2+ places — the value the source computed once above the branch and the copy machinery
|
|
1165
|
+
// sinks into every arm. Off by default; rank.ts enumerates the ON spelling as the `/merge-home`
|
|
1166
|
+
// axis — see analysis.ts AnalyzeOptions.
|
|
1167
|
+
homeMergeFeeds?: boolean;
|
|
1168
|
+
// Emit a memory read as a named temp in ITS OWN block when every place it renders sits in a
|
|
1169
|
+
// block that block strictly dominates. A per-compiler DATA lever (TargetDescription
|
|
1170
|
+
// .compilerBehaviors), not a differ-refereed axis: where the compiler has neither a scheduler
|
|
1171
|
+
// nor a code hoister, the sunk spelling is one it could not have emitted from this asm, so there
|
|
1172
|
+
// is nothing to referee. Absent ⇒ off — the target field carries the evidence a compiler owes,
|
|
1173
|
+
// analysis.ts AnalyzeOptions the refusals.
|
|
1174
|
+
readsStayWhereWritten?: boolean;
|
|
1175
|
+
// Spell unsigned compares unsigned: cast an icmp_u* operand where the rendered operands do not
|
|
1176
|
+
// guarantee it, and reconcile a mixed-claimant declaration to u32 when nothing under the name
|
|
1177
|
+
// needs signed. Off by default: a signed spelling that byte-matched was PROVED non-negative by
|
|
1178
|
+
// the compiler (it emits the unsigned branch from signed compares only then), so which spelling
|
|
1179
|
+
// the source used is genuinely ambiguous at emission — rank.ts enumerates the ON spelling as
|
|
1180
|
+
// the `/uns-cmp` axis and the differ referees.
|
|
1181
|
+
unsignedCompareSpelling?: boolean;
|
|
1182
|
+
// Merge two variables that a merge copy would join, when the values under them never interfere
|
|
1183
|
+
// (structure/namecoalesce.ts). Off by default; rank.ts enumerates the ON spelling as the
|
|
1184
|
+
// `/merge-names` axis. Which variables the compiler's own coalescer shared is not derivable from
|
|
1185
|
+
// the naming, and removing a copy is worth less than it looks — the compiler coalesces most of
|
|
1186
|
+
// them itself. What moves the score is which values share a register, and that splits per
|
|
1187
|
+
// function.
|
|
1188
|
+
coalesceMergeNames?: boolean;
|
|
1189
|
+
// Give a merge whose carrier is a FUNCTION PARAMETER its own local, instead of assigning back
|
|
1190
|
+
// into the parameter's name. Off by default; rank.ts enumerates the ON spelling as the
|
|
1191
|
+
// `/fresh-merge` axis, and `FRESH_MERGE_GATES` holds the admission and the argument for it.
|
|
1192
|
+
//
|
|
1193
|
+
// NOT `materializeJoinFeeds` widened to parameters. That axis reaches its shape by giving the
|
|
1194
|
+
// join's feed a NAME to adopt, and materialization is keyed on the defining `Op` — a parameter
|
|
1195
|
+
// has none, so there is nothing to key.
|
|
1196
|
+
freshParamMerge?: boolean;
|
|
646
1197
|
// How an unresolvable VALUE degrades (a live `opaque`, an unlowered transient op, a dropped def):
|
|
647
1198
|
// "strict" (default) — the `"?"` sentinel, tripping assertResolved at the boundary (loud in
|
|
648
1199
|
// the PROCESS);
|
|
@@ -654,22 +1205,269 @@ export interface StructureOptions {
|
|
|
654
1205
|
* bare `gSym[i]` form; `shape:'struct'`+layout spells interiors as `gSym.field`. Absent (or
|
|
655
1206
|
* a symbol not in the map) ⇒ today's usage-inferred behavior, byte-identical. */
|
|
656
1207
|
symbols?: Map<string, SymbolInfo>;
|
|
1208
|
+
/** ARRAY SHAPES DERIVED FROM THE INPUT ASSEMBLY (raise/globalshape.ts) for globals the project
|
|
1209
|
+
* map does not describe — same `SymbolInfo` shape, same readers, LOWER precedence: `symbols`
|
|
1210
|
+
* wins every name it knows, because a project declaration knows more than an inference off one
|
|
1211
|
+
* function's strides.
|
|
1212
|
+
*
|
|
1213
|
+
* A SEPARATE FIELD rather than a pre-merged map, and the separation is load-bearing twice.
|
|
1214
|
+
* `spellBitfieldMembers` is normalized against `symbols` alone, so a derived shape can never
|
|
1215
|
+
* switch the named-bitfield spelling on for a map-less row (which would silently delete the
|
|
1216
|
+
* `/no-bitfield` axis's decline — see bitfield-members.test.ts). And it keeps the derivation
|
|
1217
|
+
* ATTRIBUTABLE: everything the map does stays keyed on the map. */
|
|
1218
|
+
inferredSymbols?: Map<string, SymbolInfo>;
|
|
1219
|
+
/** THE ORDER HALF of the same derivation (raise/globalshape.ts `orderLicensedGlobals`): the
|
|
1220
|
+
* globals whose address the assembly materialized BEFORE it scaled the index. Not a shape and
|
|
1221
|
+
* not a declaration — nothing is spelled from it here — it is stamped onto every `index` node it
|
|
1222
|
+
* reaches (`Expr.baseOrdered`, l3/ast.ts) for the L3 pass that decides whether a base gets a
|
|
1223
|
+
* HOME.
|
|
1224
|
+
*
|
|
1225
|
+
* A SUPERSET of `inferredSymbols`' names, and its own field for exactly that reason: a struct
|
|
1226
|
+
* element is licensed here and has no shape there. */
|
|
1227
|
+
orderLicensedGlobals?: ReadonlySet<string>;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/** The named global an `index` node's base denotes DIRECTLY, or undefined: the bare `&gSym` an
|
|
1231
|
+
* ordinary element access indexes, and the `(struct S *)&gSym` an array-of-struct element indexes
|
|
1232
|
+
* — the reinterpret cast is the base's spelling, not a different base.
|
|
1233
|
+
*
|
|
1234
|
+
* Deliberately not a search. A symbol buried inside a base's arithmetic is a base this licence has
|
|
1235
|
+
* nothing to say about — the order fact is about the address the pool word materialized, and what
|
|
1236
|
+
* a home would bind there is the sum, not the symbol — so it goes unstamped, which costs a
|
|
1237
|
+
* candidate and never a wrong one. The same two shapes are what `l3/basecse.ts` can hoist. */
|
|
1238
|
+
function indexBaseSymbol(e: Expr): string | undefined {
|
|
1239
|
+
if (e.k === 'addr') {
|
|
1240
|
+
return e.name;
|
|
1241
|
+
}
|
|
1242
|
+
return e.k === 'cast' && e.e.k === 'addr' ? e.e.name : undefined;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** Stamp `baseOrdered` on every `index` whose base names an order-licensed global — the ONE place
|
|
1246
|
+
* the L1 order fact enters the L3 tree.
|
|
1247
|
+
*
|
|
1248
|
+
* A post-pass over the finished body rather than a `...fromOrder` at each construction site: the
|
|
1249
|
+
* index node is built across `ptrMemberElement`, `memAccess` and `arrayAccess` at more sites than
|
|
1250
|
+
* `operandOff` itself reaches, the licence is per SYMBOL rather than per site, and a walk is
|
|
1251
|
+
* exhaustive by construction where a new site would silently carry no evidence. */
|
|
1252
|
+
function stampOrderedBases(body: Stmt[], licensed: ReadonlySet<string> | undefined): Stmt[] {
|
|
1253
|
+
if (licensed === undefined || licensed.size === 0) {
|
|
1254
|
+
return body;
|
|
1255
|
+
}
|
|
1256
|
+
const stamp = (e: Expr): Expr => {
|
|
1257
|
+
const mapped = mapExprChildren(e, stamp);
|
|
1258
|
+
if (mapped.k !== 'index') {
|
|
1259
|
+
return mapped;
|
|
1260
|
+
}
|
|
1261
|
+
const sym = indexBaseSymbol(mapped.base);
|
|
1262
|
+
return sym !== undefined && licensed.has(sym) ? { ...mapped, baseOrdered: true as const } : mapped;
|
|
1263
|
+
};
|
|
1264
|
+
return body.map((s) => mapStmtExprs(s, stamp));
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/** Test-only seams. SEPARATE from `StructureOptions` on purpose: `structureOptionsFor` builds that
|
|
1268
|
+
* one by spreading a target's `compilerBehaviors`, whose fields map 1:1 onto it, so a hook living
|
|
1269
|
+
* there would be settable from a TargetDescription. */
|
|
1270
|
+
export interface StructureHooks {
|
|
1271
|
+
/** `coalesceMergeNames`'s admission rules, so a test can run the pass with one gate DROPPED —
|
|
1272
|
+
* the ablation as a value rather than as a flag compiled into the shipped path. */
|
|
1273
|
+
nameCoalesceGates?: readonly Gate<NameMerge>[];
|
|
1274
|
+
/** `freshParamMerge`'s admission rules, ablatable the same way. */
|
|
1275
|
+
freshMergeGates?: readonly Gate<FreshMergeCarrier>[];
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/** A CANDIDATE SPELLING MUST NEVER UNLOCK A FUNCTION THE PRIMARY DECLINES. `varName` is not only
|
|
1279
|
+
* how values are spelled — the loop emitters' hazard predicates read it, and several ask "does
|
|
1280
|
+
* this edge copy survive identity elision", which merging two names quietly answers `no`. A pass
|
|
1281
|
+
* that made a hazard invisible would trade a loud decline for a silent wrong answer, so the
|
|
1282
|
+
* lever-less structuring runs first and its refusal stands. That is the whole invariant, rather
|
|
1283
|
+
* than a list of individually patched guards, and it costs one extra structuring — nothing next to
|
|
1284
|
+
* the compile the candidate exists to feed.
|
|
1285
|
+
*
|
|
1286
|
+
* It rests on `structure()` not mutating `fn`, which `structure-purity.test.ts` pins.
|
|
1287
|
+
*
|
|
1288
|
+
* SCOPE: refusals thrown by `structure()` itself. A decline can also come from `structureChecked`'s
|
|
1289
|
+
* boundary contracts, which run OUTSIDE it — `rank.ts` closes that half, where the contracts are. */
|
|
1290
|
+
function assertPrimaryAccepts(fn: Fn, opts: StructureOptions, hooks: StructureHooks): void {
|
|
1291
|
+
structure(
|
|
1292
|
+
fn,
|
|
1293
|
+
{
|
|
1294
|
+
...opts,
|
|
1295
|
+
coalesceMergeNames: false,
|
|
1296
|
+
freshParamMerge: false,
|
|
1297
|
+
materializeJoinFeeds: false,
|
|
1298
|
+
homeSharedAddresses: false,
|
|
1299
|
+
homeLoopExprs: false,
|
|
1300
|
+
homeDerivedReads: false,
|
|
1301
|
+
homeMergeFeeds: false,
|
|
1302
|
+
anchorConstCopies: false,
|
|
1303
|
+
anchorLoopEntryConsts: false,
|
|
1304
|
+
},
|
|
1305
|
+
hooks,
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/** What {@link earlyReturnArm} reads beyond its arguments: block dominance and forward
|
|
1310
|
+
* reachability, both of them whole-function facts computed once per `structure()`. */
|
|
1311
|
+
interface EarlyReturnArmDeps {
|
|
1312
|
+
dom: Map<Block, Set<Block>>;
|
|
1313
|
+
reachFrom: (b: Block) => Set<Block>;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function isRet(blk: Block): boolean {
|
|
1317
|
+
return blk.ops[blk.ops.length - 1]?.opcode === 'ret';
|
|
1318
|
+
}
|
|
1319
|
+
// An early `return` out of the loop: forward-walking from `to` WITHOUT re-entering the loop `body`,
|
|
1320
|
+
// every path terminates in a `ret`. agbcc/gcc merge every `return` into ONE epilogue block and each
|
|
1321
|
+
// return site just sets the return register and branches there, so a second body exit that lands on
|
|
1322
|
+
// such a chain is an early RETURN, not a break to a live merge — which is what lets two returns
|
|
1323
|
+
// merged through a shared `bx lr` recover as a `while` with an in-body early `return` instead of
|
|
1324
|
+
// declining as "multi-exit".
|
|
1325
|
+
//
|
|
1326
|
+
// The arm is structured AT the edge, so any block in it that a second path also reaches is emitted
|
|
1327
|
+
// twice. A duplicated `return v` is harmless — both copies sit on mutually exclusive paths, so this
|
|
1328
|
+
// is a FIDELITY rule, not a soundness one: a store or call written twice is source no compiler
|
|
1329
|
+
// would have produced from this asm, and the region it drags along is unbounded. A block escapes
|
|
1330
|
+
// that on two counts. `from` dominates `to` and `to` dominates the block, so every path reaching it
|
|
1331
|
+
// runs this edge's predecessor and then this region — and only one edge into `to` can satisfy the
|
|
1332
|
+
// first half, since two predecessors cannot both dominate it. And the region must not be reachable
|
|
1333
|
+
// from the loop's own exit, which dominance does NOT rule out: an arm landing straight on the
|
|
1334
|
+
// post-loop join dominates itself, and claiming it would emit the epilogue on both paths. Testing
|
|
1335
|
+
// `to` covers the whole region — a block dominated by `to` that the exit reached would mean the
|
|
1336
|
+
// exit reached `to`. The shared epilogue an arm branches to still has to be pure.
|
|
1337
|
+
//
|
|
1338
|
+
// Returns the blocks the arm OWNS — its exclusive part, which the loop emits inside its body, ahead
|
|
1339
|
+
// of the update — or null when this is not an early-`return` arm.
|
|
1340
|
+
function earlyReturnArm(
|
|
1341
|
+
{ dom, reachFrom }: EarlyReturnArmDeps,
|
|
1342
|
+
from: Block,
|
|
1343
|
+
to: Block,
|
|
1344
|
+
body: Set<Block>,
|
|
1345
|
+
exit: Block,
|
|
1346
|
+
): Set<Block> | null {
|
|
1347
|
+
const entryOwned = dom.get(to)!.has(from) && to !== exit && !reachFrom(exit).has(to);
|
|
1348
|
+
const owned = new Set<Block>();
|
|
1349
|
+
const seen = new Set<Block>();
|
|
1350
|
+
const stack = [to];
|
|
1351
|
+
while (stack.length) {
|
|
1352
|
+
const bb = stack.pop()!;
|
|
1353
|
+
if (seen.has(bb)) {
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
seen.add(bb);
|
|
1357
|
+
if (body.has(bb)) {
|
|
1358
|
+
return null;
|
|
1359
|
+
} // re-enters the loop → not an exit
|
|
1360
|
+
if (entryOwned && dom.get(bb)!.has(to)) {
|
|
1361
|
+
owned.add(bb);
|
|
1362
|
+
} else if (bb.ops.some((op) => EFFECTFUL_OPS.has(op.opcode))) {
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
const t = bb.ops[bb.ops.length - 1];
|
|
1366
|
+
if (t.opcode === 'ret') {
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
if (t.opcode === 'br' || t.opcode === 'cond_br') {
|
|
1370
|
+
for (const s of t.successors) {
|
|
1371
|
+
stack.push(s.block);
|
|
1372
|
+
}
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
return null; // switch_br / unknown terminator → decline
|
|
1376
|
+
}
|
|
1377
|
+
return owned;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/** The two facts a merge carrier's declared type contributes to `canTakeName` — see the rule's own
|
|
1381
|
+
* comment there for why the signedness half is part of the width at sub-word sizes. */
|
|
1382
|
+
function carrierWidth(t: IrType | undefined): number {
|
|
1383
|
+
return t?.kind === 'int' ? t.width : 32;
|
|
1384
|
+
}
|
|
1385
|
+
function carrierSign(t: IrType | undefined): boolean | undefined {
|
|
1386
|
+
return t?.kind === 'int' && t.width < 32 ? t.signed : undefined;
|
|
657
1387
|
}
|
|
658
1388
|
|
|
659
|
-
export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
1389
|
+
export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureHooks = {}): SFn {
|
|
660
1390
|
const {
|
|
661
1391
|
returnsVoid = false,
|
|
662
1392
|
coalesceLoopInit = false,
|
|
663
1393
|
preserveDivergentBranchSense = true,
|
|
664
|
-
|
|
1394
|
+
negateJoinedBranchSense = preserveDivergentBranchSense,
|
|
1395
|
+
orderArgCopiesByWriteOrder = true,
|
|
1396
|
+
preferDefPosCopyOrder = false,
|
|
665
1397
|
switchAllowsNeqCase = true,
|
|
1398
|
+
switchAllowsBoundCase = false,
|
|
1399
|
+
switchArmsFollowLayout = false,
|
|
1400
|
+
spellSwitchFallthrough = true,
|
|
1401
|
+
spillSlotOrder,
|
|
1402
|
+
defOrderLoadPairs = true,
|
|
666
1403
|
anchorConstCopies = false,
|
|
1404
|
+
anchorLoopEntryConsts = false,
|
|
667
1405
|
littleEndian = true,
|
|
668
|
-
spellBitfieldMembers = true,
|
|
1406
|
+
spellBitfieldMembers: bitfieldSpellingWanted = true,
|
|
1407
|
+
spellPtrMemberElements: ptrElementSpellingWanted = true,
|
|
1408
|
+
spellDeclaredSubscripts: declRankSpellingWanted = true,
|
|
669
1409
|
rereadGlobals = false,
|
|
1410
|
+
materializeJoinFeeds = false,
|
|
1411
|
+
homeSharedAddresses = false,
|
|
1412
|
+
homeLoopExprs = false,
|
|
1413
|
+
homeDerivedReads = false,
|
|
1414
|
+
homeMergeFeeds = false,
|
|
1415
|
+
readsStayWhereWritten = false,
|
|
1416
|
+
unsignedCompareSpelling = false,
|
|
1417
|
+
coalesceMergeNames = false,
|
|
1418
|
+
freshParamMerge = false,
|
|
670
1419
|
onGap = 'strict',
|
|
671
|
-
symbols,
|
|
1420
|
+
symbols: mapSymbols,
|
|
1421
|
+
inferredSymbols,
|
|
1422
|
+
orderLicensedGlobals,
|
|
672
1423
|
} = opts;
|
|
1424
|
+
// THE shape dictionary the rendering context asks, map-first. Built as a lookup rather than a
|
|
1425
|
+
// merged Map because the map is the PROJECT's and is asked by name for a whole project's worth
|
|
1426
|
+
// of symbols — copying it per structuring is work proportional to the project, and a ranked run
|
|
1427
|
+
// structures one function thousands of times (the same argument the `laddr` name minter makes).
|
|
1428
|
+
const symbols: SymbolLookup | undefined =
|
|
1429
|
+
mapSymbols !== undefined || (inferredSymbols !== undefined && inferredSymbols.size > 0)
|
|
1430
|
+
? {
|
|
1431
|
+
get: (n) => mapSymbols?.get(n) ?? inferredSymbols?.get(n),
|
|
1432
|
+
has: (n) => mapSymbols?.has(n) === true || inferredSymbols?.has(n) === true,
|
|
1433
|
+
}
|
|
1434
|
+
: undefined;
|
|
1435
|
+
// Only the MAP makes the named bitfield spelling available, so with no map this is not a choice.
|
|
1436
|
+
// Normalized once here rather than left to each reader's own `symCtx &&` guard, because rank.ts's
|
|
1437
|
+
// `/no-bitfield` decline rests on both arms structuring the IDENTICAL tree without a map — a
|
|
1438
|
+
// second reader added outside that guard would otherwise delete a candidate silently, and nothing
|
|
1439
|
+
// reports a candidate that was never enumerated (bitfield-members.test.ts).
|
|
1440
|
+
// Against the PROJECT MAP alone, never the derived shapes: only a map carries bitfield members,
|
|
1441
|
+
// and keying this on the union would flip the `/no-bitfield` axis's zero point on a map-less row.
|
|
1442
|
+
const spellBitfieldMembers = mapSymbols !== undefined && bitfieldSpellingWanted;
|
|
1443
|
+
// These levers all change which edge copies elide as identities (extra materialization does
|
|
1444
|
+
// too), which the loop emitters' hazard predicates read — so the invariant above covers each.
|
|
1445
|
+
// A per-compiler DEFAULT is not among them, however much it materializes: the primary IS this
|
|
1446
|
+
// target's defaults, so resetting one would probe a spelling asmlift never emits here.
|
|
1447
|
+
//
|
|
1448
|
+
// THREE OF rank.ts's TEN `STRUCTURING_AXES` ARE DELIBERATE NON-MEMBERS, each for its own reason,
|
|
1449
|
+
// and the list here is the half of the split this side owns:
|
|
1450
|
+
// - `/reread-globals` (rereadGlobals) is an ANALYSIS option, and it only ever RELAXES: it
|
|
1451
|
+
// widens a load's render positions and narrows the write set that bars it, so it removes
|
|
1452
|
+
// materializations rather than minting them. Extra materialization is what this guard is
|
|
1453
|
+
// about (see above), and this axis adds none;
|
|
1454
|
+
// - `/uns-cmp` (unsignedCompareSpelling) writes `varType` and inserts casts at compares. It
|
|
1455
|
+
// touches no name and no copy, so no edge copy changes its elision under it;
|
|
1456
|
+
// - `/copy-defpos` (preferDefPosCopyOrder) REORDERS the copies of one edge and adds or drops
|
|
1457
|
+
// none. rank.ts states the same thing from the axis side, in the terms that matter there: a
|
|
1458
|
+
// reordering cannot rescue a spelling whose OFF sibling failed the boundary contracts.
|
|
1459
|
+
if (
|
|
1460
|
+
coalesceMergeNames ||
|
|
1461
|
+
freshParamMerge ||
|
|
1462
|
+
materializeJoinFeeds ||
|
|
1463
|
+
homeSharedAddresses ||
|
|
1464
|
+
homeLoopExprs ||
|
|
1465
|
+
homeDerivedReads ||
|
|
1466
|
+
homeMergeFeeds ||
|
|
1467
|
+
anchorConstCopies
|
|
1468
|
+
) {
|
|
1469
|
+
assertPrimaryAccepts(fn, opts, hooks);
|
|
1470
|
+
}
|
|
673
1471
|
const defs = defOpMap(fn);
|
|
674
1472
|
const preds = predecessorBlocks(fn);
|
|
675
1473
|
const ipdom = postDominators(fn);
|
|
@@ -681,7 +1479,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
681
1479
|
returnsVoid,
|
|
682
1480
|
{
|
|
683
1481
|
defs,
|
|
1482
|
+
dom,
|
|
684
1483
|
rereadGlobals,
|
|
1484
|
+
materializeJoinFeeds,
|
|
1485
|
+
homeSharedAddresses,
|
|
1486
|
+
homeLoopExprs,
|
|
1487
|
+
homeDerivedReads,
|
|
1488
|
+
homeMergeFeeds,
|
|
1489
|
+
readsStayWhereWritten,
|
|
685
1490
|
// the map's own declaration truth: a volatile object's read may not be duplicated or moved
|
|
686
1491
|
volatileGlobal: (n) => {
|
|
687
1492
|
const si = symbols?.get(n);
|
|
@@ -704,13 +1509,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
704
1509
|
// in this layer's namespace: params, locals, every gaddr symbol, and the project's symbol map —
|
|
705
1510
|
// none of which the frontend can see. A frontend-chosen `sp0` silently shadowed a project global
|
|
706
1511
|
// of the same name. `sp<off>` uniquified with underscores until free; one name per offset.
|
|
707
|
-
|
|
1512
|
+
// `undef` locals are minted in the same pass off the same `taken` set — they need the same
|
|
1513
|
+
// protection from the symbol map, gaddr symbols and callee names that `laddr` names do.
|
|
1514
|
+
//
|
|
1515
|
+
// The map is CONSULTED, never copied in: `taken` is only probed and added to, so asking the
|
|
1516
|
+
// name-keyed map answers the same question for every name in it. Copying it is work
|
|
1517
|
+
// proportional to the whole PROJECT's symbol count on every structuring, and a ranked run
|
|
1518
|
+
// structures one function thousands of times.
|
|
1519
|
+
const { laddr: laddrName, undef: undefName } = (() => {
|
|
708
1520
|
const taken = new Set<string>();
|
|
709
|
-
|
|
710
|
-
for (const [n] of symbols) {
|
|
711
|
-
taken.add(n);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
1521
|
+
const isTaken = (n: string): boolean => taken.has(n) || symbols?.has(n) === true;
|
|
714
1522
|
for (const b of fn.blocks) {
|
|
715
1523
|
for (const op of b.ops) {
|
|
716
1524
|
if (op.opcode === 'gaddr') {
|
|
@@ -722,27 +1530,36 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
722
1530
|
}
|
|
723
1531
|
}
|
|
724
1532
|
}
|
|
1533
|
+
const mint = (base: string): string => {
|
|
1534
|
+
let n = base;
|
|
1535
|
+
while (isTaken(n)) {
|
|
1536
|
+
n += '_';
|
|
1537
|
+
}
|
|
1538
|
+
taken.add(n);
|
|
1539
|
+
return n;
|
|
1540
|
+
};
|
|
725
1541
|
const byOff = new Map<number, string>();
|
|
726
1542
|
const names = new Map<Op, string>();
|
|
1543
|
+
const undefNames = new Map<Op, string>();
|
|
727
1544
|
for (const b of fn.blocks) {
|
|
728
1545
|
for (const op of b.ops) {
|
|
729
|
-
if (op.opcode
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
n = `sp${off}`;
|
|
736
|
-
while (taken.has(n)) {
|
|
737
|
-
n += '_';
|
|
1546
|
+
if (op.opcode === 'laddr') {
|
|
1547
|
+
const off = op.attrs.off as number;
|
|
1548
|
+
let n = byOff.get(off);
|
|
1549
|
+
if (n === undefined) {
|
|
1550
|
+
n = mint(`sp${off}`);
|
|
1551
|
+
byOff.set(off, n);
|
|
738
1552
|
}
|
|
739
|
-
|
|
740
|
-
|
|
1553
|
+
names.set(op, n);
|
|
1554
|
+
} else if (op.opcode === 'undef') {
|
|
1555
|
+
// Named from the key, like laddr's `sp<off>`: `uninit_sp8` says which frame slot to look
|
|
1556
|
+
// at in the assembly, and it stays put where a running counter would renumber every local
|
|
1557
|
+
// when an unrelated edit changed the order ops are minted in.
|
|
1558
|
+
undefNames.set(op, mint(`uninit_${String(op.attrs.key).replace('@', '')}`));
|
|
741
1559
|
}
|
|
742
|
-
names.set(op, n);
|
|
743
1560
|
}
|
|
744
1561
|
}
|
|
745
|
-
return names;
|
|
1562
|
+
return { laddr: names, undef: undefNames };
|
|
746
1563
|
})();
|
|
747
1564
|
|
|
748
1565
|
const scalarGlobals = new Set<string>();
|
|
@@ -795,9 +1612,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
795
1612
|
// Declaration-shape OVERRIDE (symbol map): a project-declared array/struct global is an
|
|
796
1613
|
// AGGREGATE whatever the usage inference saw — a lone off-0 access to `extern u16 tbl[]`
|
|
797
1614
|
// must still spell through the aggregate/array forms, never the bare scalar `tbl`.
|
|
1615
|
+
// Driven from the FUNCTION's own globals, for the same reason the name minter above consults
|
|
1616
|
+
// the map instead of copying it: a name outside `scalarGlobals` has nothing to override.
|
|
798
1617
|
if (symbols) {
|
|
799
|
-
for (const
|
|
800
|
-
|
|
1618
|
+
for (const n of [...scalarGlobals]) {
|
|
1619
|
+
const shape = symbols.get(n)?.shape;
|
|
1620
|
+
if (shape === 'array' || shape === 'struct') {
|
|
801
1621
|
scalarGlobals.delete(n);
|
|
802
1622
|
}
|
|
803
1623
|
}
|
|
@@ -807,22 +1627,45 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
807
1627
|
// Symbol-map rendering context (memAccess/arrayAccess): shape lookups + the env registry for
|
|
808
1628
|
// array-shaped globals actually referenced (they surface as SFn.globals — typed, undeclared).
|
|
809
1629
|
const shapedGlobalTypes = new Map<string, IrType>();
|
|
1630
|
+
const declaredFieldCache = new Map<string, DeclaredField[] | null>();
|
|
810
1631
|
const symCtx: SymRenderCtx | undefined = symbols
|
|
811
|
-
? {
|
|
1632
|
+
? {
|
|
1633
|
+
info: (n) => symbols.get(n),
|
|
1634
|
+
noteGlobal: (n, t) => shapedGlobalTypes.set(n, t),
|
|
1635
|
+
ptrElements: ptrElementSpellingWanted,
|
|
1636
|
+
declRank: declRankSpellingWanted,
|
|
1637
|
+
fieldsOf: (n) => {
|
|
1638
|
+
const hit = declaredFieldCache.get(n);
|
|
1639
|
+
if (hit !== undefined) {
|
|
1640
|
+
return hit;
|
|
1641
|
+
}
|
|
1642
|
+
const si = symbols.get(n);
|
|
1643
|
+
const fields =
|
|
1644
|
+
si?.shape === 'struct'
|
|
1645
|
+
? declaredFields(si.layout)
|
|
1646
|
+
: si?.shape === 'pointer'
|
|
1647
|
+
? pointeeFields(si.pointee)
|
|
1648
|
+
: null;
|
|
1649
|
+
declaredFieldCache.set(n, fields);
|
|
1650
|
+
return fields;
|
|
1651
|
+
},
|
|
1652
|
+
}
|
|
812
1653
|
: undefined;
|
|
813
1654
|
|
|
814
|
-
/** A bare `gSym` naming a
|
|
815
|
-
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
1655
|
+
/** A value the MAP declares a pointer: a bare `gSym` naming a pointer global (the VALUE of a
|
|
1656
|
+
* pointer cell), or a named MEMBER whose declaration is a pointer (`gSym.pBuf`, `gPtr->pBuf`).
|
|
1657
|
+
* Load, store and compare of such a 4-byte cell are identical for any object-pointer type, so
|
|
1658
|
+
* the declared pointee never matters to THEM; arithmetic on the loaded value is the opposite
|
|
1659
|
+
* case, where the pointee's size scales what is added and every stride must therefore be made
|
|
818
1660
|
* explicit (`(u8 *)gPtr + K`). `ctype` cannot see any of this: it types only params/locals, so
|
|
819
|
-
*
|
|
820
|
-
const
|
|
1661
|
+
* both spellings render `undefined` there. */
|
|
1662
|
+
const isPtrValue = (x: Expr): boolean =>
|
|
1663
|
+
(x.k === 'var' && symCtx?.info(x.name)?.shape === 'pointer') || ptrMemberDecl(x, symCtx) !== null;
|
|
821
1664
|
|
|
822
1665
|
/** Operands `-`/`~` cannot take as spelled: a rendered pointer, a bare `&gSym`, a pointer
|
|
823
1666
|
* global's value. All three are ill-formed C under a unary arithmetic operator — the asm did
|
|
824
1667
|
* 32-bit integer math on the address, so that is what gets spelled. */
|
|
825
|
-
const needsIntSpelling = (x: Expr): boolean => ctype(x)?.kind === 'ptr' || x.k === 'addr' ||
|
|
1668
|
+
const needsIntSpelling = (x: Expr): boolean => ctype(x)?.kind === 'ptr' || x.k === 'addr' || isPtrValue(x);
|
|
826
1669
|
|
|
827
1670
|
// --- loop discovery (loops.ts): natural loops via dominator back-edges + the nesting forest ---
|
|
828
1671
|
const forest = analyzeLoops(fn, dom);
|
|
@@ -853,11 +1696,46 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
853
1696
|
// (entered by a plain br / fall-through) is a bottom-tested loop whose body always runs
|
|
854
1697
|
// once — a single-block do-while — and is claimed by the structured-loop discovery below
|
|
855
1698
|
// instead (each header lives in exactly ONE map, so seeding stays single-pass).
|
|
856
|
-
|
|
1699
|
+
const direct = (preds.get(b) ?? []).some((pr) => isGuardShapedPred(pr, b, exit));
|
|
1700
|
+
// Or THROUGH a pure preheader: the compiler's loop-invariant motion parks computations in a
|
|
1701
|
+
// forwarding block between the guard and the header (the mask re-materialization of a busy
|
|
1702
|
+
// poll). The block must be PURE and unmaterialized — its defs then render inline wherever
|
|
1703
|
+
// the loop reads them and nothing about it needs a statement position of its own — with a
|
|
1704
|
+
// single in-edge and a plain `br` into the header, so the guard's branch is still the only
|
|
1705
|
+
// decision. Anything else keeps the unguarded do-while recovery.
|
|
1706
|
+
// The preheader claim is limited to loops whose header→exit edge carries NO args: with
|
|
1707
|
+
// nothing riding the exit, the fusion site's exit-copy obligations (staleExit, the sink) are
|
|
1708
|
+
// vacuous. The condition and zero-trip hazards stay live there, so redirecting a loop from
|
|
1709
|
+
// the do-while path yields the new shape or a LOUD decline — never silent wrong C.
|
|
1710
|
+
const exitCarriesNothing = (successorTo(b, exit)?.args ?? []).length === 0;
|
|
1711
|
+
const preheader =
|
|
1712
|
+
direct || !exitCarriesNothing
|
|
1713
|
+
? undefined
|
|
1714
|
+
: (preds.get(b) ?? []).find((P) => {
|
|
1715
|
+
const pt = P.ops[P.ops.length - 1];
|
|
1716
|
+
return (
|
|
1717
|
+
P !== b &&
|
|
1718
|
+
pt?.opcode === 'br' &&
|
|
1719
|
+
P.params.length === 0 &&
|
|
1720
|
+
P.ops.every((o) => !EFFECTFUL_OPS.has(o.opcode) && !materialize.has(o)) &&
|
|
1721
|
+
// at least one def the LOOP BODY reads — the loop-invariant-motion shape this claim
|
|
1722
|
+
// exists for. A block that only computes the init args is the do-while path's
|
|
1723
|
+
// ordinary entry chain, and that path's sink machinery handles it better.
|
|
1724
|
+
P.ops.some((o) => o.results.some((r) => (useSitesOf.get(r) ?? []).some((site) => site.blk === b))) &&
|
|
1725
|
+
(preds.get(P) ?? []).length === 1 &&
|
|
1726
|
+
isGuardShapedPred(preds.get(P)![0], P, exit)
|
|
1727
|
+
);
|
|
1728
|
+
});
|
|
1729
|
+
if (!direct && !preheader) {
|
|
857
1730
|
continue;
|
|
858
1731
|
}
|
|
859
1732
|
const back = successorTo(b, b)!;
|
|
860
|
-
loops.set(b, {
|
|
1733
|
+
loops.set(b, {
|
|
1734
|
+
header: b,
|
|
1735
|
+
exit,
|
|
1736
|
+
backArgOfParam: b.params.map((_, i) => back.args[i]),
|
|
1737
|
+
...(preheader ? { preheader } : {}),
|
|
1738
|
+
});
|
|
861
1739
|
}
|
|
862
1740
|
|
|
863
1741
|
// --- structured natural loops (test-at-top `while` / bottom-test `do-while`) ---
|
|
@@ -866,48 +1744,6 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
866
1744
|
// targets) are allowed in-body. The shape then splits on WHERE the exit lives: the HEADER exits
|
|
867
1745
|
// (pure test-at-top) → `while`; the LATCH exits (body-first) → `do-while`. Anything that fails
|
|
868
1746
|
// declines to plain if-recovery, which re-enters the header and fails loud via `onStack`.
|
|
869
|
-
const isRet = (blk: Block) => blk.ops[blk.ops.length - 1]?.opcode === 'ret';
|
|
870
|
-
// A pure "return trampoline" out of the loop: forward-walking from `start` WITHOUT re-entering the
|
|
871
|
-
// loop `body`, every path terminates in a `ret` and no block on the way carries an OBSERVABLE side
|
|
872
|
-
// effect (store/astore/call/opaque). agbcc/gcc merge every `return` into ONE epilogue block and each
|
|
873
|
-
// return site just sets the return register and branches there — so a second body exit that lands on
|
|
874
|
-
// such a chain is an early RETURN, not a break to a live merge. Structuring it on more than one exit
|
|
875
|
-
// path is sound precisely because it is side-effect-free (a duplicated `return v` is harmless).
|
|
876
|
-
// This lets two returns merged through a shared `bx lr` recover as a `while` with an in-body early
|
|
877
|
-
// `return` instead of declining as "multi-exit".
|
|
878
|
-
const leadsToReturnOnly = (start: Block, body: Set<Block>): boolean => {
|
|
879
|
-
const seen = new Set<Block>();
|
|
880
|
-
const stack = [start];
|
|
881
|
-
while (stack.length) {
|
|
882
|
-
const bb = stack.pop()!;
|
|
883
|
-
if (seen.has(bb)) {
|
|
884
|
-
continue;
|
|
885
|
-
}
|
|
886
|
-
seen.add(bb);
|
|
887
|
-
if (body.has(bb)) {
|
|
888
|
-
return false;
|
|
889
|
-
} // re-enters the loop → not a pure exit
|
|
890
|
-
if (
|
|
891
|
-
bb.ops.some(
|
|
892
|
-
(op) => op.opcode === 'store' || op.opcode === 'astore' || op.opcode === 'call' || op.opcode === 'opaque',
|
|
893
|
-
)
|
|
894
|
-
) {
|
|
895
|
-
return false;
|
|
896
|
-
}
|
|
897
|
-
const t = bb.ops[bb.ops.length - 1];
|
|
898
|
-
if (t.opcode === 'ret') {
|
|
899
|
-
continue;
|
|
900
|
-
}
|
|
901
|
-
if (t.opcode === 'br' || t.opcode === 'cond_br') {
|
|
902
|
-
for (const s of t.successors) {
|
|
903
|
-
stack.push(s.block);
|
|
904
|
-
}
|
|
905
|
-
continue;
|
|
906
|
-
}
|
|
907
|
-
return false; // switch_br / unknown terminator → decline
|
|
908
|
-
}
|
|
909
|
-
return true;
|
|
910
|
-
};
|
|
911
1747
|
const whileLoops = new Map<Block, WhileLoopInfo>();
|
|
912
1748
|
const doWhileLoops = new Map<Block, DoWhileInfo>();
|
|
913
1749
|
for (const nl of forest.byHeader.values()) {
|
|
@@ -963,14 +1799,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
963
1799
|
// whose result also feeds the body would be evaluated twice per iteration). A `load` is fine —
|
|
964
1800
|
// but NOT a materialized one: its temp assignment renders only via sideEffects(), which a
|
|
965
1801
|
// condition-only header never emits, so its uses would read an unassigned variable.
|
|
966
|
-
const headerPure = !h.ops.some(
|
|
967
|
-
(op) =>
|
|
968
|
-
op.opcode === 'store' ||
|
|
969
|
-
op.opcode === 'astore' ||
|
|
970
|
-
op.opcode === 'opaque' ||
|
|
971
|
-
op.opcode === 'call' ||
|
|
972
|
-
materialize.has(op),
|
|
973
|
-
);
|
|
1802
|
+
const headerPure = !h.ops.some((op) => EFFECTFUL_OPS.has(op.opcode) || materialize.has(op));
|
|
974
1803
|
|
|
975
1804
|
let exitFrom: Block,
|
|
976
1805
|
exit: Block,
|
|
@@ -991,14 +1820,25 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
991
1820
|
continue; // neither a clean pre-tested nor bottom-tested single-exit shape
|
|
992
1821
|
}
|
|
993
1822
|
// Single loop exit (ret-aware): the chosen exit is the ONE real exit; every OTHER edge leaving
|
|
994
|
-
// the body must be an early `return`
|
|
995
|
-
//
|
|
996
|
-
//
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1823
|
+
// the body must be an early `return` (`earlyReturnArm`) or a ret-terminated target. A second exit
|
|
1824
|
+
// that lands on a LIVE non-return merge is a genuine `break`/second structured exit → decline.
|
|
1825
|
+
// The arms are kept: emission needs to know which edges out of the body end an iteration rather
|
|
1826
|
+
// than continue it.
|
|
1827
|
+
const arms: LoopArm[] = [];
|
|
1828
|
+
let singleExit = true;
|
|
1829
|
+
for (const e of nl.exitEdges) {
|
|
1830
|
+
if (e.from === exitFrom && e.to === exit) {
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
const owned = earlyReturnArm({ dom, reachFrom }, e.from, e.to, nl.body, exit);
|
|
1834
|
+
if (owned) {
|
|
1835
|
+
arms.push({ from: e.from, to: e.to, owned });
|
|
1836
|
+
} else if (!isRet(e.to)) {
|
|
1837
|
+
singleExit = false;
|
|
1838
|
+
break;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
if (!singleExit) {
|
|
1002
1842
|
continue;
|
|
1003
1843
|
}
|
|
1004
1844
|
|
|
@@ -1010,9 +1850,10 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1010
1850
|
latch,
|
|
1011
1851
|
forwardPreds: nl.forwardPreds,
|
|
1012
1852
|
body: nl.body,
|
|
1853
|
+
arms,
|
|
1013
1854
|
});
|
|
1014
1855
|
} else {
|
|
1015
|
-
doWhileLoops.set(h, { header: h, latch, exit, forwardPreds: nl.forwardPreds, body: nl.body });
|
|
1856
|
+
doWhileLoops.set(h, { header: h, latch, exit, forwardPreds: nl.forwardPreds, body: nl.body, arms });
|
|
1016
1857
|
}
|
|
1017
1858
|
}
|
|
1018
1859
|
|
|
@@ -1028,12 +1869,27 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1028
1869
|
varName.set(p, `a${i}`);
|
|
1029
1870
|
varType.set(`a${i}`, p.type);
|
|
1030
1871
|
});
|
|
1872
|
+
/** The function's own parameters, as VALUES — `FRESH_MERGE_GATES`' `param-rooted` half. A name
|
|
1873
|
+
* test would not do: a loop variable that adopted `a0` carries the name without being the
|
|
1874
|
+
* parameter. */
|
|
1875
|
+
const entryParams = new Set<Value>(entry.params);
|
|
1876
|
+
/** Merge params that took a fresh home because `param-rooted` refused their parameter carrier —
|
|
1877
|
+
* the set's other half, so a further merge carrying one of them is refused too. It records the
|
|
1878
|
+
* merges that ended up FRESH and no others: one that refuses a parameter and then adopts an
|
|
1879
|
+
* ordinary local passes the value on under that local's name (336 firings over 6 corpus rows),
|
|
1880
|
+
* which is the chain-rooted widening rather than this rule. */
|
|
1881
|
+
const paramSeededMerges = new Set<Value>();
|
|
1031
1882
|
const backArgName = new Map<Value, string>();
|
|
1032
1883
|
// The C static type of a rendered expression, over the declared variable types — what decides
|
|
1033
1884
|
// whether a memory access's base may be dereferenced as spelled (memAccess/arrayAccess).
|
|
1034
|
-
const
|
|
1885
|
+
const vtEnv = (n: string): IrType | undefined => varType.get(n);
|
|
1886
|
+
const ctype = (e0: Expr): IrType | undefined => exprCType(e0, vtEnv);
|
|
1035
1887
|
|
|
1036
|
-
/**
|
|
1888
|
+
/** A value assigned into a TEMP THIS PASS DECLARES, spelled so the assignment is legal against
|
|
1889
|
+
* that declaration. Two inhabitants, one argument: the value's type comes from somewhere this
|
|
1890
|
+
* pass does not control, and the temp's comes from here.
|
|
1891
|
+
*
|
|
1892
|
+
* `&gSym` assigned to a `T *` local: the address of an AGGREGATE is not a pointer to its
|
|
1037
1893
|
* element. `&gArr` is `T (*)[n]`, `&gStruct` is `struct S *`, and neither is assignable to
|
|
1038
1894
|
* `T *` — yet the IR's `gaddr` value legitimately has type `T *`, because that is what the asm
|
|
1039
1895
|
* loaded. The bare spelling therefore states a type the project's own header contradicts.
|
|
@@ -1046,17 +1902,34 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1046
1902
|
* moves either way and the rule that decides it is pinned in test/deref-typing.test.ts instead.
|
|
1047
1903
|
*
|
|
1048
1904
|
* The test is whether `&gSym`'s rendered type PROVABLY equals the destination's, not whether the
|
|
1049
|
-
* symbol looks like an aggregate. A
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1905
|
+
* symbol looks like an aggregate. A SHAPE ENUMERATION MISSES THREE WAYS, each real:
|
|
1906
|
+
* `shape:'pointer'` declares a pointer cell (`void *gSym`, or `struct Tag *gSym` when the pointee
|
|
1907
|
+
* has a declarable layout), so `&gSym` is a pointer-to-pointer either way; a `shape:'scalar'`
|
|
1052
1908
|
* whose width differs from the destination's pointee gives `s32 *` for a `u16 *` slot; and a
|
|
1053
|
-
* NAME-ONLY symbol is synthesized as `extern u32 gSym;` (declare.ts), which is `u32
|
|
1054
|
-
*
|
|
1909
|
+
* NAME-ONLY symbol is synthesized as `extern u32 gSym;` (declare.ts), which is `u32 *`. So the
|
|
1910
|
+
* default is to CAST, and the cast is omitted only
|
|
1055
1911
|
* where the declared cell type is known and matches exactly. Byte-identical either way, so the
|
|
1056
1912
|
* cost of casting one time too many is a redundant `(T *)`, never a wrong address. */
|
|
1057
|
-
const
|
|
1913
|
+
const intoDeclaredTemp = (name: string, value: Expr): Expr => {
|
|
1058
1914
|
const t = varType.get(name);
|
|
1059
|
-
if (t
|
|
1915
|
+
if (t === undefined) {
|
|
1916
|
+
return value;
|
|
1917
|
+
}
|
|
1918
|
+
// ── the MAP's pointer values, whose type this side of the program does not own ──────────────
|
|
1919
|
+
// A `gaddr` at least states a type the IR knows. `gSym.pBuf` and a pointer global's own value
|
|
1920
|
+
// state one only the MAP knows, and `ctype` — which types params and locals — reads them as
|
|
1921
|
+
// `undefined`, so the test above cannot see them at all. Assigning one bare declares that the
|
|
1922
|
+
// temp's type and the project's declaration of that pointer are the same type, which nothing
|
|
1923
|
+
// here established: the project's header says `struct Unk_03005284 *` where the recovered temp
|
|
1924
|
+
// says `struct Struct0 *`, and `-Werror` makes the mismatch fatal in the tree the source is
|
|
1925
|
+
// pasted into. The destination's type is the one this pass DID choose, so unlike the
|
|
1926
|
+
// map-declared cells below it can be named exactly rather than defused through `void *`. That
|
|
1927
|
+
// holds for an INTEGER temp too, and there the diagnostic is the mirror one, `assignment makes
|
|
1928
|
+
// integer from pointer without a cast` — same site, same argument, same `(T)` answer.
|
|
1929
|
+
if (isPtrValue(value)) {
|
|
1930
|
+
return { k: 'cast', to: t, e: value };
|
|
1931
|
+
}
|
|
1932
|
+
if (t.kind !== 'ptr' || value.k !== 'addr') {
|
|
1060
1933
|
return value;
|
|
1061
1934
|
}
|
|
1062
1935
|
// The only provably-redundant case: a NON-VOLATILE scalar cell whose DECLARED type is the
|
|
@@ -1111,7 +1984,35 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1111
1984
|
paramBlock.set(pv, blk);
|
|
1112
1985
|
}
|
|
1113
1986
|
}
|
|
1987
|
+
// AND THE NAME MUST BE EXACTLY AS WIDE AS `p`. Every edge into `B` copies its argument into
|
|
1988
|
+
// `name`, which is a C assignment through `name`'s declaration, and the name's type is fixed by
|
|
1989
|
+
// its FIRST claimant — so a width mismatch loses a truncation in one direction or the other:
|
|
1990
|
+
// • name NARROWER than the merged value truncates it. `u8 a0` (raise/paramwidth.ts) adopted by
|
|
1991
|
+
// a merge of `a0` with `0x1234` emits `a0 = 4660`, which agbcc compiles to 52.
|
|
1992
|
+
// • name WIDER than a narrow carrier drops the extension the carrier's declaration WAS. A
|
|
1993
|
+
// narrow block parameter (raise/narrowlocal.ts) has had its one reading extension deleted
|
|
1994
|
+
// against the promise that reading the local re-applies it, so adopting an `s32` name emits
|
|
1995
|
+
// `*out = a0` where the graph says `*out = (s16)a0` — a silent wrong answer, not a worse one.
|
|
1996
|
+
// Register-width carriers are unaffected — a pointer or an `unknown` is a full word, so the two
|
|
1997
|
+
// narrow-typing passes are the only producers either half can see.
|
|
1998
|
+
//
|
|
1999
|
+
// AND AT A SUB-WORD WIDTH, THE SIGNEDNESS IS PART OF THE WIDTH. A narrow declaration is where
|
|
2000
|
+
// the extension went, and `u8` re-applies a DIFFERENT extension than `s8` — same bytes in the
|
|
2001
|
+
// variable, different value at every read. `sa3`'s `sub_80B4654` (the IR is built in
|
|
2002
|
+
// test/narrow-local.test.ts) merges a `zext8` arm with its own `u8` parameter and reads the merge
|
|
2003
|
+
// through `lsls #24 / asrs #24`: the carrier is `s8`, and
|
|
2004
|
+
// letting it adopt the `u8` parameter's name emits `sub_80B4FA8(a0, a1, …)`, which passes 144
|
|
2005
|
+
// where the target passes -112 for every byte with bit 7 set. Width alone said 8 === 8 and
|
|
2006
|
+
// admitted it — a silent wrong answer, not a worse score. At 32 bits the two spellings ARE the
|
|
2007
|
+
// same bytes at a read, which is the mismatch `structure/namecoalesce.ts`'s header names and this
|
|
2008
|
+
// rule deliberately still tolerates.
|
|
1114
2009
|
const canTakeName = (p: Value, B: Block, name: string, pureAlias = false): boolean => {
|
|
2010
|
+
if (carrierWidth(varType.get(name)) !== carrierWidth(p.type)) {
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
if (carrierSign(varType.get(name)) !== carrierSign(p.type)) {
|
|
2014
|
+
return false;
|
|
2015
|
+
}
|
|
1115
2016
|
if (B.params.some((q) => q !== p && varName.get(q) === name)) {
|
|
1116
2017
|
return false;
|
|
1117
2018
|
}
|
|
@@ -1142,8 +2043,68 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1142
2043
|
return false;
|
|
1143
2044
|
}
|
|
1144
2045
|
}
|
|
2046
|
+
// AND A VALUE NOBODY NAMED IS STILL A READER OF THIS NAME. The loop above asks which NAMED
|
|
2047
|
+
// values are live at `B`; an unnamed one is not stored anywhere, it is RE-DERIVED at its use
|
|
2048
|
+
// from whatever its operands are called then — so a value live into `B` whose inlined
|
|
2049
|
+
// expression mentions `name` reads the merge's assignment instead of what it was defined from.
|
|
2050
|
+
// `s32 t = a - b; if (a > 0) { a = a + b; } return a + t;` is the whole shape: `t` is inlined,
|
|
2051
|
+
// the merge takes `a0`, and the emitted `return a0 + (a0 - a1)` computes 13 where the asm
|
|
2052
|
+
// computes 10 — agbcc emits exactly that asm, so this is not a generated-IR curiosity. The
|
|
2053
|
+
// walk stops at any value with a name of its own (it reads THAT name) and at a materialized
|
|
2054
|
+
// def (it is assigned at its own position, which the clause above already judges).
|
|
2055
|
+
if (!pureAlias) {
|
|
2056
|
+
const reDerives = (w: Value, seen: Set<Value>): boolean => {
|
|
2057
|
+
if (w === p || seen.has(w)) {
|
|
2058
|
+
return false;
|
|
2059
|
+
}
|
|
2060
|
+
seen.add(w);
|
|
2061
|
+
const nm = varName.get(w);
|
|
2062
|
+
if (nm !== undefined) {
|
|
2063
|
+
return nm === name;
|
|
2064
|
+
}
|
|
2065
|
+
const d = defs.get(w);
|
|
2066
|
+
if (!d || materialize.has(d)) {
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
return d.operands.some((o) => reDerives(o, seen));
|
|
2070
|
+
};
|
|
2071
|
+
for (const w of lin) {
|
|
2072
|
+
if (reDerives(w, new Set())) {
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
1145
2077
|
return true;
|
|
1146
2078
|
};
|
|
2079
|
+
// Does the edge `pr -> b` hand `c` over as a loop variable's PRE-update value? True when `c` is a
|
|
2080
|
+
// loop header's own param and the edge leaves the loop from a latch of an emitter that places
|
|
2081
|
+
// the update at the BOTTOM of the body — the self-loop `while` and the `do-while`. `c` there
|
|
2082
|
+
// means the value the variable held at the top of the exiting iteration, so a merge param
|
|
2083
|
+
// sharing its name would read one iteration on, silently. Keeping the names apart is also what
|
|
2084
|
+
// lets those emitters sink the copy into the body; sharing them would make it a self-assignment.
|
|
2085
|
+
//
|
|
2086
|
+
// Four shapes are NOT this, and must keep coalescing:
|
|
2087
|
+
// - the loop is emitted by neither of those two. A test-at-top `while` puts its exit copies in
|
|
2088
|
+
// the sibling arm of the latch's `cond_br`, AHEAD of the update, so the name is still the
|
|
2089
|
+
// top-of-iteration value there (`while (p) { if (p->key == k) return p; p = p->next; }`);
|
|
2090
|
+
// - the edge leaves from the HEADER rather than a latch — same reason;
|
|
2091
|
+
// - the back-edge arg for `c`'s own slot is `c`: that slot is never rewritten;
|
|
2092
|
+
// - `c` is itself a back-edge arg — the compiler already carries the trailing value in a
|
|
2093
|
+
// second loop variable, so the un-rotation substitution reads it under that one's name.
|
|
2094
|
+
// Same exemption `readsClobbered` makes for a `sub`-mapped value.
|
|
2095
|
+
const carriesPreUpdate = (c: Value, pr: Block, b: Block): boolean => {
|
|
2096
|
+
const header = paramBlock.get(c);
|
|
2097
|
+
const nl = header && forest.byHeader.get(header);
|
|
2098
|
+
if (!nl || nl.body.has(b) || !nl.body.has(pr)) {
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
if (!loops.has(header!) && doWhileLoops.get(header!)?.latch !== pr) {
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
const back = successorTo(pr, header!);
|
|
2105
|
+
const k = header!.params.indexOf(c);
|
|
2106
|
+
return !!back && k >= 0 && back.args[k] !== c && !back.args.includes(c);
|
|
2107
|
+
};
|
|
1147
2108
|
// ONE seeding routine for self-loop and structured-loop headers. On a coalesceLoopInit target,
|
|
1148
2109
|
// keep the induction variable in its entry (forward-edge) value's register — reproducing a
|
|
1149
2110
|
// compiler that mutates the arg register across the loop instead of copying to a fresh local,
|
|
@@ -1169,6 +2130,29 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1169
2130
|
}
|
|
1170
2131
|
}
|
|
1171
2132
|
}
|
|
2133
|
+
// A MATERIALIZED back-edge arg is this variable's in-place update (`add r4, r4, r0`
|
|
2134
|
+
// mutates the same register the param lives in) — adopt its name so the def assigns the
|
|
2135
|
+
// loop variable directly and the update copy elides. Sound only when every read of the
|
|
2136
|
+
// param sits at-or-before the def in the header (the def's own operands included): the
|
|
2137
|
+
// assignment splits the iteration into old-value-before / new-value-after, and a later
|
|
2138
|
+
// read of the OLD value would silently get the new one — position-granular, because
|
|
2139
|
+
// liveness is block-granular and the param is killed from its own block's liveIn.
|
|
2140
|
+
if (name === undefined) {
|
|
2141
|
+
const ba = backArgs?.[i];
|
|
2142
|
+
const d = ba !== undefined ? defs.get(ba) : undefined;
|
|
2143
|
+
const nm = ba !== undefined ? varName.get(ba) : undefined;
|
|
2144
|
+
if (
|
|
2145
|
+
nm !== undefined &&
|
|
2146
|
+
!exclude.has(nm) &&
|
|
2147
|
+
d !== undefined &&
|
|
2148
|
+
materialize.has(d) &&
|
|
2149
|
+
opBlock.get(d) === header &&
|
|
2150
|
+
(useSitesOf.get(p) ?? []).every((s) => s.blk === header && s.idx <= opIndex.get(d)!) &&
|
|
2151
|
+
canTakeName(p, header, nm)
|
|
2152
|
+
) {
|
|
2153
|
+
name = nm;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
1172
2156
|
name ??= `v${fresh++}`;
|
|
1173
2157
|
varName.set(p, name);
|
|
1174
2158
|
if (!varType.has(name)) {
|
|
@@ -1176,6 +2160,15 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1176
2160
|
}
|
|
1177
2161
|
}
|
|
1178
2162
|
exclude.add(varName.get(p)!);
|
|
2163
|
+
// THE BACK-EDGE ARGUMENT TAKES THE HEADER'S NAME, UNCONDITIONALLY — so if `p` is declared
|
|
2164
|
+
// NARROW, every other reader of that argument reads it through the truncation. This site
|
|
2165
|
+
// does not check that, and deliberately: the promise is a precondition of producing a narrow
|
|
2166
|
+
// block parameter at all, held by `raise/narrowlocal.ts`'s `edge-reader` gate (every in-edge
|
|
2167
|
+
// value is read nowhere but through an extension no wider than the declaration keeps). A
|
|
2168
|
+
// width test HERE would be the wrong shape — it would refuse the loop's own update copy and
|
|
2169
|
+
// silently un-coalesce it. Any future producer of a sub-word block parameter owes the same
|
|
2170
|
+
// gate; without it this line emits `s16 v; do { … v = v + 1; } while (v < 32768);`, which
|
|
2171
|
+
// agbcc compiles to an unconditional branch.
|
|
1179
2172
|
if (backArgs) {
|
|
1180
2173
|
backArgName.set(backArgs[i], varName.get(p)!);
|
|
1181
2174
|
}
|
|
@@ -1247,32 +2240,52 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1247
2240
|
if (varName.has(p)) {
|
|
1248
2241
|
return;
|
|
1249
2242
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
for (const pr of new Set(preds.get(b) ?? [])) {
|
|
1254
|
-
for (const s of pr.ops[pr.ops.length - 1].successors) {
|
|
1255
|
-
if (s.block === b) {
|
|
1256
|
-
incoming.push(s.args[i]);
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
2243
|
+
const incoming: { v: Value; pr: Block }[] = [];
|
|
2244
|
+
for (const { pred, succ } of inEdgeRecords(preds, b)) {
|
|
2245
|
+
incoming.push({ v: succ.args[i], pr: pred });
|
|
1259
2246
|
}
|
|
1260
2247
|
// A redundant phi (every edge passes the SAME value) is a pure alias of it — sharing the
|
|
1261
2248
|
// name is sound even while the value stays live (they are equal on every path). This
|
|
1262
2249
|
// waives only the LIVENESS half of canTakeName; the sibling-param check always applies.
|
|
1263
|
-
const allSame = incoming.length > 0 && incoming.every((
|
|
2250
|
+
const allSame = incoming.length > 0 && incoming.every((c) => c.v === incoming[0].v);
|
|
1264
2251
|
// prefer a carrier that already has a name; then a loop var whose update this receives —
|
|
1265
2252
|
// but only one whose name survives the C3 interference check (else the edge copies into
|
|
1266
2253
|
// the name would clobber a still-live value).
|
|
1267
2254
|
let name: string | undefined;
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
2255
|
+
/** Set when `FRESH_MERGE_GATES` refused a carrier here — not "the one reason this merge
|
|
2256
|
+
* went unnamed", since the rejections above it `continue` without recording. Over-marking
|
|
2257
|
+
* costs one downstream merge an extra fresh home, a spelling the differ referees. */
|
|
2258
|
+
let refusedParamCarrier = false;
|
|
2259
|
+
for (const c of [
|
|
2260
|
+
...incoming.filter((c) => varName.has(c.v)),
|
|
2261
|
+
...incoming.filter((c) => backArgName.has(c.v)),
|
|
2262
|
+
]) {
|
|
2263
|
+
const nm = varName.get(c.v) ?? backArgName.get(c.v)!;
|
|
2264
|
+
if (carriesPreUpdate(c.v, c.pr, b) || !canTakeName(p, b, nm, allSame)) {
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
// `freshParamMerge` (the `/fresh-merge` axis): this merge takes its own home rather
|
|
2268
|
+
// than the carrier's name — `FRESH_MERGE_GATES` above holds the admission and the
|
|
2269
|
+
// argument for it. Absent the option nothing changes.
|
|
2270
|
+
if (
|
|
2271
|
+
freshParamMerge &&
|
|
2272
|
+
reHomesParamMerge(
|
|
2273
|
+
{ allSame, paramRooted: entryParams.has(c.v) || paramSeededMerges.has(c.v) },
|
|
2274
|
+
hooks.freshMergeGates,
|
|
2275
|
+
)
|
|
2276
|
+
) {
|
|
2277
|
+
refusedParamCarrier = true;
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
name = nm;
|
|
2281
|
+
break;
|
|
2282
|
+
}
|
|
2283
|
+
if (name === undefined) {
|
|
2284
|
+
name = `v${fresh++}`;
|
|
2285
|
+
if (refusedParamCarrier) {
|
|
2286
|
+
paramSeededMerges.add(p);
|
|
1273
2287
|
}
|
|
1274
2288
|
}
|
|
1275
|
-
name ??= `v${fresh++}`;
|
|
1276
2289
|
varName.set(p, name);
|
|
1277
2290
|
if (!varType.has(name)) {
|
|
1278
2291
|
varType.set(name, p.type);
|
|
@@ -1282,6 +2295,113 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1282
2295
|
}
|
|
1283
2296
|
}
|
|
1284
2297
|
|
|
2298
|
+
// ── copy coalescing over the interference graph (namecoalesce.ts) ────────────────────────────
|
|
2299
|
+
// The walk above adopts a name only BACKWARD along an edge, once, in address order — so a merge
|
|
2300
|
+
// parameter whose arguments were still unnamed took a fresh one and kept it. With every name now
|
|
2301
|
+
// settled, `coalesceNames` asks which two of them a would-be copy joins and whether the values
|
|
2302
|
+
// under them ever interfere. Applied here, before anything reads the names: `anchorConstCopies`
|
|
2303
|
+
// below counts the values under a name, and emission spells them.
|
|
2304
|
+
if (coalesceMergeNames) {
|
|
2305
|
+
const { renames } = coalesceNames(
|
|
2306
|
+
{
|
|
2307
|
+
blocks: fn.blocks,
|
|
2308
|
+
entry,
|
|
2309
|
+
preds,
|
|
2310
|
+
liveIn,
|
|
2311
|
+
opBlock,
|
|
2312
|
+
opIndex,
|
|
2313
|
+
useSitesOf,
|
|
2314
|
+
defs,
|
|
2315
|
+
materialize,
|
|
2316
|
+
varName,
|
|
2317
|
+
varType,
|
|
2318
|
+
loops: [...forest.byHeader.values()].map((nl) => ({ header: nl.header, body: nl.body })),
|
|
2319
|
+
},
|
|
2320
|
+
hooks.nameCoalesceGates,
|
|
2321
|
+
);
|
|
2322
|
+
for (const [v, n] of varName) {
|
|
2323
|
+
const r = renames.get(n);
|
|
2324
|
+
if (r !== undefined) {
|
|
2325
|
+
varName.set(v, r);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
for (const [v, n] of backArgName) {
|
|
2329
|
+
const r = renames.get(n);
|
|
2330
|
+
if (r !== undefined) {
|
|
2331
|
+
backArgName.set(v, r);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// ── declaration-signedness reconciliation ──────────────────────────────────────────────────
|
|
2337
|
+
// A name's declared type is its FIRST claimant's, and the first claimant of a u32 loop counter
|
|
2338
|
+
// is often an s32-typed sibling (the pre-increment value, a const's copy) — so the counter
|
|
2339
|
+
// declares s32, every compare through it renders signed, and an unsigned icmp silently
|
|
2340
|
+
// compiles to the compare the machine never did (the compare-cast at CMP_TO_BIN patches the
|
|
2341
|
+
// sites it can see, but a later re-spell can substitute the var into a compare that needed no
|
|
2342
|
+
// cast at emission — the initfirst guard swap). The declaration is the honest fix: a name
|
|
2343
|
+
// flips to u32 when SOME value under it is u32-typed and NO value under it carries signed-use
|
|
2344
|
+
// evidence (the transitive input cone of any icmp_s* / sdiv / smod / shr_s). Only int32
|
|
2345
|
+
// declarations reconcile; the flip is byte-invariant everywhere but the unsigned compares it
|
|
2346
|
+
// corrects (+/-/*/&/|/^/<< are sign-blind; the signedness-carrying pairs — `>>`/`>>>` and
|
|
2347
|
+
// `/`/`%` against `/u`/`%u` — say which they are through the backend's operand pin, so no
|
|
2348
|
+
// declaration can change what they render; and SIGNED division is evidence-blocked).
|
|
2349
|
+
if (unsignedCompareSpelling) {
|
|
2350
|
+
// Signed-use evidence is the TRANSITIVE INPUT CONE of every signed op — a claimant can feed
|
|
2351
|
+
// an icmp_slt through an inline `sub` and the flip would still render that compare unsigned
|
|
2352
|
+
// (`v - 5 >= 0`, always true in C), so direct operands are not enough. The cone also crosses
|
|
2353
|
+
// edge arg↔param identities in BOTH directions: a kept-guard's substitution (gsub) renders an
|
|
2354
|
+
// init ARG under the loop variable's name, so a signed guard over the arg is evidence against
|
|
2355
|
+
// the name even though the arg claims it in neither naming map. Over-tainting only blocks
|
|
2356
|
+
// flips: conservative-safe.
|
|
2357
|
+
const SIGNED_USE = new Set(['icmp_slt', 'icmp_sle', 'icmp_sgt', 'icmp_sge', 'sdiv', 'smod', 'shr_s']);
|
|
2358
|
+
// The arg↔param identity is `ir/core.ts`'s `mergeClasses` — a CFG/SSA fact rather than a rule
|
|
2359
|
+
// of this pass, and a hand-rolled copy here is one more thing free to drift from it. The walk
|
|
2360
|
+
// below is transitive, so seeding it with the whole CLASS is the same closure as seeding it
|
|
2361
|
+
// with the direct peers.
|
|
2362
|
+
const edgePeers = mergeClasses(fn);
|
|
2363
|
+
const signedEvidence = new Set<Value>();
|
|
2364
|
+
const work: Value[] = [];
|
|
2365
|
+
for (const b of fn.blocks) {
|
|
2366
|
+
for (const op of b.ops) {
|
|
2367
|
+
if (SIGNED_USE.has(op.opcode)) {
|
|
2368
|
+
work.push(...op.operands);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
while (work.length) {
|
|
2373
|
+
const v = work.pop()!;
|
|
2374
|
+
if (!signedEvidence.has(v)) {
|
|
2375
|
+
signedEvidence.add(v);
|
|
2376
|
+
const d = defs.get(v);
|
|
2377
|
+
if (d) {
|
|
2378
|
+
work.push(...d.operands);
|
|
2379
|
+
}
|
|
2380
|
+
work.push(...(edgePeers.get(v) ?? []));
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
// Params never reconcile: their declarations come from p.type, not varType, so a flip here
|
|
2384
|
+
// would only desync the cast site's view from the emitted declaration — and param signedness
|
|
2385
|
+
// is the sign-pin axis's dimension.
|
|
2386
|
+
const paramNames = new Set(entry.params.map((_, i) => `a${i}`));
|
|
2387
|
+
const claimants = new Map<string, Value[]>();
|
|
2388
|
+
for (const [v, n] of [...varName, ...backArgName]) {
|
|
2389
|
+
(claimants.get(n) ?? claimants.set(n, []).get(n)!).push(v);
|
|
2390
|
+
}
|
|
2391
|
+
for (const [n, vs] of claimants) {
|
|
2392
|
+
const t = varType.get(n);
|
|
2393
|
+
if (paramNames.has(n) || t?.kind !== 'int' || t.width !== 32 || !t.signed) {
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
if (
|
|
2397
|
+
vs.some((v) => v.type.kind === 'int' && v.type.width === 32 && !v.type.signed) &&
|
|
2398
|
+
vs.every((v) => !signedEvidence.has(v))
|
|
2399
|
+
) {
|
|
2400
|
+
varType.set(n, T.u(32));
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
|
|
1285
2405
|
// ── def-site anchoring of constant merge copies (anchorConstCopies) ──────────────────────────
|
|
1286
2406
|
// An edge copy `v = K` places the constant where the EDGE is, but the asm often materialized K
|
|
1287
2407
|
// earlier: `movs r9, #0` at entry ahead of a single-armed overwrite, `movs r5, #1` at the top
|
|
@@ -1294,7 +2414,15 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1294
2414
|
// REFUSAL CONDITIONS — each keeps the edge placement, never producing a different write:
|
|
1295
2415
|
// - the arg is not an UNNAMED `const` op (only a rematerializable constant carries
|
|
1296
2416
|
// unambiguous placement evidence; a named value's position is its materialized def's);
|
|
1297
|
-
// - the merge is a
|
|
2417
|
+
// - the merge is a LOOP HEADER, unless `anchorLoopEntryConsts` is also on and the loop is
|
|
2418
|
+
// entered through ONE preheader with no value living outside the body under the same name.
|
|
2419
|
+
// A loop header's entry arg is the one merge arg whose def sits legitimately far above its
|
|
2420
|
+
// edge — `int s = 0;` ahead of a `for` accumulating into it — and the carried values that
|
|
2421
|
+
// share its name are what the name rule below would refuse it on. Anchoring holds exactly
|
|
2422
|
+
// when the name is written nowhere outside the loop but at the anchored site: the entry
|
|
2423
|
+
// copy runs once, before the body, and every other write to it is a back-edge copy strictly
|
|
2424
|
+
// after. Its own flag because it is a SECOND placement decision, and one boolean covering
|
|
2425
|
+
// both would delete a spelling rather than add one (see the option's doc);
|
|
1298
2426
|
// - the const's block does not dominate every edge source passing it (the anchored write
|
|
1299
2427
|
// must precede the edge on every path);
|
|
1300
2428
|
// - the const's block or any edge source sits inside ANY loop. Block-level dominance does
|
|
@@ -1302,11 +2430,31 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1302
2430
|
// suppressed edge in iteration 2 with the variable overwritten in between, the /preinit
|
|
1303
2431
|
// sticky-arm failure class (PR #13) — so in-loop shapes are declined outright;
|
|
1304
2432
|
// - the merge variable names any OTHER SSA value (a shared name has readers and writers
|
|
1305
|
-
// between the def site and the edge that edge placement respects and anchoring would not)
|
|
2433
|
+
// between the def site and the edge that edge placement respects and anchoring would not).
|
|
2434
|
+
// A loop header's carried value is the exception above: its other claimants all live in
|
|
2435
|
+
// the body. `freshParamMerge` WIDENS what this admits — a minted home is sole by
|
|
2436
|
+
// construction — which is the pairing `/fresh-merge` and `/defsite` match `clampu8` on;
|
|
1306
2437
|
// - another anchored const of the same variable lies on a path from this one to this one's
|
|
1307
2438
|
// edge (the later write would clobber this arg's value; both stay at their edges instead).
|
|
1308
|
-
|
|
2439
|
+
//
|
|
2440
|
+
// What the list does NOT cover is whether the def site is RENDERED at all; an anchor that lands
|
|
2441
|
+
// in an elided block declines the whole function instead, at the postcondition after the render.
|
|
2442
|
+
/** Where a value is WRITTEN: its block for a param, its def op's block otherwise — the two
|
|
2443
|
+
* halves of "where is this defined" asked as one question. Used by def-site anchoring below to
|
|
2444
|
+
* order a write against an edge, and by Regime-A switch recovery (through `SwitchRecoverDeps`)
|
|
2445
|
+
* to ask whether a hoisted argument is available at the dispatch root; passed there rather than
|
|
2446
|
+
* rebuilt, since `paramBlock` and `opBlock` are this scope's own. */
|
|
2447
|
+
const blockOf = (v: Value): Block | undefined => {
|
|
2448
|
+
const d = defs.get(v);
|
|
2449
|
+
return paramBlock.get(v) ?? (d === undefined ? undefined : opBlock.get(d));
|
|
2450
|
+
};
|
|
2451
|
+
type AnchoredWrite = { name: string; arg: Value };
|
|
2452
|
+
const anchoredAt = new Map<Op, AnchoredWrite[]>();
|
|
1309
2453
|
const suppressedArgs = new Map<object, Set<number>>();
|
|
2454
|
+
/** Every anchored write `sideEffects` actually rendered. The other half of the postcondition
|
|
2455
|
+
* below: suppressing an edge copy is a PROMISE that the def site emits the write instead, and
|
|
2456
|
+
* only the render can report that the promise was kept. */
|
|
2457
|
+
const anchorsEmitted = new Set<AnchoredWrite>();
|
|
1310
2458
|
if (anchorConstCopies) {
|
|
1311
2459
|
const nameCount = new Map<string, number>();
|
|
1312
2460
|
for (const n of varName.values()) {
|
|
@@ -1323,27 +2471,47 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1323
2471
|
// conservative "a write in `a` may execute between one in `b` and `b`'s terminator": same
|
|
1324
2472
|
// block counts (op order refined by the caller where it matters), else CFG reachability
|
|
1325
2473
|
const mayFollow = (a: Block, b: Block): boolean => a === b || reachFrom(a).has(b);
|
|
2474
|
+
// A loop entered from ONE preheader — what makes "every write to the name outside the anchored
|
|
2475
|
+
// one is a back-edge copy" statable: the premise is over ONE entry edge. CONSERVATIVE: two
|
|
2476
|
+
// entry consts anchored at their own def sites would still order correctly, and this refuses
|
|
2477
|
+
// the shape rather than reasoning about it. The other half of "entered only through its
|
|
2478
|
+
// header" needs no test — `analyzeLoops` builds the body as the backward closure from the
|
|
2479
|
+
// latches with the header already in it (loops.ts), so a body block's predecessors are all in
|
|
2480
|
+
// the body by construction.
|
|
2481
|
+
const singleEntry = (nl: NaturalLoop): boolean => nl.forwardPreds.length === 1;
|
|
1326
2482
|
for (const M of fn.blocks) {
|
|
1327
|
-
if (M === entry || M.params.length === 0
|
|
2483
|
+
if (M === entry || M.params.length === 0) {
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
const loop = anchorLoopEntryConsts ? forest.byHeader.get(M) : undefined;
|
|
2487
|
+
if (forest.byHeader.has(M) && (loop === undefined || !singleEntry(loop))) {
|
|
1328
2488
|
continue;
|
|
1329
2489
|
}
|
|
1330
2490
|
M.params.forEach((p, i) => {
|
|
1331
2491
|
const name = varName.get(p)!;
|
|
1332
|
-
|
|
2492
|
+
// the name belongs to this merge alone — or, at a loop header, to this merge and the
|
|
2493
|
+
// body's own carried values, which are written only on the back edge
|
|
2494
|
+
const soleClaimant =
|
|
2495
|
+
nameCount.get(name) === 1 ||
|
|
2496
|
+
(loop !== undefined &&
|
|
2497
|
+
[...varName].every(([v, n]) => {
|
|
2498
|
+
if (n !== name || v === p) {
|
|
2499
|
+
return true;
|
|
2500
|
+
}
|
|
2501
|
+
const b = blockOf(v);
|
|
2502
|
+
return b !== undefined && loop.body.has(b);
|
|
2503
|
+
}));
|
|
2504
|
+
if (!soleClaimant) {
|
|
1333
2505
|
return;
|
|
1334
2506
|
}
|
|
1335
2507
|
// every in-edge record into M, grouped by the SSA value it passes for param i
|
|
1336
2508
|
const groups = new Map<Value, { rec: { block: Block; args: Value[] }; src: Block }[]>();
|
|
1337
|
-
for (const
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
} else {
|
|
1344
|
-
groups.set(s.args[i], [{ rec: s, src: pr }]);
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
2509
|
+
for (const { pred, succ } of inEdgeRecords(preds, M)) {
|
|
2510
|
+
const g = groups.get(succ.args[i]);
|
|
2511
|
+
if (g) {
|
|
2512
|
+
g.push({ rec: succ, src: pred });
|
|
2513
|
+
} else {
|
|
2514
|
+
groups.set(succ.args[i], [{ rec: succ, src: pred }]);
|
|
1347
2515
|
}
|
|
1348
2516
|
}
|
|
1349
2517
|
const candidates: { arg: Value; def: Op; defBlock: Block; edges: { rec: object; src: Block }[] }[] = [];
|
|
@@ -1393,115 +2561,29 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1393
2561
|
}
|
|
1394
2562
|
}
|
|
1395
2563
|
|
|
1396
|
-
// ── BITFIELD member
|
|
1397
|
-
// The
|
|
1398
|
-
//
|
|
1399
|
-
//
|
|
1400
|
-
//
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
// REGISTER value — the bits captured at the load's program position — with a fresh memory
|
|
1420
|
-
// read at each render position. Every other memory read in this file goes through the
|
|
1421
|
-
// materialization model (analysis.ts) for exactly that hazard, so the fold clears the SAME
|
|
1422
|
-
// bar with the SAME machinery: `emitPos` resolves where each extract actually renders
|
|
1423
|
-
// (transitively through its inlining consumers — an unresolvable position refuses), and
|
|
1424
|
-
// `memWriteBetween` walks every def-avoiding load→render path for a call, an opaque, or a
|
|
1425
|
-
// store not provably to a DIFFERENT named global. Path-based on purpose: the second audit
|
|
1426
|
-
// pass broke the first fix's linear-position scan with a block laid out AFTER the render in
|
|
1427
|
-
// address order but executing between load and render on the taken path — fn.blocks order is
|
|
1428
|
-
// address order, not topological order.
|
|
1429
|
-
const bitfieldSpelling = new Map<Op, { global: string; field: string }>();
|
|
1430
|
-
const absorbedLoads = new Set<Op>();
|
|
1431
|
-
if (symCtx && littleEndian && spellBitfieldMembers) {
|
|
1432
|
-
// the (name, byte) of a load's address when it resolves through defs alone — `gaddr` or
|
|
1433
|
-
// `add(gaddr, const)`; anything else (a materialized base, a variable index) declines. THE
|
|
1434
|
-
// shared L2 disjointness query (ir/alias.ts), which the materialization model consults with
|
|
1435
|
-
// the same rule, so the fold and the model cannot disagree about what a store can reach.
|
|
1436
|
-
const loadTargets = new Map<Op, GlobalCell>();
|
|
1437
|
-
const addrOf = (v: Value, off: number): GlobalCell | null => globalCellOf(defs, v, off);
|
|
1438
|
-
// A write for the fold's purposes: calls and opaques always; a store/astore unless its base
|
|
1439
|
-
// resolves to a global PROVABLY different from the folded one.
|
|
1440
|
-
const mayWrite = (sym: string) => mayWriteGlobal(defs, sym);
|
|
1441
|
-
for (const blk of fn.blocks) {
|
|
1442
|
-
for (const op of blk.ops) {
|
|
1443
|
-
if ((op.opcode !== 'shr_u' && op.opcode !== 'shr_s') || op.operands.length !== 1) {
|
|
1444
|
-
continue;
|
|
1445
|
-
}
|
|
1446
|
-
const b = op.attrs.imm as number | undefined;
|
|
1447
|
-
const inner = defs.get(op.operands[0]);
|
|
1448
|
-
if (typeof b !== 'number' || b <= 0 || b >= 32 || inner?.opcode !== 'shl' || inner.operands.length !== 1) {
|
|
1449
|
-
continue;
|
|
1450
|
-
}
|
|
1451
|
-
const a = inner.attrs.imm as number | undefined;
|
|
1452
|
-
if (typeof a !== 'number' || a < 0 || b < a) {
|
|
1453
|
-
continue;
|
|
1454
|
-
}
|
|
1455
|
-
const w = 32 - b; // extract width
|
|
1456
|
-
const lo = b - a; // low bit within the loaded value
|
|
1457
|
-
const load = defs.get(inner.operands[0]);
|
|
1458
|
-
if (load?.opcode !== 'load' || lo + w > (load.attrs.width as number) * 8) {
|
|
1459
|
-
continue;
|
|
1460
|
-
}
|
|
1461
|
-
// a materialized shl would still emit its `v = x << a` temp reading the load — the fold
|
|
1462
|
-
// would then ADD member reads on top of it; rare, refuse
|
|
1463
|
-
if (materialize.has(inner)) {
|
|
1464
|
-
continue;
|
|
1465
|
-
}
|
|
1466
|
-
const gb = addrOf(load.operands[0], load.attrs.off as number);
|
|
1467
|
-
const si = gb ? symCtx.info(gb.name) : undefined;
|
|
1468
|
-
if (!gb || si?.shape !== 'struct' || si.volatile) {
|
|
1469
|
-
continue;
|
|
1470
|
-
}
|
|
1471
|
-
// where does the member read RENDER? at the extract's own position when materialized,
|
|
1472
|
-
// else wherever each of its consumers ultimately renders (emitPos, transitively —
|
|
1473
|
-
// unresolvable refuses); every load→render path must be write-free
|
|
1474
|
-
const renders = materialize.has(op)
|
|
1475
|
-
? [{ blk: opBlock.get(op)!, idx: opIndex.get(op)! }]
|
|
1476
|
-
: [...new Set((useSitesOf.get(op.results[0]) ?? []).map((s) => s.op))].map((c) => emitPos(c));
|
|
1477
|
-
const writes = mayWrite(gb.name);
|
|
1478
|
-
if (renders.some((r) => r === null) || renders.some((r) => memWriteBetween(load, r!, writes))) {
|
|
1479
|
-
continue;
|
|
1480
|
-
}
|
|
1481
|
-
const signedRead = op.opcode === 'shr_s';
|
|
1482
|
-
const fld = declaredFields(si.layout)?.find(
|
|
1483
|
-
(f) => f.bitWidth === w && f.offset * 8 + f.bitOffset! === gb.byte * 8 + lo && f.signed === signedRead,
|
|
1484
|
-
);
|
|
1485
|
-
if (fld && memberQualsAllow(fld, si.const, false)) {
|
|
1486
|
-
bitfieldSpelling.set(op, { global: gb.name, field: fld.name });
|
|
1487
|
-
loadTargets.set(load, gb);
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
// a load is ABSORBED when every use is an shl whose every use is a spelled extract
|
|
1492
|
-
for (const load of loadTargets.keys()) {
|
|
1493
|
-
const shls = useSitesOf.get(load.results[0]) ?? [];
|
|
1494
|
-
const absorbed =
|
|
1495
|
-
shls.length > 0 &&
|
|
1496
|
-
shls.every(
|
|
1497
|
-
(u) =>
|
|
1498
|
-
u.op.opcode === 'shl' && (useSitesOf.get(u.op.results[0]) ?? []).every((v) => bitfieldSpelling.has(v.op)),
|
|
1499
|
-
);
|
|
1500
|
-
if (absorbed) {
|
|
1501
|
-
absorbedLoads.add(load);
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
2564
|
+
// ── BITFIELD member spelling (structure/bitfields.ts) ───────────────────────────────────────
|
|
2565
|
+
// The read fold, the mask-and-insert WRITE fold and the absorbed-load set, precomputed over the
|
|
2566
|
+
// ops before any rendering. Extracted behind an explicit-deps factory: every dependency below is
|
|
2567
|
+
// READ-only, so this cannot participate in the naming pipeline this file's remainder is built
|
|
2568
|
+
// around. The module header states what each fold recognizes and every refusal it carries.
|
|
2569
|
+
const {
|
|
2570
|
+
spelling: bitfieldSpelling,
|
|
2571
|
+
stores: bitfieldStore,
|
|
2572
|
+
absorbed: absorbedLoads,
|
|
2573
|
+
} = makeBitfieldSpelling({
|
|
2574
|
+
fn,
|
|
2575
|
+
defs,
|
|
2576
|
+
materialize,
|
|
2577
|
+
useSitesOf,
|
|
2578
|
+
opBlock,
|
|
2579
|
+
opIndex,
|
|
2580
|
+
emitPos,
|
|
2581
|
+
memWriteBetween,
|
|
2582
|
+
sym: symCtx,
|
|
2583
|
+
littleEndian,
|
|
2584
|
+
enabled: spellBitfieldMembers,
|
|
2585
|
+
memberQualsAllow,
|
|
2586
|
+
});
|
|
1505
2587
|
|
|
1506
2588
|
// An unresolvable value: strict mode keeps the `"?"` sentinel AND records the reason — the
|
|
1507
2589
|
// decline thrown below names the actual gaps ("unmodelled instruction 'adde'"), the same
|
|
@@ -1542,24 +2624,118 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1542
2624
|
// sign-agnostic ==/!=) spell `(u32)&gSym`, signed compares `(s32)&gSym` — exactly the
|
|
1543
2625
|
// compare the asm did. The deref folds never see a compare operand, so no named spelling is
|
|
1544
2626
|
// lost; a NARROWING cast (`(u8)&gSym`) is not a bare `addr` and keeps its truncation.
|
|
1545
|
-
// SCOPE
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1548
|
-
//
|
|
1549
|
-
// nonmatch, never a silent regression of a formerly-correct compare. Rare shape; an outer
|
|
1550
|
-
// signed cast on addr-carrying trees is the follow-up if it ever costs a row.
|
|
2627
|
+
// SCOPE: this handles BARE addr operands. An addr-carrying arithmetic tree
|
|
2628
|
+
// (`(u32)&gSym + 4`, spelled by intifyAddr below) renders unsigned and would compare
|
|
2629
|
+
// unsigned under an icmp_s*; the signed operand pin at the end of this block catches it as
|
|
2630
|
+
// one case of the general rule, needing no addr-specific reasoning.
|
|
1551
2631
|
const t = /^icmp_s/.test(d.opcode) ? T.s(32) : T.u(32);
|
|
1552
2632
|
const intifyAddrCmp = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: t, e: x } : x);
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
2633
|
+
let l = intifyAddrCmp(e(d.operands[0]));
|
|
2634
|
+
let r = intifyAddrCmp(e(d.operands[1]));
|
|
2635
|
+
// The same signedness hole for ORDINARY operands: an icmp_u* whose operands both render
|
|
2636
|
+
// as signed-promoting C (an s32-declared var carrying a u32 value — declarations take the
|
|
2637
|
+
// FIRST claimant's type; an inline `16 << t`, whose C type is the left operand's `int`)
|
|
2638
|
+
// compiles to the SIGNED compare the machine did not do. When neither side provably
|
|
2639
|
+
// promotes unsigned, one operand takes a (u32) cast — the side whose recovered VALUE type
|
|
2640
|
+
// is unsigned, the honest one — and the usual arithmetic conversions make the compare
|
|
2641
|
+
// unsigned exactly as the opcode says. A provably-unsigned operand leaves the spelling
|
|
2642
|
+
// alone, so correctly-typed compares never churn — as does a compare whose operands both
|
|
2643
|
+
// provably sit in [0, 2^31) (a `(u8)x > 4` byte test): there the signed spelling is
|
|
2644
|
+
// value-faithful and the compiler already picks the unsigned branch itself, and so does a
|
|
2645
|
+
// pointer-rendered side: `p < end` already compares unsigned, and `(u32)p` against a
|
|
2646
|
+
// pointer is the int-vs-ptr constraint violation the strict backends reject. ==/!= are
|
|
2647
|
+
// sign-agnostic; the icmp_s* direction is pinned below.
|
|
2648
|
+
const ptrSide = (x: Expr): boolean => {
|
|
2649
|
+
const t2 = ctype(x);
|
|
2650
|
+
return t2?.kind === 'ptr' || t2?.kind === 'array';
|
|
1558
2651
|
};
|
|
2652
|
+
if (
|
|
2653
|
+
unsignedCompareSpelling &&
|
|
2654
|
+
/^icmp_u/.test(d.opcode) &&
|
|
2655
|
+
!ptrSide(l) &&
|
|
2656
|
+
!ptrSide(r) &&
|
|
2657
|
+
renderedIntSignedness(l, vtEnv) !== false &&
|
|
2658
|
+
renderedIntSignedness(r, vtEnv) !== false &&
|
|
2659
|
+
!(provablyNonNegative(l, vtEnv) && provablyNonNegative(r, vtEnv))
|
|
2660
|
+
) {
|
|
2661
|
+
const irUnsigned = (v: Value): boolean => v.type.kind === 'int' && !v.type.signed;
|
|
2662
|
+
if (!irUnsigned(d.operands[0]) && irUnsigned(d.operands[1])) {
|
|
2663
|
+
r = { k: 'cast', to: T.u(32), e: r };
|
|
2664
|
+
} else {
|
|
2665
|
+
l = { k: 'cast', to: T.u(32), e: l };
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
// The SIGNED direction of the same hole, and a DEFAULT rather than an arm of that axis —
|
|
2669
|
+
// not because nothing underdetermines, but because the underdetermination is INERT. An
|
|
2670
|
+
// unsigned source compare can reach a signed opcode when the compiler proves the test is
|
|
2671
|
+
// the sign bit (`u32 a; a < 0x80000000` compiles to `cmp r0, #0; bge`; kmc-gcc and gcc
|
|
2672
|
+
// 2.7.2 fold it to `slti`), so an icmp_s* has more than one source — but the pinned
|
|
2673
|
+
// spelling reproduces that branch too (`(s32)a >= 0` is the same `cmp r0, #0; bge`), so
|
|
2674
|
+
// both sources reach ONE candidate and an axis would have doubled the fan to referee a
|
|
2675
|
+
// question with one answer. Where the spellings genuinely diverge they diverge the way the
|
|
2676
|
+
// opcode says, on every toolchain: an operand that renders unsigned makes C compare
|
|
2677
|
+
// unsigned (agbcc `bls`, IDO/kmc-gcc/gcc 2.7.2 `sltu`/`sltiu` against `slt`/`slti`, mwcc
|
|
2678
|
+
// `neg;or` against `neg;andc` in its branchless form), and against a constant the test
|
|
2679
|
+
// folds away entirely and takes the surrounding computation with it — `(u32)a / b < 0`
|
|
2680
|
+
// becomes `mov r0, #0`, deleting the `__udivsi3` call.
|
|
2681
|
+
//
|
|
2682
|
+
// `undefined` takes the cast exactly as a definite `false` does (see
|
|
2683
|
+
// renderedIntSignedness): a call's signedness is the project header's, not this function's.
|
|
2684
|
+
// A POINTER-rendered side is the one operand left alone, and not out of caution — `p < q`
|
|
2685
|
+
// is already the unsigned compare C gives two addresses, where `(s32)p < (s32)q` would
|
|
2686
|
+
// compare them signed.
|
|
2687
|
+
if (/^icmp_s/.test(d.opcode)) {
|
|
2688
|
+
const pinSigned = (x: Expr): Expr =>
|
|
2689
|
+
renderedIntSignedness(x, vtEnv) === true || ptrSide(x) ? x : { k: 'cast', to: T.s(32), e: x };
|
|
2690
|
+
l = pinSigned(l);
|
|
2691
|
+
r = pinSigned(r);
|
|
2692
|
+
}
|
|
2693
|
+
return { k: 'bin', op: CMP_TO_BIN[d.opcode], l, r };
|
|
1559
2694
|
}
|
|
1560
2695
|
if (ARITH_TO_BIN[d.opcode]) {
|
|
1561
2696
|
let l = e(d.operands[0]);
|
|
1562
2697
|
let r = d.operands.length === 2 ? e(d.operands[1]) : ({ k: 'const', value: d.attrs.imm as number } as Expr);
|
|
2698
|
+
// Commutative LOAD-PAIR operands re-spell in EVALUATION order. A commutative instruction's
|
|
2699
|
+
// operand order is an allocator artifact (`mul r0, r0, r2` reads dst-first, so the lift's
|
|
2700
|
+
// l/r is whichever load landed in the dst), but the order the compiler EVALUATED the
|
|
2701
|
+
// operands is still visible: their defs' order in the instruction stream. gcc 2.9 and IDO
|
|
2702
|
+
// both emit `w * h`'s loads w-first, so def order IS source order — verified byte-identical
|
|
2703
|
+
// on both (the bg_area rows). Scope, one gate per way the signal fails: BOTH root defs must
|
|
2704
|
+
// be same-block memory reads (a const already has its side; arithmetic defs get combined
|
|
2705
|
+
// out of source order — an ldmia-fed add reads def-reordered; cross-block positions do not
|
|
2706
|
+
// order evaluation), neither stamped `listOrder` (an ldmia-expanded load's own position is
|
|
2707
|
+
// LIST order), both operand VALUES un-named (see below), no pointer side (load-bearing for
|
|
2708
|
+
// the stride rules below), and no effect moves (call, marker).
|
|
2709
|
+
if (COMMUTATIVE_BIN.has(ARITH_TO_BIN[d.opcode]) && d.operands.length === 2) {
|
|
2710
|
+
const [da, db] = [defs.get(d.operands[0]), defs.get(d.operands[1])];
|
|
2711
|
+
if (
|
|
2712
|
+
defOrderLoadPairs &&
|
|
2713
|
+
da &&
|
|
2714
|
+
db &&
|
|
2715
|
+
(da.opcode === 'load' || da.opcode === 'aload') &&
|
|
2716
|
+
(db.opcode === 'load' || db.opcode === 'aload') &&
|
|
2717
|
+
da.attrs.listOrder !== true &&
|
|
2718
|
+
db.attrs.listOrder !== true &&
|
|
2719
|
+
// NAMED values decline: a value that renders as a name here — a materialized def
|
|
2720
|
+
// (varName), a loop-carried value in a post-loop region (activeSub) — was evaluated at
|
|
2721
|
+
// its def statement, so re-ordering the reference re-orders nothing and only churns the
|
|
2722
|
+
// spelling away from the machine order the allocator saw. An inlined def — a deref, a
|
|
2723
|
+
// field, a bare scalar global (whose `var` node lowerDef itself mints) — evaluates at
|
|
2724
|
+
// THIS site, wherever recovery spells it.
|
|
2725
|
+
!varName.has(d.operands[0]) &&
|
|
2726
|
+
!varName.has(d.operands[1]) &&
|
|
2727
|
+
activeSub?.has(d.operands[0]) !== true &&
|
|
2728
|
+
activeSub?.has(d.operands[1]) !== true &&
|
|
2729
|
+
ctype(l)?.kind !== 'ptr' &&
|
|
2730
|
+
ctype(r)?.kind !== 'ptr' &&
|
|
2731
|
+
!exprHasEffect(l) &&
|
|
2732
|
+
!exprHasEffect(r) &&
|
|
2733
|
+
opBlock.get(da) === opBlock.get(db) &&
|
|
2734
|
+
opIndex.get(da)! > opIndex.get(db)!
|
|
2735
|
+
) {
|
|
2736
|
+
[l, r] = [r, l];
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
1563
2739
|
// Pointer stride: C pointer arithmetic is ELEMENT-scaled, but the asm added a BYTE
|
|
1564
2740
|
// constant — `addi p,4` on an `s32*` walks 1 element, yet C `p + 4` walks 4. Divide the byte
|
|
1565
2741
|
// constant by the pointee size so the walk recompiles to the same address math.
|
|
@@ -1568,21 +2744,76 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1568
2744
|
// the type of the expression it actually sees, and the two diverge exactly like memAccess's
|
|
1569
2745
|
// deref bases (a value recovered `s32*` can render as an int-typed tree — C then does NO
|
|
1570
2746
|
// element scaling, so pre-dividing the constant would bake in a WRONG address that the
|
|
1571
|
-
// deref cast downstream turns into silently-wrong bytes
|
|
2747
|
+
// deref cast downstream turns into silently-wrong bytes).
|
|
1572
2748
|
// An int-rendered walk keeps its raw byte constant and derefs through the access-width cast.
|
|
1573
2749
|
// Fires only for a rendered pointer whose element size (>1) DIVIDES the constant exactly;
|
|
1574
2750
|
// otherwise raw (a misaligned/struct-array stride is left as-is; a `u8*` is size 1 so
|
|
1575
2751
|
// unchanged). Since C `(K/es) + p == p + (K/es)`, scaling the const on whichever side it
|
|
1576
2752
|
// sits fixes the bytes: `add` is commutative so the pointer may be either operand; `sub` is
|
|
1577
2753
|
// not, so only operand[0] (the minuend) may be the pointer.
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
2754
|
+
// A rendered pointer that CANNOT express the byte constant as a whole number of elements —
|
|
2755
|
+
// an inexact residual, or a pointee whose size is not knowable here (a struct) — has no
|
|
2756
|
+
// scaled spelling at all, and leaving the raw byte count is the silent wrongness the
|
|
2757
|
+
// pointer-global rule below refuses in the same words: C multiplies it back, so `p + 62` on
|
|
2758
|
+
// an `s32 *` addresses byte 248 and `p + 38` on a `struct S *` addresses byte 38 * sizeof(S).
|
|
2759
|
+
// Same answer as there — CAST THEN ADD, `(u8 *)p + 62`, the same address in every world.
|
|
2760
|
+
const bytePtr = (x: Expr): Expr => ({ k: 'cast', to: T.ptr(T.u(8)), e: x });
|
|
2761
|
+
// Set when a byte-pointer cast was applied for the ADDRESS math alone: the sum is cast back
|
|
2762
|
+
// to the pointer type it started as, so the walk changes the arithmetic and nothing else. A
|
|
2763
|
+
// bare `u8 *` sum would be a different C type from the slot it lands in (`v4 = (u8 *)a0 +
|
|
2764
|
+
// (v1 << 2)` into an `s32 *` — an mwcc error) and from the bases the deref rules read.
|
|
2765
|
+
let restoreTo: IrType | undefined;
|
|
2766
|
+
const walk = (base: Expr, c: Extract<Expr, { k: 'const' }>): { base: Expr; off: Expr } => {
|
|
2767
|
+
const t = ctype(base);
|
|
2768
|
+
if (t?.kind !== 'ptr') {
|
|
2769
|
+
return { base, off: c }; // an int-rendered walk: C scales nothing, the bytes are right
|
|
2770
|
+
}
|
|
2771
|
+
const es = ptrElemBytes(t.to);
|
|
2772
|
+
if (es === 1) {
|
|
2773
|
+
return { base, off: c }; // already a byte pointer
|
|
2774
|
+
}
|
|
2775
|
+
return es > 1 && c.value % es === 0
|
|
2776
|
+
? { base, off: { k: 'const', value: c.value / es } }
|
|
2777
|
+
: { base: bytePtr(base), off: c };
|
|
2778
|
+
};
|
|
2779
|
+
// A RUNTIME byte offset has no element spelling at all — not even an inexact one to reject,
|
|
2780
|
+
// since the residual is unknown until the program runs. The asm added bytes, so the same
|
|
2781
|
+
// answer as the inexact constant above: cast then add. Without it a `u16 *` walked by a
|
|
2782
|
+
// computed offset addresses TWICE the intended byte, and nothing downstream can see the
|
|
2783
|
+
// error — in the sa3 decomp that address is what a `CpuSet` call writes THROUGH.
|
|
2784
|
+
//
|
|
2785
|
+
// KNOWN GAP: `ptr ± ptr` is excluded, and the intify rules below do not make it right
|
|
2786
|
+
// either — `ptr + ptr` becomes `l + (s32)r`, which C scales, and a same-pointee `ptr - ptr`
|
|
2787
|
+
// is C's ELEMENT difference where the asm subtracted bytes. Both want the same cast-then-add
|
|
2788
|
+
// treatment; both are byte-identical to what this emitted before, and three functions in the
|
|
2789
|
+
// agbcc corpus carry one.
|
|
2790
|
+
//
|
|
2791
|
+
// KNOWN GAP: the inexact-CONSTANT branch above casts its base and does NOT cast the sum
|
|
2792
|
+
// back, so `v1 = (u8 *)a0 + 2` still lands in an `s32 *` slot. Copying the restore up churns
|
|
2793
|
+
// the common case, where the sum is consumed by a deref that supplies its own cast — the
|
|
2794
|
+
// real predicate is the CONSUMER, and deciding it here at the producer is what these two
|
|
2795
|
+
// branches would have to stop doing.
|
|
2796
|
+
const walkVar = (x: Expr): IrType | undefined => {
|
|
2797
|
+
const t = ctype(x);
|
|
2798
|
+
return t?.kind === 'ptr' && ptrElemBytes(t.to) !== 1 ? t : undefined;
|
|
1581
2799
|
};
|
|
1582
2800
|
if ((d.opcode === 'add' || d.opcode === 'sub') && r.k === 'const') {
|
|
1583
|
-
r =
|
|
2801
|
+
({ base: l, off: r } = walk(l, r));
|
|
1584
2802
|
} else if (d.opcode === 'add' && d.operands.length === 2 && l.k === 'const') {
|
|
1585
|
-
l =
|
|
2803
|
+
({ base: r, off: l } = walk(r, l)); // commuted `const + ptr`
|
|
2804
|
+
} else if (d.opcode === 'add' || d.opcode === 'sub') {
|
|
2805
|
+
// ONE side only. `ptr - ptr` is C's element difference and `ptr + ptr` is not C at all;
|
|
2806
|
+
// both are the intify rules' business below, and casting both operands here would hide
|
|
2807
|
+
// the shape from them.
|
|
2808
|
+
const lp = ctype(l)?.kind === 'ptr';
|
|
2809
|
+
const rp = d.operands.length === 2 && ctype(r)?.kind === 'ptr';
|
|
2810
|
+
if (lp && !rp) {
|
|
2811
|
+
restoreTo = walkVar(l);
|
|
2812
|
+
l = restoreTo ? bytePtr(l) : l;
|
|
2813
|
+
} else if (rp && !lp && d.opcode === 'add') {
|
|
2814
|
+
restoreTo = walkVar(r);
|
|
2815
|
+
r = restoreTo ? bytePtr(r) : r;
|
|
2816
|
+
}
|
|
1586
2817
|
}
|
|
1587
2818
|
// C rejects a pointer operand outright under the non-additive operators (& | ^ << >> * / %),
|
|
1588
2819
|
// under `ptr + ptr`, and as the subtrahend of `int - ptr` — the asm just does 32-bit integer
|
|
@@ -1611,42 +2842,45 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1611
2842
|
const intifyAddr = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: T.u(32), e: x } : x);
|
|
1612
2843
|
l = intifyAddr(l);
|
|
1613
2844
|
r = intifyAddr(r);
|
|
1614
|
-
// The SAME hazard one level down, for a
|
|
1615
|
-
// C scales `gPtr + K` by sizeof(*gPtr) — 1 under the map's synthesized
|
|
1616
|
-
// whatever the PROJECT's header declares (a
|
|
1617
|
-
// actually recompiles in. The asm added BYTES, so the honest
|
|
1618
|
-
// explicit: CAST-THEN-ADD, `(u8 *)gPtr + K`, the same address in
|
|
1619
|
-
// (`(u8 *)(gPtr + K)`, what the backend's deref legalization
|
|
1620
|
-
// byte-correct in exactly one of them — a silent wrongness, the
|
|
2845
|
+
// The SAME hazard one level down, for a value the MAP declares a pointer (`gPtr`, `gSym.pBuf`
|
|
2846
|
+
// — isPtrValue): C scales `gPtr + K` by sizeof(*gPtr) — 1 under the map's synthesized
|
|
2847
|
+
// `void *`, but whatever the PROJECT's header declares (a `u16 *` member, a 0x5C-byte
|
|
2848
|
+
// struct) in the world a user actually recompiles in. The asm added BYTES, so the honest
|
|
2849
|
+
// spelling makes the stride explicit: CAST-THEN-ADD, `(u8 *)gPtr + K`, the same address in
|
|
2850
|
+
// EVERY world. Add-then-cast (`(u8 *)(gPtr + K)`, what the backend's deref legalization
|
|
2851
|
+
// would otherwise produce) is byte-correct in exactly one of them — a silent wrongness, the
|
|
2852
|
+
// class this project refuses. A MEMBER is the case with no world in which the raw spelling
|
|
2853
|
+
// is right: the map declares the pointee width, so `bytes + gSym.pBuf` on a `u16 *` scales
|
|
2854
|
+
// the residual a SECOND time and addresses twice the byte the asm did.
|
|
1621
2855
|
// NOT foldable into the deref index either: `((u8 *)gPtr)[K + off]` re-scales K by the
|
|
1622
2856
|
// ACCESS width, a different address whenever that width is not 1.
|
|
1623
2857
|
// Under the non-additive operators C rejects a pointer outright, so there the honest
|
|
1624
2858
|
// spelling is integer math on the cell — exactly intifyAddr's `(u32)&gSym` rule.
|
|
1625
|
-
const
|
|
1626
|
-
const intifyPtrGlobal = (x: Expr): Expr => ({ k: 'cast', to: T.u(32), e: x });
|
|
2859
|
+
const intifyPtrValue = (x: Expr): Expr => ({ k: 'cast', to: T.u(32), e: x });
|
|
1627
2860
|
if (op === '+' || op === '-') {
|
|
1628
2861
|
// `ptr ± int` and `ptr - ptr` are byte arithmetic once both sides are byte pointers;
|
|
1629
2862
|
// `ptr + ptr` and `int - ptr` are not C at all, so the second pointer goes integer.
|
|
1630
|
-
const bothPtr =
|
|
1631
|
-
if (
|
|
2863
|
+
const bothPtr = isPtrValue(l) && isPtrValue(r);
|
|
2864
|
+
if (isPtrValue(l)) {
|
|
1632
2865
|
l = bytePtr(l);
|
|
1633
2866
|
}
|
|
1634
|
-
if (
|
|
1635
|
-
r = bothPtr && op === '-' ? bytePtr(r) : op === '+' && !bothPtr ? bytePtr(r) :
|
|
2867
|
+
if (isPtrValue(r)) {
|
|
2868
|
+
r = bothPtr && op === '-' ? bytePtr(r) : op === '+' && !bothPtr ? bytePtr(r) : intifyPtrValue(r);
|
|
1636
2869
|
}
|
|
1637
2870
|
} else if (op !== '&&' && op !== '||') {
|
|
1638
2871
|
// (`&&`/`||` take a pointer operand legally — a truth test, no arithmetic.)
|
|
1639
|
-
l =
|
|
1640
|
-
r =
|
|
2872
|
+
l = isPtrValue(l) ? intifyPtrValue(l) : l;
|
|
2873
|
+
r = isPtrValue(r) ? intifyPtrValue(r) : r;
|
|
1641
2874
|
}
|
|
1642
|
-
// (The
|
|
1643
|
-
// a language spells each with, and what cast pins the choice, is a BACKEND
|
|
1644
|
-
// l3/ast.ts BinOp and backend/cfamily.ts's
|
|
2875
|
+
// (The signedness-carrying pairs stay DISTINCT ops here — `>>>`/`>>` and `/u` `%u`/`/` `%`.
|
|
2876
|
+
// Which token a language spells each with, and what cast pins the choice, is a BACKEND
|
|
2877
|
+
// decision; see l3/ast.ts BinOp and backend/cfamily.ts's C_SPELLING.)
|
|
1645
2878
|
// SCOPE: this and intifyAddr cover the ARITHMETIC escapes. A pointer global under a
|
|
1646
2879
|
// COMPARISON (`gPtr < K` — C compares unsigned whatever the asm's icmp_s* said) is the same
|
|
1647
2880
|
// class as intifyAddrCmp's `addr` rule and is deliberately left alone here: it is valid C
|
|
1648
2881
|
// today, so closing it would churn spellings for a signedness case no row exercises.
|
|
1649
|
-
|
|
2882
|
+
const sum: Expr = { k: 'bin', op, l, r };
|
|
2883
|
+
return restoreTo ? { k: 'cast', to: restoreTo, e: sum } : sum;
|
|
1650
2884
|
}
|
|
1651
2885
|
// `-`/`~` on a pointer rendering is equally not C — same honest integer cast as above.
|
|
1652
2886
|
if (d.opcode === 'rotr' || d.opcode === 'rotl') {
|
|
@@ -1689,11 +2923,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1689
2923
|
return { k: 'un', op: '~', e: needsIntSpelling(x) ? { k: 'cast', to: T.s(32), e: x } : x };
|
|
1690
2924
|
}
|
|
1691
2925
|
// Width-narrowing casts: `zext`/`sext` widen a `width`-bit value back to 32 → C `(u8)e`/`(s8)e`.
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
2926
|
+
// The width is in BITS, and outside `CAST_WIDTHS` there is no C type to cast to — a GAP, not
|
|
2927
|
+
// the `(u2)` the backend would otherwise print and no compiler accept. No frontend produces
|
|
2928
|
+
// one (the cast idioms and PPC's `extsb`/`extsh` emit 8 and 16), so this is the loud floor
|
|
2929
|
+
// under hand-written IR; `raise/narrowlocal.ts` and `raise/paramwidth.ts` refuse to NARROW on
|
|
2930
|
+
// such a width, which is a different decision from spelling one.
|
|
2931
|
+
if (d.opcode === 'zext' || d.opcode === 'sext') {
|
|
2932
|
+
const w = d.attrs.width as number;
|
|
2933
|
+
return CAST_WIDTHS.has(w)
|
|
2934
|
+
? { k: 'cast', to: T.int(w, d.opcode === 'sext'), e: e(d.operands[0]) }
|
|
2935
|
+
: mkGap(`no C type for a ${w}-bit '${d.opcode}'`, [e(d.operands[0])]);
|
|
1697
2936
|
}
|
|
1698
2937
|
if (d.opcode === 'call') {
|
|
1699
2938
|
return { k: 'call', fn: d.attrs.target as string, args: d.operands.map(e) };
|
|
@@ -1704,10 +2943,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1704
2943
|
// see laddrName. Renders `&sp0`; the object itself is declared in `locals`.
|
|
1705
2944
|
return { k: 'addr', name: laddrName.get(d)! };
|
|
1706
2945
|
}
|
|
2946
|
+
if (d.opcode === 'undef') {
|
|
2947
|
+
// An uninitialised local. NEVER emit a definition for it: the declaration in `locals` is the
|
|
2948
|
+
// entire recovery, and `sideEffects` skips an `undef` (neither effectful nor materialized),
|
|
2949
|
+
// which is what leaves it bare.
|
|
2950
|
+
return { k: 'var', name: undefName.get(d)! };
|
|
2951
|
+
}
|
|
1707
2952
|
if (d.opcode === 'gaddr') {
|
|
1708
2953
|
// A promoted CODE symbol (frontend `code: true`) is a function pointer stored as an
|
|
1709
|
-
// integer: spelled `(u32)Name` — the source idiom — never `&Name` (
|
|
1710
|
-
//
|
|
2954
|
+
// integer: spelled `(u32)Name` — the source idiom — never `&Name` (the & form compiles,
|
|
2955
|
+
// but it is a different and non-matching spelling).
|
|
1711
2956
|
if (d.attrs.code === true) {
|
|
1712
2957
|
return { k: 'cast', to: T.int(32, false), e: { k: 'var', name: d.attrs.sym as string } };
|
|
1713
2958
|
}
|
|
@@ -1733,6 +2978,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1733
2978
|
e(d.operands[0]),
|
|
1734
2979
|
e(d.operands[1]),
|
|
1735
2980
|
d.attrs.fieldOff as number | undefined,
|
|
2981
|
+
d.attrs.memberOff as number | undefined,
|
|
1736
2982
|
d.attrs.elemSize as number,
|
|
1737
2983
|
(d.attrs.signed as boolean) ?? false,
|
|
1738
2984
|
ctype,
|
|
@@ -1763,9 +3009,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1763
3009
|
};
|
|
1764
3010
|
// The loop-emission hazard checks (readsClobbered / loopEscapeHazard / loopUpdateHazard) —
|
|
1765
3011
|
// pure decline-or-emit predicates, extracted to hazards.ts behind the explicit-deps factory.
|
|
1766
|
-
// `varName` is captured as a live
|
|
1767
|
-
//
|
|
1768
|
-
|
|
3012
|
+
// `varName` is captured as a live REFERENCE rather than copied, so each check reads the names
|
|
3013
|
+
// EMISSION sees. The map is already FINAL at this point — every write to it sits above, in the
|
|
3014
|
+
// naming walk and the coalescing that follows it, and these checks only read it — so today the
|
|
3015
|
+
// two readings coincide; the reference is what keeps them coinciding if a write ever moves down
|
|
3016
|
+
// here.
|
|
3017
|
+
const { readsClobbered, loopUpdateHazard, sinkablePreUpdateSlots, sameAtEntry, loopWriteSet } = makeLoopHazards({
|
|
3018
|
+
defs,
|
|
3019
|
+
varName,
|
|
3020
|
+
useSitesOf,
|
|
3021
|
+
liveIn,
|
|
3022
|
+
opBlock,
|
|
3023
|
+
materialize,
|
|
3024
|
+
respelledDefs: bitfieldSpelling,
|
|
3025
|
+
});
|
|
1769
3026
|
|
|
1770
3027
|
// A POST-LOOP substitution active while structuring a loop's exit region: a loop-carried value (a
|
|
1771
3028
|
// latch back-edge arg) is held in its loop-variable NAME after the loop, so any post-loop use must
|
|
@@ -1788,63 +3045,335 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1788
3045
|
// value. `sub` (used for the emitWhile un-rotation's exit copies) substitutes back-edge args to
|
|
1789
3046
|
// their header-param NAMES — post-loop the params already hold their updated values, so a merged
|
|
1790
3047
|
// exit value is read as `v` not `v-1`.
|
|
3048
|
+
//
|
|
3049
|
+
// AN UNDEFINED ARGUMENT CARRIES NOTHING, so it gets no copy — WHERE THE DESTINATION IS ITSELF
|
|
3050
|
+
// UNDEFINED THERE. `undef` is storage nothing wrote on this path (ir/opcodes.ts), so
|
|
3051
|
+
// `w = uninit_sp0;` spells a read of storage that was never written: a statement the asm has no
|
|
3052
|
+
// instruction for, whose cost is the register the undefined value then occupies across the merge.
|
|
3053
|
+
// Dropping it leaves the variable holding whatever it held — the same thing exactly when nothing
|
|
3054
|
+
// ever wrote it before this edge, and a DIFFERENT FUNCTION otherwise. That second case is real:
|
|
3055
|
+
// a merge that adopted an incoming parameter's name emits `if (a0 == 0) a0 = uninit_sp0;`, where
|
|
3056
|
+
// dropping the copy would substitute the parameter's defined value for the undefined one.
|
|
3057
|
+
//
|
|
3058
|
+
// So the test is over the destination's whole name class: no value spelled with that name may
|
|
3059
|
+
// have a definition able to execute before this edge — its home block being this predecessor, or
|
|
3060
|
+
// reaching it, which through a back edge is also how an earlier iteration's write is caught.
|
|
3061
|
+
// Unsure keeps the copy. Edge copies only: a `store` or a `ret` of an undef value is a real
|
|
3062
|
+
// instruction and emits.
|
|
3063
|
+
//
|
|
3064
|
+
// A value's home is where the copies into it run, and anchoring MOVES one: an anchored const is
|
|
3065
|
+
// written at the const's own def site instead of on the edges (anchorConstCopies, above), and
|
|
3066
|
+
// that site dominates them. Its block is a write site for the name like any other, and without it
|
|
3067
|
+
// `v0 = 0; if (c) { } store v0;` drops the undefined arm's copy and stores 0 where the machine
|
|
3068
|
+
// stores whatever the arm left — the same substitution the parameter case makes, one axis over.
|
|
3069
|
+
//
|
|
3070
|
+
// THE SECOND RELOCATION goes the other way, and this test cannot see it at all. A SUNK pre-update
|
|
3071
|
+
// exit copy (preUpdateCopies) writes the loop EXIT's param at the top of the loop BODY, so the
|
|
3072
|
+
// home this reads for it — `paramBlock`, the exit block — sits strictly LATER in the CFG than
|
|
3073
|
+
// where the copy lands. What keeps the two apart is not this test but `dest-free-inside-loop`
|
|
3074
|
+
// (hazards.ts): a merge inside the body under the exit param's name is a block param
|
|
3075
|
+
// `definedInBody` sees, so the slot is never sunk in the first place. That gate carries its own
|
|
3076
|
+
// KNOWN GAP, so the pair is a conjecture rather than a proof — `sunkCopyOverDroppedUndef` re-checks
|
|
3077
|
+
// it per function once both records below are complete, which is the only point at which they can
|
|
3078
|
+
// be: the do-while path structures its body BEFORE it mints its sunk copies.
|
|
3079
|
+
const anchoredHome = new Map<string, Block[]>();
|
|
3080
|
+
for (const [def, entries] of anchoredAt) {
|
|
3081
|
+
for (const { name } of entries) {
|
|
3082
|
+
(anchoredHome.get(name) ?? anchoredHome.set(name, []).get(name)!).push(opBlock.get(def)!);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
/** Every copy `undefCarriesNothing` dropped, with the edge it was dropped from. */
|
|
3086
|
+
const droppedUndefCopies: { name: string; pred: Block }[] = [];
|
|
3087
|
+
/** Every sunk pre-update exit copy, homed where it LANDS (the loop header whose body opens with
|
|
3088
|
+
* it) rather than where its destination param lives. These two lists are the postcondition's
|
|
3089
|
+
* whole input. */
|
|
3090
|
+
const sunkCopyHomes: { name: string; home: Block }[] = [];
|
|
3091
|
+
const undefCarriesNothing = (arg: Value, name: string, pred: Block): boolean => {
|
|
3092
|
+
if (defs.get(arg)?.opcode !== 'undef') {
|
|
3093
|
+
return false;
|
|
3094
|
+
}
|
|
3095
|
+
const writesBefore = (home: Block | undefined): boolean =>
|
|
3096
|
+
home === undefined || home === pred || reachFrom(home).has(pred);
|
|
3097
|
+
for (const [v, n] of varName) {
|
|
3098
|
+
if (n === name && writesBefore(paramBlock.get(v) ?? opBlock.get(defs.get(v)!))) {
|
|
3099
|
+
return false;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
return !(anchoredHome.get(name) ?? []).some(writesBefore);
|
|
3103
|
+
};
|
|
1791
3104
|
const tempCounter = { n: 0 }; // per-function swap-cycle temp names (sequentialize)
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
3105
|
+
/** One record per parameter slot an edge ACTUALLY carries, in the order the copies are to be
|
|
3106
|
+
* laid out — everything `argAssignsFor` does except `sequentialize`. Split out because Regime-A
|
|
3107
|
+
* switch recovery merges the records of a whole DISPATCH's edges and sequentializes the union
|
|
3108
|
+
* ONCE (`hoistedDispatchAssigns` below); sequentializing per edge and then deduping would mint
|
|
3109
|
+
* a swap-cycle temp per edge and spell a different program. */
|
|
3110
|
+
const edgeCopyRecords = (
|
|
1796
3111
|
pred: Block,
|
|
1797
3112
|
succ: { block: Block; args: Value[] },
|
|
1798
3113
|
sub: Map<Value, string> | null = null,
|
|
1799
|
-
|
|
3114
|
+
keepSlot: (i: number) => boolean = () => true,
|
|
3115
|
+
): { name: string; value: Expr; arg: Value; param: Value }[] => {
|
|
1800
3116
|
const target = succ.block;
|
|
1801
3117
|
const argExpr = sub ? exprWith(sub) : expr;
|
|
1802
|
-
const copies: { name: string; value: Expr; arg: Value }[] = [];
|
|
3118
|
+
const copies: { name: string; value: Expr; arg: Value; param: Value }[] = [];
|
|
1803
3119
|
const suppressed = suppressedArgs.get(succ);
|
|
1804
3120
|
target.params.forEach((p, i) => {
|
|
1805
|
-
if (suppressed?.has(i)) {
|
|
3121
|
+
if (suppressed?.has(i) || !keepSlot(i)) {
|
|
1806
3122
|
return;
|
|
1807
|
-
} // anchored at its const's def site —
|
|
3123
|
+
} // anchored at its const's def site (or emitted elsewhere) — this edge does not carry it
|
|
1808
3124
|
const name = varName.get(p)!;
|
|
1809
3125
|
const arg = succ.args[i];
|
|
1810
3126
|
if ((sub?.get(arg) ?? varName.get(arg)) === name) {
|
|
1811
3127
|
return;
|
|
1812
3128
|
} // identity copy — coalesced away
|
|
1813
|
-
|
|
3129
|
+
if (undefCarriesNothing(arg, name, pred)) {
|
|
3130
|
+
droppedUndefCopies.push({ name, pred });
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
copies.push({ name, value: intoDeclaredTemp(name, argExpr(arg)), arg, param: p });
|
|
1814
3134
|
});
|
|
1815
|
-
// Emit in the order the
|
|
1816
|
-
//
|
|
1817
|
-
// This is a per-compiler behavior (
|
|
1818
|
-
// that emits copies in source/param order sets it false
|
|
1819
|
-
//
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
3135
|
+
// Emit in the order the pred WROTE the destinations (ir/core.ts `WriteOrder` — the frontend's
|
|
3136
|
+
// measurement; the value graph cannot show it, because a copy is the same SSA value under a new
|
|
3137
|
+
// key). This is a per-compiler behavior (orderArgCopiesByWriteOrder), not a universal: a
|
|
3138
|
+
// compiler that emits copies in source/param order sets it false and no sort runs at all.
|
|
3139
|
+
//
|
|
3140
|
+
// THE RULE IS TWO CLAIMS, and only the first is licensed by an instruction:
|
|
3141
|
+
//
|
|
3142
|
+
// 1. CYCLIC copy sets. `sequentialize` spills the FIRST pending destination, and the record
|
|
3143
|
+
// names the destination whose FINAL value the compiler established earliest — the one whose
|
|
3144
|
+
// old value had to be saved elsewhere, since every later reader of that old value reads it
|
|
3145
|
+
// after the register stopped holding it. That is the compiler's own temp.
|
|
3146
|
+
// READ THE QUANTITY AS THE LAST WRITE, not the first overwrite: a pred commonly writes a
|
|
3147
|
+
// key several times (1,867 of 5,283 edge-destination records over klonoa, marioparty3 and
|
|
3148
|
+
// af, in 312 of the 464 functions that record anything). Both alternatives cost matches
|
|
3149
|
+
// over the 736 synthetic rows and gain none: recording the FIRST write loses `bgsplit`,
|
|
3150
|
+
// `bgswitch`, `bgswsplit` and `hipress` (all agbcc, 482 → 478); REFUSING the record for a
|
|
3151
|
+
// key written more than once loses those four plus `dmascope2` and `swmulti` (482 → 476).
|
|
3152
|
+
// 2. ACYCLIC copy sets — a SECOND, separately-licensed claim: that the compiler LAID THE
|
|
3153
|
+
// COPIES OUT in the order it wrote them. No instruction states it, and it is two-sided, so
|
|
3154
|
+
// the def-position spelling is enumerated beside the record's as a ranked candidate
|
|
3155
|
+
// (`/copy-defpos`, rank.ts) instead of being declared here. Restricting the record to
|
|
3156
|
+
// cyclic sets as the DEFAULT was measured over the 736 synthetic rows: it takes
|
|
3157
|
+
// `armfall:agbcc` 11 → 8, `memcpy1:mwcc` 23 → 19 and `memset1:mwcc` 21 → 19 — and LOSES
|
|
3158
|
+
// `gcd:agbcc` (the cyclic row's own entry edge is acyclic) and `structarr:agbcc` to
|
|
3159
|
+
// nonmatch. The CANDIDATE spans that second claim only (`preferDefPosCopyOrder`: the proxy
|
|
3160
|
+
// on acyclic sets, the record on cyclic ones), so no arm of the fan spells a cycle against
|
|
3161
|
+
// the instruction that licenses it, and the pair differs exactly where the evidence runs
|
|
3162
|
+
// out — which is what lets one function take the record on its back edge and the proxy on
|
|
3163
|
+
// its entry edge, as `gcd:agbcc` does.
|
|
3164
|
+
//
|
|
3165
|
+
// IT REACHES FURTHER THAN `sequentialize`, because it decides which copy is an arm's LAST
|
|
3166
|
+
// statement: `l3/tailmerge.ts` peels only a common last statement, so an arm-varying copy
|
|
3167
|
+
// ordered last hides an agreeing one behind it (measured there, on klonoa
|
|
3168
|
+
// `CountCollectedGems`); and `recognizeForLoops` recovers a `for` only when the induction
|
|
3169
|
+
// update is last, so this sort also decides `for` vs `while` (goldens in
|
|
3170
|
+
// `structure-goldens.test.ts`). That is why `synthetic:gcd:agbcc` is a `while` — agbcc wrote
|
|
3171
|
+
// the dividend's register last, so the modulo update is not last.
|
|
3172
|
+
//
|
|
3173
|
+
// A destination with NO RECORD keeps `NO_RECORD`, sorting first, which is where the proxy also
|
|
3174
|
+
// puts an argument the pred did not compute: on those copies the two orders AGREE rather than
|
|
3175
|
+
// one replacing the other. Pinning unwritten destinations at their param-order slot instead
|
|
3176
|
+
// loses `armdef:agbcc`, `loopfall:agbcc`, `loopset:agbcc` and `structarr:agbcc` over the same
|
|
3177
|
+
// 736 rows, and buys back only `armfall` 11 → 7 and `ucmp:kmc` 15 → 14.
|
|
3178
|
+
if (orderArgCopiesByWriteOrder) {
|
|
3179
|
+
// UNMEASURED (undefined) is not "wrote nothing": a parsed or hand-built fn carries no record
|
|
3180
|
+
// at all and its edges keep the def-position proxy, while a MEASURED pred that recorded no
|
|
3181
|
+
// destination of this edge gets an empty record. The two cannot meet inside one fn —
|
|
3182
|
+
// measurement is all-or-nothing per function and `ir/verify.ts` enforces it — so this asks
|
|
3183
|
+
// per block only to spell the wholly unmeasured fn.
|
|
3184
|
+
const predIsMeasured = fn.writeOrder?.writes.has(pred) ?? false;
|
|
3185
|
+
// The `/copy-defpos` candidate asking for the proxy on an edge the frontend DID measure —
|
|
3186
|
+
// the one place the two questions ("is there a record" and "use it") come apart.
|
|
3187
|
+
const record =
|
|
3188
|
+
predIsMeasured && (!preferDefPosCopyOrder || copySetIsCyclic(copies))
|
|
3189
|
+
? (fn.writeOrder!.lastWrite.get(pred) ?? NO_WRITTEN_DESTINATIONS)
|
|
3190
|
+
: undefined;
|
|
3191
|
+
const rank =
|
|
3192
|
+
record !== undefined
|
|
3193
|
+
? (c: (typeof copies)[number]) => record.get(c.param) ?? NO_RECORD
|
|
3194
|
+
: // opIndex is only valid for a def IN this block; a def elsewhere gets NO_RECORD too.
|
|
3195
|
+
(c: (typeof copies)[number]) => {
|
|
3196
|
+
const d = defs.get(c.arg);
|
|
3197
|
+
return d && opBlock.get(d) === pred ? opIndex.get(d)! : NO_RECORD;
|
|
3198
|
+
};
|
|
3199
|
+
copies.sort((a, b) => rank(a) - rank(b));
|
|
3200
|
+
}
|
|
3201
|
+
return copies;
|
|
3202
|
+
};
|
|
3203
|
+
// The copies for ONE specific successor record — the workhorse behind argAssigns, taken
|
|
3204
|
+
// directly by the switch_br path, whose duplicate case targets successorTo cannot
|
|
3205
|
+
// disambiguate.
|
|
3206
|
+
const argAssignsFor = (
|
|
3207
|
+
pred: Block,
|
|
3208
|
+
succ: { block: Block; args: Value[] },
|
|
3209
|
+
sub: Map<Value, string> | null = null,
|
|
3210
|
+
keepSlot: (i: number) => boolean = () => true,
|
|
3211
|
+
): Stmt[] =>
|
|
3212
|
+
sequentialize(
|
|
3213
|
+
edgeCopyRecords(pred, succ, sub, keepSlot).map(({ name, value }) => ({ name, value })),
|
|
3214
|
+
varType,
|
|
3215
|
+
tempCounter,
|
|
3216
|
+
fn.name,
|
|
3217
|
+
);
|
|
3218
|
+
/** THE DISPATCH HOIST (Regime-A switch recovery). Recovering a comparison tree COLLAPSES its
|
|
3219
|
+
* test blocks, and an edge's only emission is its parallel copy — so every copy the dispatch's
|
|
3220
|
+
* edges carried would be discarded with the tree. This re-emits them above the `switch`, ONCE
|
|
3221
|
+
* PER NAME the dispatch binds — which is not once per machine write: `sw_fall`'s three arms
|
|
3222
|
+
* each take the accumulator under a name of their own, so this tree emits `v0 = 0; v1 = 0;
|
|
3223
|
+
* v2 = 0;` where agbcc has a single `mov r1, #0`, and the one-local spelling that byte-matches
|
|
3224
|
+
* comes from the `/merge-home` ranked axis, not from here. What the position is for is the
|
|
3225
|
+
* fall-through chain: per-arm copies RE-RUN on the fall path and overwrite what the falling arm
|
|
3226
|
+
* computed, the hazard Regime B states at its own `switch_br` refusal.
|
|
3227
|
+
*
|
|
3228
|
+
* Null (⇒ the caller declines to if-recovery, which spells every copy the asm performs) on the
|
|
3229
|
+
* two things one hoisted statement cannot say. Both are stated over NAMES, because the name is
|
|
3230
|
+
* what the statement writes. NEITHER FIRES ON A CORPUS ROW: over the synthetic tier the only
|
|
3231
|
+
* functions offering the hoist a param-carrying dispatch edge are the `sw_fall*` family (four
|
|
3232
|
+
* of them, 13 edges under 10 names), it emits on all four, and what refuses is the caller's own
|
|
3233
|
+
* `availableAtRoot`. Both refusals below are pinned by hand-written `.s` in
|
|
3234
|
+
* `test/switch-arms.test.ts` and by nothing else — read them as invariants, not as evidence:
|
|
3235
|
+
*
|
|
3236
|
+
* - DISAGREEING EDGES. Two edges binding one name to different values: which one runs depends
|
|
3237
|
+
* on which test fell through, and a single statement above the tree cannot depend on that.
|
|
3238
|
+
* Order-independent, so no emission order hides it.
|
|
3239
|
+
* - A CLOBBERED LIVE NAME. The hoisted write runs on EVERY path through the dispatch,
|
|
3240
|
+
* including into arms whose own edge did not carry it. If any value under that name is
|
|
3241
|
+
* live where the switch begins, or into one of its arms, the hoist would overwrite a value
|
|
3242
|
+
* those readers still want. THE SCAN REACHES values read at or after the listed blocks'
|
|
3243
|
+
* entries, and by construction NOT: the parameters of those blocks (`analysis.ts` kills a
|
|
3244
|
+
* block's params at its own entry), a value defined in the root whose only use is a
|
|
3245
|
+
* dispatch edge arg, or a binding elided upstream (identity copy, `undefCarriesNothing`, a
|
|
3246
|
+
* suppressed anchored slot), which contribute no name to compare. Binding an arm's
|
|
3247
|
+
* parameters is what the hoist is FOR, so that blindness is wanted; the other three are
|
|
3248
|
+
* gaps nobody has turned into a wrong answer.
|
|
3249
|
+
*
|
|
3250
|
+
* `anchorConstCopies` (above) relocates an edge copy too, and states two clauses this does not;
|
|
3251
|
+
* neither absence is an oversight. Its LOOP clause does not transfer: anchoring moves a write to
|
|
3252
|
+
* the const's DEF site, which may sit outside the loop the edge is in (block dominance is not
|
|
3253
|
+
* per-iteration precedence — see that clause's own statement above), while the hoist moves a
|
|
3254
|
+
* write from a dispatch's edges to the head of that same dispatch, same iteration every time
|
|
3255
|
+
* (`switch-arms.test.ts` pins a param-carrying dispatch inside a `do`-`while`). Its NAME-COUNT
|
|
3256
|
+
* clause — refuse a name several SSA values carry — cannot be adopted, because that is this
|
|
3257
|
+
* mechanism's whole subject: one name per arm taking the accumulator. As a refusal it declines
|
|
3258
|
+
* the empty-arm fall-through switch of `retsink.test.ts` outright, for no row's benefit.
|
|
3259
|
+
*
|
|
3260
|
+
* ORDER ACROSS EDGES IS UNLICENSED. Within one edge, `edgeCopyRecords` sorts by the pred's
|
|
3261
|
+
* measured `WriteOrder`; the union below keeps the first record per name in TREE-WALK order,
|
|
3262
|
+
* which no compiler measured — there is no per-pred record spanning writes that different preds
|
|
3263
|
+
* performed. It is deterministic, and `switch-arms.test.ts` pins the three-name order `sw_fall`
|
|
3264
|
+
* emits so that a change to the walk fails loudly rather than silently re-spelling a match.
|
|
3265
|
+
*
|
|
3266
|
+
* What it does NOT re-check is that evaluating an arg AT THE ROOT rather than on its edge is
|
|
3267
|
+
* value-preserving. That rests on the collapsed blocks being pure (switch-recover.ts's PRE4)
|
|
3268
|
+
* and on every hoisted arg being available at the root, which the caller establishes before
|
|
3269
|
+
* calling — the hoist depends on those rules, it does not weaken them. */
|
|
3270
|
+
const hoistedDispatchAssigns = (
|
|
3271
|
+
edges: readonly { pred: Block; succ: { block: Block; args: Value[] } }[],
|
|
3272
|
+
liveAt: readonly Block[],
|
|
3273
|
+
): Stmt[] | null => {
|
|
3274
|
+
// `edgeCopyRecords` is not a pure query: an `undef` arg whose copy it drops is APPENDED to
|
|
3275
|
+
// `droppedUndefCopies`, which a loud postcondition below reads. This caller is SPECULATIVE — it
|
|
3276
|
+
// asks every dispatch edge and may still decline — so the records are rolled back on the
|
|
3277
|
+
// refusal paths, leaving the audit trail describing the program actually emitted (the caller
|
|
3278
|
+
// then declines to if-recovery, which re-walks the same edges through `argAssignsFor` and
|
|
3279
|
+
// records them there). `undefCarriesNothing` judges PER PRED, against what that predecessor
|
|
3280
|
+
// wrote, while the hoist emits at the ROOT — so the record belongs to the edge walk that
|
|
3281
|
+
// survives, not to this one. The rollback covers the refusals THIS function owns: a hoist that
|
|
3282
|
+
// succeeds and is declined further down (an arm refusal in switch-recover.ts) still leaves its
|
|
3283
|
+
// records, as it leaves any swap-cycle temp name `sequentialize` minted.
|
|
3284
|
+
const auditMark = droppedUndefCopies.length;
|
|
3285
|
+
const rollBack = (): null => {
|
|
3286
|
+
droppedUndefCopies.length = auditMark;
|
|
3287
|
+
return null;
|
|
3288
|
+
};
|
|
3289
|
+
const merged = new Map<string, { name: string; value: Expr; arg: Value; param: Value }>();
|
|
3290
|
+
for (const { pred, succ } of edges) {
|
|
3291
|
+
for (const c of edgeCopyRecords(pred, succ)) {
|
|
3292
|
+
const prev = merged.get(c.name);
|
|
3293
|
+
if (prev === undefined) {
|
|
3294
|
+
merged.set(c.name, c);
|
|
3295
|
+
} else if (prev.arg !== c.arg) {
|
|
3296
|
+
return rollBack(); // disagreeing edges
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
if (merged.size === 0) {
|
|
3301
|
+
return [];
|
|
3302
|
+
}
|
|
3303
|
+
const bound = new Set([...merged.values()].map((c) => c.param));
|
|
3304
|
+
for (const blk of liveAt) {
|
|
3305
|
+
for (const v of liveIn.get(blk) ?? []) {
|
|
3306
|
+
const nm = varName.get(v);
|
|
3307
|
+
if (nm !== undefined && merged.has(nm) && !bound.has(v)) {
|
|
3308
|
+
return rollBack(); // a clobbered live name
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
1827
3311
|
}
|
|
1828
3312
|
return sequentialize(
|
|
1829
|
-
|
|
3313
|
+
[...merged.values()].map(({ name, value }) => ({ name, value })),
|
|
1830
3314
|
varType,
|
|
1831
3315
|
tempCounter,
|
|
1832
3316
|
fn.name,
|
|
1833
3317
|
);
|
|
1834
3318
|
};
|
|
1835
|
-
const argAssigns = (
|
|
3319
|
+
const argAssigns = (
|
|
3320
|
+
pred: Block,
|
|
3321
|
+
target: Block,
|
|
3322
|
+
sub: Map<Value, string> | null = null,
|
|
3323
|
+
keepSlot?: (i: number) => boolean,
|
|
3324
|
+
): Stmt[] => {
|
|
1836
3325
|
const succ = successorTo(pred, target);
|
|
1837
|
-
return succ ? argAssignsFor(pred, succ, sub) : [];
|
|
3326
|
+
return succ ? argAssignsFor(pred, succ, sub, keepSlot) : [];
|
|
1838
3327
|
};
|
|
1839
3328
|
|
|
1840
3329
|
// Side-effecting ops of a block, emitted as statements in program order: memory stores; any
|
|
1841
3330
|
// EFFECTFUL op whose result nothing consumes (a void/discarded call, and an `opaque` standing for
|
|
1842
3331
|
// an instruction asmlift could not model); and MATERIALIZED defs — a call/load whose value cannot
|
|
1843
3332
|
// soundly render at its use is assigned to its named temp here, at its own program position.
|
|
3333
|
+
/** A pointer VALUE assigned into a map-declared pointer CELL, spelled so the assignment is legal
|
|
3334
|
+
* against ANY declaration of that cell. The byte-arithmetic guard renders such a right-hand
|
|
3335
|
+
* side `(u8 *)gS.pBuf + K` — the right ADDRESS in every world, which is the whole point of it,
|
|
3336
|
+
* and a `u8 *` where the declaration says `u16 *`. READING one is fine (C converts an object
|
|
3337
|
+
* pointer freely under a deref, a call argument or a compare); ASSIGNING one is `warning:
|
|
3338
|
+
* assignment from incompatible pointer type`, and this project's own `-Werror` compiler
|
|
3339
|
+
* template makes that FATAL — so a source that scores clean here fails to build in the tree a
|
|
3340
|
+
* user pastes it into, which no score gate can observe.
|
|
3341
|
+
*
|
|
3342
|
+
* `void *`, NOT the map's declared pointee: it is the one target assignment-compatible with any
|
|
3343
|
+
* object-pointer declaration, the same "same answer in every world" property the guard that
|
|
3344
|
+
* made the `u8 *` exists for. Trusting the map's pointee would put the spelling back in one
|
|
3345
|
+
* world. It costs nothing: agbcc compiles `p = (void *)((u8 *)p + 4)` and the warning-carrying
|
|
3346
|
+
* `p = (u8 *)p + 4` to BYTE-IDENTICAL objects (`-mthumb-interwork -Wimplicit -O2 -fhex-asm
|
|
3347
|
+
* -fprologue-bugfix`), so the fix is invisible to the differ and visible to the compiler. */
|
|
3348
|
+
const intoPtrCell = (lval: Expr, value: Expr): Expr => {
|
|
3349
|
+
const cell = lval.k === 'var' ? symCtx?.info(lval.name)?.shape === 'pointer' : ptrMemberDecl(lval, symCtx) !== null;
|
|
3350
|
+
if (!cell) {
|
|
3351
|
+
return value;
|
|
3352
|
+
}
|
|
3353
|
+
const vt = ctype(value);
|
|
3354
|
+
// BOTH ways a pointer value reaches here. `ctype` types params and locals, so it sees the
|
|
3355
|
+
// guard's own `(u8 *)…` and nothing else: a bare `gSym.pBuf` or a pointer global's value —
|
|
3356
|
+
// the population this rule exists for — reads `undefined` there. An already-`void *` value is
|
|
3357
|
+
// assignable as it stands.
|
|
3358
|
+
const isPtr = isPtrValue(value) || (vt?.kind === 'ptr' && vt.to.kind !== 'void');
|
|
3359
|
+
return isPtr ? { k: 'cast', to: T.ptr(T.void()), e: value } : value;
|
|
3360
|
+
};
|
|
3361
|
+
|
|
1844
3362
|
const sideEffects = (b: Block): Stmt[] => {
|
|
1845
3363
|
const out: Stmt[] = [];
|
|
1846
3364
|
for (const op of b.ops) {
|
|
1847
3365
|
if (op.opcode === 'store') {
|
|
3366
|
+
// a mask-and-insert recognized over the ops (see the precompute above): the member
|
|
3367
|
+
// assignment, not the read-mask-or-store chain
|
|
3368
|
+
const bfs = bitfieldStore.get(op);
|
|
3369
|
+
if (bfs) {
|
|
3370
|
+
out.push({
|
|
3371
|
+
k: 'store',
|
|
3372
|
+
lval: { k: 'field', base: { k: 'var', name: bfs.global }, name: bfs.field, dot: true },
|
|
3373
|
+
value: expr(bfs.value),
|
|
3374
|
+
});
|
|
3375
|
+
continue;
|
|
3376
|
+
}
|
|
1848
3377
|
// A store whose lvalue is a bare global (`gSym = v`, from an `&gSym` base at off 0) emits
|
|
1849
3378
|
// as an ASSIGN, not a store — memAccess returns a `var` node for that case.
|
|
1850
3379
|
const width = op.attrs.width as number;
|
|
@@ -1861,12 +3390,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1861
3390
|
);
|
|
1862
3391
|
if (lval0.k === 'var') {
|
|
1863
3392
|
globalNames.add(lval0.name);
|
|
1864
|
-
out.push({ k: 'assign', name: lval0.name, value: expr(op.operands[1]) });
|
|
3393
|
+
out.push({ k: 'assign', name: lval0.name, value: intoPtrCell(lval0, expr(op.operands[1])) });
|
|
1865
3394
|
continue;
|
|
1866
3395
|
}
|
|
1867
3396
|
// signedness mirrors recoverTypes' store seed (word ⇒ signed, narrow ⇒ unsigned), so an
|
|
1868
3397
|
// inserted cast declares the same scalar the recovered pointee would have.
|
|
1869
|
-
out.push({ k: 'store', lval: lval0, value: expr(op.operands[1]) });
|
|
3398
|
+
out.push({ k: 'store', lval: lval0, value: intoPtrCell(lval0, expr(op.operands[1])) });
|
|
1870
3399
|
} else if (op.opcode === 'astore') {
|
|
1871
3400
|
const elemSize = op.attrs.elemSize as number;
|
|
1872
3401
|
out.push({
|
|
@@ -1876,8 +3405,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1876
3405
|
expr(op.operands[0]),
|
|
1877
3406
|
expr(op.operands[1]),
|
|
1878
3407
|
op.attrs.fieldOff as number | undefined,
|
|
3408
|
+
op.attrs.memberOff as number | undefined,
|
|
1879
3409
|
elemSize,
|
|
1880
|
-
|
|
3410
|
+
// a member array's store spells through the member's OWN declaration, which the
|
|
3411
|
+
// recognizer records; every other astore keeps the width===4 convention
|
|
3412
|
+
(op.attrs.signed as boolean | undefined) ?? elemSize === 4,
|
|
1881
3413
|
ctype,
|
|
1882
3414
|
symCtx,
|
|
1883
3415
|
),
|
|
@@ -1899,11 +3431,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1899
3431
|
// (an absorbed load's every consumer spells a named bitfield read — emitting its temp
|
|
1900
3432
|
// here would recompile to a second load the asm does not have)
|
|
1901
3433
|
const nm = varName.get(op.results[0])!;
|
|
1902
|
-
out.push({ k: 'assign', name: nm, value:
|
|
3434
|
+
out.push({ k: 'assign', name: nm, value: intoDeclaredTemp(nm, lowerDef(op, expr)) });
|
|
1903
3435
|
}
|
|
1904
3436
|
// a merge copy anchored at this const's original position (anchorConstCopies, above)
|
|
1905
3437
|
for (const a of anchoredAt.get(op) ?? []) {
|
|
1906
3438
|
out.push({ k: 'assign', name: a.name, value: expr(a.arg) });
|
|
3439
|
+
anchorsEmitted.add(a);
|
|
1907
3440
|
}
|
|
1908
3441
|
}
|
|
1909
3442
|
return out;
|
|
@@ -1921,7 +3454,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1921
3454
|
// to `header` is a conditional continue; its other edge, when it leaves the loop, is an early exit
|
|
1922
3455
|
// (a `break` to `exit`, or an early `return` through a trampoline). Null outside any loop body —
|
|
1923
3456
|
// the early-exit branch in structureBlock is inert there.
|
|
1924
|
-
type LoopFrame = { header: Block; exit: Block; body: Set<Block
|
|
3457
|
+
type LoopFrame = { header: Block; exit: Block; body: Set<Block>; arms: LoopArm[] };
|
|
1925
3458
|
let loopCtx: LoopFrame | null = null;
|
|
1926
3459
|
const withLoop = <R>(frame: LoopFrame, run: () => R): R => {
|
|
1927
3460
|
const prev = loopCtx;
|
|
@@ -1953,8 +3486,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1953
3486
|
};
|
|
1954
3487
|
|
|
1955
3488
|
// ── Regime-A switch recovery (structure/switch-recover.ts): the recognizer's case bodies call
|
|
1956
|
-
// back into structureRegion, and Regime B (switch_br, below)
|
|
1957
|
-
|
|
3489
|
+
// back into structureRegion, and Regime B (switch_br, below) reads FOUR things from it — the
|
|
3490
|
+
// per-arm exit, the layout index, the chain linearization and where the `default:` label goes —
|
|
3491
|
+
// so neither regime states any of those four facts twice.
|
|
3492
|
+
//
|
|
3493
|
+
// `isNamed` reads `varName` LIVE, and by here THE NAMING WALK IS COMPLETE: every write to
|
|
3494
|
+
// `varName` and `backArgName` sits above, in the walk and the coalescing that follows it, and the
|
|
3495
|
+
// two places the map is handed to (`coalesceNames`, `makeLoopHazards`) only read it. So the
|
|
3496
|
+
// recognizer sees one settled naming however late a case body calls back into `structureRegion`,
|
|
3497
|
+
// and nothing here has to be rebuilt against a naming that moves underneath it.
|
|
3498
|
+
const { recognizeSwitch, analyzeArmExit, layoutIndex, defaultLayoutPos, chainArms } = makeSwitchRecovery({
|
|
1958
3499
|
fn,
|
|
1959
3500
|
defs,
|
|
1960
3501
|
dom,
|
|
@@ -1963,7 +3504,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1963
3504
|
isNamed: (v) => varName.has(v),
|
|
1964
3505
|
isCmpOpcode: (opcode) => !!CMP_TO_BIN[opcode],
|
|
1965
3506
|
switchAllowsNeqCase,
|
|
1966
|
-
|
|
3507
|
+
switchAllowsBoundCase,
|
|
3508
|
+
switchArmsFollowLayout,
|
|
3509
|
+
spellSwitchFallthrough,
|
|
3510
|
+
emitsOwnStatement: (blk) => blk.ops.some((o) => anchoredAt.has(o) || materialize.has(o)),
|
|
3511
|
+
blockOf,
|
|
3512
|
+
hoistDispatchCopies: (edges, liveAt) => hoistedDispatchAssigns(edges, liveAt),
|
|
1967
3513
|
expr: (v) => expr(v),
|
|
1968
3514
|
structureRegion: (b, stop) => structureRegion(b, stop),
|
|
1969
3515
|
});
|
|
@@ -2036,22 +3582,89 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2036
3582
|
if (defEdge.block !== merge) {
|
|
2037
3583
|
siblings.add(defEdge.block);
|
|
2038
3584
|
}
|
|
2039
|
-
//
|
|
2040
|
-
//
|
|
2041
|
-
//
|
|
2042
|
-
//
|
|
2043
|
-
|
|
3585
|
+
// ARM ORDER. Grouping the table's slots by target walks them in TABLE order, which for a dense
|
|
3586
|
+
// table is ascending case value — the neutral spelling, and the source's order only by
|
|
3587
|
+
// accident. Where the compiler has declared that its block layout IS the order the arms were
|
|
3588
|
+
// written (TargetDescription.switchArmsFollowLayout — the same fact and the same evidence
|
|
3589
|
+
// Regime A reads, and agbcc's jump tables carry it too: 8 dense arms written 5,2,0,4,1,3,6,7
|
|
3590
|
+
// lay their bodies out in that order under an ascending table), the bodies' layout is the
|
|
3591
|
+
// evidence and the arms take it.
|
|
3592
|
+
//
|
|
3593
|
+
// The policy orders the chain HEADS. A FALLING arm's position is not free — emission order is
|
|
3594
|
+
// load-bearing for correctness (the l3/ast.ts non-neutrality note), so it must sit directly
|
|
3595
|
+
// above the arm it falls into — and `chainArms` below re-threads it for exactly those, which
|
|
3596
|
+
// is the same per-SITE reading Regime A makes of the same declaration. `analyzeArmExit` does
|
|
3597
|
+
// not depend on emission order, so the exits can be settled first and reused below. Grouping
|
|
3598
|
+
// by target already gives every arm a DISTINCT entry block, so no two sort keys can be equal
|
|
3599
|
+
// and the tie-break Regime A needs on shared bodies has nothing to decide here.
|
|
3600
|
+
const exitOf = new Map<Block, ArmExit>();
|
|
3601
|
+
for (const entry of [...arms.map((a) => a.entry), defEdge.block]) {
|
|
3602
|
+
if (!exitOf.has(entry)) {
|
|
3603
|
+
exitOf.set(entry, analyzeArmExit(entry, b, merge, siblings));
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
// A language whose `case` cannot fall through (Pascal) has no spelling for this shape, and
|
|
3607
|
+
// Regime B has no second recovery to fall back on — so it fails LOUD, as it does for every
|
|
3608
|
+
// other unspellable jump table. (Regime A, which does have a fallback, declines to
|
|
3609
|
+
// if-recovery instead: switch-recover.ts's PRE2.)
|
|
3610
|
+
if (!spellSwitchFallthrough && [...exitOf.values()].some((e) => e.kind === 'fallthrough')) {
|
|
3611
|
+
throw new StructureError(
|
|
3612
|
+
`cannot structure '${fn.name}': a jump-table case runs on into the next case, and the target ` +
|
|
3613
|
+
`language has no fall-through in its case statement`,
|
|
3614
|
+
);
|
|
3615
|
+
}
|
|
3616
|
+
if (switchArmsFollowLayout) {
|
|
3617
|
+
arms.sort((x, y) => layoutIndex(x.entry) - layoutIndex(y.entry));
|
|
3618
|
+
}
|
|
3619
|
+
// …and then the CHAIN re-threads that order for the falling arms, exactly as it does for the
|
|
3620
|
+
// comparison tree: ONE definition (`chainArms`) for the linearization, the adjacency it
|
|
3621
|
+
// guarantees, and the three shapes no linear order spells. The arm-order policy above is read
|
|
3622
|
+
// for the chain HEADS only — the same per-SITE reading Regime A makes of the same
|
|
3623
|
+
// declaration, rather than a whole-switch gate on every arm being closed.
|
|
3624
|
+
const preChain = arms.map((a) => a.entry);
|
|
3625
|
+
// A table whose default block is ALSO a case target is that case's arm, not a separate one:
|
|
3626
|
+
// it is already in `preChain`, and `defaultLayoutPos` withholds the label's position for it.
|
|
3627
|
+
const dfltArm = defEdge.block !== merge && !armOf.has(defEdge.block) ? defEdge.block : null;
|
|
3628
|
+
const chained = chainArms(preChain, dfltArm, exitOf);
|
|
3629
|
+
if (chained === null) {
|
|
3630
|
+
throw new StructureError(
|
|
3631
|
+
`cannot structure '${fn.name}': the jump table's case arms do not linearize — two arms fall ` +
|
|
3632
|
+
`into one, the default arm falls into a case, or the fall-through is a cycle`,
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3635
|
+
const armByEntry = new Map(arms.map((a) => [a.entry, a]));
|
|
3636
|
+
const ordered = chained.map((e) => armByEntry.get(e)!);
|
|
3637
|
+
const orderIntact = chained.every((e, i) => e === preChain[i]);
|
|
3638
|
+
// The `default:` arm carries that evidence too, and a table hands it over the same way: the
|
|
3639
|
+
// range check BRANCHES to the default (`bhi .Ldefault`), so its block is never one the
|
|
3640
|
+
// dispatch ran into — measured, a 5-arm table lays the default's body at each of the six
|
|
3641
|
+
// positions the source can write it in exactly there. `defaultLayoutPos` states the refusals.
|
|
3642
|
+
const defaultAt =
|
|
3643
|
+
defEdge.block !== merge
|
|
3644
|
+
? defaultLayoutPos(
|
|
3645
|
+
defEdge.block,
|
|
3646
|
+
ordered.map((a) => ({ entry: a.entry, fallsThrough: exitOf.get(a.entry)!.kind === 'fallthrough' })),
|
|
3647
|
+
{ placedByDispatch: false, orderIntact },
|
|
3648
|
+
)
|
|
3649
|
+
: undefined;
|
|
3650
|
+
// ONE emission order for the whole statement: the case arms, then the default. Adjacency is
|
|
3651
|
+
// read off this array, so "falls into the next arm" needs no separate rule for a case that
|
|
3652
|
+
// falls into the default (it is the arm after the last case, and legal C). Where the LABEL is
|
|
3653
|
+
// printed is `defaultAt`, which the emission order does not follow.
|
|
3654
|
+
const emitOrder = [...ordered, { entry: defEdge.block, edge: defEdge, values: null as number[] | null }];
|
|
2044
3655
|
// Each arm's switch-edge copies, computed ONCE and in emission order: `argAssignsFor` mints
|
|
2045
3656
|
// swap-cycle temp names, so calling it twice for one edge burns a temp number and changes the
|
|
2046
3657
|
// output (the same reason emitDoWhile reuses its `updates`).
|
|
2047
3658
|
const edgeCopies = emitOrder.map((a) => argAssignsFor(b, a.edge));
|
|
2048
3659
|
const bodies = emitOrder.map((a, i) => {
|
|
2049
|
-
const exit =
|
|
3660
|
+
const exit = exitOf.get(a.entry)!;
|
|
2050
3661
|
if (exit.kind === 'unstructurable') {
|
|
2051
3662
|
throw new StructureError(`cannot structure '${fn.name}': ${exit.why}`);
|
|
2052
3663
|
}
|
|
2053
3664
|
const ft = exit.kind === 'fallthrough';
|
|
2054
3665
|
const next = emitOrder[i + 1];
|
|
3666
|
+
// Re-read off the EMISSION array rather than trusted from `chainArms` — the seam where a
|
|
3667
|
+
// position acquires control-flow meaning, checked on the same side of it as Regime A's.
|
|
2055
3668
|
if (ft && next?.entry !== exit.to) {
|
|
2056
3669
|
throw new StructureError(
|
|
2057
3670
|
`cannot structure '${fn.name}': ${a.values ? `case ${a.values.join('/')}` : 'the default arm'} falls ` +
|
|
@@ -2077,7 +3690,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2077
3690
|
fallsThrough: ft,
|
|
2078
3691
|
};
|
|
2079
3692
|
});
|
|
2080
|
-
const outCases: SwitchCase[] =
|
|
3693
|
+
const outCases: SwitchCase[] = ordered.map((a, i) => ({ values: a.values, ...bodies[i] }));
|
|
2081
3694
|
// An EMPTY default arm is not a default at all: it is where the switch ends, which is where
|
|
2082
3695
|
// an unmatched scrutinee goes anyway. Emitting the label with nothing under it says nothing
|
|
2083
3696
|
// and is not even valid C89 (a label needs a statement).
|
|
@@ -2086,7 +3699,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2086
3699
|
k: 'switch',
|
|
2087
3700
|
scrutinee: expr(term.operands[0]),
|
|
2088
3701
|
cases: outCases,
|
|
2089
|
-
...(defBody.length ? { default: defBody } : {}),
|
|
3702
|
+
...(defBody.length ? { default: defBody, ...(defaultAt !== undefined ? { defaultAt } : {}) } : {}),
|
|
2090
3703
|
};
|
|
2091
3704
|
out.push(sw);
|
|
2092
3705
|
if (merge && merge !== stop) {
|
|
@@ -2116,21 +3729,17 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2116
3729
|
return out;
|
|
2117
3730
|
}
|
|
2118
3731
|
|
|
2119
|
-
//
|
|
2120
|
-
//
|
|
2121
|
-
//
|
|
3732
|
+
// guarded self-loop: this cond_br decides "enter loop header h vs its exit". Emit the inits
|
|
3733
|
+
// unconditionally, then either a `while` whose own test subsumes this guard (fused — only under
|
|
3734
|
+
// the guard proof below), or the guard kept as its own `if` around a bottom-tested `do-while`
|
|
3735
|
+
// (gcc's "guard + do-while" shape, emitted as itself). Never claim when `b` is itself the
|
|
3736
|
+
// header (a guard-LESS single-block do-while would emit the update once before a
|
|
2122
3737
|
// wrongly-`while` loop) — require a DISTINCT dominating guard block.
|
|
2123
3738
|
for (const h of [takenB, fallB]) {
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
// uninitialized on the first test. Mirror of the headerPure gate: decline loud.
|
|
2129
|
-
if (li.header.ops.some((o) => materialize.has(o))) {
|
|
2130
|
-
throw new StructureError(
|
|
2131
|
-
`cannot structure '${fn.name}': loop header holds a materialized def its condition would read uninitialized`,
|
|
2132
|
-
);
|
|
2133
|
-
}
|
|
3739
|
+
// `h` may be the header itself or its PURE PREHEADER (the LoopInfo records which): the
|
|
3740
|
+
// guard's branch enters the loop either way, and the preheader's defs render inline.
|
|
3741
|
+
const li = loops.get(h) ?? [...loops.values()].find((l) => l.preheader === h);
|
|
3742
|
+
if (li && h !== b && li.header !== b && (takenB === li.exit || fallB === li.exit)) {
|
|
2134
3743
|
// Self-loop emitter hazards: the while condition, the header→exit args, and every
|
|
2135
3744
|
// post-loop use of a header-computed value render under the un-rotation sub — sound only
|
|
2136
3745
|
// when their loop-variable reads go through sub-mapped back-edge args (post-update). A
|
|
@@ -2138,36 +3747,206 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2138
3747
|
// holds → decline LOUD, never emit wrong code.
|
|
2139
3748
|
const sub = loopSub(li);
|
|
2140
3749
|
const updates = argAssigns(li.header, li.header);
|
|
2141
|
-
const updateWrites =
|
|
3750
|
+
const updateWrites = loopWriteSet(updates, [li.header], li.header);
|
|
2142
3751
|
const hterm = li.header.ops[li.header.ops.length - 1];
|
|
2143
3752
|
const hexitArgs = (successorTo(li.header, li.exit)?.args ?? []) as Value[];
|
|
3753
|
+
// FUSION IS ONLY SOUND WHEN EVERYTHING IT DROPS IS REDUNDANT. `isGuardShapedPred` asks about
|
|
3754
|
+
// the SHAPE alone — branches to the header, branches to the exit — and an `if` on something
|
|
3755
|
+
// else entirely has that shape too. `entryVals` maps each header param and back-edge arg to
|
|
3756
|
+
// the arg the guard passes into the loop, so reading through it models the first iteration:
|
|
3757
|
+
// the state the guard tested. Two claims are checked against it, both LOUD on failure.
|
|
3758
|
+
const guardExit = successorTo(b, li.exit)!; // the fusion condition above guarantees this edge
|
|
3759
|
+
// The loop's init edge: the guard's own edge into the header, or the preheader's `br`
|
|
3760
|
+
// when one stands between — its args are what the first iteration actually receives.
|
|
3761
|
+
const initFrom = li.preheader ?? b;
|
|
3762
|
+
const initArgs = (successorTo(initFrom, li.header)?.args ?? []) as Value[];
|
|
3763
|
+
// Params first, BACK-EDGE ARGS SECOND: one value can be both — param `i+1` of a shifting
|
|
3764
|
+
// pair is also the back-edge arg of param `i` — and the emitted expression renders it
|
|
3765
|
+
// under the un-rotation substitution, so that reading is the one that must win.
|
|
3766
|
+
const entryVals = new Map<Value, Value>();
|
|
3767
|
+
li.header.params.forEach((p, i) => initArgs[i] !== undefined && entryVals.set(p, initArgs[i]));
|
|
3768
|
+
li.header.params.forEach(
|
|
3769
|
+
(_, i) => initArgs[i] !== undefined && entryVals.set(li.backArgOfParam[i], initArgs[i]),
|
|
3770
|
+
);
|
|
3771
|
+
// (1) Is the guard PROVABLY the loop's own test? Fusing DELETES it and lets the `while`
|
|
3772
|
+
// re-test, so an unproven fuse would lose the `if` outright and run a loop the source
|
|
3773
|
+
// skipped. Structural equality is a sufficient proof, never a necessary one: `str[0]` and
|
|
3774
|
+
// `str[i]` at `i = 0`, or a signed `e <= 0` beside an unsigned `e != 0`, are the same test
|
|
3775
|
+
// spelled differently, and normalising those needs a canonical form this pass does not
|
|
3776
|
+
// have. An unproven guard therefore KEEPS its `if`, with the loop as a `do-while` inside it
|
|
3777
|
+
// — every test the asm performs is emitted as itself. The proof still gates the SINK below:
|
|
3778
|
+
// a sunk copy runs once per iteration, and only the proof (fused form) or the kept `if`
|
|
3779
|
+
// makes the zero-trip path skip it. The kept `if` would widen the sink's key to unproven
|
|
3780
|
+
// guards too; no row needs that yet, so it stays keyed on the proof.
|
|
3781
|
+
const enterIsTaken = takenB === h;
|
|
3782
|
+
const contIsTaken = hterm.successors[0].block === li.header;
|
|
3783
|
+
const guardProven = sameAtEntry(hterm.operands[0], term.operands[0], entryVals, enterIsTaken !== contIsTaken);
|
|
3784
|
+
// Fused `while` additionally requires a header free of MATERIALIZED defs: their temps are
|
|
3785
|
+
// assigned only inside the body (sideEffects), and an un-rotated `while` condition renders
|
|
3786
|
+
// BEFORE the body ever ran — reading a temp uninitialized on the first test. The kept-guard
|
|
3787
|
+
// `do-while` tests after the body, so it carries no such restriction.
|
|
3788
|
+
const fused = guardProven && !li.header.ops.some((o) => materialize.has(o));
|
|
3789
|
+
// A pre-update exit copy is repairable rather than fatal: emitted at the TOP of the body it
|
|
3790
|
+
// captures the value one iteration before the update, which is what the exit edge carries.
|
|
3791
|
+
// Its post-loop copy is then dropped, so the ZERO-TRIP path needs the guard→exit edge as a
|
|
3792
|
+
// seed — the only edge holding the never-entered value. That is why the sink demands the
|
|
3793
|
+
// proof above: it makes the fused zero-trip path load-bearing.
|
|
3794
|
+
const sunk = guardProven
|
|
3795
|
+
? sinkablePreUpdateSlots(li.header, li.exit, hexitArgs, new Set([li.header]), sub, updateWrites)
|
|
3796
|
+
: new Set<number>();
|
|
3797
|
+
// (2) Every exit copy the fused form KEEPS renders after the loop, on the zero-trip path
|
|
3798
|
+
// too — where the loop variables still hold their init values. It must therefore produce
|
|
3799
|
+
// what the guard→exit edge carries, since that edge is dropped. A sunk slot is exempt: its
|
|
3800
|
+
// seed and its body copy cover the two paths separately.
|
|
3801
|
+
const staleExit = hexitArgs.findIndex(
|
|
3802
|
+
(a, j) => !sunk.has(j) && !sameAtEntry(a, guardExit.args[j] as Value, entryVals),
|
|
3803
|
+
);
|
|
3804
|
+
if (staleExit >= 0) {
|
|
3805
|
+
throw new StructureError(
|
|
3806
|
+
`cannot structure '${fn.name}': the fused guard's exit edge carries a value the post-loop ` +
|
|
3807
|
+
`copies do not reproduce on a zero-trip run`,
|
|
3808
|
+
);
|
|
3809
|
+
}
|
|
3810
|
+
const sunkSlot = (j: number) => sunk.has(j);
|
|
3811
|
+
const keptSlot = (j: number) => !sunk.has(j);
|
|
2144
3812
|
if (
|
|
2145
3813
|
loopUpdateHazard(
|
|
2146
3814
|
hterm.operands[0],
|
|
2147
|
-
hexitArgs,
|
|
3815
|
+
hexitArgs.filter((_, j) => keptSlot(j)),
|
|
2148
3816
|
new Set([li.header]),
|
|
2149
3817
|
sub,
|
|
2150
3818
|
updateWrites,
|
|
2151
3819
|
null,
|
|
2152
|
-
new Set(li.header.params),
|
|
2153
3820
|
)
|
|
2154
3821
|
) {
|
|
2155
3822
|
throw new StructureError(
|
|
2156
3823
|
`cannot structure '${fn.name}': loop condition or a post-loop value reads a pre-update loop variable`,
|
|
2157
3824
|
);
|
|
2158
3825
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
3826
|
+
// The seed and the loop init are two copy groups from the SAME block, emitted back to back,
|
|
3827
|
+
// and nothing sequentializes them against each other: a seed that writes a name the init
|
|
3828
|
+
// then reads hands the loop the wrong starting value. Computed once — a second
|
|
3829
|
+
// `argAssigns` for one edge burns a swap-cycle temp number and changes the output.
|
|
3830
|
+
const seed = argAssigns(b, li.exit, null, sunkSlot);
|
|
3831
|
+
const seedWrites = new Set(seed.filter((st) => st.k === 'assign').map((st) => st.name));
|
|
3832
|
+
if (initArgs.some((a) => readsClobbered(a, new Map(), seedWrites))) {
|
|
3833
|
+
throw new StructureError(
|
|
3834
|
+
`cannot structure '${fn.name}': seeding the zero-trip value would overwrite a value the ` +
|
|
3835
|
+
`loop initialisation reads`,
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3838
|
+
// ZERO-TRIP reads (kept-guard form only): the body sits inside `if (guard)`, so a
|
|
3839
|
+
// materialized header def's temp is assigned only when the guard held. A kept exit arg or
|
|
3840
|
+
// a post-loop read reaching such a temp — by its NAME, which is how a materialized def
|
|
3841
|
+
// renders — reads it uninitialized on the guard-false path, silently. Loop-variable names
|
|
3842
|
+
// are exempt (the inits write them unconditionally, and staleExit already proved their
|
|
3843
|
+
// zero-trip values); so is anything named outside the header. Decline loud.
|
|
3844
|
+
if (!fused) {
|
|
3845
|
+
const paramNames = new Set(li.header.params.map((q) => varName.get(q)));
|
|
3846
|
+
const bodyTemp = (v: Value): boolean => {
|
|
3847
|
+
const d = defs.get(v);
|
|
3848
|
+
return (
|
|
3849
|
+
d !== undefined &&
|
|
3850
|
+
materialize.has(d) &&
|
|
3851
|
+
opBlock.get(d) === li.header &&
|
|
3852
|
+
varName.has(v) &&
|
|
3853
|
+
!paramNames.has(varName.get(v))
|
|
3854
|
+
);
|
|
3855
|
+
};
|
|
3856
|
+
const reachesBodyTemp = (root: Value): boolean => {
|
|
3857
|
+
const stack = [root];
|
|
3858
|
+
const seen = new Set<Value>();
|
|
3859
|
+
while (stack.length) {
|
|
3860
|
+
const v = stack.pop()!;
|
|
3861
|
+
if (seen.has(v)) {
|
|
3862
|
+
continue;
|
|
3863
|
+
}
|
|
3864
|
+
seen.add(v);
|
|
3865
|
+
if (sub.has(v)) {
|
|
3866
|
+
continue; // renders as its loop variable's name — unconditionally initialized
|
|
3867
|
+
}
|
|
3868
|
+
if (bodyTemp(v)) {
|
|
3869
|
+
return true;
|
|
3870
|
+
}
|
|
3871
|
+
if (varName.has(v)) {
|
|
3872
|
+
continue; // named outside the guarded body — assigned on both paths
|
|
3873
|
+
}
|
|
3874
|
+
const d = defs.get(v);
|
|
3875
|
+
if (d) {
|
|
3876
|
+
stack.push(...d.operands);
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
return false;
|
|
3880
|
+
};
|
|
3881
|
+
if (
|
|
3882
|
+
hexitArgs.some((a, j) => !sunk.has(j) && reachesBodyTemp(a)) ||
|
|
3883
|
+
[...(liveIn.get(li.exit) ?? [])].some(bodyTemp)
|
|
3884
|
+
) {
|
|
3885
|
+
throw new StructureError(
|
|
3886
|
+
`cannot structure '${fn.name}': a post-loop read reaches a temp the guarded body may never assign`,
|
|
3887
|
+
);
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
const inits = argAssigns(initFrom, li.header);
|
|
3891
|
+
const loopStmt = emitWhile(
|
|
3892
|
+
li,
|
|
3893
|
+
updates,
|
|
3894
|
+
preUpdateCopies(li.exit, hexitArgs, sunk, li.header),
|
|
3895
|
+
fused ? 'while' : 'dowhile',
|
|
3896
|
+
);
|
|
3897
|
+
// The guard-read substitution: an init arg reads as its loop variable's NAME. The inits
|
|
3898
|
+
// just assigned them (value-identical), and that is the source spelling — `if (n > 0)`
|
|
3899
|
+
// tests the loop variable, which is also the parked register the target's guard reads.
|
|
3900
|
+
// Not an UNMATERIALIZED const: the guard compared an immediate (`cmp rX, #0`), and an
|
|
3901
|
+
// immediate is what re-spelling it as the counter's name would un-spell.
|
|
3902
|
+
const gsub = new Map<Value, string>();
|
|
3903
|
+
li.header.params.forEach((p, i) => {
|
|
3904
|
+
const a = initArgs[i];
|
|
3905
|
+
const ad = a !== undefined ? defs.get(a) : undefined;
|
|
3906
|
+
if (a !== undefined && !(ad?.opcode === 'const' && !materialize.has(ad))) {
|
|
3907
|
+
gsub.set(a, varName.get(p)!);
|
|
3908
|
+
}
|
|
3909
|
+
});
|
|
3910
|
+
if (!fused) {
|
|
3911
|
+
// The kept guard's condition renders AFTER the seed and the inits, but tests the state
|
|
3912
|
+
// BEFORE them — a read of a just-(non-identity-)written name is sound only through
|
|
3913
|
+
// `gsub`, whose mapped values the inits deliberately hold.
|
|
3914
|
+
const writes = new Set([...seedWrites, ...updateWriteSet(inits)]);
|
|
3915
|
+
if (readsClobbered(term.operands[0], gsub, writes)) {
|
|
3916
|
+
throw new StructureError(
|
|
3917
|
+
`cannot structure '${fn.name}': the kept guard's condition reads a name the loop initialisation overwrites`,
|
|
3918
|
+
);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
out.push(...seed); // zero-trip value for the sunk copies
|
|
3922
|
+
out.push(...inits); // loop-variable initialisation
|
|
3923
|
+
if (fused) {
|
|
3924
|
+
out.push(loopStmt);
|
|
3925
|
+
} else {
|
|
3926
|
+
let gcond = exprWith(gsub)(term.operands[0]);
|
|
3927
|
+
if (!enterIsTaken) {
|
|
3928
|
+
gcond = negateCond(gcond);
|
|
3929
|
+
} // entering the loop must be `taken`
|
|
3930
|
+
out.push(mkIf(gcond, [loopStmt], []));
|
|
3931
|
+
}
|
|
2161
3932
|
// The header→exit edge may carry non-identity phi args (the exit param merges the guard-false
|
|
2162
3933
|
// value with the loop's final value). Emit those copies after the loop — dropping them returns
|
|
2163
3934
|
// a stale value. Read under the un-rotation substitution (post-loop the params hold their
|
|
2164
3935
|
// updated values), and structure the exit region under the same substitution so a post-loop
|
|
2165
3936
|
// use of a loop value reads its name.
|
|
2166
|
-
out.push(
|
|
3937
|
+
out.push(
|
|
3938
|
+
...withSub(sub, () => [...argAssigns(li.header, li.exit, sub, keptSlot), ...structureRegion(li.exit, stop)]),
|
|
3939
|
+
);
|
|
2167
3940
|
return out;
|
|
2168
3941
|
}
|
|
2169
3942
|
}
|
|
2170
3943
|
|
|
3944
|
+
// Is THIS edge one of the enclosing loop's admitted early-`return` arms? Keyed on the edge
|
|
3945
|
+
// because ownership is: a second edge into the same block is a separate question. No shape today
|
|
3946
|
+
// separates it from keying on the target alone — this holds the invariant, it does not fix an
|
|
3947
|
+
// observed bug.
|
|
3948
|
+
const isArm = (t: Block) => !!loopCtx && loopCtx.arms.some((a) => a.from === b && a.to === t);
|
|
3949
|
+
|
|
2171
3950
|
// Conditional latch / in-body early exit: one edge of this cond_br is the loop back-edge (a
|
|
2172
3951
|
// continue to `loopCtx.header`); the other LEAVES the loop. When the leaving edge lands on the
|
|
2173
3952
|
// loop's own exit block it is a `break`; when it trampolines to a `return` it is an early `return`.
|
|
@@ -2200,26 +3979,19 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2200
3979
|
// `readsClobbered` distinguishes the two at the VALUE level; on a hazard, decline (fall
|
|
2201
3980
|
// through → honest loud fail) rather than emit wrong code.
|
|
2202
3981
|
const updateCopies = argAssigns(b, loopCtx.header);
|
|
2203
|
-
const updateWrites =
|
|
3982
|
+
const updateWrites = loopWriteSet(updateCopies, loopCtx.body, loopCtx.header);
|
|
2204
3983
|
const exitArgs = (successorTo(b, exitB)?.args ?? []) as Value[];
|
|
2205
3984
|
// The exit ARM may also read loop-body-computed values directly (an exitB dominated by `b`
|
|
2206
3985
|
// — e.g. its `ret` operand), not just through edge args: apply the same escape test to the
|
|
2207
3986
|
// arm's region (blocks reachable from exitB outside the loop body).
|
|
3987
|
+
//
|
|
3988
|
+
// WHAT THE REGION IS FOR: this arm renders INSTEAD of the next iteration, behind an update
|
|
3989
|
+
// already emitted, so a read of a loop variable here wanted the value it had before that —
|
|
3990
|
+
// `if (found) { *out = i; return; }` would store `i + 1`. Handing the arm's own region makes
|
|
3991
|
+
// the escape check judge exactly those reads.
|
|
2208
3992
|
const exitRegion = new Set([exitB, ...reachFrom(exitB)].filter((x) => !loopCtx!.body.has(x)));
|
|
2209
|
-
const hazard = loopUpdateHazard(
|
|
2210
|
-
|
|
2211
|
-
exitArgs,
|
|
2212
|
-
loopCtx.body,
|
|
2213
|
-
sub,
|
|
2214
|
-
updateWrites,
|
|
2215
|
-
exitRegion,
|
|
2216
|
-
new Set(loopCtx.header.params),
|
|
2217
|
-
);
|
|
2218
|
-
if (
|
|
2219
|
-
!hazard &&
|
|
2220
|
-
!loopCtx.body.has(exitB) &&
|
|
2221
|
-
((isBreak && breakSafe) || (!isBreak && leadsToReturnOnly(exitB, loopCtx.body)))
|
|
2222
|
-
) {
|
|
3993
|
+
const hazard = loopUpdateHazard(term.operands[0], exitArgs, loopCtx.body, sub, updateWrites, exitRegion);
|
|
3994
|
+
if (!hazard && !loopCtx.body.has(exitB) && ((isBreak && breakSafe) || (!isBreak && isArm(exitB)))) {
|
|
2223
3995
|
out.push(...updateCopies); // the loop update, RAW (i++, p>>=1, …)
|
|
2224
3996
|
let leaveCond = exprWith(sub)(term.operands[0]);
|
|
2225
3997
|
if (contIsTaken) {
|
|
@@ -2244,7 +4016,27 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2244
4016
|
|
|
2245
4017
|
const cond = expr(term.operands[0]);
|
|
2246
4018
|
const ipd = ipdom.get(b) ?? null; // null ⇒ the arms diverge (both reach EXIT), no join
|
|
2247
|
-
|
|
4019
|
+
// Inside a loop body, a join OUTSIDE that body is not this `if`'s join: an arm that leaves the
|
|
4020
|
+
// loop `return`s and never comes back, so what is left reconverges at the loop's own
|
|
4021
|
+
// continuation. Post-dominance cannot see that — agbcc/gcc merge every `return` into one
|
|
4022
|
+
// epilogue, which the early return and the post-loop path both reach — and structuring the
|
|
4023
|
+
// in-loop arm towards it walks through the latch and back into the header (a loud `onStack`
|
|
4024
|
+
// decline).
|
|
4025
|
+
//
|
|
4026
|
+
// `stop` stands in for the real join, so it has to BE the real join: one side must already end
|
|
4027
|
+
// this region — an admitted arm (its `return` terminates that path) or `stop` itself. Two sides
|
|
4028
|
+
// meeting somewhere further down instead would each re-emit everything from there to the bottom,
|
|
4029
|
+
// doubling per nesting level; those keep the CFG join and the old loud decline. Lifting that
|
|
4030
|
+
// needs a post-dominator computed over the body alone with the arms DELETED — treating them as
|
|
4031
|
+
// exits does not work, since post-dominance cannot express "where the paths that did not
|
|
4032
|
+
// return meet".
|
|
4033
|
+
const clampToLoop =
|
|
4034
|
+
loopCtx !== null &&
|
|
4035
|
+
ipd !== null &&
|
|
4036
|
+
!loopCtx.body.has(ipd) &&
|
|
4037
|
+
term.successors.some((sc) => isArm(sc.block) || sc.block === stop) &&
|
|
4038
|
+
term.successors.every((sc) => loopCtx!.body.has(sc.block) || isArm(sc.block));
|
|
4039
|
+
const merge = clampToLoop ? stop : (ipd ?? stop);
|
|
2248
4040
|
// Per-successor records, NOT successorTo(b, block): a cond_br whose two edges reach the SAME
|
|
2249
4041
|
// block with different args would otherwise give both arms the first edge's copies.
|
|
2250
4042
|
const thenS = [...argAssignsFor(b, term.successors[0]), ...structureRegion(takenB, merge)];
|
|
@@ -2260,6 +4052,17 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2260
4052
|
out.push({ k: 'if', cond: negateCond(cond), then: elseS, else: thenS });
|
|
2261
4053
|
return out;
|
|
2262
4054
|
}
|
|
4055
|
+
if (negateJoinedBranchSense && ipd !== null && thenS.length && elseS.length) {
|
|
4056
|
+
// JOINED arms only (`ipd !== null` — a divergent if belongs to preserveDivergentBranchSense
|
|
4057
|
+
// above, and without the check a /flip-branch variant would fall through here and get
|
|
4058
|
+
// flipped BACK, collapsing the {divergent flipped × joined flipped} combination), and both
|
|
4059
|
+
// arms real: the flipped spelling is a genuine sibling, not noise on a one-armed if
|
|
4060
|
+
out.push({ k: 'if', cond: negateCond(cond), then: elseS, else: thenS });
|
|
4061
|
+
if (merge && merge !== stop) {
|
|
4062
|
+
out.push(...structureRegion(merge, stop));
|
|
4063
|
+
}
|
|
4064
|
+
return out;
|
|
4065
|
+
}
|
|
2263
4066
|
out.push(mkIf(cond, thenS, elseS));
|
|
2264
4067
|
if (merge && merge !== stop) {
|
|
2265
4068
|
out.push(...structureRegion(merge, stop));
|
|
@@ -2277,19 +4080,66 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2277
4080
|
};
|
|
2278
4081
|
const loopSub = (li: LoopInfo): Map<Value, string> => subFor(li.header.params, li.backArgOfParam);
|
|
2279
4082
|
|
|
2280
|
-
//
|
|
2281
|
-
//
|
|
2282
|
-
//
|
|
2283
|
-
//
|
|
2284
|
-
//
|
|
2285
|
-
|
|
4083
|
+
// The sunk exit copies, as body statements: `dest = <the arg, rebuilt here>`. The sink's gates
|
|
4084
|
+
// are stated against `exprWith(null)` — every name it stops at holds, at the top of the body,
|
|
4085
|
+
// what it held where the edge read it — so the arg is spelled with no substitution. Slot order
|
|
4086
|
+
// keeps it deterministic.
|
|
4087
|
+
//
|
|
4088
|
+
// NOT `expr`: an ambient `activeSub` is an ENCLOSING loop's post-loop naming, which the sink's
|
|
4089
|
+
// walk does not model, so a REBUILT tree here would be spelled under names that substitution has
|
|
4090
|
+
// redefined. DEFENSIVE — no input reaches it: instrumenting this line across the whole suite,
|
|
4091
|
+
// `activeSub` is null at every call. It stays because the failure it names is silent (a wrong
|
|
4092
|
+
// value, not a gap) and the cost of keeping it is one branch.
|
|
4093
|
+
//
|
|
4094
|
+
// The destination name is read as an invariant, not checked: every block param carries one.
|
|
4095
|
+
// `assertResolved` is what catches a widening that breaks that.
|
|
4096
|
+
//
|
|
4097
|
+
// `home` is the loop HEADER — the block whose body these copies open, and so the block from which
|
|
4098
|
+
// the write is reachable. Recorded rather than inferred because `exit` says the opposite (see
|
|
4099
|
+
// `sunkCopyHomes`).
|
|
4100
|
+
const preUpdateCopies = (exit: Block, exitArgs: readonly Value[], sunk: Set<number>, home: Block): Stmt[] =>
|
|
4101
|
+
[...sunk]
|
|
4102
|
+
.sort((x, y) => x - y)
|
|
4103
|
+
.map((j) => {
|
|
4104
|
+
if (activeSub !== null && !varName.has(exitArgs[j])) {
|
|
4105
|
+
throw new StructureError(
|
|
4106
|
+
`cannot structure '${fn.name}': a pre-update exit copy would rebuild a computed value ` +
|
|
4107
|
+
`inside a loop nested in another loop's post-loop naming`,
|
|
4108
|
+
);
|
|
4109
|
+
}
|
|
4110
|
+
const name = varName.get(exit.params[j])!;
|
|
4111
|
+
sunkCopyHomes.push({ name, home });
|
|
4112
|
+
return {
|
|
4113
|
+
k: 'assign' as const,
|
|
4114
|
+
name,
|
|
4115
|
+
value: exprWith(null)(exitArgs[j]),
|
|
4116
|
+
};
|
|
4117
|
+
});
|
|
4118
|
+
|
|
4119
|
+
// A guarded self-loop's body and latch test. The test reads the header's own params (back-edge
|
|
4120
|
+
// args substituted back), and the body is any sunk trailing copies, then the header's SIDE
|
|
4121
|
+
// EFFECTS in program order, then its parallel update. The side effects are required — a
|
|
4122
|
+
// copies-only body would silently delete every store/discarded call in the header. Effect order
|
|
4123
|
+
// is right by construction: statements read pre-update names, the updates land after, and a sunk
|
|
4124
|
+
// copy reads the top-of-iteration value it is there to capture.
|
|
4125
|
+
//
|
|
4126
|
+
// The SAME cond/body serve both forms: as a `while` (guard fused away — the un-rotation), the
|
|
4127
|
+
// first test reads the just-emitted init values; as a `dowhile` (guard kept as its own `if`),
|
|
4128
|
+
// every test runs after the update wrote the params' next values. Both are what that form's
|
|
4129
|
+
// source spelling means.
|
|
4130
|
+
const emitWhile = (
|
|
4131
|
+
li: LoopInfo,
|
|
4132
|
+
updates?: Stmt[],
|
|
4133
|
+
sunkCopies: Stmt[] = [],
|
|
4134
|
+
kind: 'while' | 'dowhile' = 'while',
|
|
4135
|
+
): Stmt => {
|
|
2286
4136
|
const term = li.header.ops[li.header.ops.length - 1];
|
|
2287
4137
|
let cond = exprWith(loopSub(li))(term.operands[0]);
|
|
2288
4138
|
if (term.successors[0].block !== li.header) {
|
|
2289
4139
|
cond = negateCond(cond);
|
|
2290
4140
|
} // loop-continue must be `taken`
|
|
2291
|
-
const body = [...sideEffects(li.header), ...(updates ?? argAssigns(li.header, li.header))];
|
|
2292
|
-
return { k: 'while', cond, body };
|
|
4141
|
+
const body = [...sunkCopies, ...sideEffects(li.header), ...(updates ?? argAssigns(li.header, li.header))];
|
|
4142
|
+
return kind === 'while' ? { k: 'while', cond, body } : { k: 'dowhile', cond, body };
|
|
2293
4143
|
};
|
|
2294
4144
|
|
|
2295
4145
|
// Test-at-top `while`: the header's cond_br is the loop condition. The body is a region that stops
|
|
@@ -2307,7 +4157,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2307
4157
|
// Mirror the br/cond_br cases: argAssigns then structureRegion. Structure the body under this
|
|
2308
4158
|
// loop's frame so an in-body conditional exit (break / early return) is recognised instead of
|
|
2309
4159
|
// tripping the header-re-entry `onStack` guard.
|
|
2310
|
-
const body = withLoop({ header: wl.header, exit: wl.exit, body: wl.body }, () => [
|
|
4160
|
+
const body = withLoop({ header: wl.header, exit: wl.exit, body: wl.body, arms: wl.arms }, () => [
|
|
2311
4161
|
...argAssigns(wl.header, wl.bodyEntry),
|
|
2312
4162
|
...structureRegion(wl.bodyEntry, wl.header),
|
|
2313
4163
|
]);
|
|
@@ -2334,10 +4184,67 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2334
4184
|
// path applies; on a hazard, decline LOUD.
|
|
2335
4185
|
const sub = latchSub(dw);
|
|
2336
4186
|
const updates = argAssigns(dw.latch, dw.header);
|
|
2337
|
-
const updateWrites =
|
|
4187
|
+
const updateWrites = loopWriteSet(updates, dw.body, dw.header);
|
|
2338
4188
|
const lterm = dw.latch.ops[dw.latch.ops.length - 1];
|
|
4189
|
+
// KNOWN GAP, and the reason the sink stands down rather than repairing anything. A body
|
|
4190
|
+
// block's param may adopt a LOOP VARIABLE's name (canTakeName waives the liveness half for a
|
|
4191
|
+
// pure alias, an argument that does not carry when the name came from `backArgName` — it is
|
|
4192
|
+
// then a different value's). The arm's copy into it is a real write partway through the body,
|
|
4193
|
+
// and everything rendered after it reads the name RAW: the update, the bottom test, the
|
|
4194
|
+
// header's own ops, the latch's side effects.
|
|
4195
|
+
//
|
|
4196
|
+
// The refusal is on the NAME, not on who reads it: the readers are every statement the loop
|
|
4197
|
+
// emits, which is not a set worth enumerating when the name alone is the whole signal.
|
|
4198
|
+
//
|
|
4199
|
+
// The emitted C is wrong whenever this shape occurs, sink or no sink — it is a naming-pipeline
|
|
4200
|
+
// defect, not this pass's, and repairing the exit copy does not touch it. What IS this pass's
|
|
4201
|
+
// is not to UNLOCK such a loop: with no sink these functions decline on the pre-update hazard,
|
|
4202
|
+
// so standing down keeps them loud rather than trading a decline for a silent wrong answer.
|
|
4203
|
+
const headerNames = new Set(dw.header.params.map((p) => varName.get(p)));
|
|
4204
|
+
const bodyRebinds = new Set<string>();
|
|
4205
|
+
for (const bb of dw.body) {
|
|
4206
|
+
if (bb === dw.header) {
|
|
4207
|
+
continue;
|
|
4208
|
+
}
|
|
4209
|
+
bb.params.forEach((pv, i) => {
|
|
4210
|
+
const n = varName.get(pv);
|
|
4211
|
+
if (n === undefined) {
|
|
4212
|
+
return;
|
|
4213
|
+
}
|
|
4214
|
+
// Only a copy that SURVIVES argAssigns' identity elision writes anything, so an edge whose
|
|
4215
|
+
// arg already carries the name does not count.
|
|
4216
|
+
for (const { succ } of inEdgeRecords(preds, bb)) {
|
|
4217
|
+
if (varName.get(succ.args[i]) !== n) {
|
|
4218
|
+
bodyRebinds.add(n);
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
4223
|
+
const rebindHazard = [...bodyRebinds].some((n) => headerNames.has(n));
|
|
2339
4224
|
const exitArgs = (successorTo(dw.latch, dw.exit)?.args ?? []) as Value[];
|
|
2340
|
-
|
|
4225
|
+
// A pre-update exit copy moves to the TOP of the body, where the loop variables still hold
|
|
4226
|
+
// their top-of-iteration values. No zero-trip seed here: a `do-while` always runs its body, so
|
|
4227
|
+
// any other predecessor of the exit is an ordinary edge some enclosing `if` already emits.
|
|
4228
|
+
const sunk = rebindHazard
|
|
4229
|
+
? new Set<number>()
|
|
4230
|
+
: sinkablePreUpdateSlots(dw.header, dw.exit, exitArgs, dw.body, sub, updateWrites);
|
|
4231
|
+
// The post-loop region the escaped-value check judges: everything the loop does not emit itself.
|
|
4232
|
+
// An early-`return` arm the loop OWNS renders inside the body, ahead of the update, so a read of
|
|
4233
|
+
// a loop variable there is the pre-update value it wants — counting it as post-loop would decline
|
|
4234
|
+
// the loop for a hazard that cannot happen. Blocks the arm merely reaches are not `owned`, and stay
|
|
4235
|
+
// post-loop where the other path really does read them after the update.
|
|
4236
|
+
const owned = new Set(dw.arms.flatMap((a) => [...a.owned]));
|
|
4237
|
+
const postLoop = new Set(fn.blocks.filter((bb) => !dw.body.has(bb) && !owned.has(bb)));
|
|
4238
|
+
if (
|
|
4239
|
+
loopUpdateHazard(
|
|
4240
|
+
lterm.operands[0],
|
|
4241
|
+
exitArgs.filter((_, j) => !sunk.has(j)),
|
|
4242
|
+
dw.body,
|
|
4243
|
+
sub,
|
|
4244
|
+
updateWrites,
|
|
4245
|
+
postLoop,
|
|
4246
|
+
)
|
|
4247
|
+
) {
|
|
2341
4248
|
throw new StructureError(
|
|
2342
4249
|
`cannot structure '${fn.name}': do-while condition or a post-loop value reads a pre-update loop variable`,
|
|
2343
4250
|
);
|
|
@@ -2372,25 +4279,95 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2372
4279
|
const inner =
|
|
2373
4280
|
dw.header === dw.latch
|
|
2374
4281
|
? [] // single-block self-loop: the header IS the latch — its ops render via sideEffects below
|
|
2375
|
-
: withLoop({ header: dw.header, exit: dw.exit, body: dw.body
|
|
4282
|
+
: withLoop({ header: dw.header, exit: dw.exit, body: dw.body, arms: dw.arms }, () =>
|
|
4283
|
+
structureBlock(dw.header, dw.latch),
|
|
4284
|
+
); // header..latch (exclusive of latch)
|
|
2376
4285
|
dwActive.delete(dw.header);
|
|
2377
4286
|
// The UPDATE is RAW (`v = v - 1`) — it IS the decrement; applying `sub` would make it look like the
|
|
2378
4287
|
// identity `v = v` and drop it. Only the CONDITION and EXIT copies use `sub` (post-update the param
|
|
2379
4288
|
// already holds the next value, so the latch-computed test reads `v`, not `v - 1`). `updates`
|
|
2380
4289
|
// reuses the hazard check's computation — a second argAssigns call would burn a spurious
|
|
2381
4290
|
// swap-cycle temp number.
|
|
2382
|
-
const body = [
|
|
4291
|
+
const body = [
|
|
4292
|
+
...preUpdateCopies(dw.exit, exitArgs, sunk, dw.header),
|
|
4293
|
+
...inner,
|
|
4294
|
+
...sideEffects(dw.latch),
|
|
4295
|
+
...updates,
|
|
4296
|
+
];
|
|
2383
4297
|
let cond = exprWith(sub)(lterm.operands[0]);
|
|
2384
4298
|
if (lterm.successors[1].block === dw.header) {
|
|
2385
4299
|
cond = negateCond(cond);
|
|
2386
4300
|
} // continue edge must be `taken`
|
|
2387
4301
|
const out: Stmt[] = [{ k: 'dowhile', cond, body }];
|
|
2388
4302
|
// The exit region reads latch back-edge values under `sub` (post-loop they live in the loop vars).
|
|
2389
|
-
out.push(
|
|
4303
|
+
out.push(
|
|
4304
|
+
...withSub(sub, () => [
|
|
4305
|
+
...argAssigns(dw.latch, dw.exit, sub, (j) => !sunk.has(j)),
|
|
4306
|
+
...structureRegion(dw.exit, stop),
|
|
4307
|
+
]),
|
|
4308
|
+
);
|
|
2390
4309
|
return out;
|
|
2391
4310
|
};
|
|
2392
4311
|
|
|
2393
|
-
const body = recognizeForLoops(structureRegion(entry, null));
|
|
4312
|
+
const body = stampOrderedBases(recognizeForLoops(structureRegion(entry, null)), orderLicensedGlobals);
|
|
4313
|
+
// THE OBLIGATION `anchorConstCopies` CANNOT DISCHARGE ON ITS OWN, checked now that the render is
|
|
4314
|
+
// complete. Anchoring is two edits that must both land: `suppressedArgs` DELETES a write from its
|
|
4315
|
+
// edge, and `anchoredAt` owes it back at the const's def site. Its refusal conditions establish
|
|
4316
|
+
// that the def block DOMINATES every suppressed edge — but dominating a block is not being
|
|
4317
|
+
// rendered, and a block whose every op renders inline gets no statement position at all. A loop's
|
|
4318
|
+
// claimed pure preheader is one (LoopInfo.preheader: the guarded-loop emitter takes its EDGE args
|
|
4319
|
+
// and never structures it), and there is no reason to believe it is the last. A promise half-kept
|
|
4320
|
+
// leaves the merge variable read where nothing wrote it — the one wrongness the byte differ
|
|
4321
|
+
// rewards rather than catches, since the candidate compiles, scores, and can win.
|
|
4322
|
+
//
|
|
4323
|
+
// OBSERVABLE is the test, not "emitted". A merge name the rendered body never mentions has no
|
|
4324
|
+
// reader to see the missing write and no second writer to disagree with it, and its declaration
|
|
4325
|
+
// goes with it (l3/dce.ts prunes a local nothing references) — so the copy and the value it
|
|
4326
|
+
// carried are gone together, which is a program the edge spelling can also produce. Only a name
|
|
4327
|
+
// the body still uses is a broken promise, or a GLOBAL, whose store is observed outside this
|
|
4328
|
+
// function whatever this body does with it. klonoa's `MPlayContinue` is the live inhabitant: its
|
|
4329
|
+
// `/defsite` spelling is correct and main enumerates it — a RECORD of a measurement, not a live
|
|
4330
|
+
// check, since that function is in no corpus row and no fixture here.
|
|
4331
|
+
const owed = [...anchoredAt.values()].flat().filter((w) => !anchorsEmitted.has(w));
|
|
4332
|
+
if (owed.length > 0) {
|
|
4333
|
+
const mentioned = new Set<string>(globalNames);
|
|
4334
|
+
for (const e of walkExprs(body)) {
|
|
4335
|
+
if (e.k === 'var' || e.k === 'addr') {
|
|
4336
|
+
mentioned.add(e.name);
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
const assignTargets = (ss: Stmt[]): void => {
|
|
4340
|
+
for (const st of ss) {
|
|
4341
|
+
if (st.k === 'assign') {
|
|
4342
|
+
mentioned.add(st.name);
|
|
4343
|
+
}
|
|
4344
|
+
assignTargets(stmtChildren(st));
|
|
4345
|
+
}
|
|
4346
|
+
};
|
|
4347
|
+
assignTargets(body);
|
|
4348
|
+
const broken = owed.find((w) => mentioned.has(w.name));
|
|
4349
|
+
if (broken !== undefined) {
|
|
4350
|
+
throw new StructureError(
|
|
4351
|
+
`cannot structure '${fn.name}': the merge copy into '${broken.name}' was suppressed from ` +
|
|
4352
|
+
`its edge for a def-site anchor that no rendered position emitted`,
|
|
4353
|
+
);
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
// THE OBLIGATION `undefCarriesNothing` CANNOT DISCHARGE ON ITS OWN, checked now that both records
|
|
4357
|
+
// are complete. That test reads each write site off `paramBlock`/`opBlock`, and a sunk pre-update
|
|
4358
|
+
// exit copy is written somewhere else than either map says. What keeps them apart today is
|
|
4359
|
+
// `dest-free-inside-loop`, a gate stated about something else entirely and carrying its own KNOWN
|
|
4360
|
+
// GAP — so the day a widening lets the two meet, the emitted C substitutes a DEFINED value for the
|
|
4361
|
+
// undefined one the machine leaves in place. Loud, because that failure has no other symptom.
|
|
4362
|
+
const collided = sunkCopyOverDroppedUndef(droppedUndefCopies, sunkCopyHomes, (home, pred) =>
|
|
4363
|
+
reachFrom(home).has(pred),
|
|
4364
|
+
);
|
|
4365
|
+
if (collided !== null) {
|
|
4366
|
+
throw new StructureError(
|
|
4367
|
+
`cannot structure '${fn.name}': a pre-update exit copy sunk into a loop body writes '${collided}' ` +
|
|
4368
|
+
`ahead of an edge whose undefined argument's copy was dropped as carrying nothing`,
|
|
4369
|
+
);
|
|
4370
|
+
}
|
|
2394
4371
|
// Strict-mode gaps decline HERE, naming the reasons — the same text annotate's markers
|
|
2395
4372
|
// carry, so the two mode surfaces report the same decline (the reproduction scripts run
|
|
2396
4373
|
// strict; the benchmark rows store annotate markers — fidelity holds them against each
|
|
@@ -2404,31 +4381,157 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2404
4381
|
const localNames = [...new Set([...varName.values(), ...[...varType.keys()].filter((n) => /^t\d+$/.test(n))])].filter(
|
|
2405
4382
|
(n) => /^[vt]\d+$/.test(n) && !globalNames.has(n),
|
|
2406
4383
|
);
|
|
4384
|
+
// THE FRAME COORDINATES OF EACH DECLARED LOCAL (ir/core.ts `SlotHomes`, stamped by the SSA
|
|
4385
|
+
// builder). The naming walk is what makes it a per-DECLARATION fact: several spilled values can
|
|
4386
|
+
// land under one name, and the local the source declared is the name, not the value. Where a
|
|
4387
|
+
// name covers several homes it takes the UNION and picks NOTHING — which offset is the earlier
|
|
4388
|
+
// declaration rank depends on the frame's direction, and `l3/slotorder.ts` is the one place that
|
|
4389
|
+
// holds it. Sorted, so the list is deterministic rather than insertion-ordered.
|
|
4390
|
+
//
|
|
4391
|
+
// A NAME COVERING NO HOMED VALUE GETS NO ENTRY, so `slots` is absent rather than `[]` on the
|
|
4392
|
+
// locals the asm kept in registers. Values named `a<n>` (parameters) never reach here:
|
|
4393
|
+
// `localNames` is the `v*`/`t*` set, and a parameter's storage is the caller's question.
|
|
4394
|
+
const slotsOfName = new Map<string, Set<number>>();
|
|
4395
|
+
const isLocalName = new Set(localNames);
|
|
4396
|
+
for (const [v, offs] of fn.slotHomes ?? []) {
|
|
4397
|
+
const n = varName.get(v);
|
|
4398
|
+
if (n === undefined || !isLocalName.has(n)) {
|
|
4399
|
+
continue;
|
|
4400
|
+
}
|
|
4401
|
+
const prev = slotsOfName.get(n);
|
|
4402
|
+
if (prev === undefined) {
|
|
4403
|
+
slotsOfName.set(n, new Set(offs));
|
|
4404
|
+
} else {
|
|
4405
|
+
offs.forEach((off) => prev.add(off));
|
|
4406
|
+
}
|
|
4407
|
+
}
|
|
4408
|
+
// The machine's static access counts for one frame object: every `load`/`store` rooted on an
|
|
4409
|
+
// `laddr` at the same offset. Counted over the L2 blocks, so it is the access set the asm had,
|
|
4410
|
+
// before any L3 readability pass could drop or duplicate one. Summed across the offset's
|
|
4411
|
+
// `laddr` ops — the frontend's frame-object audit re-roots accesses onto per-offset captures
|
|
4412
|
+
// and holds one object per offset, so several ops can name the same storage.
|
|
4413
|
+
//
|
|
4414
|
+
// NO record at all when the address reaches ANYTHING ELSE — a block argument, an offset
|
|
4415
|
+
// computation, a call. The count is then a floor rather than the access set, and it is read as
|
|
4416
|
+
// the access set (the l3/volatileval.ts gate), so it refuses instead of reporting a number that
|
|
4417
|
+
// undercounts. Reached rather than theoretical: an address-escaped frame scratch takes it —
|
|
4418
|
+
// `synthetic:dma_fill_uninit` and `kleod:ProcessInputAndUpdateEntities` both lose their record
|
|
4419
|
+
// here.
|
|
4420
|
+
const frameRecord = (at: Op): { frame?: { loads: number; stores: number } } => {
|
|
4421
|
+
const off = at.attrs.off as number;
|
|
4422
|
+
const roots = new Set<Value>();
|
|
4423
|
+
for (const b of fn.blocks) {
|
|
4424
|
+
for (const op of b.ops) {
|
|
4425
|
+
if (op.opcode === 'laddr' && (op.attrs.off as number) === off) {
|
|
4426
|
+
roots.add(op.results[0]);
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
let loads = 0;
|
|
4431
|
+
let stores = 0;
|
|
4432
|
+
for (const b of fn.blocks) {
|
|
4433
|
+
for (const op of b.ops) {
|
|
4434
|
+
const access = op.opcode === 'load' || op.opcode === 'store';
|
|
4435
|
+
if (access && roots.has(op.operands[0])) {
|
|
4436
|
+
if (op.opcode === 'load') {
|
|
4437
|
+
loads++;
|
|
4438
|
+
} else {
|
|
4439
|
+
stores++;
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
// every OTHER mention of the address, operands and branch arguments alike
|
|
4443
|
+
const elsewhere = op.operands.some((v, i) => roots.has(v) && !(access && i === 0));
|
|
4444
|
+
if (elsewhere || op.successors.some((sx) => sx.args.some((v) => roots.has(v)))) {
|
|
4445
|
+
return {};
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
return { frame: { loads, stores } };
|
|
4450
|
+
};
|
|
4451
|
+
// ONE `laddr` op per minted NAME, last one wins. Same answer as feeding every op's finished entry
|
|
4452
|
+
// to `new Map` — each entry was a pure function of its own op, and the Map kept the LAST value
|
|
4453
|
+
// under a key at its FIRST key's position — but stated rather than left to be read off `new Map`'s
|
|
4454
|
+
// semantics, and `frameRecord` (which walks every op of the function) then runs once per name
|
|
4455
|
+
// instead of once per op. NOT a measured speedup: over `packages/core/test` every one of the
|
|
4456
|
+
// 1,057 structurings that reach here has ops === names, so the dedup fires zero times there. It
|
|
4457
|
+
// is a bound, not a win: `laddrName` mints one name per OFFSET, and the frontend's frame-object
|
|
4458
|
+
// audit can re-root several ops onto one offset (see `frameRecord`'s own note).
|
|
4459
|
+
const lastLaddrOf = new Map<string, Op>();
|
|
4460
|
+
for (const op of fn.blocks.flatMap((b) => b.ops).filter((op) => op.opcode === 'laddr')) {
|
|
4461
|
+
lastLaddrOf.set(laddrName.get(op)!, op);
|
|
4462
|
+
}
|
|
2407
4463
|
const structs = collectStructs(fn);
|
|
2408
4464
|
return {
|
|
2409
4465
|
name: fn.name,
|
|
2410
4466
|
params: entry.params.map((p, i) => ({ name: `a${i}`, type: p.type })),
|
|
2411
4467
|
locals: [
|
|
2412
|
-
...localNames.map((n) => ({
|
|
4468
|
+
...localNames.map((n) => ({
|
|
4469
|
+
name: n,
|
|
4470
|
+
type: varType.get(n)!,
|
|
4471
|
+
...(slotsOfName.has(n) ? { slots: [...slotsOfName.get(n)!].sort((x, y) => x - y) } : {}),
|
|
4472
|
+
})),
|
|
2413
4473
|
// frame-local objects (laddr): declared with EXACTLY the access type the machine used —
|
|
2414
4474
|
// the frontend's frame-object audit proved all accesses agree, so this is a fact, not a guess
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
4475
|
+
//
|
|
4476
|
+
// NO `slots` IS STAMPED HERE, and the offset is in hand (`op.attrs.off`), so this is a
|
|
4477
|
+
// refusal and not an oversight. An RTL-time frame object is allocated at its declaration
|
|
4478
|
+
// and sits BELOW every spill slot, so its offset is not evidence of its rank among the
|
|
4479
|
+
// spilled locals — and the one row in the corpus that carries such an object next to real
|
|
4480
|
+
// spills (`synthetic:dma_fill_uninit`, a `volatile` scratch at offset 0) compiles to the
|
|
4481
|
+
// same object whether that local is sorted with the spills or left where the naming walk
|
|
4482
|
+
// put it, measured both ways. So no row can refute an ordering over these, and an ordering
|
|
4483
|
+
// no row can refute does not earn its place. THE FLIP CONDITION: the first row whose object
|
|
4484
|
+
// is decided by the declaration order of two `laddr` locals, or of an `laddr` against a
|
|
4485
|
+
// spill, is the referee — stamp them then, and `l3/slotorder.ts` needs no change to use it.
|
|
4486
|
+
...[...lastLaddrOf].map(([name, op]) => ({
|
|
4487
|
+
name,
|
|
4488
|
+
type: T.int((op.attrs.width as number) * 8, op.attrs.signed as boolean),
|
|
4489
|
+
// the asm materialized this slot's address, and this is how many times it
|
|
4490
|
+
// loaded and stored through it — both asm facts, and the gate the
|
|
4491
|
+
// l3/volatileval.ts lever reads (see the SFn.locals doc)
|
|
4492
|
+
...frameRecord(op),
|
|
4493
|
+
// an ESCAPED address makes every store observable (the DMA hardware reads it), and
|
|
4494
|
+
// the source spells the scratch volatile for that reason — see the stamp site in
|
|
4495
|
+
// frontend/thumb.ts for why it is the SPELLING that matters and not dead-store
|
|
4496
|
+
// elimination, which keeps the store either way
|
|
4497
|
+
...(op.attrs.volatile === true ? { volatile: true as const } : {}),
|
|
4498
|
+
})),
|
|
4499
|
+
// uninitialised locals (undef): declared, never assigned, typed by whatever recovery settled
|
|
4500
|
+
// on for the value. NO `slots` either, on the same footing as the frame objects above.
|
|
4501
|
+
//
|
|
4502
|
+
// THE REASON IS UNDECIDABILITY, not absence. One frame slot then yields TWO declarations:
|
|
4503
|
+
// the `undef` read of the storage and the value stored into it. They are one object in the
|
|
4504
|
+
// source, and the asm does not say which of the two took the rank — so ranking one of the
|
|
4505
|
+
// pair while leaving the other pinned would invent an answer. Refusing the undef half keeps
|
|
4506
|
+
// the stored half's rank, which is the one the spill evidence is about.
|
|
4507
|
+
//
|
|
4508
|
+
// AND IT IS PRECAUTIONARY: NO ROW REACHES IT. The co-existence is real in the tree this
|
|
4509
|
+
// function returns — `uninit_spill` declares `uninit_sp0/sp4/sp8` beside the slotted `v4@0
|
|
4510
|
+
// v5@4 v6@8`, `dma_fill_uninit` declares `uninit_sp4/sp8/sp12` beside `v4@4 v6@8 v8@12` —
|
|
4511
|
+
// but that tree is never emitted: `structure()` has exactly one caller, `structureChecked`
|
|
4512
|
+
// (pipeline.ts), and `l3/slotorder.ts` runs inside `emit`, downstream of it on every emit
|
|
4513
|
+
// path. On the tree `structureChecked` returns neither row has an `uninit` local left —
|
|
4514
|
+
// `eliminateDeadStores` (l3/dce.ts, whose last line keeps only locals the body still
|
|
4515
|
+
// references) removes all eight on `uninit_spill` and all seven on `dma_fill_uninit`,
|
|
4516
|
+
// leaving the slot-carrying locals untouched. Instrumented one spine stage at a time, not
|
|
4517
|
+
// inferred: raw 8 → mergeCommonTails 8 → eliminateDeadStores 0.
|
|
4518
|
+
//
|
|
4519
|
+
// The refusal stays HERE rather than at the ordering because this is where the stamp is
|
|
4520
|
+
// decided, and an `undef` local that survived DCE — address-taken, `volatile`, or genuinely
|
|
4521
|
+
// read — would otherwise carry it into `emit`. FLIP CONDITION, and it is not "the first
|
|
4522
|
+
// uninit local", since four rows already have them in `structure()`'s tree and none in
|
|
4523
|
+
// `structureChecked`'s: it is the first row where a slot-keyed `undef` local SURVIVES
|
|
4524
|
+
// dead-store elimination beside a slotted local at the same offset, and compile shows the
|
|
4525
|
+
// two to be two source declarations rather than one. `spill-slot-order.test.ts` pins both
|
|
4526
|
+
// halves, so a change to either fails there.
|
|
4527
|
+
...fn.blocks
|
|
4528
|
+
.flatMap((b) => b.ops)
|
|
4529
|
+
.filter((op) => op.opcode === 'undef')
|
|
4530
|
+
.map((op) => ({
|
|
4531
|
+
name: undefName.get(op)!,
|
|
4532
|
+
type: varType.get(undefName.get(op)!) ?? op.results[0].type,
|
|
4533
|
+
uninit: true as const,
|
|
4534
|
+
})),
|
|
2432
4535
|
],
|
|
2433
4536
|
...(shapedGlobalTypes.size
|
|
2434
4537
|
? {
|
|
@@ -2440,6 +4543,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
2440
4543
|
retType: returnsVoid ? T.void() : returnType(fn),
|
|
2441
4544
|
body,
|
|
2442
4545
|
...(structs.length ? { structs } : {}),
|
|
4546
|
+
// absent stays absent: the backend asks "is a direction known?", and there is no third state
|
|
4547
|
+
// at this boundary — `structureOptionsFor` dropped the target's `'unknown'` already.
|
|
4548
|
+
...(spillSlotOrder !== undefined ? { slotOrder: spillSlotOrder } : {}),
|
|
2443
4549
|
};
|
|
2444
4550
|
}
|
|
2445
4551
|
|
|
@@ -2525,6 +4631,26 @@ function recognizeForLoops(stmts: Stmt[]): Stmt[] {
|
|
|
2525
4631
|
// still-pending assignment reads; break a cycle by spilling one destination to a temp.
|
|
2526
4632
|
// `tmp` is the per-FUNCTION temp counter (threaded from structure()) so two independent
|
|
2527
4633
|
// parallel copies never reuse a temp name against conflicting types.
|
|
4634
|
+
/** Does this edge's copy set contain a CYCLE — no copy emittable without overwriting a destination
|
|
4635
|
+
* another still reads, so `sequentialize` has to spill one into a temp? `sequentialize`'s own
|
|
4636
|
+
* emittability loop, run without emitting, so the two can never disagree about which sets are
|
|
4637
|
+
* which.
|
|
4638
|
+
*
|
|
4639
|
+
* It is the LICENCE BOUNDARY the edge-copy sort draws: on a cyclic set an instruction names the
|
|
4640
|
+
* compiler's temp and the write-order record reproduces it, while on an acyclic one the record is
|
|
4641
|
+
* only an assumption about layout order. `preferDefPosCopyOrder` asks for the proxy on the second
|
|
4642
|
+
* kind only. */
|
|
4643
|
+
function copySetIsCyclic(copies: { name: string; value: Expr }[]): boolean {
|
|
4644
|
+
const pending = copies.map((c) => ({ name: c.name, reads: exprVars(c.value) }));
|
|
4645
|
+
for (;;) {
|
|
4646
|
+
const i = pending.findIndex((a) => !pending.some((b) => b !== a && b.reads.has(a.name)));
|
|
4647
|
+
if (i < 0) {
|
|
4648
|
+
return pending.length > 0;
|
|
4649
|
+
}
|
|
4650
|
+
pending.splice(i, 1);
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
4653
|
+
|
|
2528
4654
|
function sequentialize(
|
|
2529
4655
|
copies: { name: string; value: Expr }[],
|
|
2530
4656
|
varType: Map<string, IrType>,
|
|
@@ -2608,6 +4734,32 @@ function successorTo(pred: Block, target: Block) {
|
|
|
2608
4734
|
const term = pred.ops[pred.ops.length - 1];
|
|
2609
4735
|
return term.successors.find((s) => s.block === target);
|
|
2610
4736
|
}
|
|
4737
|
+
/** EVERY in-edge record into `b` — which is what {@link successorTo} cannot give: it returns only
|
|
4738
|
+
* the FIRST record to a block, so a terminator with two edges to the same block hides the second
|
|
4739
|
+
* edge's args. Callers that ask "what does every path pass for param i" need all of them.
|
|
4740
|
+
*
|
|
4741
|
+
* ORDER IS LOAD-BEARING and is `preds` insertion order, then terminator-successor order: the
|
|
4742
|
+
* naming walk takes the FIRST admissible carrier, so a different order is a different spelling.
|
|
4743
|
+
*
|
|
4744
|
+
* AND IT IS GUARDED, not merely warned about — measured, not reasoned: rewriting this walk to call
|
|
4745
|
+
* `successorTo` turns 12 tests across 5 files of `packages/core/test` red. The path it would lose
|
|
4746
|
+
* is reached constantly, 326,305 times over that suite on a `cond_br` whose two arms land on one
|
|
4747
|
+
* block and 6 more on a `switch_br`.
|
|
4748
|
+
*
|
|
4749
|
+
* The terminator is read UNGUARDED (`ops[ops.length - 1]`) on purpose: every block of a verified
|
|
4750
|
+
* Fn has one, and a guard would turn a malformed Fn's throw into a silent missing edge.
|
|
4751
|
+
*
|
|
4752
|
+
* `hasParamRootedMerge` does not go through this, deliberately: it is a pre-structuring query over
|
|
4753
|
+
* a raw Fn and walks `fn.blocks` rather than a predecessor map it would first have to build. */
|
|
4754
|
+
function* inEdgeRecords(preds: Map<Block, Block[]>, b: Block): Generator<{ pred: Block; succ: Successor }> {
|
|
4755
|
+
for (const pred of new Set(preds.get(b) ?? [])) {
|
|
4756
|
+
for (const succ of pred.ops[pred.ops.length - 1].successors) {
|
|
4757
|
+
if (succ.block === b) {
|
|
4758
|
+
yield { pred, succ };
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
2611
4763
|
|
|
2612
4764
|
// Immediate post-dominators. EXIT is represented as `null`; ret-blocks post-lead to it.
|
|
2613
4765
|
function postDominators(fn: Fn): Map<Block, Block | null> {
|