@asmlift/core 0.9.0 → 0.10.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 +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
// parallel block-argument update, sequentialised so an assignment never clobbers a
|
|
24
24
|
// value a later one still needs.
|
|
25
25
|
//
|
|
26
|
-
// Module layout: loop DISCOVERY is loops.ts; the pure ANALYSIS phase
|
|
27
|
-
// materialization) is analysis.ts; comparison-tree switch recovery is switch-recover.ts
|
|
26
|
+
// Module layout: loop DISCOVERY is ../ir/loops.ts (raise/ reads it too); the pure ANALYSIS phase
|
|
27
|
+
// (use registry, liveness, materialization) is analysis.ts; comparison-tree switch recovery is switch-recover.ts
|
|
28
28
|
// (explicit-deps factory). THIS file keeps the mutually-entangled remainder: SSA-destruction
|
|
29
29
|
// coalescing (canTakeName + seeding) and emission (structureBlock + the loop emitters) — they
|
|
30
30
|
// share varName/backArgName mutation and the activeSub/loopCtx dynamic state.
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
// bottom-test `do-while`, `while (1)` for any other latch set (a conditional `continue`, a
|
|
35
35
|
// mid-tested header), PROPERLY-nested loops, in-body `break`/early-`return`, comparison-tree and
|
|
36
36
|
// jump-table `switch`, and a switch arm that FALLS THROUGH into the next one (both regimes — see
|
|
37
|
-
// `ArmExit` in switch-recover.ts)
|
|
37
|
+
// `ArmExit` in switch-recover.ts), on some paths only, leaving the switch by a `break;` on the
|
|
38
|
+
// others (`switchFrames`). Still DECLINED (loud StructureError, never wrong code):
|
|
38
39
|
// irreducible/overlapping loops, exits to more than one live merge, a `break` out of a nested loop's
|
|
39
40
|
// body, a `break` whose exit copies would clobber, and mixed-entry self-loops (a guarded header also
|
|
40
41
|
// entered by a plain br).
|
|
@@ -42,24 +43,44 @@
|
|
|
42
43
|
// `case` cannot fall through (`spellSwitchFallthrough` false) sends Regime A back to if-recovery,
|
|
43
44
|
// and arms that do not linearize into one chain — two arms falling into the same sibling, or a fall
|
|
44
45
|
// into the `default:` — refuse in `chainArms`, which answers null.
|
|
45
|
-
import {
|
|
46
|
+
import { globalBaseOf } from '../ir/alias';
|
|
47
|
+
import { type WordCtx, incomingArgs, mayAlwaysHold, overwrittenReads, storeForwards } from '../ir/bits';
|
|
46
48
|
import {
|
|
47
49
|
Block,
|
|
50
|
+
CALL_RETURN_OBJECT,
|
|
48
51
|
Fn,
|
|
52
|
+
OWN_RETURN_OBJECT,
|
|
49
53
|
Op,
|
|
54
|
+
RETURNS_OBJECT,
|
|
50
55
|
Successor,
|
|
51
56
|
Value,
|
|
57
|
+
byValueOperands,
|
|
52
58
|
callArgs,
|
|
53
59
|
calleeName,
|
|
54
60
|
calleeValue,
|
|
55
61
|
defOpMap,
|
|
56
62
|
dominators,
|
|
57
63
|
mergeClasses,
|
|
64
|
+
reachForward,
|
|
58
65
|
successorsOf,
|
|
59
66
|
} from '../ir/core';
|
|
60
|
-
import { effectful, placedAt, qualified, qualifiedBy, spelledWhenDead } from '../ir/discipline';
|
|
61
|
-
import {
|
|
62
|
-
import {
|
|
67
|
+
import { effectful, orderSensitive, placedAt, qualified, qualifiedBy, spelledWhenDead } from '../ir/discipline';
|
|
68
|
+
import { type NaturalLoop, analyzeLoops } from '../ir/loops';
|
|
69
|
+
import { CAST_WIDTHS, FLOAT_RESIZE, FLOAT_TO_INT, INT_TO_FLOAT, NEGATED_FCMP, WIDE_BITS, opSig } from '../ir/opcodes';
|
|
70
|
+
import {
|
|
71
|
+
type IrType,
|
|
72
|
+
type StructField,
|
|
73
|
+
T,
|
|
74
|
+
UNION_VIEW_NAMES,
|
|
75
|
+
intWidth,
|
|
76
|
+
isCastView,
|
|
77
|
+
scalarTypeForAccess,
|
|
78
|
+
spellsAccessType,
|
|
79
|
+
structSpelling,
|
|
80
|
+
typeEquals,
|
|
81
|
+
unionView,
|
|
82
|
+
unionViewAt,
|
|
83
|
+
} from '../ir/types';
|
|
63
84
|
import {
|
|
64
85
|
BinOp,
|
|
65
86
|
Expr,
|
|
@@ -70,37 +91,46 @@ import {
|
|
|
70
91
|
exprHasEffect,
|
|
71
92
|
gapReasonFor,
|
|
72
93
|
isLoop,
|
|
94
|
+
keepsHelperRel,
|
|
73
95
|
mapExprChildren,
|
|
74
96
|
mapStmtExprs,
|
|
75
97
|
mentionedName,
|
|
76
98
|
negateCond,
|
|
99
|
+
regainsHelperRel,
|
|
100
|
+
splitHelperBreak,
|
|
77
101
|
stmtChildren,
|
|
78
102
|
stmtExprs,
|
|
79
103
|
stmtsEqual,
|
|
104
|
+
unionStmtsEvidence,
|
|
80
105
|
walkExprs,
|
|
81
106
|
} from '../l3/ast';
|
|
107
|
+
import { constantImageRefusal } from '../l3/constinit';
|
|
82
108
|
import { type Gate, firstRejection } from '../l3/gates';
|
|
83
|
-
import {
|
|
109
|
+
import {
|
|
110
|
+
exprCType,
|
|
111
|
+
exprIntWidth,
|
|
112
|
+
renderedIntSignedness,
|
|
113
|
+
rendersSignedOverUnsigned,
|
|
114
|
+
spelledNonNegative,
|
|
115
|
+
} from '../l3/typing';
|
|
84
116
|
import { castOffDeclaration, qualifiedMemoryAccess } from '../l3/volstore';
|
|
117
|
+
import type { ContextGlobals } from '../proto-context';
|
|
85
118
|
import { foldConstPair, isConstFoldOpcode } from '../raise/const';
|
|
86
119
|
import { stampDeclaredVolatile } from '../raise/declared-volatile';
|
|
87
120
|
import { returnType } from '../raise/recover';
|
|
121
|
+
import { storeSigned } from '../raise/storesign';
|
|
88
122
|
import { collectStructs } from '../raise/structs';
|
|
89
123
|
import {
|
|
90
124
|
type DeclaredField,
|
|
91
125
|
type SymbolInfo,
|
|
92
|
-
type SymbolStructField,
|
|
93
126
|
declaredArrayShape,
|
|
94
|
-
declaredFields,
|
|
95
127
|
isArrayField,
|
|
96
128
|
isBitfieldField,
|
|
97
|
-
isPtrField,
|
|
98
|
-
isScalarCellSize,
|
|
99
129
|
pointeeFields,
|
|
100
|
-
scalarCellType,
|
|
101
130
|
structFieldInnerExtents,
|
|
102
131
|
} from '../symbols';
|
|
103
132
|
import { analyze, shortCircuitGuardedValues } from './analysis';
|
|
133
|
+
import { ARITH_TO_BIN } from './arith-ops';
|
|
104
134
|
import { makeBitfieldSpelling } from './bitfields';
|
|
105
135
|
import {
|
|
106
136
|
addOffset,
|
|
@@ -110,6 +140,10 @@ import {
|
|
|
110
140
|
elementIndex,
|
|
111
141
|
globalByteBase,
|
|
112
142
|
globalOf,
|
|
143
|
+
memberQualsAllow,
|
|
144
|
+
memoFieldsOf,
|
|
145
|
+
ptrMemberDecl,
|
|
146
|
+
structMemberAt,
|
|
113
147
|
subscriptsFromExtents,
|
|
114
148
|
} from './globalaccess';
|
|
115
149
|
import {
|
|
@@ -121,8 +155,8 @@ import {
|
|
|
121
155
|
updateWriteSet,
|
|
122
156
|
} from './hazards';
|
|
123
157
|
import { type StaticLayout, localStaticShapes, nameLocalStatics } from './local-statics';
|
|
124
|
-
import { type NaturalLoop, analyzeLoops } from './loops';
|
|
125
158
|
import { type NameMerge, coalesceNames } from './namecoalesce';
|
|
159
|
+
import { type ArithCompilerFacts, declaresBytePointer, makePointerSpelling } from './pointer-spelling';
|
|
126
160
|
import { testRereadsOnly } from './redundant-test';
|
|
127
161
|
import { unspelledEpilogues } from './retspell';
|
|
128
162
|
import { type ArmExit, type SwitchBoundCase, makeSwitchRecovery } from './switch-recover';
|
|
@@ -162,11 +196,7 @@ interface SymRenderCtx {
|
|
|
162
196
|
* the byte residual — the `/flat-rank` arm's OFF switch, off the `spellDeclaredSubscripts`
|
|
163
197
|
* structure option. */
|
|
164
198
|
declRank: boolean;
|
|
165
|
-
/**
|
|
166
|
-
* pointee's — MEMOIZED per symbol. `declaredFields` validates every member and returns a fresh
|
|
167
|
-
* sorted copy on every call, and `isPtrValue` asks it for both operands of every binary node
|
|
168
|
-
* lowered, so an uncached lookup is an O(n log n) allocation on a hot path — inside a
|
|
169
|
-
* `structure()` a ranked run repeats once per candidate, 17,856 times on the largest fan. */
|
|
199
|
+
/** {@link memoFieldsOf} over `info`. */
|
|
170
200
|
fieldsOf(name: string): DeclaredField[] | null;
|
|
171
201
|
}
|
|
172
202
|
|
|
@@ -184,9 +214,9 @@ interface SymRenderCtx {
|
|
|
184
214
|
|
|
185
215
|
/** The global named by a pointer global's VALUE as the additive lowering spells it: the bare
|
|
186
216
|
* `gPtr`, or that value wearing the byte-pointer / u32 cast that lowering adds (cast-then-add,
|
|
187
|
-
* see the `needsIntSpelling` / pointer-global arithmetic rules
|
|
188
|
-
* address and add BYTES to it, so both fold here; a cast to any other pointer
|
|
189
|
-
* through — a `(u16 *)` base would re-scale everything added after it. */
|
|
217
|
+
* see the `needsIntSpelling` / pointer-global arithmetic rules in pointer-spelling.ts). Both
|
|
218
|
+
* denote the same address and add BYTES to it, so both fold here; a cast to any other pointer
|
|
219
|
+
* type is NOT looked through — a `(u16 *)` base would re-scale everything added after it. */
|
|
190
220
|
function ptrGlobalValueName(x: Expr): string | null {
|
|
191
221
|
if (x.k === 'var') {
|
|
192
222
|
return x.name;
|
|
@@ -245,82 +275,40 @@ function ptrGlobalBase(e: Expr, isPtrGlobal: (n: string) => boolean): PtrGlobalB
|
|
|
245
275
|
return name !== null ? { name, byte, idx } : null;
|
|
246
276
|
}
|
|
247
277
|
|
|
248
|
-
/** Does a member declared at signedness `declared` read as EXACTLY the type the cast spelling this
|
|
249
|
-
* replaces would have produced — `scalarTypeForAccess(width, signed)`? That is the whole
|
|
250
|
-
* byte-exactness argument for naming a member instead of casting: same address, same read width,
|
|
251
|
-
* same C type, so every operator downstream compiles identically. A 4-byte access renders s32
|
|
252
|
-
* whatever the load said (the ISA has one word load), so only a SIGNED member may take its place;
|
|
253
|
-
* narrower accesses carry their own signedness and must match it. An undeclared signedness (the
|
|
254
|
-
* member is not a base type — a nested struct, an enum, a pointer) is never assumed to match.
|
|
255
|
-
* Compared against THE one copy of that rule (ir/types.ts) rather than restating it. */
|
|
256
|
-
function spellsAccessType(declared: boolean | undefined, width: number, signed: boolean): boolean {
|
|
257
|
-
if (declared === undefined) {
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
return typeEquals(T.int(width * 8, declared), scalarTypeForAccess(width, signed));
|
|
261
|
-
}
|
|
262
|
-
|
|
263
278
|
/** The declared type of a frame-local object, from the three facts its `laddr` carries: `count`
|
|
264
279
|
* elements of `width` bytes, signed or not. ONE element is a SCALAR typed by the access every
|
|
265
280
|
* access agreed on. More is STORAGE the frontend's frame-object audit sized without typing —
|
|
266
281
|
* no access pins an element type there, so the audit stamps bytes and this declares the array
|
|
267
|
-
* that spells them. Both arms are the audit's own facts; neither infers anything here.
|
|
282
|
+
* that spells them. Both arms are the audit's own facts; neither infers anything here.
|
|
283
|
+
*
|
|
284
|
+
* …and storage the audit found accessed at two widths over one byte carries the VIEWS those
|
|
285
|
+
* accesses use (`views`, frame-objects.ts `unionViews`), and is declared the union of them —
|
|
286
|
+
* each view an array tiling the whole storage, `union { s32 word[3]; u16 half[6]; u8 byte[12]; }`
|
|
287
|
+
* — because a union member access is the one spelling of two widths agbcc never reads stale. */
|
|
268
288
|
function laddrType(op: Op): IrType {
|
|
269
289
|
// a call's struct-return storage: the audit typed its address as a pointer to that struct
|
|
270
290
|
const t = op.results[0].type;
|
|
271
291
|
if (op.attrs.aggregate === true && t.kind === 'ptr') {
|
|
272
292
|
return t.to;
|
|
273
293
|
}
|
|
294
|
+
const views = op.attrs.views as number[] | undefined;
|
|
295
|
+
if (views !== undefined) {
|
|
296
|
+
const count = op.attrs.count as number;
|
|
297
|
+
const widths = views.map(Math.abs);
|
|
298
|
+
const members = views.map((v): StructField => {
|
|
299
|
+
const w = Math.abs(v);
|
|
300
|
+
const elem = scalarTypeForAccess(w, v < 0);
|
|
301
|
+
const name =
|
|
302
|
+
widths.filter((x) => x === w).length > 1 ? `${v < 0 ? 's' : 'u'}${UNION_VIEW_NAMES[w]}` : UNION_VIEW_NAMES[w];
|
|
303
|
+
return { off: 0, type: count === w ? elem : T.array(elem, count / w), name };
|
|
304
|
+
});
|
|
305
|
+
return { kind: 'union', members, size: count };
|
|
306
|
+
}
|
|
274
307
|
const elem = T.int((op.attrs.width as number) * 8, op.attrs.signed as boolean);
|
|
275
308
|
const count = op.attrs.count as number;
|
|
276
309
|
return count === 1 ? elem : T.array(elem, count);
|
|
277
310
|
}
|
|
278
311
|
|
|
279
|
-
/** May a member be NAMED by an access of this direction, given the qualifiers on its declaration?
|
|
280
|
-
* The named spelling REPLACES a cast through `(u8 *)`, which carries no qualifier at all, so a
|
|
281
|
-
* qualifier the name reintroduces changes what the compiler emits:
|
|
282
|
-
* • `volatile` makes the access observable — the load may no longer be folded or reordered,
|
|
283
|
-
* which is a different instruction sequence (measured: 6 insns where the cast form was 5);
|
|
284
|
-
* • `const` under a STORE is a hard error, where the cast form merely cast the qualifier away.
|
|
285
|
-
* Either way the honest spelling is the cast form, so the member simply is not nameable here. */
|
|
286
|
-
function memberQualsAllow(f: SymbolStructField, containerConst: boolean | undefined, isStore: boolean): boolean {
|
|
287
|
-
if (f.volatile) {
|
|
288
|
-
return false;
|
|
289
|
-
}
|
|
290
|
-
return !(isStore && (f.const || containerConst));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** The map member a `field` node NAMES, or null when it names none — THE one resolver for "what
|
|
294
|
-
* does the declaration say about this member", for rules that must reason about a member's type
|
|
295
|
-
* after the access rules have already spelled it. Both named spellings resolve, through the same
|
|
296
|
-
* shared gate their spelling passed: `gSym.member` off a struct global's {@link declaredFields},
|
|
297
|
-
* `gPtr->member` off the pointee's ({@link pointeeFields}). A synthesized `field_K` — the
|
|
298
|
-
* recovered-struct spelling, which no map declares — resolves to null, and so does any base that
|
|
299
|
-
* is not a map-shaped global, which is what makes every caller refuse rather than guess.
|
|
300
|
-
*
|
|
301
|
-
* The pointee arm reaches only what the MAP lets it: `pointeeAccess` gates every `gPtr->member`
|
|
302
|
-
* spelling on `spellsAccessType(f.signed, …)`, so a pointer field declaring no signedness — which
|
|
303
|
-
* is every one in the corpus's vendored maps — is never named, and a pointer read one indirection
|
|
304
|
-
* down spells `((s32 *)gQ)[1]`. That is a fact about those maps, not about this code: `SymbolMap`
|
|
305
|
-
* is a caller-supplied input, and one field flips it (`signed: true` on a 4-byte pointer member
|
|
306
|
-
* of a pointee yields `(u8 *)gQ->pInner`, cast and all, pinned in pointer-members.test.ts). So
|
|
307
|
-
* the arm is live and tested, and the byte-arithmetic rule that reads this answer is correct for
|
|
308
|
-
* it — where resolving a pointee member to null would reopen the double-scaling hole silently. */
|
|
309
|
-
function declaredMemberOf(x: Expr, sym: SymRenderCtx | undefined): DeclaredField | null {
|
|
310
|
-
if (x.k !== 'field' || x.base.k !== 'var' || sym === undefined) {
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
return sym.fieldsOf(x.base.name)?.find((f) => f.name === x.name) ?? null;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/** The map member a `field` node names when the declaration makes it a POINTER — {@link
|
|
317
|
-
* isPtrField} being the shared two-fact test, so this and the synthesized declaration cannot
|
|
318
|
-
* disagree about what a member is. */
|
|
319
|
-
function ptrMemberDecl(x: Expr, sym: SymRenderCtx | undefined): DeclaredField | null {
|
|
320
|
-
const f = declaredMemberOf(x, sym);
|
|
321
|
-
return f !== null && isPtrField(f) ? f : null;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
312
|
/** A map-declared POINTER MEMBER as the additive lowering renders its VALUE: the bare `gSym.pBuf`,
|
|
325
313
|
* or that member wearing the byte-pointer / u32 cast the arithmetic guard adds. Both denote the
|
|
326
314
|
* same address and add BYTES to it, so both fold here; a cast to any OTHER pointer type is not
|
|
@@ -465,6 +453,23 @@ function ptrMemberElement(
|
|
|
465
453
|
// 4096 — including offsets past Thumb's immediate range, and under multi-member, across-a-call
|
|
466
454
|
// and in-a-loop shapes. It is one load whose member offset becomes the same immediate the cast
|
|
467
455
|
// form used, and unlike the indexed form it composes.
|
|
456
|
+
//
|
|
457
|
+
// ONE SHAPE IS NOT IDENTICAL: on agbcc, a STORE to an offset past Thumb's
|
|
458
|
+
// immediate range. expand_assignment (gcc/expr.c) expands only the base of a COMPONENT_REF target
|
|
459
|
+
// (:2535) and evaluates the right-hand side first in store_field (:2624), while it expands a cast
|
|
460
|
+
// (INDIRECT_REF) target whole (:2689) before the right-hand side (:2747). So the member form loads
|
|
461
|
+
// a loaded value before it materialises `base + K` and the cast form after it, and with a constant
|
|
462
|
+
// or a parameter on the right the two still allocate registers differently. Neither spelling is
|
|
463
|
+
// the source's by default: a hoisted `u8 *d = &p->f38; *d = v;` builds the cast's object, and the
|
|
464
|
+
// order in which the address and the value are computed is the evidence between them. The member
|
|
465
|
+
// is emitted here; raise/structs.ts `foldDisplacedAccesses` folds such a store into its struct by
|
|
466
|
+
// default and the `/far-cast` lift keeps it on its address, so the differ referees the two
|
|
467
|
+
// (`synthetic:sfar_b` pins the member). A LOAD's member and cast forms compile alike, but agbcc
|
|
468
|
+
// gives each member access its own `base + K`, so one materialised base read at two offsets is a
|
|
469
|
+
// held pointer, and the fold leaves it on its address (`held-pointer-offsets`, `synthetic:sfar_held`
|
|
470
|
+
// pins it); so does ido past its `addiu` reach (target.ts `farMemberBasePerAccess`). On ido, KMC,
|
|
471
|
+
// GCC 2.7.2 and mwcc one access's two forms compile alike past a 16-bit displacement, a loaded
|
|
472
|
+
// value included, and `/far-cast` is offered there for the neighbours a folded store respells.
|
|
468
473
|
|
|
469
474
|
/** The pointee a global's value may be spelled through: the members the declaration synthesis
|
|
470
475
|
* DECLARES. Null when nothing may be named through it — THE shared gate (symbols.ts
|
|
@@ -700,6 +705,39 @@ function displacementIndex(off: number, width: number): number {
|
|
|
700
705
|
return off / width;
|
|
701
706
|
}
|
|
702
707
|
|
|
708
|
+
/** A stamped narrow store (raise/storesign.ts) spelled through a MEMBER — a struct field, a union
|
|
709
|
+
* member's view, a struct-array element's field — is converted by the compiler to the member's
|
|
710
|
+
* DECLARED type, so a declaration whose sign is not the stamp's loses it: `a0->field_6 = -104`
|
|
711
|
+
* under `u16 field_6` recompiles to `li 0xff98`. Such a store is spelled instead as the flat cell
|
|
712
|
+
* at the same address, `((s16 *)a0)[3] = -104`, or `((s16 *)(a0 + a1))[1] = -104` for an
|
|
713
|
+
* element. The member's type is read off the spelled lvalue (`ctype`), so a member declared by a
|
|
714
|
+
* load, a stamp or a union's view is judged alike.
|
|
715
|
+
*
|
|
716
|
+
* `storeWidth` is `undefined` for an `astore`, which does not carry it: raise/struct-arrays.ts
|
|
717
|
+
* declares each field at the one width its accesses share, so the member's width is the store's.
|
|
718
|
+
*
|
|
719
|
+
* `lval` comes back unchanged for an unstamped store, an lvalue that is not a member, no
|
|
720
|
+
* `flatBase`, a member that is not a narrow integer of the store's width or agrees with the stamp,
|
|
721
|
+
* and an offset that is not a whole number of cells (no flat subscript spells it). */
|
|
722
|
+
function stampedStoreLval(
|
|
723
|
+
lval: Expr,
|
|
724
|
+
stamp: boolean | undefined,
|
|
725
|
+
storeWidth: number | undefined,
|
|
726
|
+
flatBase: Expr | null,
|
|
727
|
+
off: number,
|
|
728
|
+
ctype: (e: Expr) => IrType | undefined,
|
|
729
|
+
): Expr {
|
|
730
|
+
if (stamp === undefined || lval.k !== 'field' || flatBase === null) {
|
|
731
|
+
return lval;
|
|
732
|
+
}
|
|
733
|
+
const t = ctype(lval);
|
|
734
|
+
const width = storeWidth ?? (t?.kind === 'int' ? t.width / 8 : 0);
|
|
735
|
+
if (t?.kind !== 'int' || (width !== 1 && width !== 2) || t.width !== width * 8 || t.signed === stamp || off % width) {
|
|
736
|
+
return lval;
|
|
737
|
+
}
|
|
738
|
+
return { k: 'index', base: flatBase, idx: { k: 'const', value: off / width }, width, signed: stamp };
|
|
739
|
+
}
|
|
740
|
+
|
|
703
741
|
function memAccess(
|
|
704
742
|
base: Value,
|
|
705
743
|
baseExpr: Expr,
|
|
@@ -711,6 +749,7 @@ function memAccess(
|
|
|
711
749
|
sym?: SymRenderCtx,
|
|
712
750
|
isStore = false,
|
|
713
751
|
advancedBy?: number,
|
|
752
|
+
castQualified = false,
|
|
714
753
|
): Expr {
|
|
715
754
|
// A deref of a global's address collapses to the bare global: `*(&gSym)` at off 0 is `gSym`;
|
|
716
755
|
// at off N the global is an array — `gSym[N/width]` (a C global name decays to a pointer, so
|
|
@@ -762,18 +801,18 @@ function memAccess(
|
|
|
762
801
|
if (sym) {
|
|
763
802
|
const gb = globalConstByte(baseExpr, off);
|
|
764
803
|
const si = gb ? sym.info(gb.name) : undefined;
|
|
765
|
-
|
|
804
|
+
// NOT FOR AN ACCESS THE LIFT QUALIFIED BY A CAST (`castQualified`, ir/discipline.ts `qualifiedBy`):
|
|
805
|
+
// a member's name reaches the object through its declaration, which the map states plain — had
|
|
806
|
+
// it qualified the object the access would be placed `declared` — so `gState.ctl;` would compile
|
|
807
|
+
// to no read at all. The access falls to the cast forms below, where `pinnedAccess` puts the
|
|
808
|
+
// qualifier: `((volatile s32 *)&gState)[2];`.
|
|
809
|
+
if (gb && si?.shape === 'struct' && !castQualified) {
|
|
766
810
|
// THE shared spellability predicate (symbols.ts), the same call declare.ts gates its struct
|
|
767
811
|
// declaration on: a layout it declines whole is a layout with no nameable members, and a
|
|
768
812
|
// union alias it drops for the first view at that offset is a name no declaration carries.
|
|
769
|
-
//
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
// bitfield spelling has its own recognizer, on the extract shape: see lowerDef).
|
|
773
|
-
const fld = declaredFields(si.layout)?.find(
|
|
774
|
-
(f) => f.offset === gb.byte && f.size === width && !isArrayField(f) && !isBitfieldField(f),
|
|
775
|
-
);
|
|
776
|
-
if (fld && memberQualsAllow(fld, si.const, isStore)) {
|
|
813
|
+
// The named bitfield spelling has its own recognizer, on the extract shape: see lowerDef.
|
|
814
|
+
const fld = structMemberAt(si, gb.byte, width, isStore);
|
|
815
|
+
if (fld) {
|
|
777
816
|
return { k: 'field', base: { k: 'var', name: gb.name }, name: fld.name, dot: true };
|
|
778
817
|
}
|
|
779
818
|
}
|
|
@@ -838,7 +877,10 @@ function memAccess(
|
|
|
838
877
|
return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed, ...addressEvidence };
|
|
839
878
|
}
|
|
840
879
|
const bt = base.type;
|
|
841
|
-
|
|
880
|
+
// A field declared at its cell's widest view (raise/structs.ts `castViewCell`) is read at any
|
|
881
|
+
// other view as the flat cell at the same address, `((u8 *)p)[1]`: the cast the union it stands
|
|
882
|
+
// for would have spelled as a member.
|
|
883
|
+
if (bt.kind === 'ptr' && bt.to.kind === 'struct' && !isCastView(bt.to, off, width, signed, isStore)) {
|
|
842
884
|
const rt = ctype(baseExpr);
|
|
843
885
|
// `->` requires the base to render as a pointer to THIS struct (field names resolve against
|
|
844
886
|
// its declaration) AND as a non-`index` node (the printer spells an index-node base with `.`,
|
|
@@ -1020,32 +1062,25 @@ const CMP_TO_BIN: Record<string, BinOp> = {
|
|
|
1020
1062
|
icmp_eq: '==',
|
|
1021
1063
|
icmp_ne: '!=',
|
|
1022
1064
|
};
|
|
1023
|
-
export const ARITH_TO_BIN: Record<string, BinOp> = {
|
|
1024
|
-
add: '+',
|
|
1025
|
-
sub: '-',
|
|
1026
|
-
mul: '*',
|
|
1027
|
-
sdiv: '/',
|
|
1028
|
-
// the UNSIGNED quotient/remainder — the C backend spells them `/`/`%` over an operand it casts
|
|
1029
|
-
// unsigned (l3/ast.ts BinOp, backend/cfamily.ts C_SPELLING)
|
|
1030
|
-
udiv: '/u',
|
|
1031
|
-
smod: '%',
|
|
1032
|
-
umod: '%u',
|
|
1033
|
-
or: '|',
|
|
1034
|
-
and: '&',
|
|
1035
|
-
xor: '^',
|
|
1036
|
-
shl: '<<',
|
|
1037
|
-
shr_u: '>>>', // the LOGICAL right shift; the C backend spells it `>>` over an unsigned operand
|
|
1038
|
-
shr_s: '>>',
|
|
1039
|
-
logic_and: '&&',
|
|
1040
|
-
logic_or: '||', // short-circuit connectives (raise/shortcircuit.ts)
|
|
1041
|
-
};
|
|
1042
1065
|
|
|
1043
1066
|
// The float ops (ir/opcodes.ts) and their L3 operators (l3/ast.ts BinOp). A table of their own and
|
|
1044
|
-
// not entries in ARITH_TO_BIN, because every rule that table's consumers apply —
|
|
1045
|
-
// stride, the integer legalizations, the constant fold, the commutative re-spelling —
|
|
1046
|
-
// integer rule.
|
|
1067
|
+
// not entries in ARITH_TO_BIN (arith-ops.ts), because every rule that table's consumers apply —
|
|
1068
|
+
// the pointer stride, the integer legalizations, the constant fold, the commutative re-spelling —
|
|
1069
|
+
// is an integer rule.
|
|
1047
1070
|
const FLOAT_TO_BIN: Record<string, BinOp> = { fadd: 'f+', fsub: 'f-', fmul: 'f*', fdiv: 'f/' };
|
|
1048
1071
|
|
|
1072
|
+
// The float compares (ir/opcodes.ts) and their L3 operators. An ORDERED compare and `fcmp_une` are
|
|
1073
|
+
// C's relation; an unordered one is the logical NOT of the ordered relation it negates
|
|
1074
|
+
// (`NEGATED_FCMP`), and is spelled as that `!`, never as the integer negation of the relation.
|
|
1075
|
+
const FCMP_TO_BIN: Record<string, BinOp> = {
|
|
1076
|
+
fcmp_olt: 'f<',
|
|
1077
|
+
fcmp_ole: 'f<=',
|
|
1078
|
+
fcmp_ogt: 'f>',
|
|
1079
|
+
fcmp_oge: 'f>=',
|
|
1080
|
+
fcmp_oeq: 'f==',
|
|
1081
|
+
fcmp_une: 'f!=',
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1049
1084
|
// The operators whose operand order the machine does not fix — candidates for the def-order
|
|
1050
1085
|
// re-spelling in lowerDef. `&&`/`||` are excluded: short-circuit order IS semantics.
|
|
1051
1086
|
const COMMUTATIVE_BIN: ReadonlySet<BinOp> = new Set(['+', '*', '&', '|', '^']);
|
|
@@ -1098,24 +1133,39 @@ interface LoopInfo {
|
|
|
1098
1133
|
|
|
1099
1134
|
// One early-`return` exit out of a loop body, and the blocks of it the loop emits inside its body
|
|
1100
1135
|
// (`earlyReturnArm` decides which). Per-EDGE rather than flattened into per-loop sets: ownership is
|
|
1101
|
-
// decided against `from`, so a second edge into the same target is a separate question.
|
|
1102
|
-
//
|
|
1136
|
+
// decided against `from`, so a second edge into the same target is a separate question. Two arms'
|
|
1137
|
+
// `owned` sets may OVERLAP: an effectful tail only the loop's exit arms run (`armsOnly`, the
|
|
1138
|
+
// cross-jumped `h(i); return;` two arms end in) is owned by every arm that reaches it, and written
|
|
1139
|
+
// once in each. `owned` has one reader, `emitDoWhile`, which unions the arms' sets for its
|
|
1140
|
+
// pre-update hazard check — an overlap only repeats a block in that union — and a `while` hoists no
|
|
1141
|
+
// update, so it has no pre-update reads to exempt.
|
|
1103
1142
|
interface LoopArm {
|
|
1104
1143
|
from: Block;
|
|
1105
1144
|
to: Block;
|
|
1106
1145
|
owned: Set<Block>;
|
|
1107
1146
|
}
|
|
1108
1147
|
|
|
1148
|
+
// A `break` whose arm RUNS CODE first: `if (t[i] == 7) { gHit = 1; break; }`. The arm's blocks never
|
|
1149
|
+
// reach the latch, so they sit outside the natural loop as a second exit target; `codeBreakArm`
|
|
1150
|
+
// says when they are one exit with code on the way.
|
|
1151
|
+
interface BreakArm {
|
|
1152
|
+
from: Block;
|
|
1153
|
+
to: Block;
|
|
1154
|
+
region: Set<Block>;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1109
1157
|
// A loop with no test of its own, `while (1)`: its latches are conditional continues, and it leaves
|
|
1110
|
-
// by a `break` to `exit` or an early `return`.
|
|
1158
|
+
// by a `break` to `exit` or an early `return`. `exit` is null for a loop no edge leaves at all — the
|
|
1159
|
+
// main loop that never returns — which has no break, no arm, and nothing after it.
|
|
1111
1160
|
interface ForeverLoopInfo {
|
|
1112
1161
|
header: Block;
|
|
1113
1162
|
latches: Block[];
|
|
1114
1163
|
forwardPreds: Block[];
|
|
1115
|
-
exit: Block;
|
|
1164
|
+
exit: Block | null;
|
|
1116
1165
|
body: Set<Block>;
|
|
1117
1166
|
arms: LoopArm[];
|
|
1118
|
-
breaks: Set<Block>; // the body blocks whose edge to `exit` is a `break`
|
|
1167
|
+
breaks: Set<Block>; // the body blocks whose edge to `exit` is a `break`, or whose edge enters a `breakArms` arm
|
|
1168
|
+
breakArms: BreakArm[];
|
|
1119
1169
|
}
|
|
1120
1170
|
|
|
1121
1171
|
// A test-at-top multi-block `while`. The header is a pure test whose cond_br enters `bodyEntry`
|
|
@@ -1129,7 +1179,8 @@ interface WhileLoopInfo {
|
|
|
1129
1179
|
forwardPreds: Block[]; // header preds outside the loop body (the entry/init side)
|
|
1130
1180
|
body: Set<Block>; // the pure natural-loop body (for in-body vs exit classification)
|
|
1131
1181
|
arms: LoopArm[]; // the early-`return` exits out of the body (`earlyReturnArm`)
|
|
1132
|
-
breaks: Set<Block>; // the body blocks whose edge to `exit` itself is a `break`
|
|
1182
|
+
breaks: Set<Block>; // the body blocks whose edge to `exit` itself is a `break`, or enters a `breakArms` arm
|
|
1183
|
+
breakArms: BreakArm[];
|
|
1133
1184
|
}
|
|
1134
1185
|
|
|
1135
1186
|
// Opcodes whose NUMBER OF EXECUTIONS is observable. Moving one of these out of a loop changes what
|
|
@@ -1481,14 +1532,16 @@ export const ENCLOSING_CARRIER_GATES: readonly Gate<EnclosingCarrier>[] = [
|
|
|
1481
1532
|
// orderArgCopiesByWriteOrder — order edge copies by the order the pred WROTE their
|
|
1482
1533
|
// destinations.
|
|
1483
1534
|
// The last three are `compilerBehaviors` (target.ts) — this pass stays target-AGNOSTIC: it reads
|
|
1484
|
-
// booleans, never a compiler name.
|
|
1535
|
+
// booleans, never a compiler name. So are the arithmetic table's (`ArithCompilerFacts`), which this
|
|
1536
|
+
// type extends and hands to `makePointerSpelling` whole; the byte-pointer temp step below reads
|
|
1537
|
+
// `keepsPointerSumAddend` from it too.
|
|
1485
1538
|
//
|
|
1486
1539
|
// WHAT A FIELD DOC BELOW HOLDS, narrowly: what the option MEANS to `structure()`, and the suffix of
|
|
1487
1540
|
// the variation that enumerates it. A VARIATION's rationale, and any figure pricing its marginal value, live
|
|
1488
1541
|
// ONCE at its `STRUCTURE_VARIATIONS` entry in rank-variations.ts — restated here the two copies rot
|
|
1489
1542
|
// separately, and only that one is the entry the enumeration reads. Figures pricing a
|
|
1490
1543
|
// DEFAULT this pass owns (the edge-copy ordering, `spellDeclaredSubscripts`) do belong here.
|
|
1491
|
-
export interface StructureOptions {
|
|
1544
|
+
export interface StructureOptions extends ArithCompilerFacts {
|
|
1492
1545
|
returnsVoid?: boolean;
|
|
1493
1546
|
coalesceLoopInit?: boolean;
|
|
1494
1547
|
preserveDivergentBranchSense?: boolean;
|
|
@@ -1593,6 +1646,13 @@ export interface StructureOptions {
|
|
|
1593
1646
|
// that a fall-through switch has a SECOND, behaviourally identical recovery. Regime A declines
|
|
1594
1647
|
// to if-recovery when it is false; Regime B, having no fallback, fails loud. Default true.
|
|
1595
1648
|
spellSwitchFallthrough?: boolean;
|
|
1649
|
+
// Does the TARGET LANGUAGE spell a `break` that leaves a `switch` arm before its end? Set from
|
|
1650
|
+
// the caller's LanguageBackend (`spellsSwitchBreak`), for the same reason as the line above. When
|
|
1651
|
+
// false, a CLOSED arm gets no region (`structureArm`), so `switchBreakAt` never mints the
|
|
1652
|
+
// statement and the arm structures to the CFG's joins, copying the tail its early exit skips into
|
|
1653
|
+
// each path. A FALLING arm needs no gate of its own: a language without a mid-arm `break` has no
|
|
1654
|
+
// fall-through either (Pascal), and `spellSwitchFallthrough` keeps those out. Default true.
|
|
1655
|
+
spellSwitchBreak?: boolean;
|
|
1596
1656
|
// Which way this compiler hands out frame slots against DECLARATION RANK: `ascending` = the
|
|
1597
1657
|
// earlier-declared spilled local takes the LOWER `[sp,#k]`. A compiler behavior declared
|
|
1598
1658
|
// in TargetDescription.compilerBehaviors, carried to the backend on `SFn.slotOrder` and applied
|
|
@@ -1613,6 +1673,12 @@ export interface StructureOptions {
|
|
|
1613
1673
|
declaredReturns?: Readonly<Record<string, string>>;
|
|
1614
1674
|
// The emitted C is compiled as C++ (`SFn.dialect`).
|
|
1615
1675
|
dialect?: 'c++';
|
|
1676
|
+
// The compiler parses a designated initializer element (`SFn.designators`; the compiler behavior
|
|
1677
|
+
// of the same name says which do).
|
|
1678
|
+
designatedInitializers?: boolean;
|
|
1679
|
+
// The compiler's C refuses C's implicit pointer conversions (`SFn.strictConversions`; the compiler
|
|
1680
|
+
// behavior `strictPointerConversions` says which does).
|
|
1681
|
+
strictPointerConversions?: boolean;
|
|
1616
1682
|
// Commutative load pairs re-spell in def (evaluation) order — see the swap in lowerDef. Default
|
|
1617
1683
|
// true; verified byte-exact on agbcc and IDO. A compiler behavior declared in
|
|
1618
1684
|
// TargetDescription.compilerBehaviors: the first compiler whose scheduler is shown re-ordering
|
|
@@ -1650,14 +1716,6 @@ export interface StructureOptions {
|
|
|
1650
1716
|
// data. The provider already refuses to EMIT bitfield facts for a big-endian ELF; this is the
|
|
1651
1717
|
// same boundary enforced on core's side, against a hand-built map that never went through it.
|
|
1652
1718
|
littleEndian?: boolean;
|
|
1653
|
-
// HARDWARE fact from TargetDescription.capabilities.deviceRegisters, threaded by
|
|
1654
|
-
// `structureOptionsFor` like `littleEndian` above: the half-open byte window whose cells are
|
|
1655
|
-
// hardware registers rather than objects a source declares. The structurer asks it the same
|
|
1656
|
-
// question its four other readers ask — "would a source have spelled this address `volatile`" —
|
|
1657
|
-
// so it may be approximate: it decides a SPELLING, not a memory model (that is
|
|
1658
|
-
// `deviceMemoryWriters`, which no structurer rule reads). Used as a REFUSAL: absent, a dead read
|
|
1659
|
-
// at a literal address is dropped.
|
|
1660
|
-
deviceRegisters?: readonly [number, number];
|
|
1661
1719
|
// Does the compiler extend a `volatile` narrow signed read in a register, never in the load? The
|
|
1662
1720
|
// `declared` stamp structuring re-derives reads it (TargetDescription.compilerBehaviors'
|
|
1663
1721
|
// `volatileReadsExtendInRegister`), so it leaves unplaced the read the lift's stamp did.
|
|
@@ -1756,10 +1814,13 @@ export interface StructureOptions {
|
|
|
1756
1814
|
readsStayWhereWritten?: boolean;
|
|
1757
1815
|
// Spell unsigned compares unsigned: cast an icmp_u* operand where the rendered operands do not
|
|
1758
1816
|
// guarantee it, and reconcile a mixed-claimant declaration to u32 when nothing under the name
|
|
1759
|
-
// needs signed.
|
|
1760
|
-
//
|
|
1761
|
-
// the
|
|
1762
|
-
//
|
|
1817
|
+
// needs signed. ON by default, because it is the only spelling that is value-faithful without a
|
|
1818
|
+
// proof: an unsigned compare spelled over two signed-rendering operands compiles to a SIGNED
|
|
1819
|
+
// compare, which differs from the machine's wherever an operand's top bit is set. The OFF spelling
|
|
1820
|
+
// is still a real source: a signed spelling that byte-matched was PROVED non-negative by the
|
|
1821
|
+
// compiler (it emits the unsigned branch from signed compares only then), and emission's provable
|
|
1822
|
+
// set is smaller than the compiler's. So rank.ts enumerates it as the `/signed-cmp` variation and
|
|
1823
|
+
// publishes it only on a byte-exact score (`Candidate.matchOnly`).
|
|
1763
1824
|
unsignedCompareSpelling?: boolean;
|
|
1764
1825
|
// Merge two variables that a merge copy would join, when the values under them never interfere
|
|
1765
1826
|
// (structure/namecoalesce.ts). Off by default; rank.ts enumerates the ON spelling as the
|
|
@@ -1817,6 +1878,11 @@ export interface StructureOptions {
|
|
|
1817
1878
|
* they would read the map's `shape:'pointer'`. The operand-order rules do not read it: no element
|
|
1818
1879
|
* or field spelling folds from a name with no pointee. */
|
|
1819
1880
|
pointerGlobals?: ReadonlySet<string>;
|
|
1881
|
+
/** What the declaration context states about its globals (proto-context.ts `globalsFromContext`):
|
|
1882
|
+
* which are structs, unions or arrays, and which are volatile, read by the stamp this structuring
|
|
1883
|
+
* re-derives. Read with the map setting and without it: a context declaration is what every
|
|
1884
|
+
* candidate compiles against, whichever names it spells. */
|
|
1885
|
+
contextGlobals?: ContextGlobals;
|
|
1820
1886
|
}
|
|
1821
1887
|
|
|
1822
1888
|
/** The named global an `index` node's base denotes DIRECTLY, or undefined: the bare `&gSym` an
|
|
@@ -1880,9 +1946,11 @@ export interface StructureHooks {
|
|
|
1880
1946
|
* has decided both arms are real, so it cannot be computed ahead of the pass. */
|
|
1881
1947
|
onBranchSenseSite?: (site: { block: number; ordinal: number; joined: boolean; negated: boolean }) => void;
|
|
1882
1948
|
/** Every branch `followOverReturns` gave a follow: the branch's block and the follow's. Both
|
|
1883
|
-
* callers fire it — a divergent `if` under `followEarlyReturns`,
|
|
1884
|
-
*
|
|
1885
|
-
*
|
|
1949
|
+
* callers fire it — a divergent `if` under `followEarlyReturns`, EVERY jump table whose arms all
|
|
1950
|
+
* return, unconditionally, and a jump table whose default-anchored RETRY is adopted (some arms
|
|
1951
|
+
* return, the rest reach the code after it) — so the sites it reports are not the enumeration
|
|
1952
|
+
* domain of the shared-tail twins (that domain is `hasDivergentSharedRet`, which asks for a
|
|
1953
|
+
* `cond_br`). */
|
|
1886
1954
|
onEarlyReturnFollow?: (site: { block: number; follow: number }) => void;
|
|
1887
1955
|
}
|
|
1888
1956
|
|
|
@@ -1897,26 +1965,181 @@ export interface StructureHooks {
|
|
|
1897
1965
|
* It rests on `structure()` not mutating `fn`, which `structure-purity.test.ts` pins.
|
|
1898
1966
|
*
|
|
1899
1967
|
* SCOPE: refusals thrown by `structure()` itself. A decline can also come from `structureChecked`'s
|
|
1900
|
-
* boundary contracts, which run OUTSIDE it — `rank.ts` closes that half, where the contracts are.
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1968
|
+
* boundary contracts, which run OUTSIDE it — `rank.ts` closes that half, where the contracts are.
|
|
1969
|
+
*
|
|
1970
|
+
* With a `memo`, a reset already structured for this fn is answered from the memo. */
|
|
1971
|
+
function assertDefaultAccepts(
|
|
1972
|
+
fn: Fn,
|
|
1973
|
+
opts: StructureOptions,
|
|
1974
|
+
hooks: StructureHooks,
|
|
1975
|
+
memo: DefaultAcceptsMemo | undefined,
|
|
1976
|
+
): void {
|
|
1977
|
+
const reset: StructureOptions = {
|
|
1978
|
+
...opts,
|
|
1979
|
+
coalesceMergeNames: false,
|
|
1980
|
+
freshParamMerge: false,
|
|
1981
|
+
materializeJoinFeeds: false,
|
|
1982
|
+
homeSharedAddresses: false,
|
|
1983
|
+
homeLoopExprs: false,
|
|
1984
|
+
homeDerivedReads: false,
|
|
1985
|
+
homeMergeFeeds: false,
|
|
1986
|
+
homeEscapingExtensions: false,
|
|
1987
|
+
anchorConstCopies: false,
|
|
1988
|
+
anchorLoopEntryConsts: false,
|
|
1989
|
+
followEarlyReturns: false,
|
|
1990
|
+
};
|
|
1991
|
+
if (memo === undefined) {
|
|
1992
|
+
structure(fn, reset, hooks);
|
|
1993
|
+
} else {
|
|
1994
|
+
memo.check(fn, hooks, reset, () => structure(fn, reset, hooks));
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
/** A {@link DefaultAcceptsMemo} asked a question it cannot answer. */
|
|
1999
|
+
export class DefaultAcceptsMemoMisuse extends Error {
|
|
2000
|
+
constructor(message: string) {
|
|
2001
|
+
super(`DefaultAcceptsMemo: ${message}`);
|
|
2002
|
+
this.name = 'DefaultAcceptsMemoMisuse';
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/** Every block, block parameter, op, opcode, operand, result, successor and edge argument of `fn`,
|
|
2007
|
+
* in order and by identity, each list after its length. A pass that rewrites the graph in place
|
|
2008
|
+
* changes it; attributes and side tables are not in it. */
|
|
2009
|
+
function graphShape(fn: Fn): unknown[] {
|
|
2010
|
+
const shape: unknown[] = [fn.blocks.length];
|
|
2011
|
+
for (const b of fn.blocks) {
|
|
2012
|
+
shape.push(b, b.params.length, ...b.params, b.ops.length);
|
|
2013
|
+
for (const op of b.ops) {
|
|
2014
|
+
shape.push(op, op.opcode, op.operands.length, ...op.operands, op.results.length, ...op.results);
|
|
2015
|
+
shape.push(op.successors.length);
|
|
2016
|
+
for (const s of op.successors) {
|
|
2017
|
+
shape.push(s.block, s.args.length, ...s.args);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return shape;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
/** A set or array of at most this many primitives is keyed by its members. Keying costs a pass over
|
|
2025
|
+
* the members on every structuring, and the maps and sets a project hands every function (`symbols`,
|
|
2026
|
+
* `pointerGlobals`) are as large as the project, so a bigger one is keyed by identity. */
|
|
2027
|
+
const MEMO_CONTENT_KEY_LIMIT = 64;
|
|
2028
|
+
|
|
2029
|
+
/** What `assertDefaultAccepts` learned about ONE fn: for each reset it structured, whether
|
|
2030
|
+
* `structure()` accepted it or the error it threw. Settings that differ only in the flags the
|
|
2031
|
+
* guard resets share one reset, so an enumeration that structures a fn under many settings asks
|
|
2032
|
+
* the same question of it many times.
|
|
2033
|
+
*
|
|
2034
|
+
* VALID WHILE THE FN AND THE OPTIONS' CONTAINERS ARE UNCHANGED. A fn rewritten in place
|
|
2035
|
+
* (`sinkStoreTails`) needs a new memo, and a `Map`, an object, or a set or array over
|
|
2036
|
+
* `MEMO_CONTENT_KEY_LIMIT` members is keyed by identity, so one mutated between two calls would
|
|
2037
|
+
* share a key with its old contents. The `declared` stamps are not a change: `structure()`
|
|
2038
|
+
* re-derives them from the options' map before it reaches the guard, so the reset run finds them as
|
|
2039
|
+
* the first run of the same key did.
|
|
2040
|
+
*
|
|
2041
|
+
* Asked about another fn, or about its own fn once a block, op or value of it was added, removed,
|
|
2042
|
+
* replaced or reordered (`graphShape`), it throws {@link DefaultAcceptsMemoMisuse} rather than
|
|
2043
|
+
* answer. That is a wiring defect, not a decline, so `rank.ts` lets it end the enumeration.
|
|
2044
|
+
*
|
|
2045
|
+
* Only a call with no hooks is answered: a hook can change the outcome, or must observe the run.
|
|
2046
|
+
* An option holding a function or a symbol has no key, and its reset always runs. */
|
|
2047
|
+
export class DefaultAcceptsMemo {
|
|
2048
|
+
readonly #outcomes = new Map<string, { error: unknown } | 'accepted'>();
|
|
2049
|
+
readonly #ids = new WeakMap<object, number>();
|
|
2050
|
+
readonly #shape: readonly unknown[];
|
|
2051
|
+
#nextId = 0;
|
|
2052
|
+
|
|
2053
|
+
constructor(readonly fn: Fn) {
|
|
2054
|
+
this.#shape = graphShape(fn);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
/** Returns or throws what `run` did for this reset, running it only when the memo has no
|
|
2058
|
+
* outcome for it yet. */
|
|
2059
|
+
check(fn: Fn, hooks: StructureHooks, reset: StructureOptions, run: () => void): void {
|
|
2060
|
+
if (fn !== this.fn) {
|
|
2061
|
+
throw new DefaultAcceptsMemoMisuse('asked about a fn it was not made for');
|
|
2062
|
+
}
|
|
2063
|
+
const shape = graphShape(fn);
|
|
2064
|
+
if (shape.length !== this.#shape.length || shape.some((x, i) => !Object.is(x, this.#shape[i]))) {
|
|
2065
|
+
throw new DefaultAcceptsMemoMisuse(`asked about ${fn.name} after its graph was rewritten`);
|
|
2066
|
+
}
|
|
2067
|
+
const key = Object.values(hooks).every((h) => h === undefined) ? this.#keyOf(reset) : undefined;
|
|
2068
|
+
if (key === undefined) {
|
|
2069
|
+
run();
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
const known = this.#outcomes.get(key);
|
|
2073
|
+
if (known === 'accepted') {
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
if (known !== undefined) {
|
|
2077
|
+
throw known.error;
|
|
2078
|
+
}
|
|
2079
|
+
try {
|
|
2080
|
+
run();
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
this.#outcomes.set(key, { error });
|
|
2083
|
+
throw error;
|
|
2084
|
+
}
|
|
2085
|
+
this.#outcomes.set(key, 'accepted');
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
/** Every option that is set, by name and value; `undefined` when one has no key. */
|
|
2089
|
+
#keyOf(opts: StructureOptions): string | undefined {
|
|
2090
|
+
const parts: string[] = [];
|
|
2091
|
+
for (const name of Object.keys(opts).sort()) {
|
|
2092
|
+
const value: unknown = (opts as Record<string, unknown>)[name];
|
|
2093
|
+
if (value === undefined) {
|
|
2094
|
+
continue;
|
|
2095
|
+
}
|
|
2096
|
+
const part = this.#valueKey(value);
|
|
2097
|
+
if (part === undefined) {
|
|
2098
|
+
return undefined;
|
|
2099
|
+
}
|
|
2100
|
+
parts.push(`${name}=${part}`);
|
|
2101
|
+
}
|
|
2102
|
+
return parts.join(';');
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
/** A prefix-free spelling of one value, so two option sets share a key only when every value
|
|
2106
|
+
* agrees. */
|
|
2107
|
+
#valueKey(value: unknown): string | undefined {
|
|
2108
|
+
switch (typeof value) {
|
|
2109
|
+
case 'undefined':
|
|
2110
|
+
return 'u';
|
|
2111
|
+
case 'boolean':
|
|
2112
|
+
return value ? 't' : 'f';
|
|
2113
|
+
case 'number':
|
|
2114
|
+
return Object.is(value, -0) ? 'n-0' : `n${value}`;
|
|
2115
|
+
case 'bigint':
|
|
2116
|
+
return `b${value}`;
|
|
2117
|
+
case 'string':
|
|
2118
|
+
return JSON.stringify(value);
|
|
2119
|
+
case 'function':
|
|
2120
|
+
case 'symbol':
|
|
2121
|
+
return undefined;
|
|
2122
|
+
}
|
|
2123
|
+
if (value === null) {
|
|
2124
|
+
return 'null';
|
|
2125
|
+
}
|
|
2126
|
+
const members = value instanceof Set ? [...value] : Array.isArray(value) ? value : undefined;
|
|
2127
|
+
if (
|
|
2128
|
+
members !== undefined &&
|
|
2129
|
+
members.length <= MEMO_CONTENT_KEY_LIMIT &&
|
|
2130
|
+
members.every((m) => m === null || typeof m !== 'object')
|
|
2131
|
+
) {
|
|
2132
|
+
const keys = members.map((m) => this.#valueKey(m));
|
|
2133
|
+
return keys.every((k) => k !== undefined) ? `${value instanceof Set ? 'S' : 'A'}[${keys.join(',')}]` : undefined;
|
|
2134
|
+
}
|
|
2135
|
+
const container = value as object;
|
|
2136
|
+
let id = this.#ids.get(container);
|
|
2137
|
+
if (id === undefined) {
|
|
2138
|
+
id = this.#nextId++;
|
|
2139
|
+
this.#ids.set(container, id);
|
|
2140
|
+
}
|
|
2141
|
+
return `#${id}`;
|
|
2142
|
+
}
|
|
1920
2143
|
}
|
|
1921
2144
|
|
|
1922
2145
|
/** What {@link earlyReturnArm} reads beyond its arguments: block dominance and forward
|
|
@@ -1924,20 +2147,33 @@ function assertDefaultAccepts(fn: Fn, opts: StructureOptions, hooks: StructureHo
|
|
|
1924
2147
|
interface EarlyReturnArmDeps {
|
|
1925
2148
|
dom: Map<Block, Set<Block>>;
|
|
1926
2149
|
reachFrom: (b: Block) => Set<Block>;
|
|
2150
|
+
preds: ReadonlyMap<Block, Block[]>;
|
|
1927
2151
|
}
|
|
1928
2152
|
|
|
1929
2153
|
function isRet(blk: Block): boolean {
|
|
1930
2154
|
return blk.ops[blk.ops.length - 1]?.opcode === 'ret';
|
|
1931
2155
|
}
|
|
1932
|
-
/** The `ret`s reachable from EVERY successor of `b` — the region `followOverReturns` keeps
|
|
1933
|
-
*
|
|
1934
|
-
*
|
|
2156
|
+
/** The `ret`s reachable from EVERY successor of `b` — the region `followOverReturns` keeps — or,
|
|
2157
|
+
* given an `anchor`, from every successor that shares a `ret` with the anchor. Empty when there
|
|
2158
|
+
* is none, and when `b` does not branch at all. `reachFrom` is forward reachability, the start
|
|
2159
|
+
* block excluded.
|
|
1935
2160
|
*
|
|
1936
2161
|
* THE QUESTION IS ABOUT A BRANCH, not about an `if`: "the arms all return, so post-dominance gives
|
|
1937
2162
|
* this block no join — where does the construct end?" is asked by a jump table exactly as by a
|
|
1938
2163
|
* two-way branch, and the answer is derived the same way. The `if`-shaped reading belongs to the
|
|
1939
2164
|
* CALLER that wants it (`hasDivergentSharedRet`), not here.
|
|
1940
2165
|
*
|
|
2166
|
+
* THE ANCHOR names a successor taken to reach the end the question is about, and leaves out
|
|
2167
|
+
* every successor that shares no `ret` with it (`leftOutOf`). The `switch_br` caller anchors on
|
|
2168
|
+
* the dispatch's DEFAULT edge, whose target is the `default:` arm or the code after the switch: a
|
|
2169
|
+
* `case` that `return`s on its own, or falls into one that does, then no longer empties the
|
|
2170
|
+
* intersection for the arms that `break`. Leaving those successors out is sound for the follow the
|
|
2171
|
+
* caller derives from the answer: the answer is a subset of the anchor's `ret`s, so a successor
|
|
2172
|
+
* left out reaches none of them, hence no block that reaches one — in particular not the follow —
|
|
2173
|
+
* and it is structured to its own `return`. It is a candidate and not evidence that the source
|
|
2174
|
+
* ended the construct there: a `default:` that `return`s on its own is an anchor too, and the
|
|
2175
|
+
* caller states what it reads to tell the two apart. The `if` caller never anchors.
|
|
2176
|
+
*
|
|
1941
2177
|
* THE DEDUP IS DEFENSIVE AND NO CALLER CAN REACH IT TODAY. It is what makes "two successors that
|
|
1942
2178
|
* are the same block" answer `[]` rather than "a branch with a shared `ret`". Both callers ask
|
|
1943
2179
|
* only when `ipdom.get(b)` is null, and a branch whose successors are all ONE block has that block
|
|
@@ -1945,23 +2181,33 @@ function isRet(blk: Block): boolean {
|
|
|
1945
2181
|
* to this function rather than to its callers' guards, and dropping it is a no-op on the corpus:
|
|
1946
2182
|
* the reader over every row of the committed artifact (`decompile` per row, no compiling) emits
|
|
1947
2183
|
* byte-identical source with and without it. */
|
|
1948
|
-
function sharedRetsOf(b: Block, reachFrom: (x: Block) => ReadonlySet<Block
|
|
1949
|
-
const
|
|
2184
|
+
function sharedRetsOf(b: Block, reachFrom: (x: Block) => ReadonlySet<Block>, anchor: Block | null = null): Block[] {
|
|
2185
|
+
const left = anchor === null ? [] : leftOutOf(b, reachFrom, anchor);
|
|
2186
|
+
const succs = [...new Set(successorsOf(b))].filter((x) => !left.includes(x));
|
|
1950
2187
|
if (succs.length < 2) {
|
|
1951
2188
|
return [];
|
|
1952
2189
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
2190
|
+
return succs
|
|
2191
|
+
.map((x) => retsFrom(x, reachFrom))
|
|
2192
|
+
.reduce((shared, rets) => {
|
|
2193
|
+
const here = new Set(rets);
|
|
2194
|
+
return shared.filter((r) => here.has(r));
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
function retsFrom(x: Block, reachFrom: (x: Block) => ReadonlySet<Block>): Block[] {
|
|
2198
|
+
return [x, ...reachFrom(x)].filter(isRet);
|
|
2199
|
+
}
|
|
2200
|
+
/** The successors of `b` that share no `ret` with `anchor` — the ones `sharedRetsOf` leaves out. */
|
|
2201
|
+
function leftOutOf(b: Block, reachFrom: (x: Block) => ReadonlySet<Block>, anchor: Block): Block[] {
|
|
2202
|
+
const anchored = new Set(retsFrom(anchor, reachFrom));
|
|
2203
|
+
return [...new Set(successorsOf(b))].filter((x) => !retsFrom(x, reachFrom).some((r) => anchored.has(r)));
|
|
1958
2204
|
}
|
|
1959
2205
|
/** Is there an `if` whose arms reach no common block before EXIT but share a `ret` — the only shape
|
|
1960
2206
|
* `followEarlyReturns` changes? The enumeration gate of both shared-tail twins (rank.ts), asked of
|
|
1961
2207
|
* the fn as raised and again of the sunk fn; a superset, since it asks `sharedRetsOf` and none of
|
|
1962
2208
|
* the follow's three later refusals. An `if`, specifically: see the restriction at the `some`. */
|
|
1963
2209
|
export function hasDivergentSharedRet(fn: Fn): boolean {
|
|
1964
|
-
const ipdom =
|
|
2210
|
+
const ipdom = functionPostDominators(fn);
|
|
1965
2211
|
const reach = new Map<Block, Set<Block>>();
|
|
1966
2212
|
const reachFrom = (b: Block): Set<Block> => {
|
|
1967
2213
|
let out = reach.get(b);
|
|
@@ -2002,6 +2248,141 @@ function reachesAvoiding(from: Block, to: Block, avoid: Block): boolean {
|
|
|
2002
2248
|
}
|
|
2003
2249
|
return false;
|
|
2004
2250
|
}
|
|
2251
|
+
/** The arm `from -> to` breaks to `exit` after running code: the region `to` heads re-enters no
|
|
2252
|
+
* `body` block and leaves only for `exit` or by a `ret` of its own. Returns that region, or null.
|
|
2253
|
+
* The region is written inside the loop, ahead of the `break` — a `ret` among it as the `return` it
|
|
2254
|
+
* is: `if (t[i] == 7) { if (k) return; gHit = 1; break; }`.
|
|
2255
|
+
*
|
|
2256
|
+
* UNSHARED (the default, and the header-exit caller), `to` is entered by that edge alone and the
|
|
2257
|
+
* region admits no edge from outside itself, so each region block is written once and the region
|
|
2258
|
+
* owns its `ret`s: the `return` is written where the asm holds its one copy.
|
|
2259
|
+
*
|
|
2260
|
+
* A TAIL SEVERAL BREAKS SHARE, with `shared` (what `exitArmOf` passes for every in-body code arm):
|
|
2261
|
+
* `if (a) f |= 1; else if (b) f |= 2; else continue; gHit = i; break;` compiles to two arms that
|
|
2262
|
+
* each run their own code and jump to one `gHit = i` block on the way out (agbcc and gcc2.7.2
|
|
2263
|
+
* cross-jump arms written with a tail each into that same shape, jump.c). The region is then
|
|
2264
|
+
* admitted when the arm's head is entered only from the body (a body block, or a block every path
|
|
2265
|
+
* into which comes from the body without passing `exit`: the other arm's own blocks, whose exit
|
|
2266
|
+
* edge the classifier must admit as an arm of its own, or the loop declines), every other block an
|
|
2267
|
+
* edge from outside enters is one the loop's own exit path does not run (`offExitPath`), and the
|
|
2268
|
+
* region holds no cycle. Each arm writes the shared blocks again ahead of its own `break`, a shared
|
|
2269
|
+
* `ret` as one `return` per arm, and a path from outside the loop (an `else` before it) structures
|
|
2270
|
+
* its own copy where the block sits. Those paths are exclusive — a path that left the body by an arm
|
|
2271
|
+
* reaches a region block again only through `exit`, and the exit does not reach it — so every path
|
|
2272
|
+
* runs the tail once, as the asm does, whatever the tail does; where the compiler cross-jumps, the
|
|
2273
|
+
* copies are the same code (gcc2.7.2kmc byte-identical, agbcc up to register allocation).
|
|
2274
|
+
*
|
|
2275
|
+
* THE COST where it does not: ido keeps one tail, and only the source's single copy at the arms'
|
|
2276
|
+
* join recompiles to it. `synthetic:brmerge:ido7.1`'s row source is byte-identical to its target;
|
|
2277
|
+
* the per-arm spelling compiles to 52 instructions against 50 (an extra `lui` and `sb`), so that
|
|
2278
|
+
* row cannot MATCH this way. The single copy needs a `continue` that runs the latch's update — a
|
|
2279
|
+
* `for` whose `inc` is that update, or a `do`/`while` whose test the `continue` reaches — and
|
|
2280
|
+
* structure writes `continue` only to a `while (1)` header, while the L3 `for` respell refuses a
|
|
2281
|
+
* body holding one (l3/ast.ts, the `for` node). */
|
|
2282
|
+
function codeBreakArm(
|
|
2283
|
+
preds: ReadonlyMap<Block, Block[]>,
|
|
2284
|
+
dom: ReadonlyMap<Block, ReadonlySet<Block>>,
|
|
2285
|
+
from: Block,
|
|
2286
|
+
to: Block,
|
|
2287
|
+
body: ReadonlySet<Block>,
|
|
2288
|
+
exit: Block,
|
|
2289
|
+
shared = false,
|
|
2290
|
+
): Set<Block> | null {
|
|
2291
|
+
if (to === exit || body.has(to)) {
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2294
|
+
const region = new Set<Block>();
|
|
2295
|
+
let leaves = false;
|
|
2296
|
+
for (const stack = [to]; stack.length > 0;) {
|
|
2297
|
+
const x = stack.pop()!;
|
|
2298
|
+
if (x === exit) {
|
|
2299
|
+
leaves = true;
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
if (region.has(x)) {
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
const t = x.ops[x.ops.length - 1];
|
|
2306
|
+
if (body.has(x) || (t.opcode !== 'br' && t.opcode !== 'cond_br' && t.opcode !== 'ret')) {
|
|
2307
|
+
return null; // re-enters the loop, or dispatches
|
|
2308
|
+
}
|
|
2309
|
+
region.add(x);
|
|
2310
|
+
stack.push(...successorsOf(x));
|
|
2311
|
+
}
|
|
2312
|
+
// Shared: the arm's head is entered from another arm when every path into the entering block
|
|
2313
|
+
// comes from the body without passing `exit` — the body block itself, or the other arm's own
|
|
2314
|
+
// blocks. Never from outside the loop: an edge straight from the body into a block others enter
|
|
2315
|
+
// too is a `break` to it, which `while (1)` writes once, after the loop.
|
|
2316
|
+
const armOfBody = onlyFromBody(preds, body, exit);
|
|
2317
|
+
// Shared, past the head: a block may be entered from anywhere — another arm, or outside the loop —
|
|
2318
|
+
// when the loop's own exit path does not run it (`offExitPathOf`). agbcc cross-jumps a `break`
|
|
2319
|
+
// that assigns a `u8` with an `else` before the loop that assigns the same `u8` into one `lsr`
|
|
2320
|
+
// truncation block, and a `gLast = id;` both write into one `strb` (jump.c); gcc2.7.2kmc keeps one
|
|
2321
|
+
// `jal note` the arm's `beql` and the `else` both enter. The per-arm copy is the source's spelling.
|
|
2322
|
+
const offExitPath = offExitPathOf(exit, dom);
|
|
2323
|
+
let entersShared = false;
|
|
2324
|
+
const entered = (x: Block) =>
|
|
2325
|
+
(preds.get(x) ?? []).every(
|
|
2326
|
+
(p) =>
|
|
2327
|
+
region.has(p) ||
|
|
2328
|
+
(x === to && p === from) ||
|
|
2329
|
+
(shared && (x === to ? armOfBody(p) : offExitPath(x)) && (entersShared = true)),
|
|
2330
|
+
);
|
|
2331
|
+
if (!leaves || ![...region].every(entered)) {
|
|
2332
|
+
return null;
|
|
2333
|
+
}
|
|
2334
|
+
// a region another arm enters is written once per arm, so it must be one a walk writes once: no
|
|
2335
|
+
// cycle. One only its own edge enters is written once wherever it sits.
|
|
2336
|
+
return !entersShared || [...region].every((x) => acyclicThrough(x, region)) ? region : null;
|
|
2337
|
+
}
|
|
2338
|
+
/** The duplication policy's admission (written at `earlyReturnArm`): is a block one the loop's own
|
|
2339
|
+
* exit path does not run? Forward only (`reachForward`): round an enclosing loop's back-edge the exit
|
|
2340
|
+
* reaches the inner loop again, which is the next iteration running the block again in the asm too,
|
|
2341
|
+
* not the fall-through path running it after the loop. Computed on first use. */
|
|
2342
|
+
function offExitPathOf(exit: Block, dom: ReadonlyMap<Block, ReadonlySet<Block>>): (b: Block) => boolean {
|
|
2343
|
+
let fromExit: Set<Block> | undefined;
|
|
2344
|
+
return (b) => !(fromExit ??= reachForward(exit, dom)).has(b);
|
|
2345
|
+
}
|
|
2346
|
+
/** Does every path into a block come from `body` without passing `exit`? True of a body block, and
|
|
2347
|
+
* of a block outside whose every predecessor is one — a loop exit arm's own blocks, and a tail only
|
|
2348
|
+
* those arms enter. Memoised per call; a cycle back to a block is no path from the body. */
|
|
2349
|
+
function onlyFromBody(
|
|
2350
|
+
preds: ReadonlyMap<Block, Block[]>,
|
|
2351
|
+
body: ReadonlySet<Block>,
|
|
2352
|
+
exit: Block,
|
|
2353
|
+
): (b: Block) => boolean {
|
|
2354
|
+
const fromBody = new Map<Block, boolean>();
|
|
2355
|
+
const armOfBody = (p: Block): boolean => {
|
|
2356
|
+
if (body.has(p)) {
|
|
2357
|
+
return true;
|
|
2358
|
+
}
|
|
2359
|
+
const known = fromBody.get(p);
|
|
2360
|
+
if (known !== undefined) {
|
|
2361
|
+
return known;
|
|
2362
|
+
}
|
|
2363
|
+
fromBody.set(p, false);
|
|
2364
|
+
const into = preds.get(p) ?? [];
|
|
2365
|
+
const ok = p !== exit && into.length > 0 && into.every(armOfBody);
|
|
2366
|
+
fromBody.set(p, ok);
|
|
2367
|
+
return ok;
|
|
2368
|
+
};
|
|
2369
|
+
return armOfBody;
|
|
2370
|
+
}
|
|
2371
|
+
/** Does no path inside `region` lead from `x` back to `x`? */
|
|
2372
|
+
function acyclicThrough(x: Block, region: ReadonlySet<Block>): boolean {
|
|
2373
|
+
const seen = new Set<Block>();
|
|
2374
|
+
for (const stack = successorsOf(x); stack.length > 0;) {
|
|
2375
|
+
const y = stack.pop()!;
|
|
2376
|
+
if (y === x) {
|
|
2377
|
+
return false;
|
|
2378
|
+
}
|
|
2379
|
+
if (region.has(y) && !seen.has(y)) {
|
|
2380
|
+
seen.add(y);
|
|
2381
|
+
stack.push(...successorsOf(y));
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
return true;
|
|
2385
|
+
}
|
|
2005
2386
|
// An early `return` out of the loop: forward-walking from `to` WITHOUT re-entering the loop `body`,
|
|
2006
2387
|
// every path terminates in a `ret`. agbcc/gcc merge every `return` into ONE epilogue block and each
|
|
2007
2388
|
// return site just sets the return register and branches there, so a second body exit that lands on
|
|
@@ -2011,26 +2392,67 @@ function reachesAvoiding(from: Block, to: Block, avoid: Block): boolean {
|
|
|
2011
2392
|
//
|
|
2012
2393
|
// The arm is structured AT the edge, so any block in it that a second path also reaches is emitted
|
|
2013
2394
|
// twice. A duplicated `return v` is harmless — both copies sit on mutually exclusive paths, so this
|
|
2014
|
-
// is a FIDELITY rule, not a soundness one
|
|
2015
|
-
//
|
|
2016
|
-
//
|
|
2395
|
+
// is a FIDELITY rule, not a soundness one.
|
|
2396
|
+
//
|
|
2397
|
+
// ONE DUPLICATION POLICY for loop exit arms, here and in `codeBreakArm`'s shared mode. A block with
|
|
2398
|
+
// effects may be written once per arm when the loop's own exit path does not run it
|
|
2399
|
+
// (`offExitPathOf`) and it holds no cycle, whoever else enters it — another arm, or a path from
|
|
2400
|
+
// before the loop (an `else` beside it). A path that left the body by an arm reaches the block only
|
|
2401
|
+
// through that arm, because the exit does not reach it; a path from before the loop never ran an arm.
|
|
2402
|
+
// The paths are exclusive, so each runs one copy once, the arm's or the one written where the block
|
|
2403
|
+
// sits; on a compiler that cross-jumps (agbcc, gcc2.7.2: jump.c) those copies are the source the one
|
|
2404
|
+
// block came from. Whether the block has an effect does not enter into it. A block the loop's own
|
|
2405
|
+
// exit path also runs is written once, after the loop: copying it into an arm writes a store or call
|
|
2406
|
+
// twice where the asm holds it once on the fall-through path, and drags the rest of the function
|
|
2407
|
+
// along. `codeBreakArm`'s shared region stops at `exit`, so the loop's own arms bound it and it
|
|
2408
|
+
// takes no size limit. This walk applies the same policy to a tail that ends in its own `return`:
|
|
2409
|
+
// an effectful block is admitted only on one of the two counts below, and refused otherwise.
|
|
2410
|
+
//
|
|
2411
|
+
// THE EDGE OWNS IT. `from` dominates `to` and `to` dominates the block, so every path reaching it
|
|
2017
2412
|
// runs this edge's predecessor and then this region — and only one edge into `to` can satisfy the
|
|
2018
2413
|
// first half, since two predecessors cannot both dominate it. And the region must not be reachable
|
|
2019
|
-
// from the loop's own exit, which dominance does NOT rule out: an arm
|
|
2020
|
-
// post-loop join dominates itself, and claiming it would emit the epilogue
|
|
2021
|
-
// `to` covers the whole region — a block dominated by `to` that the exit
|
|
2022
|
-
//
|
|
2414
|
+
// forward from the loop's own exit (`offExitPathOf`), which dominance does NOT rule out: an arm
|
|
2415
|
+
// landing straight on the post-loop join dominates itself, and claiming it would emit the epilogue
|
|
2416
|
+
// on both paths. Testing `to` covers the whole region — a block dominated by `to` that the exit
|
|
2417
|
+
// reached would mean the exit reached `to`.
|
|
2418
|
+
//
|
|
2419
|
+
// ONLY THE LOOP'S EXIT ARMS RUN IT (`armsOnly`): the policy's count, plus every path into it comes
|
|
2420
|
+
// from the body without passing `exit` (`onlyFromBody`). Two arms ending in the same `h(i); return;`,
|
|
2421
|
+
// which agbcc and gcc2.7.2kmc cross-jump into one block no edge dominates, write it once each: the
|
|
2422
|
+
// arms run on exclusive paths, so each path still runs it once. The `while` reading that takes one
|
|
2423
|
+
// such arm for the exit stays refused — the other arm's tail is then reachable from it — so the loop
|
|
2424
|
+
// falls to the reading whose exit runs nothing of the tail. A shared epilogue the exit also reaches
|
|
2425
|
+
// still has to be pure.
|
|
2023
2426
|
//
|
|
2024
|
-
//
|
|
2025
|
-
//
|
|
2427
|
+
// WHY A RETURN TAIL ASKS MORE THAN THE POLICY. An entry from before the loop is sound to copy past —
|
|
2428
|
+
// the paths are exclusive — but into a tail that returns it is, in the common shape, the loop's own
|
|
2429
|
+
// zero-trip guard landing on the post-loop epilogue: `for (i = 0; i < n; i++) { if (t[i] == 7) {
|
|
2430
|
+
// ping(i); return; } } out(0);`. Admitting it lets the `while` reading take the early-return arm for
|
|
2431
|
+
// the exit and copy `out(0); return;` into the body, where the asm holds it once after the loop
|
|
2432
|
+
// (ido7.1 and mwcc_242_81 re-spell `while (1)` that way, equivalent but not the source's shape).
|
|
2433
|
+
// What it would buy is an `else` before the loop that cross-jumps into the arms' tail (agbcc's
|
|
2434
|
+
// `else { pong(9); hit(a); return; }`), and that lifts only through the same re-spelling.
|
|
2435
|
+
//
|
|
2436
|
+
// Returns the blocks the arm OWNS — the ones the loop emits inside its body for this arm, ahead of
|
|
2437
|
+
// the update — or null when this is not an early-`return` arm. Not exclusive to the arm: a block the
|
|
2438
|
+
// `armsOnly` count admits is owned by every arm that reaches it, so two arms' sets can share it.
|
|
2026
2439
|
function earlyReturnArm(
|
|
2027
|
-
{ dom, reachFrom }: EarlyReturnArmDeps,
|
|
2440
|
+
{ dom, reachFrom, preds }: EarlyReturnArmDeps,
|
|
2028
2441
|
from: Block,
|
|
2029
2442
|
to: Block,
|
|
2030
2443
|
body: Set<Block>,
|
|
2031
2444
|
exit: Block,
|
|
2032
2445
|
): Set<Block> | null {
|
|
2033
|
-
|
|
2446
|
+
// does the loop's own exit path run it? forward only (`offExitPathOf`)
|
|
2447
|
+
const offExitPath = offExitPathOf(exit, dom);
|
|
2448
|
+
const entryOwned = dom.get(to)!.has(from) && to !== exit && offExitPath(to);
|
|
2449
|
+
// the duplication policy's admission: the loop's own exit path does not run it, and no cycle holds
|
|
2450
|
+
// it — and, for a tail that returns, only the loop's exit arms enter it (`armsOnly`, below). Never
|
|
2451
|
+
// the arm's own head: an edge straight from the body into a block others enter too is a `break` to
|
|
2452
|
+
// it, which `while (1)` writes once, after the loop.
|
|
2453
|
+
const armOfBody = onlyFromBody(preds, body, exit);
|
|
2454
|
+
const armsOnly = (bb: Block) =>
|
|
2455
|
+
bb !== to && bb !== exit && armOfBody(bb) && offExitPath(bb) && !reachFrom(bb).has(bb);
|
|
2034
2456
|
const owned = new Set<Block>();
|
|
2035
2457
|
const seen = new Set<Block>();
|
|
2036
2458
|
const stack = [to];
|
|
@@ -2043,7 +2465,7 @@ function earlyReturnArm(
|
|
|
2043
2465
|
if (body.has(bb)) {
|
|
2044
2466
|
return null;
|
|
2045
2467
|
} // re-enters the loop → not an exit
|
|
2046
|
-
if (entryOwned && dom.get(bb)!.has(to)) {
|
|
2468
|
+
if ((entryOwned && dom.get(bb)!.has(to)) || (bb.ops.some(effectful) && armsOnly(bb))) {
|
|
2047
2469
|
owned.add(bb);
|
|
2048
2470
|
} else if (bb.ops.some(effectful)) {
|
|
2049
2471
|
return null;
|
|
@@ -2072,7 +2494,12 @@ function carrierSign(t: IrType | undefined): boolean | undefined {
|
|
|
2072
2494
|
return t?.kind === 'int' && t.width < 32 ? t.signed : undefined;
|
|
2073
2495
|
}
|
|
2074
2496
|
|
|
2075
|
-
export function structure(
|
|
2497
|
+
export function structure(
|
|
2498
|
+
fn: Fn,
|
|
2499
|
+
opts: StructureOptions = {},
|
|
2500
|
+
hooks: StructureHooks = {},
|
|
2501
|
+
memo?: DefaultAcceptsMemo,
|
|
2502
|
+
): SFn {
|
|
2076
2503
|
const {
|
|
2077
2504
|
returnsVoid = false,
|
|
2078
2505
|
coalesceLoopInit = false,
|
|
@@ -2087,10 +2514,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2087
2514
|
switchArmsFollowLayout = false,
|
|
2088
2515
|
switchRequiresFrontLoadedTests = false,
|
|
2089
2516
|
spellSwitchFallthrough = true,
|
|
2517
|
+
spellSwitchBreak = true,
|
|
2090
2518
|
spillSlotOrder,
|
|
2091
2519
|
declaredArgs,
|
|
2092
2520
|
declaredReturns,
|
|
2093
2521
|
dialect,
|
|
2522
|
+
designatedInitializers = false,
|
|
2523
|
+
strictPointerConversions = false,
|
|
2094
2524
|
defOrderLoadPairs = true,
|
|
2095
2525
|
anchorConstCopies = false,
|
|
2096
2526
|
anchorLoopEntryConsts = false,
|
|
@@ -2107,8 +2537,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2107
2537
|
homeEscapingExtensions = false,
|
|
2108
2538
|
readsStayWhereWritten = false,
|
|
2109
2539
|
contractsFloatProducts = false,
|
|
2540
|
+
keepsPointerSumAddend = false,
|
|
2110
2541
|
staticLayout,
|
|
2111
|
-
unsignedCompareSpelling =
|
|
2542
|
+
unsignedCompareSpelling = true,
|
|
2112
2543
|
coalesceMergeNames = false,
|
|
2113
2544
|
freshParamMerge = false,
|
|
2114
2545
|
followEarlyReturns = false,
|
|
@@ -2117,12 +2548,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2117
2548
|
inferredSymbols,
|
|
2118
2549
|
orderLicensedGlobals,
|
|
2119
2550
|
pointerGlobals,
|
|
2551
|
+
contextGlobals,
|
|
2120
2552
|
} = opts;
|
|
2121
2553
|
// A read this stamp is the first to place was plain to every raising pass, and in a `&&`/`||`'s
|
|
2122
2554
|
// guarded operand a short-circuit fold may have lifted it out of the arm it ran in: which side of
|
|
2123
2555
|
// the branch the asm read it on is lost. The analysis names a counted op it finds there above the
|
|
2124
2556
|
// branch, which is right only for one no fold could move, so this read declines instead.
|
|
2125
|
-
const stampedLate = stampDeclaredVolatile(fn, mapSymbols,
|
|
2557
|
+
const stampedLate = stampDeclaredVolatile(fn, mapSymbols, {
|
|
2558
|
+
volatileReadsExtendInRegister: opts.volatileReadsExtendInRegister,
|
|
2559
|
+
contextGlobals,
|
|
2560
|
+
});
|
|
2126
2561
|
if (stampedLate.length > 0) {
|
|
2127
2562
|
const defsNow = defOpMap(fn);
|
|
2128
2563
|
const guarded = shortCircuitGuardedValues(fn, defsNow);
|
|
@@ -2143,6 +2578,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2143
2578
|
// shape is its definition's, never the one raise/globalshape.ts infers from its accesses.
|
|
2144
2579
|
const localStatics = localStaticShapes(fn, staticLayout);
|
|
2145
2580
|
if ('refused' in localStatics) {
|
|
2581
|
+
if (fn.localObjects?.get(localStatics.symbol)?.anonymous) {
|
|
2582
|
+
throw new StructureError(
|
|
2583
|
+
`cannot structure '${fn.name}': reads the compiler's anonymous constant ('${localStatics.symbol}') ${localStatics.refused}`,
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2146
2586
|
throw new StructureError(
|
|
2147
2587
|
`cannot structure '${fn.name}': names a function-scope static ('${localStatics.symbol}') ${localStatics.refused}`,
|
|
2148
2588
|
);
|
|
@@ -2181,8 +2621,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2181
2621
|
// widens a load's render positions and narrows the write set that bars it, so it removes
|
|
2182
2622
|
// materializations rather than minting them. Extra materialization is what this guard is
|
|
2183
2623
|
// about (see above), and this variation adds none;
|
|
2184
|
-
// - `/
|
|
2185
|
-
// touches no name and no copy, so no edge copy changes its
|
|
2624
|
+
// - `/signed-cmp` (unsignedCompareSpelling OFF) drops the casts at compares and the u32
|
|
2625
|
+
// reconciliation of `varType`. It touches no name and no copy, so no edge copy changes its
|
|
2626
|
+
// elision under it;
|
|
2186
2627
|
// - `/copy-defpos` (preferDefPosCopyOrder) REORDERS the copies of one edge and adds or drops
|
|
2187
2628
|
// none. rank.ts states the same thing from the variation side, in the terms that matter there: a
|
|
2188
2629
|
// reordering cannot rescue a spelling whose OFF sibling failed the boundary contracts.
|
|
@@ -2198,12 +2639,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2198
2639
|
anchorConstCopies ||
|
|
2199
2640
|
followEarlyReturns
|
|
2200
2641
|
) {
|
|
2201
|
-
assertDefaultAccepts(fn, opts, hooks);
|
|
2642
|
+
assertDefaultAccepts(fn, opts, hooks, memo);
|
|
2202
2643
|
}
|
|
2203
2644
|
const defs = defOpMap(fn);
|
|
2645
|
+
// The word evaluator's view of `fn`, built at the first `neg_s` (no other rule asks it).
|
|
2646
|
+
let wordCtx: WordCtx | undefined;
|
|
2204
2647
|
const preds = predecessorBlocks(fn);
|
|
2205
|
-
const ipdom = postDominators(fn);
|
|
2206
2648
|
const dom = dominators(fn);
|
|
2649
|
+
// --- loop discovery (ir/loops.ts): natural loops via dominator back-edges + the nesting forest ---
|
|
2650
|
+
const forest = analyzeLoops(fn, dom);
|
|
2651
|
+
const ipdom = functionPostDominators(fn, forest);
|
|
2207
2652
|
|
|
2208
2653
|
/** The byte step `raise/const.ts` recorded on the literal that feeds this access's address, or
|
|
2209
2654
|
* `undefined` where the address was not reached by advancing a register. Read at the two
|
|
@@ -2213,6 +2658,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2213
2658
|
return typeof step === 'number' ? step : undefined;
|
|
2214
2659
|
};
|
|
2215
2660
|
|
|
2661
|
+
/** THE view a `store` (`width` bytes) or `astore` (`elemSize`) lvalue is spelled through, for
|
|
2662
|
+
* both branches below: `storeSigned`, the scalar the recovered pointee was seeded at, so an
|
|
2663
|
+
* inserted cast declares the same type. An UNSTAMPED narrow store of a constant both views store
|
|
2664
|
+
* alike (narrow sign bit clear) takes its base's own int pointee instead and asks for no cast,
|
|
2665
|
+
* since nothing witnessed either type. */
|
|
2666
|
+
const storeView = (op: Op, width: number): boolean => {
|
|
2667
|
+
const t = op.operands[0].type;
|
|
2668
|
+
const own = t.kind === 'ptr' && t.to.kind === 'int' && t.to.width === width * 8 ? t.to.signed : undefined;
|
|
2669
|
+
const v = defs.get(op.operands[op.opcode === 'astore' ? 2 : 1]);
|
|
2670
|
+
const k = v?.opcode === 'const' ? v.attrs.value : undefined;
|
|
2671
|
+
const neutral = typeof k === 'number' && Number.isInteger(k) && k >= 0 && k < 2 ** (width * 8 - 1) && width < 4;
|
|
2672
|
+
return op.attrs.signed === undefined && neutral && own !== undefined ? own : storeSigned(op, width);
|
|
2673
|
+
};
|
|
2674
|
+
|
|
2216
2675
|
// ── analysis phase (structure/analysis.ts): use registry, liveness, materialization ──
|
|
2217
2676
|
const { useSitesOf, opIndex, opBlock, liveIn, materialize, preUpdateHomes, reachFrom, emitPos, memWriteBetween } =
|
|
2218
2677
|
analyze(fn, returnsVoid, {
|
|
@@ -2238,7 +2697,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2238
2697
|
// rank.ts's `/shared-ret` twin, and as its `/shared-tail` twin after the store-tail sink: as a
|
|
2239
2698
|
// default it costs rows, measured there.
|
|
2240
2699
|
//
|
|
2241
|
-
// WHICH REGION: the `ret`s reachable from EVERY successor
|
|
2700
|
+
// WHICH REGION: the `ret`s reachable from EVERY successor — or, given an `anchor`, from every
|
|
2701
|
+
// successor that shares a `ret` with the anchor (`sharedRetsOf`; only the `switch_br` caller's
|
|
2702
|
+
// retry passes one, the dispatch's default edge). Every block that cannot reach one of
|
|
2242
2703
|
// them is an early-return region and is deleted; the follow is `b`'s post-dominator over what is
|
|
2243
2704
|
// left. Sound for the same reason an ordinary follow is: every kept path from `b` passes it, and a
|
|
2244
2705
|
// deleted block reaches no kept `ret` and so never the follow, so each arm structures up to the
|
|
@@ -2255,7 +2716,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2255
2716
|
// PER `if`, never function-wide: the deletion set is a function of that `if`'s own shared `ret`s
|
|
2256
2717
|
// (memoized on them), because a nested `if` shares a different set, or none.
|
|
2257
2718
|
//
|
|
2258
|
-
// REFUSES (keeping the divergent arms) when no `ret`
|
|
2719
|
+
// REFUSES (keeping the divergent arms) when that reading leaves no `ret` — none reachable from
|
|
2720
|
+
// every successor, or from every successor the anchor keeps — when the
|
|
2259
2721
|
// kept graph gives `b` no post-dominator but EXIT, and when the enclosing region's `stop` is
|
|
2260
2722
|
// reachable from `b` without passing the follow — that path must reach `stop`, and structuring
|
|
2261
2723
|
// the arms towards a different follow would emit `stop`'s region inside an arm.
|
|
@@ -2267,10 +2729,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2267
2729
|
// an in-loop branch reach is reachable from the latch too, so the intersection keeps the whole
|
|
2268
2730
|
// graph, `pd` is `ipdom` itself — and the caller only asks when `ipdom.get(b)` is already null,
|
|
2269
2731
|
// so the answer comes back null and the `?? stop` fallback stands. That is an observation about
|
|
2270
|
-
// the shape rather than a check, which is why it is written down here.
|
|
2732
|
+
// the shape rather than a check, which is why it is written down here. The anchored RETRY that
|
|
2733
|
+
// caller adds is no exception in practice: inside a loop the latch carries every arm to every
|
|
2734
|
+
// `ret`, so the arm that returns shares one with the default edge and is kept, and nothing is
|
|
2735
|
+
// left out (measured on five in-loop switches with one returning arm, compiled with agbcc and
|
|
2736
|
+
// IDO: every one declines). That is a reach this does not have, not a guard.
|
|
2271
2737
|
const followsByShared = new Map<string, Map<Block, Block | null>>();
|
|
2272
|
-
const followOverReturns = (b: Block, stop: Block | null): Block | null => {
|
|
2273
|
-
const shared = sharedRetsOf(b, reachFrom);
|
|
2738
|
+
const followOverReturns = (b: Block, stop: Block | null, anchor: Block | null = null): Block | null => {
|
|
2739
|
+
const shared = sharedRetsOf(b, reachFrom, anchor);
|
|
2274
2740
|
if (shared.length === 0) {
|
|
2275
2741
|
return null;
|
|
2276
2742
|
}
|
|
@@ -2288,7 +2754,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2288
2754
|
if (follow === null || (stop !== null && follow !== stop && reachesAvoiding(b, stop, follow))) {
|
|
2289
2755
|
return null;
|
|
2290
2756
|
}
|
|
2291
|
-
|
|
2757
|
+
if (anchor === null) {
|
|
2758
|
+
hooks.onEarlyReturnFollow?.({ block: fn.blocks.indexOf(b), follow: fn.blocks.indexOf(follow) });
|
|
2759
|
+
}
|
|
2292
2760
|
return follow;
|
|
2293
2761
|
};
|
|
2294
2762
|
|
|
@@ -2349,9 +2817,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2349
2817
|
const byOff = new Map<number, string>();
|
|
2350
2818
|
const names = new Map<Op, string>();
|
|
2351
2819
|
const undefNames = new Map<Op, string>();
|
|
2820
|
+
const undefByKey = new Map<string, string>();
|
|
2352
2821
|
for (const b of fn.blocks) {
|
|
2353
2822
|
for (const op of b.ops) {
|
|
2354
|
-
if (op.opcode === 'laddr') {
|
|
2823
|
+
if (op.opcode === 'laddr' && op.attrs[OWN_RETURN_OBJECT] === true) {
|
|
2824
|
+
// the function's own return object, which no frame offset names
|
|
2825
|
+
names.set(op, mint('ret'));
|
|
2826
|
+
} else if (op.opcode === 'laddr' && typeof op.attrs[CALL_RETURN_OBJECT] === 'string') {
|
|
2827
|
+
// the struct a call returned in a register, which no frame offset names either
|
|
2828
|
+
names.set(op, mint(`${op.attrs[CALL_RETURN_OBJECT]}_ret`));
|
|
2829
|
+
} else if (op.opcode === 'laddr') {
|
|
2355
2830
|
const off = op.attrs.off as number;
|
|
2356
2831
|
let n = byOff.get(off);
|
|
2357
2832
|
if (n === undefined) {
|
|
@@ -2363,7 +2838,23 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2363
2838
|
// Named from the key, like laddr's `sp<off>`: `uninit_sp8` says which frame slot to look
|
|
2364
2839
|
// at in the assembly, and it stays put where a running counter would renumber every local
|
|
2365
2840
|
// when an unrelated edit changed the order ops are minted in.
|
|
2366
|
-
|
|
2841
|
+
// A `copied` undef is the same storage's content as the undef its key names, so it
|
|
2842
|
+
// shares that one's name (and declaration) once both are minted, below.
|
|
2843
|
+
if (op.attrs.copied !== true) {
|
|
2844
|
+
const n = mint(`uninit_${String(op.attrs.key).replace('@', '')}`);
|
|
2845
|
+
undefNames.set(op, n);
|
|
2846
|
+
undefByKey.set(String(op.attrs.key), n);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
for (const b of fn.blocks) {
|
|
2852
|
+
for (const op of b.ops) {
|
|
2853
|
+
if (op.opcode === 'undef' && op.attrs.copied === true) {
|
|
2854
|
+
const key = String(op.attrs.key);
|
|
2855
|
+
const n = undefByKey.get(key) ?? mint(`uninit_${key.replace('@', '')}`);
|
|
2856
|
+
undefByKey.set(key, n);
|
|
2857
|
+
undefNames.set(op, n);
|
|
2367
2858
|
}
|
|
2368
2859
|
}
|
|
2369
2860
|
}
|
|
@@ -2450,17 +2941,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2450
2941
|
}
|
|
2451
2942
|
}
|
|
2452
2943
|
}
|
|
2453
|
-
// Declaration-shape OVERRIDE (symbol map
|
|
2454
|
-
//
|
|
2455
|
-
//
|
|
2944
|
+
// Declaration-shape OVERRIDE (symbol map, or the declaration context for a global the map does
|
|
2945
|
+
// not describe): a project-declared array/struct global is an AGGREGATE whatever the usage
|
|
2946
|
+
// inference saw — a lone off-0 access to `extern u16 tbl[]` must still spell through the
|
|
2947
|
+
// aggregate/array forms, never the bare scalar `tbl`.
|
|
2456
2948
|
// Driven from the FUNCTION's own globals, for the same reason the name minter above consults
|
|
2457
2949
|
// the map instead of copying it: a name outside `scalarGlobals` has nothing to override.
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
scalarGlobals.delete(n);
|
|
2463
|
-
}
|
|
2950
|
+
for (const n of [...scalarGlobals]) {
|
|
2951
|
+
const shape = symbols?.get(n)?.shape;
|
|
2952
|
+
if (shape === 'array' || shape === 'struct' || contextGlobals?.aggregates.has(n) === true) {
|
|
2953
|
+
scalarGlobals.delete(n);
|
|
2464
2954
|
}
|
|
2465
2955
|
}
|
|
2466
2956
|
}
|
|
@@ -2468,162 +2958,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2468
2958
|
// Symbol-map rendering context (memAccess/arrayAccess): shape lookups + the env registry for
|
|
2469
2959
|
// array-shaped globals actually referenced (they surface as SFn.globals — typed, undeclared).
|
|
2470
2960
|
const shapedGlobalTypes = new Map<string, IrType>();
|
|
2471
|
-
const declaredFieldCache = new Map<string, DeclaredField[] | null>();
|
|
2472
2961
|
const symCtx: SymRenderCtx | undefined = symbols
|
|
2473
2962
|
? {
|
|
2474
2963
|
info: (n) => symbols.get(n),
|
|
2475
2964
|
noteGlobal: (n, t) => shapedGlobalTypes.set(n, t),
|
|
2476
2965
|
ptrElements: ptrElementSpellingWanted,
|
|
2477
2966
|
declRank: declRankSpellingWanted,
|
|
2478
|
-
fieldsOf: (n) =>
|
|
2479
|
-
const hit = declaredFieldCache.get(n);
|
|
2480
|
-
if (hit !== undefined) {
|
|
2481
|
-
return hit;
|
|
2482
|
-
}
|
|
2483
|
-
const si = symbols.get(n);
|
|
2484
|
-
const fields =
|
|
2485
|
-
si?.shape === 'struct'
|
|
2486
|
-
? declaredFields(si.layout)
|
|
2487
|
-
: si?.shape === 'pointer'
|
|
2488
|
-
? pointeeFields(si.pointee)
|
|
2489
|
-
: null;
|
|
2490
|
-
declaredFieldCache.set(n, fields);
|
|
2491
|
-
return fields;
|
|
2492
|
-
},
|
|
2967
|
+
fieldsOf: memoFieldsOf((n) => symbols.get(n)),
|
|
2493
2968
|
}
|
|
2494
2969
|
: undefined;
|
|
2495
2970
|
|
|
2496
|
-
/** The declared shape of a global as the pointer-value rules below read it: the map's, or a
|
|
2497
|
-
* pointer the map declares to a structuring that does not spell from it (`pointerGlobals`). */
|
|
2498
|
-
const declaredShape = (name: string): SymbolInfo['shape'] =>
|
|
2499
|
-
symCtx?.info(name)?.shape ?? (pointerGlobals?.has(name) ? 'pointer' : undefined);
|
|
2500
|
-
/** Whether no declaration a spelling folds from types the global: the map's is the only one.
|
|
2501
|
-
* The operand-order rules read this, not declaredShape: a declared pointer keeps `x + (u8 *)p`
|
|
2502
|
-
* for the element and field spellings to read, and a `pointerGlobals` name has no pointee for
|
|
2503
|
-
* them, so its sum is spelled in the asm's order like any other undeclared global's. */
|
|
2504
|
-
const mapUndeclared = (name: string): boolean => symCtx?.info(name)?.shape === undefined;
|
|
2505
|
-
|
|
2506
|
-
/** A POINTER VALUE whose type the project's header owns: a bare `gSym` naming a pointer global
|
|
2507
|
-
* (the VALUE of a pointer cell), or a named MEMBER whose declaration is a pointer (`gSym.pBuf`,
|
|
2508
|
-
* `gPtr->pBuf`). Load, store and compare of such a 4-byte cell are identical for any
|
|
2509
|
-
* object-pointer type, so the declared pointee never matters to THEM; arithmetic on the loaded
|
|
2510
|
-
* value is the opposite case, where the pointee's size scales what is added and every stride
|
|
2511
|
-
* must therefore be made explicit (`(u8 *)gPtr + K`). `ctype` cannot see any of this: it types
|
|
2512
|
-
* only params/locals, so both spellings render `undefined` there.
|
|
2513
|
-
*
|
|
2514
|
-
* A bare global is one when its declared shape is `'pointer'` (declaredShape), or when no
|
|
2515
|
-
* declaration this pass can read says anything (no map, a symtab-only name, entries that
|
|
2516
|
-
* disagree and were dropped to the bare name, a name the map lacks) and the IR loads it as a
|
|
2517
|
-
* pointer at least once. The project still declares it, typically `struct S *`. A map
|
|
2518
|
-
* `shape:'scalar'` is a declaration and is excluded; so is a global the IR never loads as a
|
|
2519
|
-
* pointer (`gCount + 1`, or the `u8` index in `gIdx + gItems`), where casting would make a
|
|
2520
|
-
* pointer of an integer the source added as one. */
|
|
2521
|
-
const isPtrValue = (x: Expr): boolean => {
|
|
2522
|
-
if (x.k !== 'var') {
|
|
2523
|
-
return ptrMemberDecl(x, symCtx) !== null;
|
|
2524
|
-
}
|
|
2525
|
-
const shape = declaredShape(x.name);
|
|
2526
|
-
return shape === 'pointer' || (shape === undefined && pointerLoadedGlobals.has(x.name));
|
|
2527
|
-
};
|
|
2528
|
-
|
|
2529
|
-
/** A global's word VALUE that no declaration this pass can read types, whatever the IR loaded it
|
|
2530
|
-
* as. */
|
|
2531
|
-
const isUndeclaredGlobalValue = (x: Expr): boolean =>
|
|
2532
|
-
x.k === 'var' && wordLoadedGlobals.has(x.name) && declaredShape(x.name) === undefined;
|
|
2533
|
-
|
|
2534
|
-
/** A pointer value or a global's word as the integer the asm added. A global no declaration
|
|
2535
|
-
* types and the IR never loads as a pointer may be declared a float, whose value `(u32)g`
|
|
2536
|
-
* converts, so it goes through `(u8 *)` first: `(u32)(u8 *)g` is `(u32)g`'s bytes under every
|
|
2537
|
-
* integer and pointer declaration, and no C under a float one. KNOWN GAP: under an array or a
|
|
2538
|
-
* function declaration `g` decays to its address, which this spells with no diagnostic; only a
|
|
2539
|
-
* pun reads the word there, and intoDeclaredTemp's KNOWN GAP says why none is spelled. */
|
|
2540
|
-
const globalWord = (x: Expr): Expr => ({
|
|
2541
|
-
k: 'cast',
|
|
2542
|
-
to: T.u(32),
|
|
2543
|
-
e:
|
|
2544
|
-
x.k === 'var' && declaredShape(x.name) === undefined && !pointerLoadedGlobals.has(x.name)
|
|
2545
|
-
? { k: 'cast', to: T.ptr(T.u(8)), e: x }
|
|
2546
|
-
: x,
|
|
2547
|
-
});
|
|
2548
|
-
|
|
2549
|
-
/** An integer operand with every undeclared global it adds or subtracts made a word as well:
|
|
2550
|
-
* bare, `a0 + g` is pointer arithmetic under a pointer declaration of g. Other operators reject
|
|
2551
|
-
* a pointer operand, and a load or a cast types its own value. */
|
|
2552
|
-
const intWords = (x: Expr): Expr =>
|
|
2553
|
-
isUndeclaredGlobalValue(x)
|
|
2554
|
-
? globalWord(x)
|
|
2555
|
-
: x.k === 'bin' && (x.op === '+' || x.op === '-')
|
|
2556
|
-
? { ...x, l: intWords(x.l), r: intWords(x.r) }
|
|
2557
|
-
: x;
|
|
2558
|
-
|
|
2559
|
-
/** A byte sum the pointer-value arithmetic rule below made of a global's value (`(u8 *)g + K`),
|
|
2560
|
-
* and the same sum as the integer it also is (`(u32)g + K`), which an integer added in front of
|
|
2561
|
-
* it takes. KNOWN GAP: an integer READER of the byte sum converts a pointer to an integer. The
|
|
2562
|
-
* backend casts it where the reader's type is known (an assignment, a store through a typed
|
|
2563
|
-
* slot, a return: cfamily `legalizePointerWrites`), and a compare against an integer compares
|
|
2564
|
-
* it as a `u32`. A call argument and a global no declaration types keep the pointer, which agbcc
|
|
2565
|
-
* and KMC gcc warn about and CodeWarrior rejects; only a pointer cell's sum passed to a parameter
|
|
2566
|
-
* the prototype declares an integer is converted (l3/ptrcell.ts). The integer sum is no fix: gcc
|
|
2567
|
-
* orders a pointer sum's operands and an integer sum's differently, so the bytes differ. */
|
|
2568
|
-
const castGlobal = (x: Expr, undeclared = false): x is Extract<Expr, { k: 'cast' }> =>
|
|
2569
|
-
x.k === 'cast' &&
|
|
2570
|
-
typeEquals(x.to, T.ptr(T.u(8))) &&
|
|
2571
|
-
x.e.k === 'var' &&
|
|
2572
|
-
(isPtrValue(x.e) || isUndeclaredGlobalValue(x.e)) &&
|
|
2573
|
-
(!undeclared || mapUndeclared(x.e.name));
|
|
2574
|
-
/** The integer sum the rule spells in the asm's order instead (`(u32)g + x`), cast back to the
|
|
2575
|
-
* byte pointer it stands for: `(u8 *)((u32)g + x)`. A byte sum converted whole (byteSumAsInt's
|
|
2576
|
-
* `(u32)((u8 *)g + x - gB)`) is one of its words. */
|
|
2577
|
-
const intGlobalWord = (x: Expr, undeclared: boolean): boolean =>
|
|
2578
|
-
x.k === 'cast' &&
|
|
2579
|
-
typeEquals(x.to, T.u(32)) &&
|
|
2580
|
-
(castGlobal(x.e, undeclared) ||
|
|
2581
|
-
(x.e.k === 'bin' && isByteGlobalSum(x.e, undeclared)) ||
|
|
2582
|
-
(x.e.k === 'var' &&
|
|
2583
|
-
(isPtrValue(x.e) || isUndeclaredGlobalValue(x.e)) &&
|
|
2584
|
-
(!undeclared || mapUndeclared(x.e.name))));
|
|
2585
|
-
const isIntGlobalSum = (x: Expr, undeclared: boolean): boolean =>
|
|
2586
|
-
x.k === 'bin' &&
|
|
2587
|
-
(x.op === '+' || x.op === '-') &&
|
|
2588
|
-
(intGlobalWord(x.l, undeclared) ||
|
|
2589
|
-
intGlobalWord(x.r, undeclared) ||
|
|
2590
|
-
isIntGlobalSum(x.l, undeclared) ||
|
|
2591
|
-
isIntGlobalSum(x.r, undeclared));
|
|
2592
|
-
const restoredIntSum = (x: Expr, undeclared: boolean): Expr | undefined =>
|
|
2593
|
-
x.k === 'cast' && typeEquals(x.to, T.ptr(T.u(8))) && isIntGlobalSum(x.e, undeclared) ? x.e : undefined;
|
|
2594
|
-
const isByteGlobalSum = (x: Expr, undeclared = false): boolean =>
|
|
2595
|
-
restoredIntSum(x, undeclared) !== undefined ||
|
|
2596
|
-
(x.k === 'bin' &&
|
|
2597
|
-
(x.op === '+' || x.op === '-') &&
|
|
2598
|
-
(castGlobal(x.l, undeclared) ||
|
|
2599
|
-
castGlobal(x.r, undeclared) ||
|
|
2600
|
-
isByteGlobalSum(x.l, undeclared) ||
|
|
2601
|
-
isByteGlobalSum(x.r, undeclared)));
|
|
2602
|
-
/** The integer a byte sum is. Every global it adds bare goes a word with it. A lone global it
|
|
2603
|
-
* subtracts stays the pointer difference, `(u32)((u8 *)g + x - gB)`: the asm's integer under an
|
|
2604
|
-
* integer or a byte-pointer declaration of `gB`, and no C under a wider pointer or array, a
|
|
2605
|
-
* function or a float one, where the word `(u32)(u8 *)gB` would subtract an address. Under a byte
|
|
2606
|
-
* array it subtracts the array's address: globalWord's KNOWN GAP. A difference has one operand
|
|
2607
|
-
* order, so the asm's needs no integer sum. A sum of globals it subtracts goes words: bare,
|
|
2608
|
-
* `gB2 - gB3` is an element count under a wider pointer declaration. */
|
|
2609
|
-
const byteSumAsInt = (x: Expr): Expr =>
|
|
2610
|
-
restoredIntSum(x, false) ??
|
|
2611
|
-
(castGlobal(x)
|
|
2612
|
-
? globalWord(x.e)
|
|
2613
|
-
: x.k === 'bin' && isByteGlobalSum(x)
|
|
2614
|
-
? x.op === '-' && isUndeclaredGlobalValue(x.r)
|
|
2615
|
-
? { k: 'cast', to: T.u(32), e: x }
|
|
2616
|
-
: { ...x, l: byteSumAsInt(x.l), r: byteSumAsInt(x.r) }
|
|
2617
|
-
: intWords(x));
|
|
2618
|
-
|
|
2619
|
-
/** Operands `-`/`~` cannot take as spelled: a rendered pointer, a bare `&gSym`, a pointer
|
|
2620
|
-
* global's value. All three are ill-formed C under a unary arithmetic operator — the asm did
|
|
2621
|
-
* 32-bit integer math on the address, so that is what gets spelled. */
|
|
2622
|
-
const needsIntSpelling = (x: Expr): boolean => ctype(x)?.kind === 'ptr' || x.k === 'addr' || isPtrValue(x);
|
|
2623
|
-
|
|
2624
|
-
// --- loop discovery (loops.ts): natural loops via dominator back-edges + the nesting forest ---
|
|
2625
|
-
const forest = analyzeLoops(fn, dom);
|
|
2626
|
-
|
|
2627
2971
|
// GUARDED self-loop headers (a block that is its own successor, entered through a guard-shaped
|
|
2628
2972
|
// cond_br) — recovered by the guard-fusion + emitWhile un-rotation path below (the gcc "guard +
|
|
2629
2973
|
// do-while" → `while` shape; countdown/shifts). UNGUARDED self-loops register as single-block
|
|
@@ -2715,13 +3059,29 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2715
3059
|
[...forest.byHeader.values()].every(
|
|
2716
3060
|
(l2) => l2.header === nl.header || !nl.body.has(l2.header) || [...l2.body].every((b) => nl.body.has(b)),
|
|
2717
3061
|
) && [...nl.body].every((bb) => bb === nl.header || (preds.get(bb) ?? []).every((p) => nl.body.has(p)));
|
|
3062
|
+
// An edge out of `nl` that does not land on `exit` itself, read the one way both the `while` and the
|
|
3063
|
+
// `while (1)` classifiers read it: a `break` that runs `region` first, or an early return whose
|
|
3064
|
+
// tail the loop emits (`owned`). The break arm is asked first: where the code past `exit` is a pure
|
|
3065
|
+
// `return`, `earlyReturnArm` would also take the arm and copy that post-loop `return` into it, where
|
|
3066
|
+
// the asm holds one copy after the loop (two code arms ahead of `return 3;` would lift as `gHit = i;
|
|
3067
|
+
// return 3;` twice, not a `break` each and one `return 3`). The break arm copies nothing past
|
|
3068
|
+
// `exit` — the duplication policy written at `earlyReturnArm`.
|
|
3069
|
+
// `codeArms` is the classifier's own admission of a code-running break; never out of an inner body.
|
|
3070
|
+
const exitArmOf = (nl: NaturalLoop, e: { from: Block; to: Block }, exit: Block, codeArms: boolean) => {
|
|
3071
|
+
const region =
|
|
3072
|
+
codeArms && !innerBodyHas(nl, e.from) ? codeBreakArm(preds, dom, e.from, e.to, nl.body, exit, true) : null;
|
|
3073
|
+
const owned = region === null ? earlyReturnArm({ dom, reachFrom, preds }, e.from, e.to, nl.body, exit) : null;
|
|
3074
|
+
return { region, owned };
|
|
3075
|
+
};
|
|
2718
3076
|
const whileLoops = new Map<Block, WhileLoopInfo>();
|
|
2719
3077
|
const doWhileLoops = new Map<Block, DoWhileInfo>();
|
|
3078
|
+
const keptGuardDoWhiles = new Map<Block, DoWhileInfo>();
|
|
2720
3079
|
for (const nl of forest.byHeader.values()) {
|
|
2721
3080
|
const h = nl.header;
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
3081
|
+
// Guarded self-loops use emitWhile (above); UNGUARDED ones are single-block do-whiles. A
|
|
3082
|
+
// guarded one's do-while reading is kept aside all the same (`keptGuardDoWhiles`), for the
|
|
3083
|
+
// guard that emitWhile cannot fuse and then declines.
|
|
3084
|
+
const guarded = nl.selfLoop && loops.has(h);
|
|
2725
3085
|
if (!nl.selfLoop && nl.latches.length !== 1) {
|
|
2726
3086
|
continue;
|
|
2727
3087
|
} // single latch only
|
|
@@ -2751,7 +3111,36 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2751
3111
|
exit: Block,
|
|
2752
3112
|
kind: 'while' | 'dowhile',
|
|
2753
3113
|
bodyEntry: Block | null = null;
|
|
2754
|
-
|
|
3114
|
+
// The header's way out is not the loop's exit when it enters an arm that runs code and then
|
|
3115
|
+
// joins another exit edge (`codeBreakArm`) whose tail has an effect: that arm is a `break`, and
|
|
3116
|
+
// the loop a `while (1)`. Where the tail only returns, the `while` reading copies it into the
|
|
3117
|
+
// other edge as an early `return`, and keeps that spelling — unless the tail is a bare `return`
|
|
3118
|
+
// and the arm itself runs code. gcc2.7.2's `for (i = 0; i < n; i++) { if (t[i] == 7) { gHit = 1;
|
|
3119
|
+
// break; } }` keeps the arm inside a guarded bottom-tested loop, so the arm is the one in the
|
|
3120
|
+
// loop, and a bare `return` copies no code: `while (t[i] != 7) { ...; if (i >= n) return; }
|
|
3121
|
+
// gHit = 1;` is the spelling the bytes do not hold (breaknend_code 21/26 against 4/18).
|
|
3122
|
+
// The arm is asked in shared mode, as `exitArmOf` asks it for `while (1)`: a break whose tail
|
|
3123
|
+
// another arm or an `else` before the loop also enters is a break all the same, and the `while`
|
|
3124
|
+
// reading that took it for the exit would leave the other exit edge with no spelling.
|
|
3125
|
+
const runsEffect = (x: Block) => [x, ...reachFrom(x)].some((y) => !nl.body.has(y) && y.ops.some(effectful));
|
|
3126
|
+
const hOutIsBreakArm =
|
|
3127
|
+
hOut.length === 1 &&
|
|
3128
|
+
nl.exitEdges.some((e) => {
|
|
3129
|
+
if (e.to === hOut[0].block) {
|
|
3130
|
+
return false;
|
|
3131
|
+
}
|
|
3132
|
+
const region = codeBreakArm(preds, dom, h, hOut[0].block, nl.body, e.to, true);
|
|
3133
|
+
const bareReturn = e.to.ops.length === 1 && e.to.ops[0].opcode === 'ret' && e.to.ops[0].operands.length === 0;
|
|
3134
|
+
return region !== null && (runsEffect(e.to) || (bareReturn && [...region].some((y) => y.ops.some(effectful))));
|
|
3135
|
+
});
|
|
3136
|
+
if (
|
|
3137
|
+
!nl.selfLoop &&
|
|
3138
|
+
hTerm.opcode === 'cond_br' &&
|
|
3139
|
+
hInBody.length === 1 &&
|
|
3140
|
+
hOut.length === 1 &&
|
|
3141
|
+
headerPure &&
|
|
3142
|
+
!hOutIsBreakArm
|
|
3143
|
+
) {
|
|
2755
3144
|
kind = 'while';
|
|
2756
3145
|
exitFrom = h;
|
|
2757
3146
|
exit = hOut[0].block;
|
|
@@ -2780,17 +3169,23 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2780
3169
|
// to know which edges out of the body end an iteration rather than continue it.
|
|
2781
3170
|
const arms: LoopArm[] = [];
|
|
2782
3171
|
const breaks = new Set<Block>();
|
|
3172
|
+
const breakArms: BreakArm[] = [];
|
|
2783
3173
|
let singleExit = true;
|
|
2784
3174
|
for (const e of nl.exitEdges) {
|
|
2785
3175
|
if (e.from === exitFrom && e.to === exit) {
|
|
2786
3176
|
continue;
|
|
2787
3177
|
}
|
|
2788
|
-
|
|
3178
|
+
// a `while` takes a code-running break only into an exit that takes no copies: the header's exit
|
|
3179
|
+
// copies are what `breakRule` shares with a break, and an arm's edge into the exit is not one
|
|
3180
|
+
const { region, owned } = exitArmOf(nl, e, exit, kind === 'while' && exit.params.length === 0);
|
|
2789
3181
|
if (owned) {
|
|
2790
3182
|
arms.push({ from: e.from, to: e.to, owned });
|
|
2791
3183
|
}
|
|
2792
3184
|
if (kind === 'while' && e.to === exit && !innerBodyHas(nl, e.from)) {
|
|
2793
3185
|
breaks.add(e.from);
|
|
3186
|
+
} else if (region !== null) {
|
|
3187
|
+
breaks.add(e.from);
|
|
3188
|
+
breakArms.push({ from: e.from, to: e.to, region });
|
|
2794
3189
|
} else if (!owned && (!isRet(e.to) || (kind === 'dowhile' && e.to === exit))) {
|
|
2795
3190
|
singleExit = false;
|
|
2796
3191
|
break;
|
|
@@ -2810,9 +3205,17 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2810
3205
|
body: nl.body,
|
|
2811
3206
|
arms,
|
|
2812
3207
|
breaks,
|
|
3208
|
+
breakArms,
|
|
2813
3209
|
});
|
|
2814
3210
|
} else {
|
|
2815
|
-
|
|
3211
|
+
(guarded ? keptGuardDoWhiles : doWhileLoops).set(h, {
|
|
3212
|
+
header: h,
|
|
3213
|
+
latch,
|
|
3214
|
+
exit,
|
|
3215
|
+
forwardPreds: nl.forwardPreds,
|
|
3216
|
+
body: nl.body,
|
|
3217
|
+
arms,
|
|
3218
|
+
});
|
|
2816
3219
|
}
|
|
2817
3220
|
}
|
|
2818
3221
|
|
|
@@ -2828,21 +3231,42 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2828
3231
|
// that `return` for the break and the code after the loop for an early return.
|
|
2829
3232
|
const foreverLoops = new Map<Block, ForeverLoopInfo>();
|
|
2830
3233
|
const foreverShape = (nl: NaturalLoop): ForeverLoopInfo | null => {
|
|
3234
|
+
// NO EDGE LEAVES: the loop never ends, and C spells that `while (1)` with no `break` and nothing
|
|
3235
|
+
// after it. A block laid out past the loop is reached from elsewhere or not at all, and the
|
|
3236
|
+
// region holding the loop ends at the loop either way (`emitForever`).
|
|
3237
|
+
if (nl.exitEdges.length === 0) {
|
|
3238
|
+
return {
|
|
3239
|
+
header: nl.header,
|
|
3240
|
+
latches: nl.latches,
|
|
3241
|
+
forwardPreds: nl.forwardPreds,
|
|
3242
|
+
exit: null,
|
|
3243
|
+
body: nl.body,
|
|
3244
|
+
arms: [],
|
|
3245
|
+
breaks: new Set(),
|
|
3246
|
+
breakArms: [],
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
2831
3249
|
const last = Math.max(...[...nl.body].map((b) => fn.blocks.indexOf(b)));
|
|
2832
3250
|
const rank = (x: Block) => fn.blocks.indexOf(x) + (fn.blocks.indexOf(x) > last ? 0 : fn.blocks.length);
|
|
2833
3251
|
const targets = [...new Set(nl.exitEdges.map((e) => e.to))].sort((x, y) => rank(x) - rank(y));
|
|
2834
3252
|
for (const exit of targets) {
|
|
2835
3253
|
const arms: LoopArm[] = [];
|
|
2836
3254
|
const breaks = new Set<Block>();
|
|
3255
|
+
const breakArms: BreakArm[] = [];
|
|
2837
3256
|
const fits = nl.exitEdges.every((e) => {
|
|
2838
3257
|
if (e.to === exit) {
|
|
2839
3258
|
breaks.add(e.from);
|
|
2840
3259
|
return !innerBodyHas(nl, e.from);
|
|
2841
3260
|
}
|
|
2842
|
-
const
|
|
3261
|
+
const { region, owned } = exitArmOf(nl, e, exit, true);
|
|
2843
3262
|
if (owned) {
|
|
2844
3263
|
arms.push({ from: e.from, to: e.to, owned });
|
|
2845
3264
|
}
|
|
3265
|
+
if (region !== null) {
|
|
3266
|
+
breaks.add(e.from);
|
|
3267
|
+
breakArms.push({ from: e.from, to: e.to, region });
|
|
3268
|
+
return true;
|
|
3269
|
+
}
|
|
2846
3270
|
// a `ret` target the edge does not own is copied into the body where the edge leaves, so it
|
|
2847
3271
|
// must hold nothing a second copy would run twice in the source
|
|
2848
3272
|
return owned !== null || (isRet(e.to) && !e.to.ops.some(effectful));
|
|
@@ -2856,6 +3280,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2856
3280
|
body: nl.body,
|
|
2857
3281
|
arms,
|
|
2858
3282
|
breaks,
|
|
3283
|
+
breakArms,
|
|
2859
3284
|
};
|
|
2860
3285
|
}
|
|
2861
3286
|
}
|
|
@@ -2900,85 +3325,33 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
2900
3325
|
const vtEnv = (n: string): IrType | undefined => varType.get(n);
|
|
2901
3326
|
const ctype = (e0: Expr): IrType | undefined => exprCType(e0, vtEnv);
|
|
2902
3327
|
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
if (t === undefined) {
|
|
2931
|
-
return value;
|
|
2932
|
-
}
|
|
2933
|
-
// ── pointer values whose type this side of the program does not own ────────────────────────
|
|
2934
|
-
// A `gaddr` at least states a type the IR knows. `gSym.pBuf` and a pointer global's own value
|
|
2935
|
-
// (isPtrValue) state one only the project's header knows, and `ctype` — which types params and
|
|
2936
|
-
// locals — reads them as `undefined`, so the test above cannot see them at all. Assigning one
|
|
2937
|
-
// bare declares that the temp's type and the project's declaration of that pointer are the
|
|
2938
|
-
// same type, which nothing here established: the project's header says
|
|
2939
|
-
// `struct Unk_03005284 *` where the recovered temp says `struct Struct0 *`, and `-Werror` makes
|
|
2940
|
-
// the mismatch fatal in the tree the source is pasted into. The destination's type is the one
|
|
2941
|
-
// this pass DID choose, so unlike the map-declared cells below it can be named exactly rather
|
|
2942
|
-
// than defused through `void *`. That holds for an INTEGER temp too, and there the diagnostic
|
|
2943
|
-
// is the mirror one, `assignment makes integer from pointer without a cast` — same site, same
|
|
2944
|
-
// argument, same `(T)` answer.
|
|
2945
|
-
//
|
|
2946
|
-
// A value that RENDERS a pointer of another type is the same assignment with its type in plain
|
|
2947
|
-
// sight: the byte arithmetic below spells `(u8 *)gPtr + 3544` and `(u8 *)a0 + 2` for the
|
|
2948
|
-
// address alone, and a `u16 *` or `s32 *` temp takes neither without the cast.
|
|
2949
|
-
//
|
|
2950
|
-
// KNOWN GAP: a global no declaration types, read at offset 0 only, may be declared an ARRAY
|
|
2951
|
-
// (`u16 *gArr[4]`, the asm reading `gArr[0]`). Its value spelling is then the array's address,
|
|
2952
|
-
// and this cast compiles that wrong value with no diagnostic, where the bare value keeps
|
|
2953
|
-
// agbcc's `incompatible pointer type`, fatal under -Werror. The cast is load-bearing all the
|
|
2954
|
-
// same: a substitution variation (`/unmerge`) carries the temp's value into the arithmetic it
|
|
2955
|
-
// feeds, and bare, that arithmetic scales by the declared pointee. A pun, `*(T *)&gArr`, is no
|
|
2956
|
-
// fix: where the declaration is not exactly T it loses gcc's fold of the read (agbcc
|
|
2957
|
-
// c-typeck.c), it drops a `volatile` declaration's qualifier, and agbcc's strict aliasing at
|
|
2958
|
-
// -O2 moves it past a store through the declared type.
|
|
2959
|
-
const vt = ctype(value);
|
|
2960
|
-
if (isPtrValue(value) || (vt?.kind === 'ptr' && !typeEquals(vt, t))) {
|
|
2961
|
-
return { k: 'cast', to: t, e: value };
|
|
2962
|
-
}
|
|
2963
|
-
if (t.kind !== 'ptr' || value.k !== 'addr') {
|
|
2964
|
-
return value;
|
|
2965
|
-
}
|
|
2966
|
-
// The only provably-redundant case: a NON-VOLATILE scalar cell whose DECLARED type is the
|
|
2967
|
-
// destination's pointee, where `&gSym` already denotes exactly `T *`.
|
|
2968
|
-
//
|
|
2969
|
-
// `scalarCellType` and not `scalarTypeForAccess`: the latter answers what an ACCESS of that
|
|
2970
|
-
// width reads and collapses every 4-byte access to `s32`, so it called a `u32` cell equal to an
|
|
2971
|
-
// `s32 *` destination and let the incompatible assignment through. And a `volatile` cell makes
|
|
2972
|
-
// `&gSym` a `volatile T *`, so omitting the cast would DISCARD the qualifier — the same class of
|
|
2973
|
-
// fatal-under-a-strict-build defect this rule exists to remove.
|
|
2974
|
-
const si = symCtx?.info(value.name);
|
|
2975
|
-
if (si?.shape === 'scalar' && !si.volatile && isScalarCellSize(si.size)) {
|
|
2976
|
-
if (typeEquals(scalarCellType(si.size, si.signed), t.to)) {
|
|
2977
|
-
return value;
|
|
2978
|
-
}
|
|
2979
|
-
}
|
|
2980
|
-
return { k: 'cast', to: t, e: value };
|
|
2981
|
-
};
|
|
3328
|
+
// ── POINTER/INTEGER spelling (structure/pointer-spelling.ts) ────────────────────────────────
|
|
3329
|
+
// Whether an operand, a temp's value or a pointer cell's value is written as a pointer or as an
|
|
3330
|
+
// integer, and through which cast. `varType` and `varName` are read live: the naming below is
|
|
3331
|
+
// still declaring temps.
|
|
3332
|
+
const {
|
|
3333
|
+
pointerGlobal,
|
|
3334
|
+
castsIntoGlobal,
|
|
3335
|
+
needsIntSpelling,
|
|
3336
|
+
intoDeclaredTemp,
|
|
3337
|
+
intoPtrCell,
|
|
3338
|
+
arith: spellArith,
|
|
3339
|
+
} = makePointerSpelling({
|
|
3340
|
+
sym: symCtx,
|
|
3341
|
+
pointerGlobals,
|
|
3342
|
+
pointerLoadedGlobals,
|
|
3343
|
+
wordLoadedGlobals,
|
|
3344
|
+
scalarGlobals,
|
|
3345
|
+
varType,
|
|
3346
|
+
compiler: opts,
|
|
3347
|
+
strictPointerConversions,
|
|
3348
|
+
...(dialect !== undefined ? { dialect } : {}),
|
|
3349
|
+
refuse: (why) => {
|
|
3350
|
+
throw new StructureError(`cannot structure '${fn.name}': ${why}`);
|
|
3351
|
+
},
|
|
3352
|
+
useSitesOf,
|
|
3353
|
+
isNamed: (v) => varName.has(v),
|
|
3354
|
+
});
|
|
2982
3355
|
|
|
2983
3356
|
let fresh = 0;
|
|
2984
3357
|
// Materialized defs are named FIRST: the temp is the register the compiler held the
|
|
@@ -3688,6 +4061,36 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3688
4061
|
}
|
|
3689
4062
|
}
|
|
3690
4063
|
|
|
4064
|
+
// ── a temp holding a pointer global's value, summed with `x + K`, is a byte pointer ──────────
|
|
4065
|
+
// (pointer-spelling.ts `declaresBytePointer` says when.) Decided over every value under the name
|
|
4066
|
+
// once the names are settled, so a name that also holds anything else keeps its integer.
|
|
4067
|
+
if (keepsPointerSumAddend) {
|
|
4068
|
+
const inArgs = (v: Value): Value[] | undefined => {
|
|
4069
|
+
const b = paramBlock.get(v);
|
|
4070
|
+
if (b === undefined || entryParams.has(v)) {
|
|
4071
|
+
return undefined;
|
|
4072
|
+
}
|
|
4073
|
+
const i = b.params.indexOf(v);
|
|
4074
|
+
return [...inEdgeRecords(preds, b)].map(({ succ }) => succ.args[i]);
|
|
4075
|
+
};
|
|
4076
|
+
const ir = {
|
|
4077
|
+
defOf: (v: Value) => defs.get(v),
|
|
4078
|
+
inArgs,
|
|
4079
|
+
usesOf: (v: Value) => (useSitesOf.get(v) ?? []).map((u) => u.op),
|
|
4080
|
+
isNamed: (v: Value) => varName.has(v),
|
|
4081
|
+
};
|
|
4082
|
+
const holders = new Map<string, Value[]>();
|
|
4083
|
+
for (const [v, n] of [...varName, ...backArgName]) {
|
|
4084
|
+
(holders.get(n) ?? holders.set(n, []).get(n)!).push(v);
|
|
4085
|
+
}
|
|
4086
|
+
for (const [n, vs] of holders) {
|
|
4087
|
+
const t = varType.get(n);
|
|
4088
|
+
if (t?.kind === 'int' && t.width === 32 && declaresBytePointer(vs, ir, { pointerGlobal, castsIntoGlobal })) {
|
|
4089
|
+
varType.set(n, T.ptr(T.u(8)));
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
|
|
3691
4094
|
// ── def-site anchoring of constant merge copies (anchorConstCopies) ──────────────────────────
|
|
3692
4095
|
// An edge copy `v = K` places the constant where the EDGE is, but the asm often materialized K
|
|
3693
4096
|
// earlier: `movs r9, #0` at entry ahead of a single-armed overwrite, `movs r5, #1` at the top
|
|
@@ -3762,7 +4165,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3762
4165
|
// entry consts anchored at their own def sites would still order correctly, and this refuses
|
|
3763
4166
|
// the shape rather than reasoning about it. The other half of "entered only through its
|
|
3764
4167
|
// header" needs no test — `analyzeLoops` builds the body as the backward closure from the
|
|
3765
|
-
// latches with the header already in it (loops.ts), so a body block's predecessors are all in
|
|
4168
|
+
// latches with the header already in it (ir/loops.ts), so a body block's predecessors are all in
|
|
3766
4169
|
// the body by construction.
|
|
3767
4170
|
const singleEntry = (nl: NaturalLoop): boolean => nl.forwardPreds.length === 1;
|
|
3768
4171
|
for (const M of fn.blocks) {
|
|
@@ -3891,12 +4294,70 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3891
4294
|
// hoist. It carries it where ir/discipline.ts `qualifiedBy` says: on its cast, or through the
|
|
3892
4295
|
// object's declaration and on its cast where it reaches the object through one, since a cast's
|
|
3893
4296
|
// pointee drops the declaration's qualifiers.
|
|
4297
|
+
//
|
|
4298
|
+
// Whether a cast stands between the access and the object is the PRINTER's question
|
|
4299
|
+
// (backend/cfamily.ts `legalizedIndexBase`), so it is asked of the env the printer judges by:
|
|
4300
|
+
// the declared variables over the globals this structuring typed for the tree (`SFn.globals`,
|
|
4301
|
+
// l3/typing.ts `declaredTypes`). Asked of the variables alone, a global subscript the printer
|
|
4302
|
+
// spells through its own name (`gVA[i]`, already volatile by its declaration) reads as one it
|
|
4303
|
+
// casts, and the qualified cast agbcc compiles differently is put on a name that needed none.
|
|
4304
|
+
const printedType = (n: string): IrType | undefined => varType.get(n) ?? shapedGlobalTypes.get(n);
|
|
4305
|
+
const printedCType = (e0: Expr): IrType | undefined => exprCType(e0, printedType);
|
|
4306
|
+
// A declared access reaches its object through the declaration only where its address is spelled
|
|
4307
|
+
// from the object's own name. Through a local or a parameter — a pointer one of two addresses of
|
|
4308
|
+
// the object joined into, or one a loop steps through it — the declaration qualifies nothing, and
|
|
4309
|
+
// the qualifier goes on a cast.
|
|
4310
|
+
const throughLocal = (access: Expr): boolean => {
|
|
4311
|
+
let b: Expr | undefined = access.k === 'index' || access.k === 'field' ? access.base : undefined;
|
|
4312
|
+
while (b !== undefined) {
|
|
4313
|
+
if (b.k === 'var') {
|
|
4314
|
+
return varType.has(b.name);
|
|
4315
|
+
}
|
|
4316
|
+
if (b.k === 'index' || b.k === 'field') {
|
|
4317
|
+
b = b.base;
|
|
4318
|
+
} else if (b.k === 'cast') {
|
|
4319
|
+
b = b.e;
|
|
4320
|
+
} else if (b.k === 'bin' && (b.op === '+' || b.op === '-')) {
|
|
4321
|
+
b = b.l.k === 'const' ? b.r : b.l;
|
|
4322
|
+
} else {
|
|
4323
|
+
return false;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
return false;
|
|
4327
|
+
};
|
|
3894
4328
|
const pinnedAccess = (op: Op, access: Expr): Expr => {
|
|
3895
4329
|
const by = qualifiedBy(op);
|
|
3896
|
-
if (by === null || (by === 'declaration' && !castOffDeclaration(access,
|
|
4330
|
+
if (by === null || (by === 'declaration' && !castOffDeclaration(access, printedCType) && !throughLocal(access))) {
|
|
3897
4331
|
return access;
|
|
3898
4332
|
}
|
|
3899
|
-
|
|
4333
|
+
// A bare global's declaration owns its qualifiers. Where the project declares the name — the map
|
|
4334
|
+
// knows it, or a declaration context is in play — a declaration that qualified it would have
|
|
4335
|
+
// placed the access `declared`, so one the lift qualified by a CAST (`device`:
|
|
4336
|
+
// frontend/dead-reads.ts, frontend/device-pins.ts) is declared plain there, and `gR;` compiles
|
|
4337
|
+
// to no read at all: the cast goes on the object's address. A name nothing declares gets the
|
|
4338
|
+
// synthesized declaration, which carries the qualifier (rank-declare.ts `bareGlobalSymbols`),
|
|
4339
|
+
// and keeps the bare spelling — on gcc 2.7.2 the cast compiles to another address sequence.
|
|
4340
|
+
if (
|
|
4341
|
+
by === 'cast' &&
|
|
4342
|
+
access.k === 'var' &&
|
|
4343
|
+
(mapSymbols?.has(access.name) === true || contextGlobals !== undefined)
|
|
4344
|
+
) {
|
|
4345
|
+
const width = op.attrs.width as number;
|
|
4346
|
+
const signed = op.attrs.signed === true;
|
|
4347
|
+
return {
|
|
4348
|
+
k: 'index',
|
|
4349
|
+
base: {
|
|
4350
|
+
k: 'cast',
|
|
4351
|
+
to: T.ptr(scalarTypeForAccess(width, signed)),
|
|
4352
|
+
volatile: true,
|
|
4353
|
+
e: { k: 'addr', name: access.name },
|
|
4354
|
+
},
|
|
4355
|
+
idx: { k: 'const', value: 0 },
|
|
4356
|
+
width,
|
|
4357
|
+
signed,
|
|
4358
|
+
};
|
|
4359
|
+
}
|
|
4360
|
+
const q = qualifiedMemoryAccess(access, printedCType);
|
|
3900
4361
|
if (q === null) {
|
|
3901
4362
|
throw new StructureError(
|
|
3902
4363
|
`cannot structure '${fn.name}': a ${placedAt(op)} ${op.opcode} that must stay volatile is reached through a ` +
|
|
@@ -3906,46 +4367,248 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3906
4367
|
return q;
|
|
3907
4368
|
};
|
|
3908
4369
|
|
|
3909
|
-
//
|
|
3910
|
-
//
|
|
3911
|
-
//
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
4370
|
+
// A FRAME OBJECT DECLARED AS A UNION (`laddrType`) is read and written through the view of each
|
|
4371
|
+
// access's own width and extension — `sp0.half[1]`, `sp0.word[0]` — on the object itself, never
|
|
4372
|
+
// through a cast on its storage: a cast there is the spelling agbcc may read stale, and the
|
|
4373
|
+
// union is declared because it does not. The audit admitted only accesses at a fixed offset on
|
|
4374
|
+
// the object and aligned to their own width (frame-objects.ts `unionRefusal`), so a view names
|
|
4375
|
+
// each one; an access with none is an internal error, not a decline to fall back from.
|
|
4376
|
+
const frameUnionAccess = (base: Value, off: number, width: number, signed: boolean, isStore: boolean) => {
|
|
4377
|
+
const d = defs.get(base);
|
|
4378
|
+
if (d?.opcode !== 'laddr' || d.attrs.views === undefined) {
|
|
4379
|
+
return null;
|
|
3915
4380
|
}
|
|
3916
|
-
|
|
3917
|
-
|
|
4381
|
+
const u = laddrType(d);
|
|
4382
|
+
const { view, index } =
|
|
4383
|
+
u.kind === 'union' ? unionView(u, off, width, signed, isStore) : { view: undefined, index: null };
|
|
4384
|
+
if (view === undefined || off % width !== 0) {
|
|
4385
|
+
throw new Error(
|
|
4386
|
+
`internal: a ${width}-byte ${isStore ? 'store' : 'load'} at byte ${off} of the frame union '${laddrName.get(d)}' has no view`,
|
|
4387
|
+
);
|
|
3918
4388
|
}
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
4389
|
+
const cell: Expr = { k: 'field', base: { k: 'var', name: laddrName.get(d)! }, name: view.name, dot: true };
|
|
4390
|
+
return index === null
|
|
4391
|
+
? cell
|
|
4392
|
+
: ({ k: 'index', base: cell, idx: { k: 'const', value: index }, width, signed, subscript: true } as const);
|
|
4393
|
+
};
|
|
4394
|
+
// A FRAME OBJECT DECLARED AS A STRUCT (a call's return storage or a struct passed by value,
|
|
4395
|
+
// `laddrType`) is read and written through the member an access names exactly — its offset, its
|
|
4396
|
+
// width, and the type the cast it replaces would spell (`spellsAccessType`, a load's alone: a
|
|
4397
|
+
// store truncates to the member's bytes whatever their type); any other access keeps the cast.
|
|
4398
|
+
// Unqualified only: a qualified access keeps the cast its qualifier goes on.
|
|
4399
|
+
const frameStructMember = (op: Op, off: number, width: number, signed: boolean | null): Expr | null => {
|
|
4400
|
+
const d = defs.get(op.operands[0]);
|
|
4401
|
+
const t = d?.results[0].type;
|
|
4402
|
+
if (d?.opcode !== 'laddr' || d.attrs.aggregate !== true || t?.kind !== 'ptr' || t.to.kind !== 'struct') {
|
|
4403
|
+
return null;
|
|
3924
4404
|
}
|
|
3925
|
-
|
|
3926
|
-
|
|
4405
|
+
const f = t.to.fields.find((m) => m.off === off);
|
|
4406
|
+
if (f === undefined || f.type.kind !== 'int' || f.type.width !== width * 8 || qualifiedBy(op) !== null) {
|
|
4407
|
+
return null;
|
|
3927
4408
|
}
|
|
3928
|
-
|
|
3929
|
-
|
|
4409
|
+
return signed === null || spellsAccessType(f.type.signed, width, signed)
|
|
4410
|
+
? { k: 'field', base: { k: 'var', name: laddrName.get(d)! }, name: f.name, dot: true }
|
|
4411
|
+
: null;
|
|
4412
|
+
};
|
|
4413
|
+
// A PART OF THE ONE FRAME OBJECT a copy proved one object of the source's (frame-objects.ts
|
|
4414
|
+
// `provenParts`, stamped `part`) is read and written through the element an access names exactly
|
|
4415
|
+
// — at a whole element, inside the array, of the element's width, and of the type the cast it
|
|
4416
|
+
// replaces would spell (a load's alone, as for a struct member) — `sp16[5]`, not
|
|
4417
|
+
// `((u8 *)sp16)[5]`. The two are one C value, and not one compile: with a second array at sp+16
|
|
4418
|
+
// whose address agbcc already holds for its `memcpy`, `b[5]` reads `ldrb [r4, #0x5]` through it
|
|
4419
|
+
// while the cast re-forms `sp + 0x15`. Only a part: the one object's bytes are members and spills
|
|
4420
|
+
// of any type, which the cast spells honestly, and a lone array at sp+0 compiles the same either
|
|
4421
|
+
// way. Unqualified only, like the member.
|
|
4422
|
+
const frameArrayElement = (op: Op, off: number, width: number, signed: boolean | null): Expr | null => {
|
|
4423
|
+
const d = defs.get(op.operands[0]);
|
|
4424
|
+
if (d?.opcode !== 'laddr' || d.attrs.part !== true || d.attrs.views !== undefined) {
|
|
4425
|
+
return null;
|
|
3930
4426
|
}
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
4427
|
+
const t = laddrType(d);
|
|
4428
|
+
if (t.kind !== 'array' || t.elem.kind !== 'int' || t.elem.width !== width * 8 || qualifiedBy(op) !== null) {
|
|
4429
|
+
return null;
|
|
4430
|
+
}
|
|
4431
|
+
if (off < 0 || off % width !== 0 || off / width >= (d.attrs.count as number)) {
|
|
4432
|
+
return null;
|
|
4433
|
+
}
|
|
4434
|
+
return signed === null || spellsAccessType(t.elem.signed, width, signed)
|
|
4435
|
+
? {
|
|
4436
|
+
k: 'index',
|
|
4437
|
+
base: { k: 'var', name: laddrName.get(d)! },
|
|
4438
|
+
idx: { k: 'const', value: off / width },
|
|
4439
|
+
width,
|
|
4440
|
+
signed: t.elem.signed,
|
|
4441
|
+
subscript: true,
|
|
4442
|
+
}
|
|
4443
|
+
: null;
|
|
4444
|
+
};
|
|
4445
|
+
// A FLOATING MEMBER READ AS ITS INTEGER BITS is spelled `*(s32 *)&s`, which C converts wherever
|
|
4446
|
+
// a float is what the reader takes (`gF = *(s32 *)&s;` against `float gF` is `bl __floatsisf` on
|
|
4447
|
+
// agbcc, a `cvt` on MIPS), which the bytes never did. So such a read is spelled only where every
|
|
4448
|
+
// reader is a store into an lvalue whose integer type this lift states (`floatBitsStoreTarget`):
|
|
4449
|
+
// a whole-struct copy lowered to words, `((s32 *)&gV3)[1] = ((s32 *)&s)[1];`, passes the bits
|
|
4450
|
+
// through. Any other reader (a named global, a declared member, a call, a return, a join)
|
|
4451
|
+
// declines. A store of bits into such a member is the bits, and keeps the cast.
|
|
4452
|
+
const floatMemberBits = (op: Op): { name: string; width: number } | null => {
|
|
4453
|
+
if (op.opcode !== 'load') {
|
|
4454
|
+
return null;
|
|
4455
|
+
}
|
|
4456
|
+
const width = op.attrs.width as number;
|
|
4457
|
+
const t = defs.get(op.operands[0])?.results[0].type;
|
|
4458
|
+
const f = t?.kind === 'ptr' && t.to.kind === 'struct' ? t.to.fields.find((m) => m.off === op.attrs.off) : undefined;
|
|
4459
|
+
return f?.type.kind === 'float' && f.type.width === width * 8 ? { name: f.name, width } : null;
|
|
4460
|
+
};
|
|
4461
|
+
const refuseFloatBits = (m: { name: string; width: number }): never => {
|
|
4462
|
+
throw new StructureError(
|
|
4463
|
+
`a ${m.width}-byte read of the floating member '${m.name}' of a struct local is an integer here, and C ` +
|
|
4464
|
+
'would convert it where a float is read — not modelled',
|
|
4465
|
+
);
|
|
4466
|
+
};
|
|
4467
|
+
const refuseFloatMemberRead = (op: Op): void => {
|
|
4468
|
+
const m = floatMemberBits(op);
|
|
4469
|
+
const v = op.results[0];
|
|
4470
|
+
const storedOnly = (useSitesOf.get(v) ?? []).every(
|
|
4471
|
+
({ op: u }) =>
|
|
4472
|
+
(u.opcode === 'store' && u.operands[1] === v && u.operands[0] !== v) ||
|
|
4473
|
+
(u.opcode === 'astore' && u.operands[2] === v && u.operands[0] !== v && u.operands[1] !== v),
|
|
4474
|
+
);
|
|
4475
|
+
if (m !== null && !storedOnly) {
|
|
4476
|
+
refuseFloatBits(m);
|
|
4477
|
+
}
|
|
4478
|
+
};
|
|
4479
|
+
// …and the store side: the lvalue must be one whose type the lift spells, an integer of the
|
|
4480
|
+
// read's width: a cast cell (`((s32 *)&g)[1]`), or a local's or a parameter's cell. A bare global
|
|
4481
|
+
// (`gF = …`, `gA[i] = …`) has the project's type, whatever that is.
|
|
4482
|
+
const floatBitsStoreTarget = (value: Value, lval: Expr): void => {
|
|
4483
|
+
const d = defs.get(value);
|
|
4484
|
+
const m = d === undefined ? null : floatMemberBits(d);
|
|
4485
|
+
if (m === null) {
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4488
|
+
let root: Expr = lval;
|
|
4489
|
+
while (root.k === 'index' || root.k === 'field') {
|
|
4490
|
+
root = root.base;
|
|
4491
|
+
}
|
|
4492
|
+
const t = ctype(lval);
|
|
4493
|
+
const own = root.k !== 'var' || varType.has(root.name);
|
|
4494
|
+
if (!own || (lval.k !== 'index' && lval.k !== 'field') || t?.kind !== 'int' || t.width !== m.width * 8) {
|
|
4495
|
+
refuseFloatBits(m);
|
|
4496
|
+
}
|
|
4497
|
+
};
|
|
4498
|
+
// A STRUCT LOCAL'S BYTES STORED INTO A BARE ARRAY ELEMENT keep the cast cell. The bare `gA[i]`
|
|
4499
|
+
// (globalaccess.ts `bareArrayLead`) checks the element's width, not its kind, and the bytes of
|
|
4500
|
+
// `gA[i] = mk(x);` over `struct S gA[4]` are the bytes of a scalar array's store: `gA[a1] =
|
|
4501
|
+
// *(s32 *)&mk_ret;` assigns an int to the struct element (agbcc: incompatible types in
|
|
4502
|
+
// assignment). `((s32 *)&gA)[a1]` addresses the same bytes under any declaration. A recovered row
|
|
4503
|
+
// subscript (`gA[r][i]`) has no flat index here to put in the cell, so it declines.
|
|
4504
|
+
// ONLY an array-declared symbol: a store through a POINTER global's value (`gIP[i] = s.x`) has
|
|
4505
|
+
// the same `var` base, and `&gIP` would address the pointer variable, not its pointee.
|
|
4506
|
+
const structBytesCell = (value: Value, lval: Expr): Expr => {
|
|
4507
|
+
const d = defs.get(value);
|
|
4508
|
+
const local = d?.opcode === 'load' ? defs.get(d.operands[0]) : undefined;
|
|
4509
|
+
const t = local?.results[0].type;
|
|
4510
|
+
const fromStruct = local?.opcode === 'laddr' && t?.kind === 'ptr' && t.to.kind === 'struct';
|
|
4511
|
+
if (
|
|
4512
|
+
!fromStruct ||
|
|
4513
|
+
lval.k !== 'index' ||
|
|
4514
|
+
lval.base.k !== 'var' ||
|
|
4515
|
+
varType.has(lval.base.name) ||
|
|
4516
|
+
symCtx?.info(lval.base.name)?.shape !== 'array'
|
|
4517
|
+
) {
|
|
4518
|
+
return lval;
|
|
4519
|
+
}
|
|
4520
|
+
if (lval.lead?.some((l) => l.k !== 'const' || l.value !== 0)) {
|
|
4521
|
+
throw new StructureError(
|
|
4522
|
+
`a struct local's bytes stored into an element of '${lval.base.name}' by a row subscript have no cast cell to spell`,
|
|
4523
|
+
);
|
|
4524
|
+
}
|
|
4525
|
+
const { lead: _lead, ...cell } = lval;
|
|
4526
|
+
return { ...cell, base: { k: 'addr', name: lval.base.name } };
|
|
4527
|
+
};
|
|
4528
|
+
// …and with no qualifier: `pinnedAccess` would put one on through a cast, the very spelling, and
|
|
4529
|
+
// the audit refuses the union where its accesses would need one (`unionRefusal`)
|
|
4530
|
+
const unqualifiedView = (op: Op, viewed: Expr): Expr => {
|
|
4531
|
+
if (qualifiedBy(op) !== null) {
|
|
4532
|
+
throw new Error(
|
|
4533
|
+
`internal: '${fn.name}': a ${placedAt(op)} ${op.opcode} on a frame union must stay volatile, ` +
|
|
4534
|
+
'which no view of the union carries',
|
|
4535
|
+
);
|
|
4536
|
+
}
|
|
4537
|
+
return viewed;
|
|
4538
|
+
};
|
|
4539
|
+
|
|
4540
|
+
// Lower ONE def's operation to an Expr, rendering operands through `e`. Shared between the
|
|
4541
|
+
// inline-at-use path (exprWith) and the materialized-temp path (sideEffects), so both spell a
|
|
4542
|
+
// given op identically.
|
|
4543
|
+
const lowerDef = (d: Op, e: (v: Value) => Expr): Expr => {
|
|
4544
|
+
if (d.opcode === 'const') {
|
|
4545
|
+
return { k: 'const', value: d.attrs.value as number };
|
|
4546
|
+
}
|
|
4547
|
+
if (d.opcode === 'fconst') {
|
|
4548
|
+
return { k: 'fconst', bits: d.attrs.bits as string };
|
|
4549
|
+
}
|
|
4550
|
+
// a bitfield extract recognized over the ops (see the precompute above): the member read,
|
|
4551
|
+
// not the shift pair
|
|
4552
|
+
const bf = bitfieldSpelling.get(d);
|
|
4553
|
+
if (bf) {
|
|
4554
|
+
return { k: 'field', base: { k: 'var', name: bf.global }, name: bf.field, dot: true };
|
|
4555
|
+
}
|
|
4556
|
+
if (FLOAT_TO_BIN[d.opcode]) {
|
|
4557
|
+
return { k: 'bin', op: FLOAT_TO_BIN[d.opcode], l: e(d.operands[0]), r: e(d.operands[1]) };
|
|
4558
|
+
}
|
|
4559
|
+
if (d.opcode === 'fneg') {
|
|
4560
|
+
return { k: 'un', op: 'f-', e: e(d.operands[0]) };
|
|
4561
|
+
}
|
|
4562
|
+
if (FCMP_TO_BIN[d.opcode] || FCMP_TO_BIN[NEGATED_FCMP[d.opcode] ?? '']) {
|
|
4563
|
+
const op = FCMP_TO_BIN[d.opcode] ?? FCMP_TO_BIN[NEGATED_FCMP[d.opcode]];
|
|
4564
|
+
const spelled = typeof d.attrs.relation === 'string' ? FCMP_TO_BIN[d.attrs.relation] : undefined;
|
|
4565
|
+
const rel: Expr = {
|
|
4566
|
+
k: 'bin',
|
|
4567
|
+
op,
|
|
4568
|
+
l: e(d.operands[0]),
|
|
4569
|
+
r: e(d.operands[1]),
|
|
4570
|
+
...(spelled !== undefined ? { helperRel: spelled } : {}),
|
|
4571
|
+
};
|
|
4572
|
+
return FCMP_TO_BIN[d.opcode] ? rel : { k: 'un', op: '!', e: rel };
|
|
4573
|
+
}
|
|
4574
|
+
// A conversion is C's cast to the type it converts to. Converting an integer, the cast's
|
|
4575
|
+
// operand must BE signed, because C converts an unsigned one with the unsigned conversion,
|
|
4576
|
+
// which agbcc open-codes and an FPU computes differently: the pin `icmp_s*` takes below. A cast
|
|
4577
|
+
// does not promote its operand, so a `u8` or `u16` name is unsigned here, where it would
|
|
4578
|
+
// promote to `int` in arithmetic: agbcc compiles `(float)c` over an `unsigned char` as the
|
|
4579
|
+
// unsigned conversion and `(float)(s32)c` as `__floatsisf`.
|
|
4580
|
+
if (FLOAT_TO_INT.has(d.opcode)) {
|
|
4581
|
+
const w = intWidth(d.results[0].type) === WIDE_BITS ? WIDE_BITS : 32;
|
|
4582
|
+
return { k: 'cast', to: T.int(w, d.opcode === 'fptosi'), e: e(d.operands[0]) };
|
|
4583
|
+
}
|
|
4584
|
+
if (INT_TO_FLOAT.has(d.opcode)) {
|
|
4585
|
+
const x = e(d.operands[0]);
|
|
4586
|
+
const own = ['var', 'postincr', 'cast', 'index', 'field'].includes(x.k) ? exprCType(x, vtEnv) : undefined;
|
|
4587
|
+
const isSigned = own?.kind === 'int' && own.width < 32 ? own.signed : renderedIntSignedness(x, vtEnv) === true;
|
|
4588
|
+
const signed: Expr = isSigned ? x : { k: 'cast', to: T.s(exprIntWidth(x, vtEnv)), e: x };
|
|
4589
|
+
return { k: 'cast', to: d.results[0].type, e: signed };
|
|
4590
|
+
}
|
|
4591
|
+
if (FLOAT_RESIZE[d.opcode]) {
|
|
4592
|
+
return { k: 'cast', to: d.results[0].type, e: e(d.operands[0]) };
|
|
4593
|
+
}
|
|
4594
|
+
if (CMP_TO_BIN[d.opcode]) {
|
|
4595
|
+
// A bare global address `&gSym` as a COMPARISON operand is the same unspelled escape as the
|
|
4596
|
+
// arithmetic case (pointer-spelling.ts ARITH_ROWS): its C type comes from the PROJECT's own
|
|
4597
|
+
// declaration, unknowable here. Worse, the compare's SIGNEDNESS lives in the operand types
|
|
4598
|
+
// (CMP_TO_BIN maps icmp_ult and icmp_slt to the same '<'), so leaving `&gSym` untyped lets
|
|
4599
|
+
// the project's declaration pick the compare the compiler emits — silently byte-inexact
|
|
4600
|
+
// whenever it disagrees with the asm. The honest spelling is integer math on the address
|
|
4601
|
+
// with the cast AGREEING with the opcode's signedness: unsigned compares (and the
|
|
4602
|
+
// sign-agnostic ==/!=) spell `(u32)&gSym`, signed compares `(s32)&gSym` — exactly the
|
|
4603
|
+
// compare the asm did. The deref folds never see a compare operand, so no named spelling is
|
|
4604
|
+
// lost; a NARROWING cast (`(u8)&gSym`) is not a bare `addr` and keeps its truncation.
|
|
4605
|
+
// SCOPE: this handles BARE addr operands. An addr-carrying arithmetic tree
|
|
4606
|
+
// (`(u32)&gSym + 4`, spelled by the arithmetic table) renders unsigned and would compare
|
|
4607
|
+
// unsigned under an icmp_s*; the signed operand pin at the end of this block catches it as
|
|
4608
|
+
// one case of the general rule, needing no addr-specific reasoning.
|
|
4609
|
+
const t = /^icmp_s/.test(d.opcode) ? T.s(32) : T.u(32);
|
|
4610
|
+
const intifyAddrCmp = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: t, e: x } : x);
|
|
4611
|
+
let l = intifyAddrCmp(e(d.operands[0]));
|
|
3949
4612
|
let r = intifyAddrCmp(e(d.operands[1]));
|
|
3950
4613
|
// The same signedness hole for ORDINARY operands: an icmp_u* whose operands both render
|
|
3951
4614
|
// as signed-promoting C (an s32-declared var carrying a u32 value — declarations take the
|
|
@@ -3964,15 +4627,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
3964
4627
|
const t2 = ctype(x);
|
|
3965
4628
|
return t2?.kind === 'ptr' || t2?.kind === 'array';
|
|
3966
4629
|
};
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
) {
|
|
4630
|
+
// `pinned` places the cast (l3/typing.ts `spelledNonNegative`). Whether an uncast spelling
|
|
4631
|
+
// compares different values is not decided here: the compare carries `unsignedCmp` evidence,
|
|
4632
|
+
// and rank.ts asks `signedOverUnsignedCompare` of the finished tree, after every pass that
|
|
4633
|
+
// could substitute into it. The two differ on an `|`/`^` of non-negative sides, which stays
|
|
4634
|
+
// pinned (the pin is codegen-visible) and owes no proof uncast.
|
|
4635
|
+
const unsignedOrdering = /^icmp_u(lt|le|gt|ge)$/.test(d.opcode);
|
|
4636
|
+
const pinned = /^icmp_u/.test(d.opcode) && rendersSignedOverUnsigned(l, r, vtEnv, spelledNonNegative);
|
|
4637
|
+
if (pinned && unsignedCompareSpelling) {
|
|
3976
4638
|
const irUnsigned = (v: Value): boolean => v.type.kind === 'int' && !v.type.signed;
|
|
3977
4639
|
// AT THE OPERAND'S OWN RANK: `(u32)a` over a 64-bit `a` pins nothing, it truncates the
|
|
3978
4640
|
// compare to its low half. `exprIntWidth` is total and answers 64 only where it proves it.
|
|
@@ -4020,298 +4682,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4020
4682
|
r = { k: 'cast', to: T.u(32), e: r };
|
|
4021
4683
|
}
|
|
4022
4684
|
}
|
|
4023
|
-
return { k: 'bin', op: CMP_TO_BIN[d.opcode], l, r };
|
|
4685
|
+
return { k: 'bin', op: CMP_TO_BIN[d.opcode], l, r, ...(unsignedOrdering ? { unsignedCmp: true as const } : {}) };
|
|
4024
4686
|
}
|
|
4025
4687
|
if (ARITH_TO_BIN[d.opcode]) {
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
// Commutative LOAD-PAIR operands re-spell in EVALUATION order. A commutative instruction's
|
|
4029
|
-
// operand order is an allocator artifact (`mul r0, r0, r2` reads dst-first, so the lift's
|
|
4030
|
-
// l/r is whichever load landed in the dst), but the order the compiler EVALUATED the
|
|
4031
|
-
// operands is still visible: their defs' order in the instruction stream. gcc 2.9 and IDO
|
|
4032
|
-
// both emit `w * h`'s loads w-first, so def order IS source order — verified byte-identical
|
|
4033
|
-
// on both (the bg_area rows). Scope, one gate per way the signal fails: BOTH root defs must
|
|
4034
|
-
// be same-block memory reads (a const already has its side; arithmetic defs get combined
|
|
4035
|
-
// out of source order — an ldmia-fed add reads def-reordered; cross-block positions do not
|
|
4036
|
-
// order evaluation), neither stamped `listOrder` (an ldmia-expanded load's own position is
|
|
4037
|
-
// LIST order), both operand VALUES un-named (see below), no pointer side (load-bearing for
|
|
4038
|
-
// the stride rules below), and no effect moves (call, marker). A global's value that the
|
|
4039
|
-
// pointer-value rule below spells as a base is a pointer side too, though `ctype` types no
|
|
4040
|
-
// global: that rule keeps the add's operand order, which a swap here would undo.
|
|
4041
|
-
const ptrGlobalSide = (x: Expr): boolean =>
|
|
4042
|
-
x.k === 'var' &&
|
|
4043
|
-
mapUndeclared(x.name) &&
|
|
4044
|
-
(pointerLoadedGlobals.has(x.name) || (d.results[0]?.type.kind === 'ptr' && wordLoadedGlobals.has(x.name)));
|
|
4045
|
-
if (COMMUTATIVE_BIN.has(ARITH_TO_BIN[d.opcode]) && d.operands.length === 2) {
|
|
4046
|
-
const [da, db] = [defs.get(d.operands[0]), defs.get(d.operands[1])];
|
|
4047
|
-
if (
|
|
4048
|
-
defOrderLoadPairs &&
|
|
4049
|
-
da &&
|
|
4050
|
-
db &&
|
|
4051
|
-
(da.opcode === 'load' || da.opcode === 'aload') &&
|
|
4052
|
-
(db.opcode === 'load' || db.opcode === 'aload') &&
|
|
4053
|
-
da.attrs.listOrder !== true &&
|
|
4054
|
-
db.attrs.listOrder !== true &&
|
|
4055
|
-
// NAMED values decline: a value that renders as a name here — a materialized def
|
|
4056
|
-
// (varName), a loop-carried value in a post-loop region (activeSub) — was evaluated at
|
|
4057
|
-
// its def statement, so re-ordering the reference re-orders nothing and only churns the
|
|
4058
|
-
// spelling away from the machine order the allocator saw. An inlined def — a deref, a
|
|
4059
|
-
// field, a bare scalar global (whose `var` node lowerDef itself mints) — evaluates at
|
|
4060
|
-
// THIS site, wherever recovery spells it.
|
|
4061
|
-
!varName.has(d.operands[0]) &&
|
|
4062
|
-
!varName.has(d.operands[1]) &&
|
|
4063
|
-
activeSub?.has(d.operands[0]) !== true &&
|
|
4064
|
-
activeSub?.has(d.operands[1]) !== true &&
|
|
4065
|
-
ctype(l)?.kind !== 'ptr' &&
|
|
4066
|
-
ctype(r)?.kind !== 'ptr' &&
|
|
4067
|
-
!ptrGlobalSide(l) &&
|
|
4068
|
-
!ptrGlobalSide(r) &&
|
|
4069
|
-
!exprHasEffect(l) &&
|
|
4070
|
-
!exprHasEffect(r) &&
|
|
4071
|
-
opBlock.get(da) === opBlock.get(db) &&
|
|
4072
|
-
opIndex.get(da)! > opIndex.get(db)!
|
|
4073
|
-
) {
|
|
4074
|
-
[l, r] = [r, l];
|
|
4075
|
-
}
|
|
4076
|
-
}
|
|
4077
|
-
// Pointer stride: C pointer arithmetic is ELEMENT-scaled, but the asm added a BYTE
|
|
4078
|
-
// constant — `addi p,4` on an `s32*` walks 1 element, yet C `p + 4` walks 4. Divide the byte
|
|
4079
|
-
// constant by the pointee size so the walk recompiles to the same address math.
|
|
4080
|
-
//
|
|
4081
|
-
// Keyed on the operand's RENDERED C type, never the IR value's recovered type: C scales by
|
|
4082
|
-
// the type of the expression it actually sees, and the two diverge exactly like memAccess's
|
|
4083
|
-
// deref bases (a value recovered `s32*` can render as an int-typed tree — C then does NO
|
|
4084
|
-
// element scaling, so pre-dividing the constant would bake in a WRONG address that the
|
|
4085
|
-
// deref cast downstream turns into silently-wrong bytes).
|
|
4086
|
-
// An int-rendered walk keeps its raw byte constant and derefs through the access-width cast.
|
|
4087
|
-
// Fires only for a rendered pointer whose element size (>1) DIVIDES the constant exactly;
|
|
4088
|
-
// otherwise raw (a misaligned/struct-array stride is left as-is; a `u8*` is size 1 so
|
|
4089
|
-
// unchanged). Since C `(K/es) + p == p + (K/es)`, scaling the const on whichever side it
|
|
4090
|
-
// sits fixes the bytes: `add` is commutative so the pointer may be either operand; `sub` is
|
|
4091
|
-
// not, so only operand[0] (the minuend) may be the pointer.
|
|
4092
|
-
// A rendered pointer that CANNOT express the byte constant as a whole number of elements —
|
|
4093
|
-
// an inexact residual, or a pointee whose size is not knowable here (a struct) — has no
|
|
4094
|
-
// scaled spelling at all, and leaving the raw byte count is the silent wrongness the
|
|
4095
|
-
// pointer-global rule below refuses in the same words: C multiplies it back, so `p + 62` on
|
|
4096
|
-
// an `s32 *` addresses byte 248 and `p + 38` on a `struct S *` addresses byte 38 * sizeof(S).
|
|
4097
|
-
// Same answer as there — CAST THEN ADD, `(u8 *)p + 62`, the same address in every world.
|
|
4098
|
-
const bytePtr = (x: Expr): Expr => ({ k: 'cast', to: T.ptr(T.u(8)), e: x });
|
|
4099
|
-
// Set when a byte-pointer cast was applied for the ADDRESS math alone: the sum is cast back
|
|
4100
|
-
// to the pointer type it started as, so the walk changes the arithmetic and nothing else. A
|
|
4101
|
-
// bare `u8 *` sum would be a different C type from the slot it lands in (`v4 = (u8 *)a0 +
|
|
4102
|
-
// (v1 << 2)` into an `s32 *` — an mwcc error) and from the bases the deref rules read.
|
|
4103
|
-
let restoreTo: IrType | undefined;
|
|
4104
|
-
const walk = (base: Expr, c: Extract<Expr, { k: 'const' }>): { base: Expr; off: Expr } => {
|
|
4105
|
-
const t = ctype(base);
|
|
4106
|
-
if (t?.kind !== 'ptr') {
|
|
4107
|
-
return { base, off: c }; // an int-rendered walk: C scales nothing, the bytes are right
|
|
4108
|
-
}
|
|
4109
|
-
const es = ptrElemBytes(t.to);
|
|
4110
|
-
if (es === 1) {
|
|
4111
|
-
return { base, off: c }; // already a byte pointer
|
|
4112
|
-
}
|
|
4113
|
-
return es > 1 && c.value % es === 0
|
|
4114
|
-
? { base, off: { k: 'const', value: c.value / es } }
|
|
4115
|
-
: { base: bytePtr(base), off: c };
|
|
4116
|
-
};
|
|
4117
|
-
// A RUNTIME byte offset has no element spelling at all — not even an inexact one to reject,
|
|
4118
|
-
// since the residual is unknown until the program runs. The asm added bytes, so the same
|
|
4119
|
-
// answer as the inexact constant above: cast then add. Without it a `u16 *` walked by a
|
|
4120
|
-
// computed offset addresses TWICE the intended byte, and nothing downstream can see the
|
|
4121
|
-
// error — in the sa3 decomp that address is what a `CpuSet` call writes THROUGH.
|
|
4122
|
-
//
|
|
4123
|
-
// `ptr - ptr` is C's ELEMENT difference, where the asm subtracted bytes: agbcc compiles `(q -
|
|
4124
|
-
// p) + m` on an `int *` to `sub; asr #2; add`, and spelled `(v0 - a0) >> 2` the lift divided
|
|
4125
|
-
// by 4 twice. Both sides go byte pointers, `(u8 *)v0 - (u8 *)a0`, the byte count in every
|
|
4126
|
-
// world — unless both already are.
|
|
4127
|
-
//
|
|
4128
|
-
// The inexact-CONSTANT branch above casts its base and does not cast the sum back: a deref
|
|
4129
|
-
// supplies its own cast, and a temp of another pointer type takes the sum through
|
|
4130
|
-
// intoDeclaredTemp's cast.
|
|
4131
|
-
const walkVar = (x: Expr): IrType | undefined => {
|
|
4132
|
-
const t = ctype(x);
|
|
4133
|
-
return t?.kind === 'ptr' && ptrElemBytes(t.to) !== 1 ? t : undefined;
|
|
4134
|
-
};
|
|
4135
|
-
if ((d.opcode === 'add' || d.opcode === 'sub') && r.k === 'const') {
|
|
4136
|
-
({ base: l, off: r } = walk(l, r));
|
|
4137
|
-
} else if (d.opcode === 'add' && d.operands.length === 2 && l.k === 'const') {
|
|
4138
|
-
({ base: r, off: l } = walk(r, l)); // commuted `const + ptr`
|
|
4139
|
-
} else if (d.opcode === 'add' || d.opcode === 'sub') {
|
|
4140
|
-
// ONE side only. `ptr - ptr` is C's element difference and `ptr + ptr` is not C at all;
|
|
4141
|
-
// both are the intify rules' business below, and casting both operands here would hide
|
|
4142
|
-
// the shape from them.
|
|
4143
|
-
const lp = ctype(l)?.kind === 'ptr';
|
|
4144
|
-
const rp = d.operands.length === 2 && ctype(r)?.kind === 'ptr';
|
|
4145
|
-
if (lp && rp && d.opcode === 'sub') {
|
|
4146
|
-
if (walkVar(l) || walkVar(r)) {
|
|
4147
|
-
l = bytePtr(l);
|
|
4148
|
-
r = bytePtr(r);
|
|
4149
|
-
}
|
|
4150
|
-
} else if (lp && !rp) {
|
|
4151
|
-
restoreTo = walkVar(l);
|
|
4152
|
-
l = restoreTo ? bytePtr(l) : l;
|
|
4153
|
-
} else if (rp && !lp && d.opcode === 'add') {
|
|
4154
|
-
restoreTo = walkVar(r);
|
|
4155
|
-
r = restoreTo ? bytePtr(r) : r;
|
|
4156
|
-
}
|
|
4157
|
-
}
|
|
4158
|
-
// C rejects a pointer operand outright under the non-additive operators (& | ^ << >> * / %),
|
|
4159
|
-
// under `ptr + ptr`, and as the subtrahend of `int - ptr` — the asm just does 32-bit integer
|
|
4160
|
-
// math on the address, so the honest spelling is the value cast to its integer self. Only a
|
|
4161
|
-
// DEFINITELY-pointer rendering is cast (same conservative direction as memAccess); the
|
|
4162
|
-
// additive ops keep C's legal pointer arithmetic untouched.
|
|
4163
|
-
const op = ARITH_TO_BIN[d.opcode];
|
|
4164
|
-
const intify = (x: Expr): Expr =>
|
|
4165
|
-
restoredIntSum(x, false) ?? (ctype(x)?.kind === 'ptr' ? { k: 'cast', to: T.s(32), e: x } : x);
|
|
4166
|
-
if (!['+', '-', '&&', '||'].includes(op)) {
|
|
4167
|
-
l = intify(l);
|
|
4168
|
-
r = intify(r);
|
|
4169
|
-
} else if (op === '+' && ctype(l)?.kind === 'ptr' && ctype(r)?.kind === 'ptr') {
|
|
4170
|
-
// ptr + ptr is not C; ptr + (s32)ptr is, and C scales it by the left pointee. So the left
|
|
4171
|
-
// side is the base, and the sum goes back to its type as the walk's does.
|
|
4172
|
-
r = intify(r);
|
|
4173
|
-
if (!restoreTo) {
|
|
4174
|
-
restoreTo = walkVar(l);
|
|
4175
|
-
l = restoreTo ? bytePtr(l) : l;
|
|
4176
|
-
}
|
|
4177
|
-
} else if (op === '-' && ctype(l)?.kind !== 'ptr' && ctype(r)?.kind === 'ptr') {
|
|
4178
|
-
// int - ptr is not C. A byte sum this rule made integer leaves a global it is taken from a
|
|
4179
|
-
// pointer under a pointer declaration, which the integer would scale; a word, it is the
|
|
4180
|
-
// asm's under any integer or pointer one.
|
|
4181
|
-
if (restoredIntSum(r, false) !== undefined) {
|
|
4182
|
-
l = intWords(l);
|
|
4183
|
-
}
|
|
4184
|
-
r = intify(r);
|
|
4185
|
-
}
|
|
4186
|
-
// A bare global address `&gSym` under ANY of these operators is never emitted as-is: its C
|
|
4187
|
-
// type comes from the PROJECT's own declaration (unknowable here — exprCType types `addr`
|
|
4188
|
-
// undefined, so the ptr-keyed intify above never fires on it), which makes `&gSym + K`
|
|
4189
|
-
// byte-INEXACT (C scales K by sizeof(gSym)) and `&gSym & K` ill-formed. The honest spelling
|
|
4190
|
-
// is integer math on the address — `(u32)&gSym + K`, exactly the arithmetic the asm did.
|
|
4191
|
-
// The deref folds (globalOf / globalConstByte, via addrIn) look through this cast, so every
|
|
4192
|
-
// access that CAN spell a named element/field still does; only a genuine value-context
|
|
4193
|
-
// escape (a call argument, a stored address, a compare) keeps it — previously such an
|
|
4194
|
-
// escape tripped assertDerefsTyped's interior-pointer rule and declined the whole function.
|
|
4195
|
-
const intifyAddr = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: T.u(32), e: x } : x);
|
|
4196
|
-
l = intifyAddr(l);
|
|
4197
|
-
r = intifyAddr(r);
|
|
4198
|
-
// The SAME hazard one level down, for a pointer VALUE (`gPtr`, `gSym.pBuf` — isPtrValue):
|
|
4199
|
-
// C scales `gPtr + K` by sizeof(*gPtr) — 1 under the map's synthesized `void *`, but
|
|
4200
|
-
// whatever the PROJECT's header declares (a `u16 *` member, a 0x5C-byte struct) in the
|
|
4201
|
-
// world a user actually recompiles in. The asm added BYTES, so the honest
|
|
4202
|
-
// spelling makes the stride explicit: CAST-THEN-ADD, `(u8 *)gPtr + K`, the same address in
|
|
4203
|
-
// EVERY world. Add-then-cast (`(u8 *)(gPtr + K)`, what the backend's deref legalization
|
|
4204
|
-
// would otherwise produce) is byte-correct in exactly one of them — a silent wrongness, the
|
|
4205
|
-
// class this project refuses. A MEMBER is the case with no world in which the raw spelling
|
|
4206
|
-
// is right: the map declares the pointee width, so `bytes + gSym.pBuf` on a `u16 *` scales
|
|
4207
|
-
// the residual a SECOND time and addresses twice the byte the asm did.
|
|
4208
|
-
// NOT foldable into the deref index either: `((u8 *)gPtr)[K + off]` re-scales K by the
|
|
4209
|
-
// ACCESS width, a different address whenever that width is not 1.
|
|
4210
|
-
// Under the non-additive operators C rejects a pointer outright, so there the honest
|
|
4211
|
-
// spelling is integer math on the cell — exactly intifyAddr's `(u32)&gSym` rule.
|
|
4212
|
-
const intifyPtrValue = globalWord;
|
|
4213
|
-
if (op === '+' || op === '-') {
|
|
4214
|
-
// `ptr ± int` and `ptr - ptr` are byte arithmetic once both sides are byte pointers;
|
|
4215
|
-
// `ptr + ptr` and `int - ptr` are not C at all, so the second pointer goes integer. The
|
|
4216
|
-
// other side is a pointer when it is a pointer value too or when it already renders one
|
|
4217
|
-
// (a pointer temp, the walk's `(u8 *)v1`), and then the global's value is the side that
|
|
4218
|
-
// goes integer.
|
|
4219
|
-
//
|
|
4220
|
-
// A global's value no declaration types, added to an integer, is a pointer value too
|
|
4221
|
-
// when the IR types the SUM a pointer: `(u8 *)g + x` is the asm's address under every
|
|
4222
|
-
// declaration of `g` once `x` renders an integer, whichever operand the source held as
|
|
4223
|
-
// the pointer. An integer side that IS an address (`(u32)&gArr + gIdx`) says the address
|
|
4224
|
-
// is the base and the global its index, so that sum stays as it is. An address under a
|
|
4225
|
-
// load (`((u16 *)&gTbl)[a0]`) is a loaded value, and that side is an integer. A global
|
|
4226
|
-
// that is no pointer value renders an integer too, though `ctype` types no global.
|
|
4227
|
-
const isAddr = (x: Expr): boolean =>
|
|
4228
|
-
x.k === 'addr' ||
|
|
4229
|
-
(x.k === 'cast' && isAddr(x.e)) ||
|
|
4230
|
-
(x.k === 'bin' && (x.op === '+' || x.op === '-') && (isAddr(x.l) || isAddr(x.r)));
|
|
4231
|
-
const intSide = (x: Expr): boolean =>
|
|
4232
|
-
!isAddr(x) && (ctype(x)?.kind === 'int' || (x.k === 'var' && ctype(x) === undefined && !isPtrValue(x)));
|
|
4233
|
-
const sumBase =
|
|
4234
|
-
d.results[0]?.type.kind === 'ptr' && !isPtrValue(l) && !isPtrValue(r)
|
|
4235
|
-
? isUndeclaredGlobalValue(l) && intSide(r)
|
|
4236
|
-
? l
|
|
4237
|
-
: op === '+' && isUndeclaredGlobalValue(r) && intSide(l)
|
|
4238
|
-
? r
|
|
4239
|
-
: undefined
|
|
4240
|
-
: undefined;
|
|
4241
|
-
const ptrValue = (x: Expr): boolean => isPtrValue(x) || x === sumBase;
|
|
4242
|
-
// A global no declaration types in the base's partner is added as an integer, which it is
|
|
4243
|
-
// under an integer declaration and a pointer one alike.
|
|
4244
|
-
if (sumBase !== undefined) {
|
|
4245
|
-
l = l !== sumBase ? intWords(l) : l;
|
|
4246
|
-
r = r !== sumBase ? intWords(r) : r;
|
|
4247
|
-
}
|
|
4248
|
-
const rendersPtr = (x: Expr): boolean => ptrValue(x) || ctype(x)?.kind === 'ptr';
|
|
4249
|
-
const bothPtr = rendersPtr(l) && rendersPtr(r);
|
|
4250
|
-
// `x + (u8 *)g` is pointer arithmetic, and gcc makes the pointer the first operand of the
|
|
4251
|
-
// add, which swaps the asm's. So under an integer left side, the value of a global no
|
|
4252
|
-
// declaration types, or a byte sum this rule made of one, is added as an integer in the
|
|
4253
|
-
// asm's order, and the cast keeps the sum the byte pointer it would have been. A declared
|
|
4254
|
-
// pointer keeps `x + (u8 *)p`, the operand the element and field spellings read.
|
|
4255
|
-
const undeclaredPtr = (x: Expr): boolean => x.k === 'var' && mapUndeclared(x.name);
|
|
4256
|
-
// The same global LEFT of a runtime offset is added as an integer too: CodeWarrior at -O4
|
|
4257
|
-
// puts the index first in every pointer sum, where an integer sum keeps the source's
|
|
4258
|
-
// order, so `(u8 *)g + x` is the asm's order on agbcc, KMC gcc and IDO only. The partner
|
|
4259
|
-
// goes integer with it, or a pointer partner would scale the sum. A constant offset folds
|
|
4260
|
-
// into the access and keeps `(u8 *)g + K`.
|
|
4261
|
-
const constValued = (x: Expr): boolean =>
|
|
4262
|
-
x.k === 'const' ||
|
|
4263
|
-
(x.k === 'cast' && constValued(x.e)) ||
|
|
4264
|
-
(x.k === 'bin' && constValued(x.l) && constValued(x.r));
|
|
4265
|
-
const intSum = op === '+' && !constValued(r) && ptrValue(l) && undeclaredPtr(l);
|
|
4266
|
-
if (intSum) {
|
|
4267
|
-
l = intifyPtrValue(l);
|
|
4268
|
-
r = ptrValue(r)
|
|
4269
|
-
? intifyPtrValue(r)
|
|
4270
|
-
: ctype(r)?.kind === 'ptr'
|
|
4271
|
-
? isByteGlobalSum(r)
|
|
4272
|
-
? byteSumAsInt(r)
|
|
4273
|
-
: { k: 'cast', to: T.u(32), e: r }
|
|
4274
|
-
: intWords(r);
|
|
4275
|
-
restoreTo ??= T.ptr(T.u(8));
|
|
4276
|
-
} else if (ptrValue(l)) {
|
|
4277
|
-
l = op === '+' && bothPtr && !ptrValue(r) ? intifyPtrValue(l) : bytePtr(l);
|
|
4278
|
-
}
|
|
4279
|
-
if (intSum) {
|
|
4280
|
-
// both operands are integers now
|
|
4281
|
-
} else if (
|
|
4282
|
-
op === '+' &&
|
|
4283
|
-
!bothPtr &&
|
|
4284
|
-
!restoreTo &&
|
|
4285
|
-
((ptrValue(r) && undeclaredPtr(r)) || (ctype(r)?.kind === 'ptr' && isByteGlobalSum(r, true)))
|
|
4286
|
-
) {
|
|
4287
|
-
r = ptrValue(r) ? intifyPtrValue(r) : byteSumAsInt(r);
|
|
4288
|
-
// A left global no declaration types may be a pointer in the project's header, which
|
|
4289
|
-
// would scale the integer sum; as an integer it is the asm's word under any declaration.
|
|
4290
|
-
l = intWords(l);
|
|
4291
|
-
restoreTo = T.ptr(T.u(8));
|
|
4292
|
-
} else if (ptrValue(r)) {
|
|
4293
|
-
r = bothPtr && op === '-' ? bytePtr(r) : op === '+' && !bothPtr ? bytePtr(r) : intifyPtrValue(r);
|
|
4294
|
-
}
|
|
4295
|
-
// A byte sum less an integer sum of globals no declaration types: bare, `gB2 - gB3` is an
|
|
4296
|
-
// element count under a wider pointer declaration of them, where the asm subtracted bytes.
|
|
4297
|
-
if (op === '-' && r.k === 'bin' && !rendersPtr(r) && isByteGlobalSum(l)) {
|
|
4298
|
-
r = intWords(r);
|
|
4299
|
-
}
|
|
4300
|
-
if (op === '-' && ctype(l)?.kind === 'ptr' && ctype(r)?.kind === 'ptr') {
|
|
4301
|
-
restoreTo = undefined; // a byte pointer less a byte pointer is the byte count, an integer
|
|
4302
|
-
}
|
|
4303
|
-
} else if (op !== '&&' && op !== '||') {
|
|
4304
|
-
// (`&&`/`||` take a pointer operand legally — a truth test, no arithmetic.)
|
|
4305
|
-
l = isPtrValue(l) ? intifyPtrValue(l) : l;
|
|
4306
|
-
r = isPtrValue(r) ? intifyPtrValue(r) : r;
|
|
4307
|
-
}
|
|
4308
|
-
// (The signedness-carrying pairs stay DISTINCT ops here — `>>>`/`>>` and `/u` `%u`/`/` `%`.
|
|
4309
|
-
// Which token a language spells each with, and what cast pins the choice, is a BACKEND
|
|
4310
|
-
// decision; see l3/ast.ts BinOp and backend/cfamily.ts's C_SPELLING.)
|
|
4311
|
-
// SCOPE: this and intifyAddr cover the ARITHMETIC escapes. A pointer value under a
|
|
4312
|
-
// COMPARISON (`gPtr < K`, `(u8 *)gPtr + K < a0`) needs nothing here: an icmp_s* pins every
|
|
4313
|
-
// operand that does not provably render signed, a pointer included (pinSigned above).
|
|
4314
|
-
//
|
|
4688
|
+
const l = e(d.operands[0]);
|
|
4689
|
+
const r = d.operands.length === 2 ? e(d.operands[1]) : ({ k: 'const', value: d.attrs.imm as number } as Expr);
|
|
4315
4690
|
// A `+`/`|` over two IR `const`s that both RENDER as literals is the literal it is. After
|
|
4316
4691
|
// pre-recovery there is only one way such an op still exists: `raise/const.ts` refuses to fold
|
|
4317
4692
|
// one shape — a register the compiler held live across a branch, whose value on this path is a
|
|
@@ -4323,7 +4698,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4323
4698
|
// stake, so `const-fold.test.ts` pins this on the emitted STRING. Re-folding HERE rather than
|
|
4324
4699
|
// back in the IR is the point: the pair must survive pre-recovery for the enumeration gate to
|
|
4325
4700
|
// see the merge feed, and only at rendering is it settled that this candidate named neither
|
|
4326
|
-
// half.
|
|
4701
|
+
// half. Two literals are spelled as they are by the arithmetic table, so the fold reads them
|
|
4702
|
+
// before it.
|
|
4327
4703
|
//
|
|
4328
4704
|
// `foldsFromIrConsts` keeps the reach honest and is not redundant with `l`/`r` being `const`
|
|
4329
4705
|
// Exprs: a BLOCK PARAMETER resolved to a constant on this arm also renders as a literal, and
|
|
@@ -4351,12 +4727,52 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4351
4727
|
if (l.k === 'const' && r.k === 'const' && d.operands.every((o) => foldsFromIrConsts(o))) {
|
|
4352
4728
|
const value = foldConstPair(d.opcode, l.value, r.value);
|
|
4353
4729
|
if (value !== null) {
|
|
4354
|
-
|
|
4355
|
-
return restoreTo ? { k: 'cast', to: restoreTo, e: folded } : folded;
|
|
4730
|
+
return { k: 'const', value };
|
|
4356
4731
|
}
|
|
4357
4732
|
}
|
|
4358
|
-
|
|
4359
|
-
|
|
4733
|
+
// Commutative LOAD-PAIR operands re-spell in EVALUATION order. A commutative instruction's
|
|
4734
|
+
// operand order is an allocator artifact (`mul r0, r0, r2` reads dst-first, so the lift's
|
|
4735
|
+
// l/r is whichever load landed in the dst), but the order the compiler EVALUATED the
|
|
4736
|
+
// operands is still visible: their defs' order in the instruction stream. gcc 2.9 and IDO
|
|
4737
|
+
// both emit `w * h`'s loads w-first, so def order IS source order — verified byte-identical
|
|
4738
|
+
// on both (the bg_area rows). Scope, one gate per way the signal fails: BOTH root defs must
|
|
4739
|
+
// be same-block memory reads (a const already has its side; arithmetic defs get combined
|
|
4740
|
+
// out of source order — an ldmia-fed add reads def-reordered; cross-block positions do not
|
|
4741
|
+
// order evaluation), neither stamped `listOrder` (an ldmia-expanded load's own position is
|
|
4742
|
+
// LIST order), both operand VALUES un-named (see below), and no effect moves (call, marker).
|
|
4743
|
+
// Which sides may move is the arithmetic's own decision (structure/pointer-spelling.ts
|
|
4744
|
+
// `pointerSide`, load-bearing for the stride rules), so the pair is handed to it.
|
|
4745
|
+
const [da, db] = [defs.get(d.operands[0]), defs.get(d.operands[1])];
|
|
4746
|
+
const loadPairReversed =
|
|
4747
|
+
COMMUTATIVE_BIN.has(ARITH_TO_BIN[d.opcode]) &&
|
|
4748
|
+
d.operands.length === 2 &&
|
|
4749
|
+
defOrderLoadPairs &&
|
|
4750
|
+
da !== undefined &&
|
|
4751
|
+
db !== undefined &&
|
|
4752
|
+
(da.opcode === 'load' || da.opcode === 'aload') &&
|
|
4753
|
+
(db.opcode === 'load' || db.opcode === 'aload') &&
|
|
4754
|
+
da.attrs.listOrder !== true &&
|
|
4755
|
+
db.attrs.listOrder !== true &&
|
|
4756
|
+
// NAMED values decline: a value that renders as a name here — a materialized def
|
|
4757
|
+
// (varName), a loop-carried value in a post-loop region (activeSub) — was evaluated at
|
|
4758
|
+
// its def statement, so re-ordering the reference re-orders nothing and only churns the
|
|
4759
|
+
// spelling away from the machine order the allocator saw. An inlined def — a deref, a
|
|
4760
|
+
// field, a bare scalar global (whose `var` node lowerDef itself mints) — evaluates at
|
|
4761
|
+
// THIS site, wherever recovery spells it.
|
|
4762
|
+
!varName.has(d.operands[0]) &&
|
|
4763
|
+
!varName.has(d.operands[1]) &&
|
|
4764
|
+
activeSub?.has(d.operands[0]) !== true &&
|
|
4765
|
+
activeSub?.has(d.operands[1]) !== true &&
|
|
4766
|
+
!exprHasEffect(l) &&
|
|
4767
|
+
!exprHasEffect(r) &&
|
|
4768
|
+
opBlock.get(da) === opBlock.get(db) &&
|
|
4769
|
+
opIndex.get(da)! > opIndex.get(db)!;
|
|
4770
|
+
// The operand order, the pointer stride, the integer legalizations and the pointer-value
|
|
4771
|
+
// byte arithmetic: structure/pointer-spelling.ts `arith`. (The signedness-carrying pairs
|
|
4772
|
+
// stay DISTINCT ops — `>>>`/`>>` and `/u` `%u`/`/` `%`. Which token a language spells each
|
|
4773
|
+
// with, and what cast pins the choice, is a BACKEND decision; see l3/ast.ts BinOp and
|
|
4774
|
+
// backend/cfamily.ts's C_SPELLING.)
|
|
4775
|
+
return spellArith(d, l, r, loadPairReversed);
|
|
4360
4776
|
}
|
|
4361
4777
|
// `-`/`~` on a pointer rendering is equally not C — same honest integer cast as above.
|
|
4362
4778
|
if (d.opcode === 'rotr' || d.opcode === 'rotl') {
|
|
@@ -4390,6 +4806,37 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4390
4806
|
r: { k: 'bin', op: far, l: x, r: w },
|
|
4391
4807
|
};
|
|
4392
4808
|
}
|
|
4809
|
+
if (d.opcode === 'neg_s') {
|
|
4810
|
+
// The MATHEMATICAL negation (ir/opcodes.ts), so it must not be spelled over an unsigned 32-bit
|
|
4811
|
+
// operand: `-(u32)b` wraps, and at b == 0x80000000 a signed compare reads `(s32)-b` as INT_MIN
|
|
4812
|
+
// where the machine's sum was +2^31. Negating the operand as s32 instead is exact wherever C
|
|
4813
|
+
// defines it — it is undefined at exactly that one input. The operand keeps its own spelling
|
|
4814
|
+
// where it already renders signed (`renderedIntSignedness`, the rule the icmp_s* operand
|
|
4815
|
+
// pin uses); a cast it did not need costs no byte.
|
|
4816
|
+
//
|
|
4817
|
+
// That one input is a value the operand MIGHT hold. An operand that ALWAYS holds it is a
|
|
4818
|
+
// different thing: every call then lands on the undefined input, and the C a compiler makes
|
|
4819
|
+
// of `-(s32)0x80000000` (INT_MIN again) is not the machine's +2^31. Whether it always does is
|
|
4820
|
+
// what the compiler can FOLD, not which opcodes built it — a pool word, `1 << 31`, a
|
|
4821
|
+
// `sub`/`orr`/`eor` of constants, a param both arms of a diamond set, a reload of a store
|
|
4822
|
+
// nothing can have overwritten, a reload of memory this function wrote on every path through
|
|
4823
|
+
// an address it cannot match — so the question goes to the shared word evaluator, and fails
|
|
4824
|
+
// closed: a constant it cannot compute refuses too.
|
|
4825
|
+
// An operand that is INT_MIN on some paths only still lifts; that is the "might hold" case.
|
|
4826
|
+
// Only hand-written asm reaches the refusal — agbcc folds the negation of a constant.
|
|
4827
|
+
const x = e(d.operands[0]);
|
|
4828
|
+
wordCtx ??= {
|
|
4829
|
+
defs,
|
|
4830
|
+
incoming: incomingArgs(fn.blocks),
|
|
4831
|
+
forwarded: storeForwards(fn.blocks),
|
|
4832
|
+
overwritten: overwrittenReads(fn.blocks),
|
|
4833
|
+
};
|
|
4834
|
+
if (mayAlwaysHold(wordCtx, d.operands[0], 0x80000000)) {
|
|
4835
|
+
return mkGap('the mathematical negation of the constant 0x80000000 has no int spelling', [x]);
|
|
4836
|
+
}
|
|
4837
|
+
const cast = needsIntSpelling(x) || renderedIntSignedness(x, vtEnv) !== true;
|
|
4838
|
+
return { k: 'un', op: '-', e: cast ? { k: 'cast', to: T.s(32), e: x } : x };
|
|
4839
|
+
}
|
|
4393
4840
|
if (d.opcode === 'neg') {
|
|
4394
4841
|
const x = e(d.operands[0]);
|
|
4395
4842
|
return { k: 'un', op: '-', e: needsIntSpelling(x) ? { k: 'cast', to: T.s(32), e: x } : x };
|
|
@@ -4513,12 +4960,36 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4513
4960
|
// assigned to a 32-bit one truncates — but it is a pass this branch did not set out to move.
|
|
4514
4961
|
const wide64 = d.results[0] !== undefined && intWidth(d.results[0].type) === 64;
|
|
4515
4962
|
const through = calleeValue(d);
|
|
4963
|
+
// a struct passed by value is the object its operand addresses — a frame object stamped as
|
|
4964
|
+
// that struct, or a global — spelled bare (frontend/call-plan.ts `foldByValueArgs`)
|
|
4965
|
+
const byValue = byValueOperands(d).map(({ at, param }) => {
|
|
4966
|
+
const o = defs.get(d.operands[at]);
|
|
4967
|
+
const t = d.operands[at].type;
|
|
4968
|
+
const type = t.kind === 'ptr' && t.to.kind === 'struct' ? t.to : undefined;
|
|
4969
|
+
const object =
|
|
4970
|
+
type !== undefined &&
|
|
4971
|
+
(o?.opcode === 'laddr'
|
|
4972
|
+
? o.attrs.aggregate === true
|
|
4973
|
+
: o?.opcode === 'gaddr' && o.attrs.passedAs === structSpelling(type));
|
|
4974
|
+
if (!object) {
|
|
4975
|
+
throw new Error(
|
|
4976
|
+
`internal: a by-value argument of '${d.attrs.target}' is not the address of an object of the struct it passes`,
|
|
4977
|
+
);
|
|
4978
|
+
}
|
|
4979
|
+
return { at, param, type };
|
|
4980
|
+
});
|
|
4981
|
+
const args = callArgs(d).map((v, i): Expr => {
|
|
4982
|
+
const x = e(v);
|
|
4983
|
+
return byValue.some((b) => b.at === i) && x.k === 'addr' ? { k: 'var', name: x.name } : x;
|
|
4984
|
+
});
|
|
4516
4985
|
return {
|
|
4517
4986
|
k: 'call',
|
|
4518
4987
|
fn: through === undefined ? (d.attrs.target as string) : e(through),
|
|
4519
|
-
args
|
|
4988
|
+
args,
|
|
4520
4989
|
...(wide64 ? { wide64: true as const } : {}),
|
|
4521
4990
|
...(d.attrs.sret === true ? { sret: d.results[0].type } : {}),
|
|
4991
|
+
...(d.attrs.sret !== true && d.results[0]?.type.kind === 'struct' ? { returns: d.results[0].type } : {}),
|
|
4992
|
+
...(byValue.length > 0 ? { byValue } : {}),
|
|
4522
4993
|
};
|
|
4523
4994
|
}
|
|
4524
4995
|
if (d.opcode === 'laddr') {
|
|
@@ -4544,6 +5015,35 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4544
5015
|
return { k: 'addr', name: d.attrs.sym as string };
|
|
4545
5016
|
}
|
|
4546
5017
|
if (d.opcode === 'load') {
|
|
5018
|
+
const viewed = frameUnionAccess(
|
|
5019
|
+
d.operands[0],
|
|
5020
|
+
d.attrs.off as number,
|
|
5021
|
+
d.attrs.width as number,
|
|
5022
|
+
(d.attrs.signed as boolean) ?? false,
|
|
5023
|
+
false,
|
|
5024
|
+
);
|
|
5025
|
+
if (viewed !== null) {
|
|
5026
|
+
return unqualifiedView(d, viewed);
|
|
5027
|
+
}
|
|
5028
|
+
const member = frameStructMember(
|
|
5029
|
+
d,
|
|
5030
|
+
d.attrs.off as number,
|
|
5031
|
+
d.attrs.width as number,
|
|
5032
|
+
(d.attrs.signed as boolean) ?? false,
|
|
5033
|
+
);
|
|
5034
|
+
if (member !== null) {
|
|
5035
|
+
return member;
|
|
5036
|
+
}
|
|
5037
|
+
const element = frameArrayElement(
|
|
5038
|
+
d,
|
|
5039
|
+
d.attrs.off as number,
|
|
5040
|
+
d.attrs.width as number,
|
|
5041
|
+
(d.attrs.signed as boolean) ?? false,
|
|
5042
|
+
);
|
|
5043
|
+
if (element !== null) {
|
|
5044
|
+
return element;
|
|
5045
|
+
}
|
|
5046
|
+
refuseFloatMemberRead(d);
|
|
4547
5047
|
const access = memAccess(
|
|
4548
5048
|
d.operands[0],
|
|
4549
5049
|
e(d.operands[0]),
|
|
@@ -4555,6 +5055,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4555
5055
|
symCtx,
|
|
4556
5056
|
false,
|
|
4557
5057
|
advanceStepOf(d.operands[0]),
|
|
5058
|
+
qualifiedBy(d) === 'cast',
|
|
4558
5059
|
);
|
|
4559
5060
|
return pinnedAccess(d, access);
|
|
4560
5061
|
}
|
|
@@ -4646,6 +5147,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4646
5147
|
// which over-declines, the refusing direction.
|
|
4647
5148
|
let zeroTripStale: Map<Value, string> | null = null;
|
|
4648
5149
|
const expr = (v: Value): Expr => exprWith(activeSub)(v);
|
|
5150
|
+
// A return that hands back the function's own struct return object (frontend/own-struct-return.ts)
|
|
5151
|
+
// returns that local by value, `return ret;`, where its address would print `&ret`.
|
|
5152
|
+
const returnedValue = (term: Op): Expr => {
|
|
5153
|
+
const value = expr(term.operands[0]);
|
|
5154
|
+
return term.attrs[RETURNS_OBJECT] === true && value.k === 'addr' ? { k: 'var', name: value.name } : value;
|
|
5155
|
+
};
|
|
4649
5156
|
const withSub = <R>(sub: Map<Value, string>, run: () => R): R => {
|
|
4650
5157
|
const prev = activeSub;
|
|
4651
5158
|
activeSub = prev ? new Map([...prev, ...sub]) : sub;
|
|
@@ -4710,10 +5217,32 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4710
5217
|
const NO_COPIES_AT_A_DEF: ReadonlyMap<Op, Stmt[]> = new Map();
|
|
4711
5218
|
/** The `atDef` copies `sideEffects` has spelled — `assertSunkCopiesPlaced`'s input. */
|
|
4712
5219
|
const sunkCopiesEmitted = new Set<Stmt>();
|
|
4713
|
-
const undefCarriesNothing = (arg: Value, name: string, pred: Block): boolean => {
|
|
4714
|
-
|
|
5220
|
+
const undefCarriesNothing = (arg: Value, name: string, pred: Block, coalescing = false): boolean => {
|
|
5221
|
+
// A `copied` undef is another storage's content moved into this one (`x = y` of two
|
|
5222
|
+
// uninitialised locals): the machine ran that move, so the copy is spelled.
|
|
5223
|
+
if (defs.get(arg)?.opcode !== 'undef' || defs.get(arg)!.attrs.copied === true) {
|
|
4715
5224
|
return false;
|
|
4716
5225
|
}
|
|
5226
|
+
// AN UNDEFINED VALUE READ UNDER ANOTHER NAME IS A RELATION. Dropping the copy leaves `name`
|
|
5227
|
+
// holding whatever it held, which equals the undefined value only where nothing else reads that
|
|
5228
|
+
// value: `int x, z; z = x; if (c) x = 5; return x * 2 + z;` at gcc 2.7.2 keeps x in v0 and
|
|
5229
|
+
// reads it as z too, so on `c == 0` the join's local IS z, and an unassigned `v0` beside
|
|
5230
|
+
// `uninit_v0` loses that. So the copy is dropped only where every other read of the value goes
|
|
5231
|
+
// through `name` itself: the coalesced undef below (which the coalescing test asks before it
|
|
5232
|
+
// has renamed it), or edge copies into joins of the same name.
|
|
5233
|
+
if (!coalescing) {
|
|
5234
|
+
const op = defs.get(arg)!;
|
|
5235
|
+
for (const site of useSitesOf.get(arg) ?? []) {
|
|
5236
|
+
if (site.op.operands.includes(arg) && undefName.get(op) !== name) {
|
|
5237
|
+
return false;
|
|
5238
|
+
}
|
|
5239
|
+
for (const s of site.op.successors) {
|
|
5240
|
+
if (s.args.some((a, i) => a === arg && varName.get(s.block.params[i]) !== name)) {
|
|
5241
|
+
return false;
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
4717
5246
|
const writesBefore = (home: Block | undefined): boolean =>
|
|
4718
5247
|
home === undefined || home === pred || reachFrom(home).has(pred);
|
|
4719
5248
|
for (const [v, n] of varName) {
|
|
@@ -4723,6 +5252,67 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4723
5252
|
}
|
|
4724
5253
|
return !(anchoredHome.get(name) ?? []).some(writesBefore);
|
|
4725
5254
|
};
|
|
5255
|
+
// AN UNDEF ONE JOIN TAKES IS THAT JOIN'S LOCAL, READ BEFORE ITS ASSIGNMENT. `int t; if (k > 5)
|
|
5256
|
+
// g = t; switch (k) { case 0: t = …; } … t …` reads one source variable on both sides of the
|
|
5257
|
+
// switch, and the asm reads one register: the entry value directly, and again through the join
|
|
5258
|
+
// the arms assign. Named from its key it would be two declarations, `uninit_t7` beside the join's
|
|
5259
|
+
// `v9`, which the compiler allocates as two. So the undef takes the join's name when that name is
|
|
5260
|
+
// never written where any of its reads happen: every edge copy into the join is one
|
|
5261
|
+
// `undefCarriesNothing` drops, and every block that reads it — through the ops rendered inline
|
|
5262
|
+
// over it — passes the same test, so each read sees the name before anything assigned it.
|
|
5263
|
+
// A merge that adopted a parameter's name is never one (`v<n>` only). The reads are recorded with
|
|
5264
|
+
// the dropped copies, for the sunk-copy postcondition below to check them the same way.
|
|
5265
|
+
const coalescedUndefs = new Set<Op>();
|
|
5266
|
+
for (const op of fn.blocks.flatMap((b) => b.ops)) {
|
|
5267
|
+
if (op.opcode !== 'undef') {
|
|
5268
|
+
continue;
|
|
5269
|
+
}
|
|
5270
|
+
const u = op.results[0];
|
|
5271
|
+
const joins = new Set<string>();
|
|
5272
|
+
const intoJoin: Block[] = [];
|
|
5273
|
+
const readIn = new Set<Block>();
|
|
5274
|
+
let unnamed = false;
|
|
5275
|
+
for (const site of useSitesOf.get(u) ?? []) {
|
|
5276
|
+
for (const s of site.op.successors) {
|
|
5277
|
+
s.args.forEach((a, i) => {
|
|
5278
|
+
if (a === u) {
|
|
5279
|
+
const n = varName.get(s.block.params[i]);
|
|
5280
|
+
if (n === undefined) {
|
|
5281
|
+
unnamed = true;
|
|
5282
|
+
} else {
|
|
5283
|
+
joins.add(n);
|
|
5284
|
+
}
|
|
5285
|
+
intoJoin.push(site.blk);
|
|
5286
|
+
}
|
|
5287
|
+
});
|
|
5288
|
+
}
|
|
5289
|
+
if (site.op.operands.includes(u)) {
|
|
5290
|
+
const work = [site];
|
|
5291
|
+
for (let w = work.pop(); w !== undefined; w = work.pop()) {
|
|
5292
|
+
readIn.add(w.blk);
|
|
5293
|
+
if (!materialize.has(w.op)) {
|
|
5294
|
+
work.push(...w.op.results.flatMap((r) => useSitesOf.get(r) ?? []));
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
5298
|
+
}
|
|
5299
|
+
const [name] = joins;
|
|
5300
|
+
if (
|
|
5301
|
+
unnamed ||
|
|
5302
|
+
joins.size !== 1 ||
|
|
5303
|
+
readIn.size === 0 ||
|
|
5304
|
+
!/^v\d+$/.test(name) ||
|
|
5305
|
+
globalNames.has(name) ||
|
|
5306
|
+
![...intoJoin, ...readIn].every((b) => undefCarriesNothing(u, name, b, true))
|
|
5307
|
+
) {
|
|
5308
|
+
continue;
|
|
5309
|
+
}
|
|
5310
|
+
undefName.set(op, name);
|
|
5311
|
+
coalescedUndefs.add(op);
|
|
5312
|
+
for (const b of readIn) {
|
|
5313
|
+
droppedUndefCopies.push({ name, pred: b });
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
4726
5316
|
const tempCounter = { n: 0 }; // per-function swap-cycle temp names (sequentialize)
|
|
4727
5317
|
/** One record per parameter slot an edge ACTUALLY carries, in the order the copies are to be
|
|
4728
5318
|
* laid out — everything `argAssignsFor` does except `sequentialize`. Split out because Regime-A
|
|
@@ -4952,127 +5542,32 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
4952
5542
|
// EFFECTFUL op whose result nothing consumes (a void/discarded call, and an `opaque` standing for
|
|
4953
5543
|
// an instruction asmlift could not model); and MATERIALIZED defs — a call/load whose value cannot
|
|
4954
5544
|
// soundly render at its use is assigned to its named temp here, at its own program position.
|
|
4955
|
-
/** A pointer VALUE assigned into a pointer CELL (isPtrValue), spelled so the assignment is legal
|
|
4956
|
-
* against ANY pointer declaration of that cell. The byte-arithmetic guard renders such a
|
|
4957
|
-
* right-hand side `(u8 *)gS.pBuf + K` — the right ADDRESS in every world, which is the whole
|
|
4958
|
-
* point of it, and a `u8 *` where the declaration says `u16 *`. READING one is fine (C converts
|
|
4959
|
-
* an object pointer freely under a deref, a call argument or a compare); ASSIGNING one is
|
|
4960
|
-
* `warning: assignment from incompatible pointer type`, and this project's own `-Werror`
|
|
4961
|
-
* compiler template makes that FATAL — so a source that scores clean here fails to build in the
|
|
4962
|
-
* tree a user pastes it into, which no score gate can observe.
|
|
4963
|
-
*
|
|
4964
|
-
* `void *`, NOT the map's declared pointee: it is the one target assignment-compatible with any
|
|
4965
|
-
* object-pointer declaration, the same "same answer in every world" property the guard that
|
|
4966
|
-
* made the `u8 *` exists for. Trusting the map's pointee would put the spelling back in one
|
|
4967
|
-
* world. It costs nothing: agbcc compiles `p = (void *)((u8 *)p + 4)` and the warning-carrying
|
|
4968
|
-
* `p = (u8 *)p + 4` to BYTE-IDENTICAL objects (`-mthumb-interwork -Wimplicit -O2 -fhex-asm
|
|
4969
|
-
* -fprologue-bugfix`), so the fix is invisible to the differ and visible to the compiler.
|
|
4970
|
-
*
|
|
4971
|
-
* A project may still declare a cell no map types an INTEGER (`u32 gSym;`), where `(void *)`
|
|
4972
|
-
* makes an integer from a pointer: agbcc warns, CodeWarrior rejects it. The candidate's own world
|
|
4973
|
-
* declares the cell `void *` (l3/symbol-refs.ts), and `/int-cell` (l3/intcell.ts) spells the
|
|
4974
|
-
* integer declaration's source, published only at a byte-exact score. */
|
|
4975
|
-
const intoPtrCell = (lval: Expr, value: Expr): Expr => {
|
|
4976
|
-
if (!isPtrValue(lval)) {
|
|
4977
|
-
return value;
|
|
4978
|
-
}
|
|
4979
|
-
const vt = ctype(value);
|
|
4980
|
-
// BOTH ways a pointer value reaches here. `ctype` types params and locals, so it sees the
|
|
4981
|
-
// guard's own `(u8 *)…` and nothing else: a bare `gSym.pBuf` or a pointer global's value —
|
|
4982
|
-
// the population this rule exists for — reads `undefined` there. An already-`void *` value is
|
|
4983
|
-
// assignable as it stands.
|
|
4984
|
-
const isPtr = isPtrValue(value) || (vt?.kind === 'ptr' && vt.to.kind !== 'void');
|
|
4985
|
-
return isPtr ? { k: 'cast', to: T.ptr(T.void()), e: value } : value;
|
|
4986
|
-
};
|
|
4987
5545
|
|
|
4988
|
-
/** THE REFUSAL for the read half of `unreadResult`:
|
|
4989
|
-
*
|
|
4990
|
-
*
|
|
4991
|
-
*
|
|
4992
|
-
*
|
|
4993
|
-
*
|
|
4994
|
-
* spelled only where both of these hold:
|
|
4995
|
-
*
|
|
4996
|
-
* 1. EVIDENCE — would a source plausibly have declared this access `volatile`? The answer has
|
|
4997
|
-
* to come from DATA: the target's declared device-register window
|
|
4998
|
-
* (`capabilities.deviceRegisters`). `volatile` is a CORRECTNESS claim about an address, not a
|
|
4999
|
-
* spelling preference, and asserting one about ordinary RAM is a wrong answer rather than a
|
|
5000
|
-
* wrong spelling.
|
|
5001
|
-
* 2. REACHABILITY — the base value has a use OTHER than this read: l3/volatileptr.ts qualifies a
|
|
5002
|
-
* pointer LOCAL, and l3/basecse.ts only mints that local for a base something else also
|
|
5003
|
-
* touches. A single-access read — `*(s32 *)0x04000200;`, the `REG_IF` acknowledge idiom, and
|
|
5004
|
-
* the shape a WRONG `returnsVoid` on a register accessor produces — has no local to qualify
|
|
5005
|
-
* and never will, however plainly its address is a device register. "Some other use" is
|
|
5006
|
-
* NECESSARY for that local, not sufficient; the gate states the necessary half. Where no
|
|
5007
|
-
* qualifier can land, the statement would be a permanent bare deref in the DEFAULT source,
|
|
5008
|
-
* which is what the playground pins and what a decomp author copies.
|
|
5009
|
-
*
|
|
5010
|
-
* The address is a LITERAL, which inhabits only `/raw-globals`: with a symbol map the frontend
|
|
5011
|
-
* spells a pool word as `gaddr`, so `constAddressOf` returns null, and a read of a named object
|
|
5012
|
-
* is plain because the map does not declare it volatile.
|
|
5546
|
+
/** THE REFUSAL for the read half of `unreadResult`: is this dead read QUALIFIED (ir/discipline.ts
|
|
5547
|
+
* `qualified`)? `pinnedAccess` then puts the qualifier on its spelling, from the dead-read pin
|
|
5548
|
+
* (frontend/dead-reads.ts), the device pin, or the map's declaration of the object it reads, and
|
|
5549
|
+
* `gStatus;` under `extern volatile u32 gStatus;` compiles differently from its own absence. A
|
|
5550
|
+
* plain read is dropped: an unqualified `p[2];` compiles to the same bytes as no statement at all
|
|
5551
|
+
* (measured on agbcc, IDO and mwcc), so spelling it claims an access no candidate makes.
|
|
5013
5552
|
*
|
|
5014
|
-
*
|
|
5015
|
-
*
|
|
5016
|
-
*
|
|
5017
|
-
*
|
|
5018
|
-
*
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
return true;
|
|
5022
|
-
}
|
|
5023
|
-
if (op.opcode !== 'load') {
|
|
5024
|
-
return false;
|
|
5025
|
-
}
|
|
5026
|
-
const window = opts.deviceRegisters;
|
|
5027
|
-
if (!window) {
|
|
5028
|
-
return false;
|
|
5029
|
-
}
|
|
5030
|
-
const addr = constAddressOf(defs, op.operands[0], typeof op.attrs.off === 'number' ? op.attrs.off : 0);
|
|
5031
|
-
if (addr === null || addr < window[0] || addr >= window[1]) {
|
|
5032
|
-
return false;
|
|
5033
|
-
}
|
|
5034
|
-
return (useSitesOf.get(op.operands[0]) ?? []).some((s) => s.op !== op);
|
|
5035
|
-
};
|
|
5553
|
+
* ONE DECIDER. Whether a read nobody uses was `volatile` is the dead-read pin's question, asked
|
|
5554
|
+
* once of the raw lift, at any address, with its refusals tabled and reported. The read that
|
|
5555
|
+
* reaches this walk unused because a DECLARED void return suppressed the `ret` operand is the
|
|
5556
|
+
* same question, and the pin answers it too: it does not count that operand as a use. So no
|
|
5557
|
+
* address window, base-local or second-access rule is asked here; a read the pin refused is
|
|
5558
|
+
* plain, and dropped. */
|
|
5559
|
+
const volatileQualifiable = (op: Op): boolean => qualified(op);
|
|
5036
5560
|
|
|
5037
5561
|
/** Ops the `sideEffects` walk must SPELL even though nothing consumes their result — the
|
|
5038
5562
|
* question ir/discipline.ts asks of every op (`spelledWhenDead`), so the next op to acquire the
|
|
5039
|
-
* property needs no edit here, plus the
|
|
5563
|
+
* property needs no edit here, plus the qualifier refusal above for the memory-read half.
|
|
5040
5564
|
*
|
|
5041
5565
|
* A memory READ is not `effectful`, deliberately: ir/discipline.ts calls a load deletable when
|
|
5042
5566
|
* dead, because nothing observes a read nobody reads. That is the C claim. The COMPILER
|
|
5043
5567
|
* claim points the other way — an optimizing compiler deletes every dead read it is allowed to
|
|
5044
5568
|
* delete, so one still in the target is evidence the source's access was `volatile`, and
|
|
5045
|
-
* dropping it deletes an instruction the machine executed.
|
|
5046
|
-
*
|
|
5047
|
-
* The statement earns nothing by itself: an unqualified `p[2];` compiles to the same bytes as
|
|
5048
|
-
* no statement at all (measured on agbcc, IDO and mwcc). What it does is put the access where a
|
|
5049
|
-
* qualifier can reach it — and `volatileQualifiable` is the condition under which one can.
|
|
5050
|
-
*
|
|
5051
|
-
* WHICH VARIATION REACHES IT, because "a qualifier" is two variations and only one of them does:
|
|
5052
|
-
* l3/volatileptr.ts's `/volatile` qualifies the pointer LOCAL the read is spelled through, and
|
|
5053
|
-
* that is the arm every match here rides. l3/volstore.ts's `/vol-store` mints `volatile` at an
|
|
5054
|
-
* inline cast STORE and never visits an `exprstmt`, so on a tree with no base local it emits the
|
|
5055
|
-
* cell qualified for its writes and plain for this read — a candidate that cannot reproduce the
|
|
5056
|
-
* surviving `ldr`. Wasted rather than wrong (the differ refuses it); teaching that variation the
|
|
5057
|
-
* read is a widening with its own window census to pay for, priced in its header.
|
|
5058
|
-
*
|
|
5059
|
-
* TWO THINGS DELIBERATELY NOT DONE HERE, priced rather than left for a reader to rediscover.
|
|
5060
|
-
*
|
|
5061
|
-
* 1. THE LITERAL ARM IS A BACKWARDS DEFAULT AND ITS PREIMAGE IS NOT EMPTY. docs/level-tower.md
|
|
5062
|
-
* admits a default that reads the map backwards only when the backwards mapping is ITSELF a
|
|
5063
|
-
* function, and "a surviving dead read implies the source said `volatile`" is not quite one:
|
|
5064
|
-
* the other preimage is a function whose `returnsVoid` fact is WRONG, so its return value
|
|
5065
|
-
* arrived here as a dead read. The second-use clause removes the common shape of that (a bare
|
|
5066
|
-
* register accessor), leaving a function that both STORES to a device register and reads one
|
|
5067
|
-
* back — narrow, but not proven empty. A qualified read has no such problem: `gStatus;` under
|
|
5068
|
-
* `extern volatile u32 gStatus;` compiles differently from its own absence. The clean fix — spell the read only in candidates that also qualify it, paired
|
|
5069
|
-
* the way `/livebase/volatile` already pairs — moves every device row's fan shape and wants
|
|
5070
|
-
* its own round and zero-flip gate over BOTH tiers.
|
|
5071
|
-
* 2. THE REFUSAL IS SILENT. When this returns false for a `load`, the machine performed a read
|
|
5072
|
-
* that no statement stands for and nothing records the decision — against this project's own
|
|
5073
|
-
* "instrument the refusal" rule; `structure()` has no diagnostic sink to write into. What
|
|
5074
|
-
* exists instead is the ADMISSION side's zero point, `synthetic:dmareadback`, which fails
|
|
5075
|
-
* loudly if the rule stops firing. */
|
|
5569
|
+
* dropping it deletes an instruction the machine executed. The pin makes that claim; this walk
|
|
5570
|
+
* spells what it stamped. */
|
|
5076
5571
|
/** Does `op` render AT ITS OWN POSITION, as `v = f(…)`?
|
|
5077
5572
|
*
|
|
5078
5573
|
* ONE DEFINITION, TWO READERS, on purpose: `isSpelled`'s base case is a MODEL of what
|
|
@@ -5161,42 +5656,84 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5161
5656
|
// A store whose lvalue is a bare global (`gSym = v`, from an `&gSym` base at off 0) emits
|
|
5162
5657
|
// as an ASSIGN, not a store — memAccess returns a `var` node for that case.
|
|
5163
5658
|
const width = op.attrs.width as number;
|
|
5164
|
-
const
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5659
|
+
const off = op.attrs.off as number;
|
|
5660
|
+
const stamp = op.attrs.signed as boolean | undefined;
|
|
5661
|
+
const viewed = frameUnionAccess(op.operands[0], off, width, storeView(op, width), true);
|
|
5662
|
+
if (viewed !== null) {
|
|
5663
|
+
const lval = unqualifiedView(op, viewed);
|
|
5664
|
+
floatBitsStoreTarget(op.operands[1], lval);
|
|
5665
|
+
out.push({ k: 'store', lval, value: intoPtrCell(lval, expr(op.operands[1])) });
|
|
5666
|
+
continue;
|
|
5667
|
+
}
|
|
5668
|
+
const member = frameStructMember(op, off, width, null) ?? frameArrayElement(op, off, width, null);
|
|
5669
|
+
if (member !== null) {
|
|
5670
|
+
floatBitsStoreTarget(op.operands[1], member);
|
|
5671
|
+
out.push({ k: 'store', lval: member, value: intoPtrCell(member, expr(op.operands[1])) });
|
|
5672
|
+
continue;
|
|
5673
|
+
}
|
|
5674
|
+
const baseExpr = expr(op.operands[0]);
|
|
5675
|
+
const lval0 = structBytesCell(
|
|
5676
|
+
op.operands[1],
|
|
5677
|
+
memAccess(
|
|
5678
|
+
op.operands[0],
|
|
5679
|
+
baseExpr,
|
|
5680
|
+
off,
|
|
5681
|
+
width,
|
|
5682
|
+
storeView(op, width),
|
|
5683
|
+
ctype,
|
|
5684
|
+
scalarGlobals,
|
|
5685
|
+
symCtx,
|
|
5686
|
+
true, // an lvalue: a member whose declaration is const cannot be NAMED as the target
|
|
5687
|
+
advanceStepOf(op.operands[0]),
|
|
5688
|
+
qualifiedBy(op) === 'cast',
|
|
5689
|
+
),
|
|
5175
5690
|
);
|
|
5691
|
+
floatBitsStoreTarget(op.operands[1], lval0);
|
|
5176
5692
|
if (lval0.k === 'var') {
|
|
5177
5693
|
globalNames.add(lval0.name);
|
|
5178
5694
|
out.push({ k: 'assign', name: lval0.name, value: intoPtrCell(lval0, expr(op.operands[1])) });
|
|
5179
5695
|
continue;
|
|
5180
5696
|
}
|
|
5181
|
-
//
|
|
5182
|
-
|
|
5183
|
-
const lval = pinnedAccess(
|
|
5697
|
+
// a member whose declared sign is not the stamp's is spelled as the flat cell (stampedStoreLval)
|
|
5698
|
+
const bt = op.operands[0].type;
|
|
5699
|
+
const lval = pinnedAccess(
|
|
5700
|
+
op,
|
|
5701
|
+
stampedStoreLval(
|
|
5702
|
+
lval0,
|
|
5703
|
+
stamp,
|
|
5704
|
+
width,
|
|
5705
|
+
bt.kind === 'ptr' && bt.to.kind === 'struct' ? baseExpr : null,
|
|
5706
|
+
off,
|
|
5707
|
+
ctype,
|
|
5708
|
+
),
|
|
5709
|
+
);
|
|
5184
5710
|
out.push({ k: 'store', lval, value: intoPtrCell(lval, expr(op.operands[1])) });
|
|
5185
5711
|
} else if (op.opcode === 'astore') {
|
|
5186
5712
|
const elemSize = op.attrs.elemSize as number;
|
|
5187
|
-
const
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5713
|
+
const fieldOff = op.attrs.fieldOff as number | undefined;
|
|
5714
|
+
const stamp = op.attrs.signed as boolean | undefined;
|
|
5715
|
+
const lval0 = structBytesCell(
|
|
5716
|
+
op.operands[2],
|
|
5717
|
+
arrayAccess(
|
|
5718
|
+
op.operands[0],
|
|
5719
|
+
expr(op.operands[0]),
|
|
5720
|
+
expr(op.operands[1]),
|
|
5721
|
+
fieldOff,
|
|
5722
|
+
op.attrs.memberOff as number | undefined,
|
|
5723
|
+
elemSize,
|
|
5724
|
+
storeView(op, elemSize),
|
|
5725
|
+
ctype,
|
|
5726
|
+
symCtx,
|
|
5727
|
+
),
|
|
5199
5728
|
);
|
|
5729
|
+
// a struct-array element's field: `base[i].field_K`, whose flat cell is
|
|
5730
|
+
// `(base + i)[K / width]` — C scales `base + i` by the element, exactly as `base[i]` does
|
|
5731
|
+
const el = lval0.k === 'field' && lval0.base.k === 'index' ? lval0.base : null;
|
|
5732
|
+
const lval =
|
|
5733
|
+
el === null || fieldOff === undefined
|
|
5734
|
+
? lval0
|
|
5735
|
+
: stampedStoreLval(lval0, stamp, undefined, { k: 'bin', op: '+', l: el.base, r: el.idx }, fieldOff, ctype);
|
|
5736
|
+
floatBitsStoreTarget(op.operands[2], lval);
|
|
5200
5737
|
out.push({
|
|
5201
5738
|
k: 'store',
|
|
5202
5739
|
lval: pinnedAccess(op, lval),
|
|
@@ -5252,7 +5789,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5252
5789
|
// to `header` is a conditional continue; its other edge, when it leaves the loop, is an early exit
|
|
5253
5790
|
// (a `break` to `exit`, or an early `return` through a trampoline). Null outside any loop body —
|
|
5254
5791
|
// the early-exit branch in structureBlock is inert there.
|
|
5255
|
-
type LoopFrame = {
|
|
5792
|
+
type LoopFrame = {
|
|
5793
|
+
header: Block;
|
|
5794
|
+
exit: Block | null;
|
|
5795
|
+
body: Set<Block>;
|
|
5796
|
+
arms: LoopArm[];
|
|
5797
|
+
breaks: ReadonlySet<Block>;
|
|
5798
|
+
breakArms?: readonly BreakArm[];
|
|
5799
|
+
};
|
|
5256
5800
|
let loopCtx: LoopFrame | null = null;
|
|
5257
5801
|
const withLoop = <R>(frame: LoopFrame, run: () => R): R => {
|
|
5258
5802
|
const prev = loopCtx;
|
|
@@ -5264,6 +5808,87 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5264
5808
|
}
|
|
5265
5809
|
};
|
|
5266
5810
|
|
|
5811
|
+
// THE SWITCH ARMS being structured, innermost last — one frame per arm, in both regimes, so a
|
|
5812
|
+
// `break;` can be checked against what C binds it to. A C `break` leaves the innermost enclosing
|
|
5813
|
+
// loop OR switch, so a switch-scoped one is only spelled where this arm's frame is the innermost
|
|
5814
|
+
// switch frame AND no loop was entered since the arm began (`loop` is the loop frame the arm sits
|
|
5815
|
+
// in). `region` is the arm's own blocks (`ArmExit`), closed or falling; null on an arm the exit
|
|
5816
|
+
// analysis gave none (one that leaves sideways), which structures to the CFG's joins and stops AT
|
|
5817
|
+
// the merge without the statement. A frame with no region still matters: it is what tells a
|
|
5818
|
+
// `break;` reached inside a nested switch that it would bind the inner one, and the loop-exit
|
|
5819
|
+
// rules that a loop's `break` cannot be minted here. `fallsTo` is the sibling a falling arm runs
|
|
5820
|
+
// into (`armJoin` reads it).
|
|
5821
|
+
type SwitchFrame = {
|
|
5822
|
+
merge: Block | null;
|
|
5823
|
+
loop: LoopFrame | null;
|
|
5824
|
+
region: ReadonlySet<Block> | null;
|
|
5825
|
+
fallsTo: Block | null;
|
|
5826
|
+
};
|
|
5827
|
+
const switchFrames: SwitchFrame[] = [];
|
|
5828
|
+
// Is `b` the merge of an arm with a region, reached before that arm's end, and may `break;` say so
|
|
5829
|
+
// from here? Undefined when no such arm ends at `b`. (At an arm's own end, `stop` is the merge and
|
|
5830
|
+
// `structureRegion` returns before asking.) Throws when one does but a nested loop or switch would
|
|
5831
|
+
// capture the statement.
|
|
5832
|
+
const switchBreakAt = (b: Block): Stmt[] | undefined => {
|
|
5833
|
+
let i = switchFrames.length - 1;
|
|
5834
|
+
while (i >= 0 && (switchFrames[i].region === null || switchFrames[i].merge !== b)) {
|
|
5835
|
+
i--;
|
|
5836
|
+
}
|
|
5837
|
+
if (i < 0) {
|
|
5838
|
+
return undefined;
|
|
5839
|
+
}
|
|
5840
|
+
if (i !== switchFrames.length - 1 || switchFrames[i].loop !== loopCtx) {
|
|
5841
|
+
throw new StructureError(
|
|
5842
|
+
`cannot structure '${fn.name}': a case body leaves its switch from inside a nested ` +
|
|
5843
|
+
`${i !== switchFrames.length - 1 ? 'switch' : 'loop'}, where \`break\` would leave that instead`,
|
|
5844
|
+
);
|
|
5845
|
+
}
|
|
5846
|
+
return [{ k: 'break', ofSwitch: true }];
|
|
5847
|
+
};
|
|
5848
|
+
// THE JOIN OF A BRANCH INSIDE AN ARM. The CFG's join for `if (c) break;` lies past the switch —
|
|
5849
|
+
// in a falling arm the fall-through path reaches the merge too, through the next arm, and in a
|
|
5850
|
+
// closed arm the merge IS the join — so structuring to it would copy the next arm, or the rest of
|
|
5851
|
+
// this one, into both sides. Inside the arm, an edge to the merge is a `break;` and a `ret` a
|
|
5852
|
+
// `return`: jumps, which end their path wherever they sit, so those edges are DELETED, as
|
|
5853
|
+
// `loopJoin` deletes a loop body's exits (`regionJoins`). The edge into the sibling the arm falls
|
|
5854
|
+
// into is no jump: its path runs to the arm's END, so it goes to EXIT, the caller's `stop`. Sent
|
|
5855
|
+
// to EXIT too, a guard whose one side breaks would be post-dominated by EXIT alone and every
|
|
5856
|
+
// statement after it would nest in its `else`; deleted, a guard whose one side falls would join
|
|
5857
|
+
// on the other side's path, which the falling path cannot jump over. Null ⇒ the paths meet only
|
|
5858
|
+
// at the arm's end. Asked only in the innermost arm frame and outside any loop entered since,
|
|
5859
|
+
// which is where `switchBreakAt` would spell the `break;`.
|
|
5860
|
+
const armJoins = new Map<ReadonlySet<Block>, Map<Block, Block | null>>();
|
|
5861
|
+
const armJoin = (b: Block, stop: Block | null): Block | null | undefined => {
|
|
5862
|
+
const f = switchFrames[switchFrames.length - 1];
|
|
5863
|
+
if (f === undefined || f.region === null || !f.region.has(b) || f.loop !== loopCtx) {
|
|
5864
|
+
return undefined;
|
|
5865
|
+
}
|
|
5866
|
+
const region = f.region;
|
|
5867
|
+
let pd = armJoins.get(region);
|
|
5868
|
+
if (pd === undefined) {
|
|
5869
|
+
pd = regionJoins(region, null, false, f.fallsTo);
|
|
5870
|
+
armJoins.set(region, pd);
|
|
5871
|
+
}
|
|
5872
|
+
const join = pd.get(b) ?? null;
|
|
5873
|
+
// IN A CLOSED ARM, AN EDGE TO THE MERGE IS USUALLY THE ARM'S OWN END, not a `break;`: in
|
|
5874
|
+
// `case 7: s *= 2; if (s > 100) s = 1; break;` the guard's edge past `s = 1` is where the arm
|
|
5875
|
+
// runs off, and deleting it makes `s = 1` the join and spells `if (!(s > 100)) break; s = 1;`.
|
|
5876
|
+
// What the deletion is for is a TAIL two of the arm's paths share behind a mid-arm `break;`
|
|
5877
|
+
// (`if (a) { if (b) break; g(1); } h(2); break;`, where `h(2)` is the join). So a closed arm
|
|
5878
|
+
// takes its own join only where the join is such a meeting point, two of the arm's blocks
|
|
5879
|
+
// entering it, or where the region being structured already ends INSIDE the arm (`stop` is one
|
|
5880
|
+
// of its blocks: an enclosing guard found such a tail, and a path to the merge from here jumps
|
|
5881
|
+
// over it). Otherwise it reads the CFG.
|
|
5882
|
+
if (
|
|
5883
|
+
f.fallsTo === null &&
|
|
5884
|
+
!(stop !== null && region.has(stop)) &&
|
|
5885
|
+
(join === null || new Set(preds.get(join)?.filter((p) => region.has(p))).size < 2)
|
|
5886
|
+
) {
|
|
5887
|
+
return undefined;
|
|
5888
|
+
}
|
|
5889
|
+
return join;
|
|
5890
|
+
};
|
|
5891
|
+
|
|
5267
5892
|
// THE IMPLICIT CONTINUE. A branch with one edge back to the loop header spells that edge as
|
|
5268
5893
|
// nothing at all: control falls to the bottom of the body and the loop goes round. That holds only
|
|
5269
5894
|
// where the region being structured ends at the loop bottom (`stop === frame.header`). An `if` in
|
|
@@ -5293,12 +5918,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5293
5918
|
// that render: an exit parameter that takes its argument's own name renders no copy, yet the exit
|
|
5294
5919
|
// still reads that name, and a latch update that wrote it hands the exit the updated value.
|
|
5295
5920
|
const breakRule = (frame: LoopFrame, from: Block, writes: Set<string>): { refusal: string } | { bare: boolean } => {
|
|
5921
|
+
const exit = frame.exit;
|
|
5922
|
+
if (exit === null) {
|
|
5923
|
+
return { refusal: 'leaves a loop that has no exit' }; // DEFENSIVE: no edge leaves such a loop
|
|
5924
|
+
}
|
|
5296
5925
|
// A `while (1)` has no copies of its own after the loop, so a break carries its own, computed
|
|
5297
5926
|
// where it leaves; what is left is the exit region's re-derivations. An inner loop's value the
|
|
5298
5927
|
// region reads under its name (`emitForever`) is no re-derivation.
|
|
5299
5928
|
const fl = foreverLoops.get(frame.header);
|
|
5300
5929
|
if (fl !== undefined) {
|
|
5301
|
-
const exitRegion = new Set([
|
|
5930
|
+
const exitRegion = new Set([exit, ...reachFrom(exit)].filter((x) => !frame.body.has(x)));
|
|
5302
5931
|
const { sub } = latchInnerSub(fl.header, fl.body, [...fl.breaks], exitRegion);
|
|
5303
5932
|
return loopEscapeHazard(frame.body, sub, writes, exitRegion)
|
|
5304
5933
|
? { refusal: 'reaches an exit region that reads a loop value under a name this iteration already rewrote' }
|
|
@@ -5307,11 +5936,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5307
5936
|
if (!whileLoops.has(frame.header)) {
|
|
5308
5937
|
return { refusal: 'leaves a do-while' };
|
|
5309
5938
|
}
|
|
5310
|
-
const headerCopies = argAssigns(frame.header,
|
|
5939
|
+
const headerCopies = argAssigns(frame.header, exit);
|
|
5311
5940
|
let bare = false;
|
|
5312
5941
|
if (headerCopies.length !== 0) {
|
|
5313
|
-
const headerArgs = successorTo(frame.header,
|
|
5314
|
-
const breakArgs = successorTo(from,
|
|
5942
|
+
const headerArgs = successorTo(frame.header, exit)!.args;
|
|
5943
|
+
const breakArgs = successorTo(from, exit)!.args;
|
|
5315
5944
|
const reads = new Set<string>();
|
|
5316
5945
|
for (const s of headerCopies) {
|
|
5317
5946
|
stmtExprs(s).forEach((e) => exprVars(e, reads));
|
|
@@ -5324,7 +5953,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5324
5953
|
return { refusal: 'would run the copies the loop header hands its exit' };
|
|
5325
5954
|
}
|
|
5326
5955
|
}
|
|
5327
|
-
const exitRegion = new Set([
|
|
5956
|
+
const exitRegion = new Set([exit, ...reachFrom(exit)].filter((x) => !frame.body.has(x)));
|
|
5328
5957
|
if (loopEscapeHazard(frame.body, new Map(), writes, exitRegion)) {
|
|
5329
5958
|
return { refusal: 'reaches an exit region that reads a loop value under a name this iteration already rewrote' };
|
|
5330
5959
|
}
|
|
@@ -5357,6 +5986,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5357
5986
|
if (b === stop) {
|
|
5358
5987
|
return [];
|
|
5359
5988
|
}
|
|
5989
|
+
// The merge of the switch a falling arm also leaves: the edge copies are already out, and
|
|
5990
|
+
// `break;` is the jump.
|
|
5991
|
+
const brk = switchBreakAt(b);
|
|
5992
|
+
if (brk !== undefined) {
|
|
5993
|
+
return brk;
|
|
5994
|
+
}
|
|
5360
5995
|
// Back to a `while (1)` header from a region that ends short of the loop bottom: the edge's
|
|
5361
5996
|
// copies are already out, and `continue` is the jump.
|
|
5362
5997
|
if (loopCtx !== null && b === loopCtx.header && foreverLoops.has(b)) {
|
|
@@ -5387,6 +6022,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5387
6022
|
// two places the map is handed to (`coalesceNames`, `makeLoopHazards`) only read it. So the
|
|
5388
6023
|
// recognizer sees one settled naming however late a case body calls back into `structureRegion`,
|
|
5389
6024
|
// and nothing here has to be rebuilt against a naming that moves underneath it.
|
|
6025
|
+
// One switch arm, from `entry` to `stop`, under its frame (`switchFrames`).
|
|
6026
|
+
const structureArm = (entry: Block, stop: Block | null, merge: Block | null, exit: ArmExit | undefined): Stmt[] => {
|
|
6027
|
+
// A backend with no mid-arm `break` (`spellSwitchBreak`) gets no region for a closed arm, and
|
|
6028
|
+
// so no `break;` from `switchBreakAt` and no arm-local join: it structures to the CFG's.
|
|
6029
|
+
const region =
|
|
6030
|
+
exit === undefined || exit.kind === 'unstructurable' || (exit.kind === 'break' && !spellSwitchBreak)
|
|
6031
|
+
? null
|
|
6032
|
+
: (exit.region ?? null);
|
|
6033
|
+
switchFrames.push({ merge, loop: loopCtx, region, fallsTo: exit?.kind === 'fallthrough' ? exit.to : null });
|
|
6034
|
+
try {
|
|
6035
|
+
return structureRegion(entry, stop);
|
|
6036
|
+
} finally {
|
|
6037
|
+
switchFrames.pop();
|
|
6038
|
+
}
|
|
6039
|
+
};
|
|
5390
6040
|
const { recognizeSwitch, analyzeArmExit, layoutIndex, defaultLayoutPos, chainArms } = makeSwitchRecovery({
|
|
5391
6041
|
fn,
|
|
5392
6042
|
defs,
|
|
@@ -5402,16 +6052,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5402
6052
|
spellSwitchFallthrough,
|
|
5403
6053
|
emitsOwnStatement: (blk) => blk.ops.some((o) => anchoredAt.has(o) || materialize.has(o)),
|
|
5404
6054
|
blockOf,
|
|
6055
|
+
enclosingLoop: () => loopCtx,
|
|
5405
6056
|
hoistDispatchCopies: (edges, liveAt) => hoistedDispatchAssigns(edges, liveAt),
|
|
5406
6057
|
expr: (v) => expr(v),
|
|
5407
6058
|
structureRegion: (b, stop) => structureRegion(b, stop),
|
|
6059
|
+
structureArm,
|
|
5408
6060
|
});
|
|
5409
6061
|
|
|
5410
6062
|
const structureBlock = (b: Block, stop: Block | null): Stmt[] => {
|
|
5411
6063
|
// Bottom-test `do-while`: this block is a do-while header (the body-first loop entry). Emit the
|
|
5412
6064
|
// do-while — its body includes `b`'s own ops (structured via structureRegion with the hook masked),
|
|
5413
6065
|
// so do NOT emit sideEffects(b) here. The init was already emitted by the predecessor's argAssigns.
|
|
5414
|
-
|
|
6066
|
+
// A guarded self-loop its guard did not claim (`keptGuardDoWhiles`) is a do-while like any
|
|
6067
|
+
// other: read as a plain block, its back edge would re-enter it and decline.
|
|
6068
|
+
const dw = doWhileLoops.get(b) ?? keptGuardDoWhiles.get(b);
|
|
5415
6069
|
if (dw && !dwActive.has(b)) {
|
|
5416
6070
|
return emitDoWhile(dw, stop);
|
|
5417
6071
|
}
|
|
@@ -5424,7 +6078,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5424
6078
|
const term = b.ops[b.ops.length - 1];
|
|
5425
6079
|
if (term.opcode === 'ret') {
|
|
5426
6080
|
// A void function's `bx lr` leaves whatever in r0; suppress that phantom return value.
|
|
5427
|
-
const value = returnsVoid || !term.operands.length ? undefined :
|
|
6081
|
+
const value = returnsVoid || !term.operands.length ? undefined : returnedValue(term);
|
|
5428
6082
|
// The `value === undefined` half is a GUARD: `l3/tailret.ts` re-checks it before deleting
|
|
5429
6083
|
// anything, so marking a value-carrying return would change no output. The mark claims the asm
|
|
5430
6084
|
// shows no `return;` STATEMENT here, which is a claim about a VOID return only.
|
|
@@ -5469,8 +6123,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5469
6123
|
// more functions therefore has to BE the default, and it is the DIVERGENT reading that a
|
|
5470
6124
|
// variation would have to offer back — a candidate nothing mints today, and the one thing
|
|
5471
6125
|
// this line costs. `?? stop` keeps the shape where no `ret` is reachable from every
|
|
5472
|
-
// successor.
|
|
5473
|
-
const
|
|
6126
|
+
// successor — and where that reading declines, the default-anchored RETRY below gets one try.
|
|
6127
|
+
const joined = ipdom.get(b) ?? followOverReturns(b, stop);
|
|
6128
|
+
let merge = joined ?? stop;
|
|
5474
6129
|
const succ = term.successors;
|
|
5475
6130
|
const caseVals = term.attrs.cases as number[];
|
|
5476
6131
|
// Switch edges CARRY phi args (frontend/ssa.ts appends them terminator-generically) — each
|
|
@@ -5503,13 +6158,6 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5503
6158
|
}
|
|
5504
6159
|
a.values.push(caseVals[i]);
|
|
5505
6160
|
});
|
|
5506
|
-
// The blocks an arm could fall INTO. The default block counts only when it is a block of its
|
|
5507
|
-
// own: when it IS the merge, "the default" is just where the switch ends, and an arm reaching
|
|
5508
|
-
// it is a plain `break`.
|
|
5509
|
-
const siblings = new Set<Block>(arms.map((a) => a.entry));
|
|
5510
|
-
if (defEdge.block !== merge) {
|
|
5511
|
-
siblings.add(defEdge.block);
|
|
5512
|
-
}
|
|
5513
6161
|
// ARM ORDER. Grouping the table's slots by target walks them in TABLE order, which for a dense
|
|
5514
6162
|
// table is ascending case value — the neutral spelling, and the source's order only by
|
|
5515
6163
|
// accident. Where the compiler has declared that its block layout IS the order the arms were
|
|
@@ -5521,39 +6169,208 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5521
6169
|
// The policy orders the chain HEADS. A FALLING arm's position is not free — emission order is
|
|
5522
6170
|
// load-bearing for correctness (the l3/ast.ts non-neutrality note), so it must sit directly
|
|
5523
6171
|
// above the arm it falls into — and `chainArms` below re-threads it for exactly those, which
|
|
5524
|
-
// is the same per-SITE reading Regime A makes of the same declaration.
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
6172
|
+
// is the same per-SITE reading Regime A makes of the same declaration.
|
|
6173
|
+
if (switchArmsFollowLayout) {
|
|
6174
|
+
arms.sort((x, y) => layoutIndex(x.entry) - layoutIndex(y.entry));
|
|
6175
|
+
}
|
|
6176
|
+
// …and then the CHAIN re-threads that order for the falling arms, exactly as it does for the
|
|
6177
|
+
// comparison tree: ONE definition (`chainArms`) for the linearization, the adjacency it
|
|
6178
|
+
// guarantees, and the three shapes no linear order spells. The arm-order policy above is read
|
|
6179
|
+
// for the chain HEADS only — the same per-SITE reading Regime A makes of the same
|
|
6180
|
+
// declaration, rather than a whole-switch gate on every arm being closed.
|
|
6181
|
+
const preChain = arms.map((a) => a.entry);
|
|
6182
|
+
// Everything below that depends on WHERE THE SWITCH ENDS, read for one candidate merge: the
|
|
6183
|
+
// arms' exits, their linearization, and whether the emission below would decline them. Read
|
|
6184
|
+
// twice at most — see the retry after it.
|
|
6185
|
+
// A default edge the dispatch never takes: a frontend whose dispatch is entered only past a
|
|
6186
|
+
// bounds test that already sent every other value elsewhere says so with `exhaustive`.
|
|
6187
|
+
const deadDefaultEdge = term.attrs.exhaustive === true;
|
|
6188
|
+
const readArms = (at: Block | null) => {
|
|
6189
|
+
// The blocks an arm could fall INTO. The default block counts only when it is a block of
|
|
6190
|
+
// its own: when it IS the merge, "the default" is just where the switch ends, and an arm
|
|
6191
|
+
// reaching it is a plain `break`.
|
|
6192
|
+
const siblings = new Set<Block>(arms.map((a) => a.entry));
|
|
6193
|
+
if (defEdge.block !== at) {
|
|
6194
|
+
siblings.add(defEdge.block);
|
|
6195
|
+
}
|
|
6196
|
+
// `analyzeArmExit` does not depend on emission order, so the exits can be settled before
|
|
6197
|
+
// the chain and reused below. Grouping by target already gives every arm a DISTINCT entry
|
|
6198
|
+
// block, so no two sort keys can be equal and the tie-break Regime A needs on shared bodies
|
|
6199
|
+
// has nothing to decide here.
|
|
6200
|
+
const exits = new Map<Block, ArmExit>();
|
|
6201
|
+
for (const entry of [...arms.map((a) => a.entry), defEdge.block]) {
|
|
6202
|
+
if (!exits.has(entry)) {
|
|
6203
|
+
exits.set(entry, analyzeArmExit(entry, b, at, siblings));
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
const falls = [...exits.values()].some((e) => e.kind === 'fallthrough');
|
|
6207
|
+
// A table whose default block is ALSO a case target is that case's arm, not a separate
|
|
6208
|
+
// one: it is already in `preChain`, and `defaultLayoutPos` withholds the label's position.
|
|
6209
|
+
const dflt = defEdge.block !== at && !armOf.has(defEdge.block) ? defEdge.block : null;
|
|
6210
|
+
const chain = !spellSwitchFallthrough && falls ? null : chainArms(preChain, dflt, exits);
|
|
6211
|
+
const refusal = chain === null ? null : armRefusal(exits, chain);
|
|
6212
|
+
return { exits, falls, chain, refusal, declines: chain === null || refusal !== null };
|
|
6213
|
+
};
|
|
6214
|
+
// THE EMISSION'S PER-ARM REFUSALS, the one definition of them. Every refusal the emission
|
|
6215
|
+
// below throws is a null chain (which a fall-through in a language without one forces too —
|
|
6216
|
+
// `readArms` reads that one) or one of three asked here per arm, in emission order, without
|
|
6217
|
+
// emitting anything: an arm whose exit is unstructurable, a fall-through into an arm that is
|
|
6218
|
+
// not the next one emitted, and a fall-through into an arm that opens with switch-edge
|
|
6219
|
+
// copies. The answer is the arm it refuses at and a `raise` that throws the refusal, and the
|
|
6220
|
+
// emission calls exactly the one `readArms` carries for the reading it adopted — so the
|
|
6221
|
+
// retry's gate and the emission cannot disagree about what declines, and a reading that
|
|
6222
|
+
// declines fails with the message it always did. Each refusal stays a literal
|
|
6223
|
+
// `throw new StructureError(…)` so the decline-message harvest (apps/web declines.test.ts)
|
|
6224
|
+
// still reads it.
|
|
6225
|
+
//
|
|
6226
|
+
// `edgeCopyRecords` stands in for `argAssignsFor` because it mints no swap-cycle temp — a
|
|
6227
|
+
// name minted here would change the output of every arm emitted after it — and it answers
|
|
6228
|
+
// the same question: `sequentialize` emits at least one statement per record and none for
|
|
6229
|
+
// none. It is not a pure query either: an `undef` copy it drops is recorded in
|
|
6230
|
+
// `droppedUndefCopies`, which a postcondition reads. So the records this question leaves are
|
|
6231
|
+
// rolled back, as the speculative dispatch hoist does with its own.
|
|
6232
|
+
const armRefusal = (exits: Map<Block, ArmExit>, chain: Block[]): { at: number; raise: () => never } | null => {
|
|
6233
|
+
const order: { entry: Block; edge: (typeof succ)[number]; values: number[] | null }[] = [
|
|
6234
|
+
...chain.map((e) => armOf.get(e)!),
|
|
6235
|
+
{ entry: defEdge.block, edge: defEdge, values: null },
|
|
6236
|
+
];
|
|
6237
|
+
const auditMark = droppedUndefCopies.length;
|
|
6238
|
+
try {
|
|
6239
|
+
for (const [i, a] of order.entries()) {
|
|
6240
|
+
const exit = exits.get(a.entry)!;
|
|
6241
|
+
if (exit.kind === 'unstructurable') {
|
|
6242
|
+
const { why } = exit;
|
|
6243
|
+
return {
|
|
6244
|
+
at: i,
|
|
6245
|
+
raise: () => {
|
|
6246
|
+
throw new StructureError(`cannot structure '${fn.name}': ${why}`);
|
|
6247
|
+
},
|
|
6248
|
+
};
|
|
6249
|
+
}
|
|
6250
|
+
if (exit.kind !== 'fallthrough') {
|
|
6251
|
+
continue;
|
|
6252
|
+
}
|
|
6253
|
+
const next = order[i + 1];
|
|
6254
|
+
if (next?.entry !== exit.to) {
|
|
6255
|
+
const { values } = a;
|
|
6256
|
+
return {
|
|
6257
|
+
at: i,
|
|
6258
|
+
raise: () => {
|
|
6259
|
+
throw new StructureError(
|
|
6260
|
+
`cannot structure '${fn.name}': ${values ? `case ${values.join('/')}` : 'the default arm'} falls ` +
|
|
6261
|
+
`through into an arm that is not the next one emitted — C fall-through only reaches the arm below`,
|
|
6262
|
+
);
|
|
6263
|
+
},
|
|
6264
|
+
};
|
|
6265
|
+
}
|
|
6266
|
+
// The arm fallen INTO opens with its own switch-edge copies, which are how the dispatch
|
|
6267
|
+
// hands it its block parameters. On the fall-through path those copies would RE-RUN and
|
|
6268
|
+
// overwrite what the falling arm just computed (`case 0: v=7; case 1: v=5;` — the case-0
|
|
6269
|
+
// path calling with 5). They cannot simply be dropped either: entering that arm by its
|
|
6270
|
+
// own case value needs them. Hoisting them above the switch is possible but not always
|
|
6271
|
+
// safe (another arm may read the same name first), so this shape declines LOUD;
|
|
6272
|
+
// recovering it is future work. The one edge that needs no copies is a default edge the
|
|
6273
|
+
// dispatch never takes (the op's `exhaustive` attr): nothing enters the default by it,
|
|
6274
|
+
// so its copies are not emitted (`edgeCopies` below) and cannot re-run.
|
|
6275
|
+
if (!(deadDefaultEdge && next.edge === defEdge) && edgeCopyRecords(b, next.edge).length > 0) {
|
|
6276
|
+
return {
|
|
6277
|
+
at: i,
|
|
6278
|
+
raise: () => {
|
|
6279
|
+
throw new StructureError(
|
|
6280
|
+
`cannot structure '${fn.name}': the case fallen into takes a value from the switch edge, ` +
|
|
6281
|
+
`which the fall-through path would re-run`,
|
|
6282
|
+
);
|
|
6283
|
+
},
|
|
6284
|
+
};
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
return null;
|
|
6288
|
+
} finally {
|
|
6289
|
+
droppedUndefCopies.length = auditMark;
|
|
6290
|
+
}
|
|
6291
|
+
};
|
|
6292
|
+
let read = readArms(merge);
|
|
6293
|
+
// THE RETRY: A SWITCH WHOSE ARMS DO NOT ALL REACH THE CODE AFTER IT. When neither
|
|
6294
|
+
// post-dominance nor the every-successor reading gives the table an end, the `?? stop`
|
|
6295
|
+
// reading above puts the default block among the siblings, so every plain `break` reads as
|
|
6296
|
+
// falling INTO it — and the function declines wherever that does not spell. One arm that
|
|
6297
|
+
// `return`s on its own is enough to cause it (`case 4: …; return TRUE;` beside four `break`s
|
|
6298
|
+
// and a `return FALSE;` after the switch): its `ret` is reached by no other successor, so
|
|
6299
|
+
// the every-successor intersection is empty although the rest plainly share an end. So is a
|
|
6300
|
+
// `case 1:` that falls into a returning `case 2:`, and so are four returning arms beside one
|
|
6301
|
+
// `break` that carries a value to the code after the switch.
|
|
6302
|
+
//
|
|
6303
|
+
// The DEFAULT-ANCHORED reading (`sharedRetsOf`) names that end by the dispatch's DEFAULT
|
|
6304
|
+
// edge, which goes to the `default:` arm or, where the source wrote none, straight to the
|
|
6305
|
+
// code after the switch. It keeps the successors that share a `ret` with the default edge's
|
|
6306
|
+
// target, and derives the follow from them alone. That is a candidate, not evidence: a
|
|
6307
|
+
// `default:` that `return`s on its own has a `ret` of its own, and the follow it derives
|
|
6308
|
+
// then sits inside that arm, with every `break` spelled as a `return` — the same program,
|
|
6309
|
+
// and not the one the source wrote (KMC and gcc 2.7.2 recompile it to different code).
|
|
6310
|
+
//
|
|
6311
|
+
// THE LAYOUT TELLS THEM APART, and nothing in the graph does: `case 0: …; break;` beside
|
|
6312
|
+
// returning arms and a returning `default:` is the same CFG as returning arms beside one
|
|
6313
|
+
// arm that carries a value past the switch to a store the default edge shares. What
|
|
6314
|
+
// differs is where the code the anchor leaves out sits. The code after a switch is laid
|
|
6315
|
+
// out after every arm, so an arm the anchor leaves out — one that returns on its own, or
|
|
6316
|
+
// reaches the switch's real end — sits above that end only if the candidate is the real
|
|
6317
|
+
// end. Where any block a left-out successor reaches is laid out past the candidate, the
|
|
6318
|
+
// candidate is inside an arm and the retry is refused.
|
|
6319
|
+
//
|
|
6320
|
+
// That reads `layoutIndex` at a strength of its own: only that no block of an arm was moved
|
|
6321
|
+
// past the code after the switch — not the arm order `switchArmsFollowLayout` places, so it
|
|
6322
|
+
// is not behind that declaration. Where it fails — PPC's synthetic return blocks sort after
|
|
6323
|
+
// every real one — it can only refuse, so its cost is a decline the `stop` reading already
|
|
6324
|
+
// made, never a spelling.
|
|
6325
|
+
//
|
|
6326
|
+
// A RETRY, NOT A THIRD LINK IN THE `??` CHAIN. Where the `stop` reading spells, it is legal
|
|
6327
|
+
// C already, and asking the anchored reading first re-spells three shapes whose `stop`
|
|
6328
|
+
// spelling is published (a real fall-through moved after the switch, a value-carrying
|
|
6329
|
+
// fall-through edge lifted where it declined, a Pascal decline re-worded) with no row asking
|
|
6330
|
+
// for it. So it is read only where the `stop` reading DECLINES — any refusal `readArms`
|
|
6331
|
+
// reports, which is every refusal the emission below throws — and adopted only where it
|
|
6332
|
+
// declines nothing, so it can turn a decline into a lift and never one lift into another,
|
|
6333
|
+
// nor one decline's stated reason into another's.
|
|
6334
|
+
//
|
|
6335
|
+
// NEVER AT A CASE ARM. Where the arms that return on their own are the many and the arms that
|
|
6336
|
+
// share an end are one arm and the arm it falls into (`case 0: …` running on into `case 1:`,
|
|
6337
|
+
// every other arm its own `return`), the follow the anchored reading can derive is that
|
|
6338
|
+
// second arm's ENTRY. Ending the switch there spells `case 1:` as an empty arm and moves its
|
|
6339
|
+
// body after the switch — the same program, but a spelling no source writes and the one the
|
|
6340
|
+
// `stop` reading's own refusal is the honest answer for. So the retry ends the switch only
|
|
6341
|
+
// at the default block or past every arm.
|
|
6342
|
+
//
|
|
6343
|
+
// THE DEFAULT BLOCK STAYS ELIGIBLE WHEN THE TABLE'S HOLES MAKE IT AN ARM. A slot no `case`
|
|
6344
|
+
// names holds the default edge's target, so on a table with a hole that block is an arm by
|
|
6345
|
+
// construction, but not one with a body: a hole runs what an unmatched value runs, and ending
|
|
6346
|
+
// the switch there spells it `case 1: break;`. The rule above refuses an arm that is NOT the
|
|
6347
|
+
// default edge's target, whose body would move after the switch, and only that arm.
|
|
6348
|
+
if (joined === null && read.declines) {
|
|
6349
|
+
const found = followOverReturns(b, stop, defEdge.block);
|
|
6350
|
+
const pastEnd = (end: Block): boolean =>
|
|
6351
|
+
leftOutOf(b, reachFrom, defEdge.block).some((s) =>
|
|
6352
|
+
[s, ...reachFrom(s)].some((x) => x !== b && layoutIndex(x) > layoutIndex(end)),
|
|
6353
|
+
);
|
|
6354
|
+
const over = found !== null && (found === defEdge.block || !armOf.has(found)) && !pastEnd(found) ? found : null;
|
|
6355
|
+
const retry = over === null ? null : readArms(over);
|
|
6356
|
+
if (retry !== null && !retry.declines) {
|
|
6357
|
+
hooks.onEarlyReturnFollow?.({ block: fn.blocks.indexOf(b), follow: fn.blocks.indexOf(over!) });
|
|
6358
|
+
merge = over;
|
|
6359
|
+
read = retry;
|
|
5532
6360
|
}
|
|
5533
6361
|
}
|
|
6362
|
+
const exitOf = read.exits;
|
|
5534
6363
|
// A language whose `case` cannot fall through (Pascal) has no spelling for this shape, and
|
|
5535
6364
|
// Regime B has no second recovery to fall back on — so it fails LOUD, as it does for every
|
|
5536
6365
|
// other unspellable jump table. (Regime A, which does have a fallback, declines to
|
|
5537
6366
|
// if-recovery instead: switch-recover.ts's PRE2.)
|
|
5538
|
-
if (!spellSwitchFallthrough &&
|
|
6367
|
+
if (!spellSwitchFallthrough && read.falls) {
|
|
5539
6368
|
throw new StructureError(
|
|
5540
6369
|
`cannot structure '${fn.name}': a jump-table case runs on into the next case, and the target ` +
|
|
5541
6370
|
`language has no fall-through in its case statement`,
|
|
5542
6371
|
);
|
|
5543
6372
|
}
|
|
5544
|
-
|
|
5545
|
-
arms.sort((x, y) => layoutIndex(x.entry) - layoutIndex(y.entry));
|
|
5546
|
-
}
|
|
5547
|
-
// …and then the CHAIN re-threads that order for the falling arms, exactly as it does for the
|
|
5548
|
-
// comparison tree: ONE definition (`chainArms`) for the linearization, the adjacency it
|
|
5549
|
-
// guarantees, and the three shapes no linear order spells. The arm-order policy above is read
|
|
5550
|
-
// for the chain HEADS only — the same per-SITE reading Regime A makes of the same
|
|
5551
|
-
// declaration, rather than a whole-switch gate on every arm being closed.
|
|
5552
|
-
const preChain = arms.map((a) => a.entry);
|
|
5553
|
-
// A table whose default block is ALSO a case target is that case's arm, not a separate one:
|
|
5554
|
-
// it is already in `preChain`, and `defaultLayoutPos` withholds the label's position for it.
|
|
5555
|
-
const dfltArm = defEdge.block !== merge && !armOf.has(defEdge.block) ? defEdge.block : null;
|
|
5556
|
-
const chained = chainArms(preChain, dfltArm, exitOf);
|
|
6373
|
+
const chained = read.chain;
|
|
5557
6374
|
if (chained === null) {
|
|
5558
6375
|
throw new StructureError(
|
|
5559
6376
|
`cannot structure '${fn.name}': the jump table's case arms do not linearize — two arms fall ` +
|
|
@@ -5583,38 +6400,34 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5583
6400
|
// Each arm's switch-edge copies, computed ONCE and in emission order: `argAssignsFor` mints
|
|
5584
6401
|
// swap-cycle temp names, so calling it twice for one edge burns a temp number and changes the
|
|
5585
6402
|
// output (the same reason emitDoWhile reuses its `updates`).
|
|
5586
|
-
|
|
6403
|
+
// A dead default edge carries none: the default runs only when a case falls into it, and that
|
|
6404
|
+
// case's own edge copies hand it its values.
|
|
6405
|
+
const edgeCopies = emitOrder.map((a) => (deadDefaultEdge && a.edge === defEdge ? [] : argAssignsFor(b, a.edge)));
|
|
6406
|
+
// A default edge the dispatch never takes (the op's `exhaustive` attr) gets no arm: its block
|
|
6407
|
+
// runs under whatever the dispatch path left in the registers, a state the machine never gives
|
|
6408
|
+
// it there. Unless a case runs on into that block — then the label is where that case goes.
|
|
6409
|
+
// A table whose hole names that block already gives it a case arm (`dflt` is null in
|
|
6410
|
+
// `readArms`), and a fall into the block lands under that arm's labels, never the default's.
|
|
6411
|
+
const deadDefault =
|
|
6412
|
+
deadDefaultEdge &&
|
|
6413
|
+
(armOf.has(defEdge.block) ||
|
|
6414
|
+
![...exitOf.values()].some((e) => e.kind === 'fallthrough' && e.to === defEdge.block));
|
|
5587
6415
|
const bodies = emitOrder.map((a, i) => {
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
const next = emitOrder[i + 1];
|
|
5594
|
-
// Re-read off the EMISSION array rather than trusted from `chainArms` — the seam where a
|
|
5595
|
-
// position acquires control-flow meaning, checked on the same side of it as Regime A's.
|
|
5596
|
-
if (ft && next?.entry !== exit.to) {
|
|
5597
|
-
throw new StructureError(
|
|
5598
|
-
`cannot structure '${fn.name}': ${a.values ? `case ${a.values.join('/')}` : 'the default arm'} falls ` +
|
|
5599
|
-
`through into an arm that is not the next one emitted — C fall-through only reaches the arm below`,
|
|
5600
|
-
);
|
|
6416
|
+
// The refusal `armRefusal` found for this reading, thrown at the arm it names, so an
|
|
6417
|
+
// earlier arm's own refusal speaks first. Its order is this emission array, so "the next
|
|
6418
|
+
// arm" is read off the seam where a position acquires control-flow meaning, as Regime A's is.
|
|
6419
|
+
if (read.refusal !== null && read.refusal.at === i) {
|
|
6420
|
+
read.refusal.raise();
|
|
5601
6421
|
}
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
// what the falling arm just computed (`case 0: v=7; case 1: v=5;` — the case-0 path calling
|
|
5605
|
-
// with 5). They cannot simply be dropped either: entering that arm by its own case value
|
|
5606
|
-
// needs them. Hoisting them above the switch is possible but not always safe (another arm may
|
|
5607
|
-
// read the same name first), so this shape declines LOUD; recovering it is future work.
|
|
5608
|
-
if (ft && edgeCopies[i + 1].length) {
|
|
5609
|
-
throw new StructureError(
|
|
5610
|
-
`cannot structure '${fn.name}': the case fallen into takes a value from the switch edge, ` +
|
|
5611
|
-
`which the fall-through path would re-run`,
|
|
5612
|
-
);
|
|
6422
|
+
if (deadDefault && i === emitOrder.length - 1) {
|
|
6423
|
+
return { body: [], fallsThrough: false };
|
|
5613
6424
|
}
|
|
6425
|
+
const exit = exitOf.get(a.entry)!;
|
|
6426
|
+
const ft = exit.kind === 'fallthrough';
|
|
5614
6427
|
return {
|
|
5615
6428
|
// A falling-through arm stops AT its successor arm, which then emits that body once under
|
|
5616
6429
|
// its own labels; a closed arm runs to the merge as before.
|
|
5617
|
-
body: [...edgeCopies[i], ...
|
|
6430
|
+
body: [...edgeCopies[i], ...structureArm(a.entry, ft ? exit.to : merge, merge, exit)],
|
|
5618
6431
|
fallsThrough: ft,
|
|
5619
6432
|
};
|
|
5620
6433
|
});
|
|
@@ -5696,24 +6509,64 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5696
6509
|
li.header.params.forEach(
|
|
5697
6510
|
(_, i) => initArgs[i] !== undefined && entryVals.set(li.backArgOfParam[i], initArgs[i]),
|
|
5698
6511
|
);
|
|
6512
|
+
// A CALL OR A LOAD IS ONE EVALUATION, AT ONE POINT. Read through, it equals another of one
|
|
6513
|
+
// callee or one address on the same operands only where each runs at its own test. The
|
|
6514
|
+
// guard's runs there when it sits in the guard block with no effect after it. The loop's
|
|
6515
|
+
// runs there when it sits in the header or its pure preheader: the fused `while` renders the
|
|
6516
|
+
// header's test at its top, so its first evaluation runs right after the guard's, and the
|
|
6517
|
+
// `while`'s first test is the evaluation that replaces the guard's — a store or call ahead
|
|
6518
|
+
// of it in the header is the previous iteration's body, at the top of this one. Read through
|
|
6519
|
+
// anywhere else, a cached result would equal a fresh one: a call the function made once
|
|
6520
|
+
// ahead of both (`if (v < f(a)) do {} while (v < v)` would lose the `f(a)`), or a load ahead
|
|
6521
|
+
// of a call or store that changes what it reads (`v = *p; g(); if (i < *p) do {} while
|
|
6522
|
+
// (i < v)` would lose the guard and its reload).
|
|
6523
|
+
const evaluatedAtTest = (v: Value): boolean => {
|
|
6524
|
+
const d = defs.get(v);
|
|
6525
|
+
if (d === undefined || !orderSensitive(d)) {
|
|
6526
|
+
return true;
|
|
6527
|
+
}
|
|
6528
|
+
const at = opBlock.get(d);
|
|
6529
|
+
if (at === b) {
|
|
6530
|
+
return !b.ops.slice(opIndex.get(d)! + 1).some(effectful);
|
|
6531
|
+
}
|
|
6532
|
+
return at !== undefined && (at === li.header || at === li.preheader);
|
|
6533
|
+
};
|
|
5699
6534
|
// (1) Is the guard PROVABLY the loop's own test? Fusing DELETES it and lets the `while`
|
|
5700
6535
|
// re-test, so an unproven fuse would lose the `if` outright and run a loop the source
|
|
5701
6536
|
// skipped. Structural equality is a sufficient proof, never a necessary one: `str[0]` and
|
|
5702
6537
|
// `str[i]` at `i = 0`, or a signed `e <= 0` beside an unsigned `e != 0`, are the same test
|
|
5703
|
-
// spelled differently
|
|
5704
|
-
//
|
|
5705
|
-
//
|
|
5706
|
-
//
|
|
5707
|
-
//
|
|
5708
|
-
//
|
|
6538
|
+
// spelled differently. The first is one of the identities `sameAtEntry` (ir/entry.ts) reads
|
|
6539
|
+
// under `normalize`, which the raise re-roll turns on; this caller keeps it off, since it
|
|
6540
|
+
// re-spells rows (agbcc `while (s[i]) i++` fuses into a `for`). The second has no reading.
|
|
6541
|
+
// An unproven guard KEEPS its `if`, with the loop as a `do-while` inside it — every test
|
|
6542
|
+
// the asm performs is emitted as itself. The proof still gates the SINK below: a sunk copy
|
|
6543
|
+
// runs once per iteration, and only the proof (fused form) or the kept `if` makes the
|
|
6544
|
+
// zero-trip path skip it. The kept `if` would widen the sink's key to unproven guards too;
|
|
6545
|
+
// no row needs that yet, so it stays keyed on the proof.
|
|
5709
6546
|
const enterIsTaken = takenB === h;
|
|
5710
6547
|
const contIsTaken = hterm.successors[0].block === li.header;
|
|
5711
|
-
const guardProven = sameAtEntry(
|
|
6548
|
+
const guardProven = sameAtEntry(
|
|
6549
|
+
hterm.operands[0],
|
|
6550
|
+
term.operands[0],
|
|
6551
|
+
entryVals,
|
|
6552
|
+
enterIsTaken !== contIsTaken,
|
|
6553
|
+
evaluatedAtTest,
|
|
6554
|
+
);
|
|
5712
6555
|
// Fused `while` additionally requires a header free of MATERIALIZED defs: their temps are
|
|
5713
6556
|
// assigned only inside the body (sideEffects), and an un-rotated `while` condition renders
|
|
5714
6557
|
// BEFORE the body ever ran — reading a temp uninitialized on the first test. The kept-guard
|
|
5715
6558
|
// `do-while` tests after the body, so it carries no such restriction.
|
|
5716
6559
|
const fused = guardProven && !li.header.ops.some((o) => materialize.has(o));
|
|
6560
|
+
// A KEPT GUARD IS AN ORDINARY `if` AROUND AN ORDINARY `do-while`, and emitWhile's hazards
|
|
6561
|
+
// below are the price of merging the guard's exit edge into the loop's post-loop copies,
|
|
6562
|
+
// which only the fused `while` needs, since it drops that edge. So where a kept guard meets
|
|
6563
|
+
// one of them, it is not claimed here: the `if` recovery below takes the guard, carrying each
|
|
6564
|
+
// exit edge's copies on that edge, and the loop reaches `structureBlock` as the do-while it
|
|
6565
|
+
// is (`keptGuardDoWhiles`), with the do-while's repairs — a `++` for the test's own
|
|
6566
|
+
// pre-update read, the exit-copy sink, the pre-update homes. Asked only where emitWhile
|
|
6567
|
+
// declines, so a guarded loop that structures here keeps its spelling. IDO's stolen-slot
|
|
6568
|
+
// latch reaches it (`strlen1`: `if (*s) do { t = s[1]; n++; s++; } while (t);`).
|
|
6569
|
+
const handBack = (): boolean => !fused && keptGuardDoWhiles.has(li.header);
|
|
5717
6570
|
// A pre-update exit copy is repairable rather than fatal: emitted INSIDE the body, ahead of
|
|
5718
6571
|
// the update, it captures the value the exit edge carries rather than the updated one.
|
|
5719
6572
|
// Its post-loop copy is then dropped, so the ZERO-TRIP path needs the guard→exit edge as a
|
|
@@ -5729,7 +6582,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5729
6582
|
sub,
|
|
5730
6583
|
updateWrites,
|
|
5731
6584
|
hooks.preUpdateSinkGates ?? PREUPDATE_SINK_GATES,
|
|
5732
|
-
(j) => !sameAtEntry(hexitArgs[j], guardExit.args[j] as Value, entryVals),
|
|
6585
|
+
(j) => !sameAtEntry(hexitArgs[j], guardExit.args[j] as Value, entryVals, false, evaluatedAtTest),
|
|
5733
6586
|
)
|
|
5734
6587
|
: new Map<number, Op | null>();
|
|
5735
6588
|
// (2) Every exit copy the fused form KEEPS renders after the loop, on the zero-trip path
|
|
@@ -5737,9 +6590,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5737
6590
|
// what the guard→exit edge carries, since that edge is dropped. A sunk slot is exempt: its
|
|
5738
6591
|
// seed and its body copy cover the two paths separately.
|
|
5739
6592
|
const staleExit = hexitArgs.findIndex(
|
|
5740
|
-
(a, j) => !sunk.has(j) && !sameAtEntry(a, guardExit.args[j] as Value, entryVals),
|
|
6593
|
+
(a, j) => !sunk.has(j) && !sameAtEntry(a, guardExit.args[j] as Value, entryVals, false, evaluatedAtTest),
|
|
5741
6594
|
);
|
|
5742
6595
|
if (staleExit >= 0) {
|
|
6596
|
+
if (handBack()) {
|
|
6597
|
+
break;
|
|
6598
|
+
}
|
|
5743
6599
|
throw new StructureError(
|
|
5744
6600
|
`cannot structure '${fn.name}': the fused guard's exit edge carries a value the post-loop ` +
|
|
5745
6601
|
`copies do not reproduce on a zero-trip run`,
|
|
@@ -5757,6 +6613,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5757
6613
|
null,
|
|
5758
6614
|
)
|
|
5759
6615
|
) {
|
|
6616
|
+
if (handBack()) {
|
|
6617
|
+
break;
|
|
6618
|
+
}
|
|
5760
6619
|
// NO `n++` REPAIR HERE, and the SHAPE is why rather than an oversight. The fold
|
|
5761
6620
|
// (`preUpdateCondFold`) trades the update copy at the foot of the body for a `++` at the
|
|
5762
6621
|
// leaf, which is the same program only for a BOTTOM test: it runs once per iteration, in
|
|
@@ -5774,6 +6633,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5774
6633
|
const seed = argAssigns(b, li.exit, null, sunkSlot);
|
|
5775
6634
|
const seedWrites = new Set(seed.filter((st) => st.k === 'assign').map((st) => st.name));
|
|
5776
6635
|
if (initArgs.some((a) => readsClobbered(a, new Map(), seedWrites))) {
|
|
6636
|
+
if (handBack()) {
|
|
6637
|
+
break;
|
|
6638
|
+
}
|
|
5777
6639
|
throw new StructureError(
|
|
5778
6640
|
`cannot structure '${fn.name}': seeding the zero-trip value would overwrite a value the ` +
|
|
5779
6641
|
`loop initialisation reads`,
|
|
@@ -5826,6 +6688,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5826
6688
|
hexitArgs.some((a, j) => !sunk.has(j) && reachesBodyTemp(a)) ||
|
|
5827
6689
|
[...(liveIn.get(li.exit) ?? [])].some(bodyTemp)
|
|
5828
6690
|
) {
|
|
6691
|
+
if (handBack()) {
|
|
6692
|
+
break;
|
|
6693
|
+
}
|
|
5829
6694
|
throw new StructureError(
|
|
5830
6695
|
`cannot structure '${fn.name}': a post-loop read reaches a temp the guarded body may never assign`,
|
|
5831
6696
|
);
|
|
@@ -5857,6 +6722,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5857
6722
|
// `gsub`, whose mapped values the inits deliberately hold.
|
|
5858
6723
|
const writes = new Set([...seedWrites, ...updateWriteSet(inits)]);
|
|
5859
6724
|
if (readsClobbered(term.operands[0], gsub, writes)) {
|
|
6725
|
+
if (handBack()) {
|
|
6726
|
+
break;
|
|
6727
|
+
}
|
|
5860
6728
|
throw new StructureError(
|
|
5861
6729
|
`cannot structure '${fn.name}': the kept guard's condition reads a name the loop initialisation overwrites`,
|
|
5862
6730
|
);
|
|
@@ -5956,7 +6824,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5956
6824
|
//
|
|
5957
6825
|
// Taken only where the implicit continue holds (`continueFallsToBottom`). Where it does not, a
|
|
5958
6826
|
// break edge is refused by name below, and an arm re-enters the header and declines loud.
|
|
5959
|
-
|
|
6827
|
+
// A switch arm opened in this loop, with no loop entered since: C binds a `break;` here to the
|
|
6828
|
+
// switch, so neither rule below may mint the loop's (see the second).
|
|
6829
|
+
const armFrame = switchFrames.length > 0 ? switchFrames[switchFrames.length - 1] : undefined;
|
|
6830
|
+
const armCapturesBreak = loopCtx !== null && armFrame !== undefined && armFrame.loop === loopCtx;
|
|
6831
|
+
if (
|
|
6832
|
+
loopCtx &&
|
|
6833
|
+
continueFallsToBottom(loopCtx, stop) &&
|
|
6834
|
+
(takenB === loopCtx.header || fallB === loopCtx.header) &&
|
|
6835
|
+
!(armCapturesBreak && (takenB === loopCtx.exit || fallB === loopCtx.exit))
|
|
6836
|
+
) {
|
|
5960
6837
|
const contIsTaken = takenB === loopCtx.header;
|
|
5961
6838
|
const exitB = contIsTaken ? fallB : takenB;
|
|
5962
6839
|
const isBreak = exitB === loopCtx.exit;
|
|
@@ -5998,7 +6875,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
5998
6875
|
} // continue is `taken` → leave when NOT it
|
|
5999
6876
|
const exitArm =
|
|
6000
6877
|
rule && 'bare' in rule
|
|
6001
|
-
? [...(rule.bare ? [] : argAssigns(b,
|
|
6878
|
+
? [...(rule.bare ? [] : argAssigns(b, exitB, sub)), { k: 'break' } as Stmt] // break to the loop exit
|
|
6002
6879
|
: withSub(sub, () => [...argAssigns(b, exitB, sub), ...structureRegion(exitB, stop)]); // early return
|
|
6003
6880
|
out.push(mkIf(leaveCond, exitArm, []));
|
|
6004
6881
|
return out;
|
|
@@ -6023,11 +6900,29 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
6023
6900
|
// always loud. The others are loud, since the loop was admitted on the promise of this spelling,
|
|
6024
6901
|
// unless an arm owns the edge or the exit ends in a `ret`: then if-recovery below copies the
|
|
6025
6902
|
// exit's tail into the arm and returns.
|
|
6026
|
-
//
|
|
6027
|
-
//
|
|
6028
|
-
|
|
6903
|
+
// Inside a switch arm opened in THIS loop (no loop entered since), C binds `break;` to the
|
|
6904
|
+
// switch, so the loop's `break` cannot be spelled here. An arm spells an edge to the loop's exit
|
|
6905
|
+
// only where that exit is the switch's own merge: the code after the switch then IS the exit
|
|
6906
|
+
// region, and the edge structures as any other edge to the merge — the arm's `break;`
|
|
6907
|
+
// (`switchBreakAt`), or nothing in a closed arm, which stops there. An exit whose tail returns
|
|
6908
|
+
// (or that an early-return arm owns) needs no `break`: if-recovery below copies the tail into the
|
|
6909
|
+
// arm, as it does outside a switch when the break rule refuses. Any other exit is refused here,
|
|
6910
|
+
// loud, rather than handed to the printer as a `break` it would rebind.
|
|
6911
|
+
if (loopCtx && loopCtx.breaks.has(b) && armCapturesBreak) {
|
|
6912
|
+
const exit = loopCtx.exit;
|
|
6913
|
+
const tailReturns = exit !== null && (isArm(exit) || isRet(exit));
|
|
6914
|
+
if ((exit === null || armFrame!.merge !== exit) && !tailReturns) {
|
|
6915
|
+
throw new StructureError(
|
|
6916
|
+
`cannot structure '${fn.name}': a case body leaves its switch for a loop exit past the ` +
|
|
6917
|
+
`switch's end (block #${fn.blocks.indexOf(b)}), where \`break\` would leave only the switch`,
|
|
6918
|
+
);
|
|
6919
|
+
}
|
|
6920
|
+
} else if (loopCtx && loopCtx.breaks.has(b)) {
|
|
6029
6921
|
const frame = loopCtx;
|
|
6030
|
-
|
|
6922
|
+
// An arm that runs code before it breaks (`codeBreakArm`) is rendered inside the `if`, ahead of
|
|
6923
|
+
// the `break`, and its last block's copies into the exit are the break's.
|
|
6924
|
+
const arm = frame.breakArms?.find((a) => a.from === b && (a.to === takenB || a.to === fallB));
|
|
6925
|
+
const breakIsTaken = takenB === (arm?.to ?? frame.exit);
|
|
6031
6926
|
const stayB = breakIsTaken ? fallB : takenB;
|
|
6032
6927
|
const continueRefused = stayB === frame.header && !continueFallsToBottom(frame, stop);
|
|
6033
6928
|
const rule = continueRefused
|
|
@@ -6041,11 +6936,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
6041
6936
|
leaveCond = negateCond(leaveCond);
|
|
6042
6937
|
}
|
|
6043
6938
|
const [breakEdge, stayEdge] = breakIsTaken ? term.successors : [term.successors[1], term.successors[0]];
|
|
6044
|
-
out.push(
|
|
6939
|
+
out.push(
|
|
6940
|
+
mkIf(
|
|
6941
|
+
leaveCond,
|
|
6942
|
+
[
|
|
6943
|
+
...(rule.bare ? [] : argAssignsFor(b, breakEdge)),
|
|
6944
|
+
...(arm !== undefined ? structureRegion(arm.to, frame.exit) : []),
|
|
6945
|
+
{ k: 'break' },
|
|
6946
|
+
],
|
|
6947
|
+
[],
|
|
6948
|
+
),
|
|
6949
|
+
);
|
|
6045
6950
|
out.push(...argAssignsFor(b, stayEdge), ...structureRegion(stayB, stop));
|
|
6046
6951
|
return out;
|
|
6047
6952
|
}
|
|
6048
|
-
if (continueRefused || (!isArm(frame.exit) && !isRet(frame.exit))) {
|
|
6953
|
+
if (continueRefused || frame.exit === null || (!isArm(frame.exit) && !isRet(frame.exit))) {
|
|
6049
6954
|
throw new StructureError(
|
|
6050
6955
|
`cannot structure '${fn.name}': a break out of block #${fn.blocks.indexOf(b)} ${rule.refusal}`,
|
|
6051
6956
|
);
|
|
@@ -6078,10 +6983,9 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
6078
6983
|
// `stop` stands in for the real join, so it has to BE the real join: one side must already end
|
|
6079
6984
|
// this region — an admitted arm (its `return` terminates that path) or `stop` itself. Two sides
|
|
6080
6985
|
// meeting somewhere further down instead would each re-emit everything from there to the bottom,
|
|
6081
|
-
// doubling per nesting level; those
|
|
6082
|
-
//
|
|
6083
|
-
//
|
|
6084
|
-
// return meet".
|
|
6986
|
+
// doubling per nesting level; those take the post-dominator computed over the body alone with
|
|
6987
|
+
// the back edges and the arms DELETED (`loopJoin`, below) — treating the arms as exits does
|
|
6988
|
+
// not work, since post-dominance cannot express "where the paths that did not return meet".
|
|
6085
6989
|
const clampToLoop =
|
|
6086
6990
|
loopCtx !== null &&
|
|
6087
6991
|
ipd !== null &&
|
|
@@ -6089,11 +6993,44 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
6089
6993
|
term.successors.some((sc) => isArm(sc.block) || sc.block === stop) &&
|
|
6090
6994
|
term.successors.every((sc) => loopCtx!.body.has(sc.block) || isArm(sc.block));
|
|
6091
6995
|
// In a `while (1)` body the join is the body's own, which the CFG's post-dominance cannot see
|
|
6092
|
-
// (`
|
|
6996
|
+
// (`loopJoin`): a branch whose one arm continues is post-dominated, in the CFG, by wherever
|
|
6093
6997
|
// the other arm leaves the loop from.
|
|
6094
6998
|
const inForever = loopCtx === null ? undefined : foreverLoops.get(loopCtx.header);
|
|
6095
|
-
|
|
6096
|
-
const
|
|
6999
|
+
// A break arm's own blocks join as any code outside the loop does: they are not the body's.
|
|
7000
|
+
const inArm = loopCtx?.breakArms?.some((a) => a.region.has(b)) === true;
|
|
7001
|
+
const bodyJoin = inForever === undefined || inArm ? undefined : loopJoin(inForever, b, true);
|
|
7002
|
+
// THE SAME QUESTION IN A TESTED LOOP. A branch whose one way leads out of the body by a `break` —
|
|
7003
|
+
// the `else break;` of an if/else-if chain — has the loop's exit as its CFG join, and its in-body
|
|
7004
|
+
// arm walked from there through the latch back into the header (`onStack`). Its join is where its
|
|
7005
|
+
// paths that stay in the body meet: the post-dominator over the body with the back edges and the
|
|
7006
|
+
// exits deleted (`loopJoin`). Asked only where the CFG join leaves the body, no clamp applies,
|
|
7007
|
+
// one successor stays in the body, and every way out of the body from here is one the loop
|
|
7008
|
+
// renders in place (`leavesByOwnedEdges`).
|
|
7009
|
+
const tested =
|
|
7010
|
+
loopCtx !== null &&
|
|
7011
|
+
inForever === undefined &&
|
|
7012
|
+
!inArm &&
|
|
7013
|
+
ipd !== null &&
|
|
7014
|
+
!loopCtx.body.has(ipd) &&
|
|
7015
|
+
!clampToLoop &&
|
|
7016
|
+
loopCtx.body.has(b) &&
|
|
7017
|
+
term.successors.some((sc) => loopCtx!.body.has(sc.block) && sc.block !== loopCtx!.header) &&
|
|
7018
|
+
leavesByOwnedEdges(loopCtx, b)
|
|
7019
|
+
? loopJoin(loopCtx, b, false)
|
|
7020
|
+
: undefined;
|
|
7021
|
+
// Inside a switch arm, the arm's own join wins over every reading above: each of them reads the
|
|
7022
|
+
// CFG, where the arm's paths meet past the switch.
|
|
7023
|
+
const inArmJoin = armJoin(b, stop);
|
|
7024
|
+
const merge =
|
|
7025
|
+
inArmJoin !== undefined
|
|
7026
|
+
? (inArmJoin ?? stop)
|
|
7027
|
+
: bodyJoin !== undefined
|
|
7028
|
+
? (bodyJoin ?? stop)
|
|
7029
|
+
: tested !== undefined
|
|
7030
|
+
? (tested ?? stop)
|
|
7031
|
+
: clampToLoop
|
|
7032
|
+
? stop
|
|
7033
|
+
: (ipd ?? stop);
|
|
6097
7034
|
// Per-successor records, NOT successorTo(b, block): a cond_br whose two edges reach the SAME
|
|
6098
7035
|
// block with different args would otherwise give both arms the first edge's copies.
|
|
6099
7036
|
const thenS = [...argAssignsFor(b, term.successors[0]), ...structureRegion(takenB, merge)];
|
|
@@ -6206,9 +7143,21 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
6206
7143
|
stmtsEqual(thenS, elseS) &&
|
|
6207
7144
|
testRereadsOnly(term, b, { defs, useSitesOf, dom, ipdom, loops: forest.byHeader.values() })
|
|
6208
7145
|
) {
|
|
6209
|
-
out.push(...thenS);
|
|
7146
|
+
out.push(...unionStmtsEvidence(thenS, elseS));
|
|
6210
7147
|
} else {
|
|
6211
|
-
|
|
7148
|
+
// A compare whose runtime helper names the relation the source spelled DECIDES its own sense,
|
|
7149
|
+
// one-armed or two: the arm order is the one that spells that relation, because no spelling
|
|
7150
|
+
// of its negation calls the same helper (l3/ast.ts `keepsHelperRel`). `if (a == b) {} else`
|
|
7151
|
+
// is how the source wrote agbcc's `bl __eqsf2; cmp r0, #0; beq` over the arm.
|
|
7152
|
+
const helperSense = keepsHelperRel(cond) ? false : regainsHelperRel(cond) ? true : undefined;
|
|
7153
|
+
const negate = helperSense ?? negateHere;
|
|
7154
|
+
out.push(
|
|
7155
|
+
negate
|
|
7156
|
+
? { k: 'if', cond: negateCond(cond), then: elseS, else: thenS }
|
|
7157
|
+
: helperSense === false
|
|
7158
|
+
? { k: 'if', cond, then: thenS, else: elseS }
|
|
7159
|
+
: mkIf(cond, thenS, elseS),
|
|
7160
|
+
);
|
|
6212
7161
|
}
|
|
6213
7162
|
if (merge && merge !== stop) {
|
|
6214
7163
|
out.push(...structureRegion(merge, stop));
|
|
@@ -6347,42 +7296,139 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
6347
7296
|
// Mirror the br/cond_br cases: argAssigns then structureRegion. Structure the body under this
|
|
6348
7297
|
// loop's frame so an in-body conditional exit (break / early return) is recognised instead of
|
|
6349
7298
|
// tripping the header-re-entry `onStack` guard.
|
|
6350
|
-
const body = withLoop(
|
|
6351
|
-
|
|
6352
|
-
...structureRegion(wl.bodyEntry, wl.header),
|
|
6353
|
-
|
|
7299
|
+
const body = withLoop(
|
|
7300
|
+
{ header: wl.header, exit: wl.exit, body: wl.body, arms: wl.arms, breaks: wl.breaks, breakArms: wl.breakArms },
|
|
7301
|
+
() => [...argAssigns(wl.header, wl.bodyEntry), ...structureRegion(wl.bodyEntry, wl.header)],
|
|
7302
|
+
);
|
|
7303
|
+
// A test whose tail is a float compare the stay sense moves off its runtime helper's relation
|
|
7304
|
+
// (l3/ast.ts `splitHelperBreak`) leaves through a break guard, ahead of the edge's copies: the
|
|
7305
|
+
// header evaluated it before them.
|
|
7306
|
+
const split = splitHelperBreak(cond);
|
|
7307
|
+
if (split !== null) {
|
|
7308
|
+
cond = split.head;
|
|
7309
|
+
body.unshift({ k: 'if', cond: split.guard, then: [{ k: 'break' }], else: [] });
|
|
7310
|
+
}
|
|
6354
7311
|
const out: Stmt[] = [{ k: 'while', cond, body }];
|
|
6355
7312
|
out.push(...argAssigns(wl.header, wl.exit)); // phi args carried on the header→exit edge, if any
|
|
6356
7313
|
out.push(...structureRegion(wl.exit, stop));
|
|
6357
7314
|
return out;
|
|
6358
7315
|
};
|
|
6359
7316
|
|
|
6360
|
-
//
|
|
6361
|
-
//
|
|
6362
|
-
//
|
|
6363
|
-
//
|
|
6364
|
-
//
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
7317
|
+
// MAY A BRANCH IN A TESTED LOOP JOIN INSIDE THE BODY? Only where every way out of the body from `b`
|
|
7318
|
+
// is one the loop's own spelling renders in place: a `break` (an edge of `breaks`, or the edge the
|
|
7319
|
+
// loop tests at its header or latch) or an early-`return` arm the loop owns. Any other edge out
|
|
7320
|
+
// leaves for code that is copied wherever it is reached, and a join inside the body would write
|
|
7321
|
+
// that tail into the arm as well.
|
|
7322
|
+
const leavesByOwnedEdges = (frame: LoopFrame, b: Block): boolean => {
|
|
7323
|
+
const exitFrom = whileLoops.get(frame.header)?.header ?? doWhileLoops.get(frame.header)?.latch;
|
|
7324
|
+
const seen = new Set<Block>();
|
|
7325
|
+
for (const stack = [b]; stack.length > 0;) {
|
|
7326
|
+
const x = stack.pop()!;
|
|
7327
|
+
if (x === frame.header || seen.has(x)) {
|
|
7328
|
+
continue;
|
|
7329
|
+
}
|
|
7330
|
+
seen.add(x);
|
|
7331
|
+
for (const sx of successorsOf(x)) {
|
|
7332
|
+
if (frame.body.has(sx)) {
|
|
7333
|
+
stack.push(sx);
|
|
7334
|
+
} else if (
|
|
7335
|
+
!frame.arms.some((a) => a.from === x && a.to === sx) &&
|
|
7336
|
+
!(sx === frame.exit && (frame.breaks.has(x) || x === exitFrom))
|
|
7337
|
+
) {
|
|
7338
|
+
return false;
|
|
7339
|
+
}
|
|
7340
|
+
}
|
|
7341
|
+
}
|
|
7342
|
+
return true;
|
|
7343
|
+
};
|
|
7344
|
+
// THE JOIN OF A BRANCH IN A LOOP BODY, over the body with its back edges and exits deleted: where
|
|
7345
|
+
// the paths from `b` that stay in the body meet, its nearest post-dominator over the rest. Null
|
|
7346
|
+
// when none goes on, and the region's own `stop` ends it. Without the deletion, a branch whose one
|
|
7347
|
+
// arm continues joins at the loop bottom, and whatever the other arm runs before its own continue
|
|
7348
|
+
// is copied into both arms; in a tested loop, the `else break;` of an if/else-if chain joins at
|
|
7349
|
+
// the loop's exit.
|
|
7350
|
+
//
|
|
7351
|
+
// `endsCut` is the `while (1)` rule, and only `emitForever`'s frames take it: a block that only
|
|
7352
|
+
// jumps back to the header or out of the loop, rendering nothing but that edge's copies, ENDS its
|
|
7353
|
+
// path — a `continue`, a `break` — so it constrains no join either. A block that ends every path
|
|
7354
|
+
// but TESTS first is still where its predecessors meet, and so is a latch two body paths reach: the
|
|
7355
|
+
// foot of `for (…) { if (c) {…} }`, where deleting it moves the `if`'s join into one arm, which then
|
|
7356
|
+
// copies the latch and adds a `continue` the source never wrote. A break block two paths share
|
|
7357
|
+
// stays an end, each path its own `break`. A tested loop's latch is where its test renders, so
|
|
7358
|
+
// there it constrains the join like any block.
|
|
7359
|
+
/** per header, one cache for each value of `endsCut` */
|
|
7360
|
+
const loopJoins = [new Map<Block, Map<Block, Block | null>>(), new Map<Block, Map<Block, Block | null>>()];
|
|
7361
|
+
const loopJoin = (loop: { header: Block; body: Set<Block> }, b: Block, endsCut: boolean): Block | null => {
|
|
7362
|
+
const cache = loopJoins[endsCut ? 1 : 0];
|
|
7363
|
+
let ipdom = cache.get(loop.header);
|
|
6373
7364
|
if (ipdom === undefined) {
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
const ends = (n: Block): boolean =>
|
|
6377
|
-
inBody(n).length === 0 &&
|
|
6378
|
-
n.ops[n.ops.length - 1].opcode === 'br' &&
|
|
6379
|
-
n.ops.every((op) => !effectful(op) && !materialize.has(op)) &&
|
|
6380
|
-
!(successorsOf(n)[0] === fl.header && new Set(preds.get(n)?.filter((q) => fl.body.has(q))).size > 1);
|
|
6381
|
-
ipdom = postDominators(fn, fl.body, (n) => inBody(n).filter((x) => !ends(x)));
|
|
6382
|
-
foreverJoins.set(fl.header, ipdom);
|
|
7365
|
+
ipdom = regionJoins(loop.body, loop.header, endsCut);
|
|
7366
|
+
cache.set(loop.header, ipdom);
|
|
6383
7367
|
}
|
|
6384
7368
|
return ipdom.get(b) ?? null;
|
|
6385
7369
|
};
|
|
7370
|
+
// The post-dominators `loopJoin` reads, over `body` with every edge out of it and every edge back
|
|
7371
|
+
// to `header` (when there is one) deleted — save an edge to `toExit`, which goes to EXIT. Also
|
|
7372
|
+
// the join inside a switch arm (`armJoin`), whose `toExit` is the sibling a falling arm runs into
|
|
7373
|
+
// and null for a closed one.
|
|
7374
|
+
function regionJoins(
|
|
7375
|
+
body: ReadonlySet<Block>,
|
|
7376
|
+
header: Block | null,
|
|
7377
|
+
endsCut: boolean,
|
|
7378
|
+
toExit: Block | null = null,
|
|
7379
|
+
): Map<Block, Block | null> {
|
|
7380
|
+
const inBody = (n: Block): Block[] => [...new Set(successorsOf(n))].filter((x) => body.has(x) && x !== header);
|
|
7381
|
+
const ends = (n: Block): boolean =>
|
|
7382
|
+
endsCut &&
|
|
7383
|
+
inBody(n).length === 0 &&
|
|
7384
|
+
n.ops[n.ops.length - 1].opcode === 'br' &&
|
|
7385
|
+
n.ops.every((op) => !effectful(op) && !materialize.has(op)) &&
|
|
7386
|
+
!(successorsOf(n)[0] === header && new Set(preds.get(n)?.filter((q) => body.has(q))).size > 1);
|
|
7387
|
+
const exits = (n: Block): boolean => toExit !== null && successorsOf(n).includes(toExit);
|
|
7388
|
+
// IN A FALLING ARM, A PATH THAT CANNOT FALL ENDS WHERE IT LEAVES THE FALLING PATHS. Deleting the
|
|
7389
|
+
// edge to the merge or a `ret` covers only a jump straight off the branch (`if (b) break;`).
|
|
7390
|
+
// When the leaving path runs code first — `if (b) { g(7); break; }`, or a `return k` through a
|
|
7391
|
+
// shared epilogue — its block keeps no edge on, and a block with none is post-dominated only by
|
|
7392
|
+
// itself, so the guard above it would find no join and copy the rest of the arm into both sides.
|
|
7393
|
+
// Such a block is a jump all the same: an edge into a block that can no longer reach the
|
|
7394
|
+
// fall-through is deleted too.
|
|
7395
|
+
let falls: Set<Block> | null = null;
|
|
7396
|
+
if (toExit !== null) {
|
|
7397
|
+
const live = new Set<Block>();
|
|
7398
|
+
for (let grew = true; grew;) {
|
|
7399
|
+
grew = false;
|
|
7400
|
+
for (const n of body) {
|
|
7401
|
+
if (!live.has(n) && (exits(n) || inBody(n).some((x) => live.has(x) && !ends(x)))) {
|
|
7402
|
+
live.add(n);
|
|
7403
|
+
grew = true;
|
|
7404
|
+
}
|
|
7405
|
+
}
|
|
7406
|
+
}
|
|
7407
|
+
falls = live;
|
|
7408
|
+
}
|
|
7409
|
+
const onward = (n: Block): Block[] => inBody(n).filter((x) => !ends(x) && (falls === null || falls.has(x)));
|
|
7410
|
+
// A CYCLE NO PATH LEAVES. An inner loop at the foot of the body — the vblank wait closing a
|
|
7411
|
+
// `while (1)` main loop, `while (q() == 3);` — keeps only its own back edge once the edges
|
|
7412
|
+
// to the header and to the ends are deleted, so its paths never end and post-dominance
|
|
7413
|
+
// over it is undefined: no branch above it would find a join, and each would re-emit the rest
|
|
7414
|
+
// of the body, doubling per `if`. Such a block ends its path at the edge it leaves the cycle
|
|
7415
|
+
// by. A block that reaches an end keeps its edges as they are, so no defined join moves.
|
|
7416
|
+
const reachesEnd = new Set<Block>();
|
|
7417
|
+
for (let grew = true; grew;) {
|
|
7418
|
+
grew = false;
|
|
7419
|
+
for (const n of body) {
|
|
7420
|
+
if (!reachesEnd.has(n) && (onward(n).length === 0 || exits(n) || onward(n).some((x) => reachesEnd.has(x)))) {
|
|
7421
|
+
reachesEnd.add(n);
|
|
7422
|
+
grew = true;
|
|
7423
|
+
}
|
|
7424
|
+
}
|
|
7425
|
+
}
|
|
7426
|
+
return postDominators(fn, body, (n) =>
|
|
7427
|
+
!exits(n) && (reachesEnd.has(n) || successorsOf(n).every((x) => onward(n).includes(x)))
|
|
7428
|
+
? onward(n)
|
|
7429
|
+
: [...onward(n), null],
|
|
7430
|
+
);
|
|
7431
|
+
}
|
|
6386
7432
|
|
|
6387
7433
|
// Does rendering `root` under `map` re-derive one of `targets`: reach it through the def tree
|
|
6388
7434
|
// without passing a mapped or a named value, either of which renders as a name?
|
|
@@ -6434,15 +7480,22 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
6434
7480
|
// neither reading is the value, decline LOUD, as the do-while latch does.
|
|
6435
7481
|
const emitForever = (fl: ForeverLoopInfo, stop: Block | null): Stmt[] => {
|
|
6436
7482
|
dwActive.add(fl.header);
|
|
6437
|
-
const
|
|
6438
|
-
|
|
6439
|
-
|
|
7483
|
+
const frame = {
|
|
7484
|
+
header: fl.header,
|
|
7485
|
+
exit: fl.exit,
|
|
7486
|
+
body: fl.body,
|
|
7487
|
+
arms: fl.arms,
|
|
7488
|
+
breaks: fl.breaks,
|
|
7489
|
+
breakArms: fl.breakArms,
|
|
7490
|
+
};
|
|
7491
|
+
const body = withLoop(frame, () => structureBlock(fl.header, fl.header));
|
|
6440
7492
|
dwActive.delete(fl.header);
|
|
6441
7493
|
const loop: Stmt = { k: 'while', cond: { k: 'const', value: 1 }, body };
|
|
6442
|
-
|
|
7494
|
+
const exit = fl.exit;
|
|
7495
|
+
if (exit === null || !breaksOut(body)) {
|
|
6443
7496
|
return [loop];
|
|
6444
7497
|
}
|
|
6445
|
-
const region = exitRegion(
|
|
7498
|
+
const region = exitRegion(exit, fl.body, stop);
|
|
6446
7499
|
const { sub: innerSub, unreadable } = latchInnerSub(fl.header, fl.body, [...fl.breaks], region);
|
|
6447
7500
|
if (unreadable.size > 0 && rootsOf(region).some((r) => rendersThrough(r, innerSub, unreadable))) {
|
|
6448
7501
|
throw new StructureError(
|
|
@@ -6452,7 +7505,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
6452
7505
|
}
|
|
6453
7506
|
return [
|
|
6454
7507
|
loop,
|
|
6455
|
-
...(innerSub.size > 0 ? withSub(innerSub, () => structureRegion(
|
|
7508
|
+
...(innerSub.size > 0 ? withSub(innerSub, () => structureRegion(exit, stop)) : structureRegion(exit, stop)),
|
|
6456
7509
|
];
|
|
6457
7510
|
};
|
|
6458
7511
|
|
|
@@ -6967,11 +8020,19 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
6967
8020
|
// `synthetic:dma_fill_uninit` loses its record here, and so did `kleod:ProcessInputAndUpdateEntities`
|
|
6968
8021
|
// before that row was retired (2026-09-13).
|
|
6969
8022
|
const frameRecord = (at: Op): { frame?: { loads: number; stores: number } } => {
|
|
8023
|
+
if (at.attrs[OWN_RETURN_OBJECT] === true || at.attrs[CALL_RETURN_OBJECT] !== undefined) {
|
|
8024
|
+
return {};
|
|
8025
|
+
}
|
|
6970
8026
|
const off = at.attrs.off as number;
|
|
6971
8027
|
const roots = new Set<Value>();
|
|
6972
8028
|
for (const b of fn.blocks) {
|
|
6973
8029
|
for (const op of b.ops) {
|
|
6974
|
-
if (
|
|
8030
|
+
if (
|
|
8031
|
+
op.opcode === 'laddr' &&
|
|
8032
|
+
(op.attrs.off as number) === off &&
|
|
8033
|
+
op.attrs[OWN_RETURN_OBJECT] !== true &&
|
|
8034
|
+
op.attrs[CALL_RETURN_OBJECT] === undefined
|
|
8035
|
+
) {
|
|
6975
8036
|
roots.add(op.results[0]);
|
|
6976
8037
|
}
|
|
6977
8038
|
}
|
|
@@ -6988,8 +8049,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
6988
8049
|
stores++;
|
|
6989
8050
|
}
|
|
6990
8051
|
}
|
|
8052
|
+
// a struct passed by value is read whole at the call: the word loads the asm made of it
|
|
8053
|
+
const passed = byValueOperands(op)
|
|
8054
|
+
.map((b) => b.at)
|
|
8055
|
+
.filter((at) => roots.has(op.operands[at]));
|
|
8056
|
+
passed.forEach((at) => {
|
|
8057
|
+
const t = op.operands[at].type;
|
|
8058
|
+
loads += t.kind === 'ptr' && t.to.kind === 'struct' ? Math.ceil((t.to.size ?? 4) / 4) : 1;
|
|
8059
|
+
});
|
|
6991
8060
|
// every OTHER mention of the address, operands and branch arguments alike
|
|
6992
|
-
const elsewhere = op.operands.some((v, i) => roots.has(v) && !(access && i === 0));
|
|
8061
|
+
const elsewhere = op.operands.some((v, i) => roots.has(v) && !(access && i === 0) && !passed.includes(i));
|
|
6993
8062
|
if (elsewhere || op.successors.some((sx) => sx.args.some((v) => roots.has(v)))) {
|
|
6994
8063
|
return {};
|
|
6995
8064
|
}
|
|
@@ -7075,7 +8144,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
7075
8144
|
// halves, so a change to either fails there.
|
|
7076
8145
|
...fn.blocks
|
|
7077
8146
|
.flatMap((b) => b.ops)
|
|
7078
|
-
.filter((op) => op.opcode === 'undef')
|
|
8147
|
+
.filter((op) => op.opcode === 'undef' && !coalescedUndefs.has(op))
|
|
8148
|
+
.filter((op, i, all) => all.findIndex((o) => undefName.get(o) === undefName.get(op)) === i)
|
|
7079
8149
|
.map((op) => ({
|
|
7080
8150
|
name: undefName.get(op)!,
|
|
7081
8151
|
type: varType.get(undefName.get(op)!) ?? op.results[0].type,
|
|
@@ -7090,6 +8160,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
7090
8160
|
}
|
|
7091
8161
|
: {}),
|
|
7092
8162
|
...(localStatics.statics.length ? { statics: localStatics.statics } : {}),
|
|
8163
|
+
...(localStatics.images.length ? { images: localStatics.images } : {}),
|
|
7093
8164
|
retType: returnsVoid ? T.void() : returnType(fn),
|
|
7094
8165
|
body,
|
|
7095
8166
|
...(structs.length ? { structs } : {}),
|
|
@@ -7098,6 +8169,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
7098
8169
|
...(spillSlotOrder !== undefined ? { slotOrder: spillSlotOrder } : {}),
|
|
7099
8170
|
...calledOnly({ declaredArgs, declaredReturns }, body),
|
|
7100
8171
|
...(dialect !== undefined ? { dialect } : {}),
|
|
8172
|
+
...(designatedInitializers ? { designators: true as const } : {}),
|
|
8173
|
+
...(strictPointerConversions ? { strictConversions: true as const } : {}),
|
|
7101
8174
|
};
|
|
7102
8175
|
if (fn.localObjects === undefined) {
|
|
7103
8176
|
return tree;
|
|
@@ -7108,9 +8181,30 @@ export function structure(fn: Fn, opts: StructureOptions = {}, hooks: StructureH
|
|
|
7108
8181
|
`cannot structure '${fn.name}': names a function-scope static ('${named.symbol}') ${named.refused}`,
|
|
7109
8182
|
);
|
|
7110
8183
|
}
|
|
8184
|
+
// The compiler's anonymous constant is spelled only as the initializer of the locals its copies
|
|
8185
|
+
// fill, which the emitter folds (l3/constinit.ts says why there). Asked here, so a function
|
|
8186
|
+
// whose image cannot fold declines loudly rather than reaching an emitter that cannot print it.
|
|
8187
|
+
const image = constantImageRefusal(named);
|
|
8188
|
+
if (image !== null) {
|
|
8189
|
+
throw new StructureError(
|
|
8190
|
+
`cannot structure '${fn.name}': reads the compiler's anonymous constant ('${image.image}') ${image.refused}`,
|
|
8191
|
+
);
|
|
8192
|
+
}
|
|
7111
8193
|
return named;
|
|
7112
8194
|
}
|
|
7113
8195
|
|
|
8196
|
+
/** Each declared-callee table's own key order, name → position, computed once per table object: the
|
|
8197
|
+
* ranked pass structures every candidate against the same tables. */
|
|
8198
|
+
const keyOrders = new WeakMap<object, ReadonlyMap<string, number>>();
|
|
8199
|
+
function keyOrder(table: object): ReadonlyMap<string, number> {
|
|
8200
|
+
let order = keyOrders.get(table);
|
|
8201
|
+
if (order === undefined) {
|
|
8202
|
+
order = new Map(Object.keys(table).map((n, i) => [n, i]));
|
|
8203
|
+
keyOrders.set(table, order);
|
|
8204
|
+
}
|
|
8205
|
+
return order;
|
|
8206
|
+
}
|
|
8207
|
+
|
|
7114
8208
|
/** Of each table of declared callees, the entries this body calls — every tree the ranked pass
|
|
7115
8209
|
* structures carries its own, and is keyed by its whole text. A table this body calls nothing in is
|
|
7116
8210
|
* absent. */
|
|
@@ -7124,9 +8218,17 @@ function calledOnly(
|
|
|
7124
8218
|
callees.add(e.fn);
|
|
7125
8219
|
}
|
|
7126
8220
|
}
|
|
8221
|
+
// Look up the few names this body calls, not every entry of the table: a project's table holds
|
|
8222
|
+
// every function its ELF declares (thousands for pokeemerald), and this runs on each structuring
|
|
8223
|
+
// of each candidate. The result keeps the TABLE's key order, as a whole-table filter would,
|
|
8224
|
+
// because the tree is keyed by its whole text.
|
|
7127
8225
|
const of = <T>(table: Readonly<Record<string, T>> | undefined): Record<string, T> | undefined => {
|
|
7128
|
-
|
|
7129
|
-
|
|
8226
|
+
if (table === undefined || callees.size === 0) {
|
|
8227
|
+
return undefined;
|
|
8228
|
+
}
|
|
8229
|
+
const order = keyOrder(table);
|
|
8230
|
+
const names = [...callees].filter((n) => order.has(n)).sort((a, b) => order.get(a)! - order.get(b)!);
|
|
8231
|
+
return names.length > 0 ? Object.fromEntries(names.map((n) => [n, table[n]])) : undefined;
|
|
7130
8232
|
};
|
|
7131
8233
|
const declaredArgs = of(tables.declaredArgs);
|
|
7132
8234
|
const declaredReturns = of(tables.declaredReturns);
|
|
@@ -7295,7 +8397,7 @@ function substVar(e: Expr, from: string, to: string): Expr {
|
|
|
7295
8397
|
|
|
7296
8398
|
// empty-then peephole: `if (c) {} else { S }` → `if (!c) { S }`
|
|
7297
8399
|
function mkIf(cond: Expr, thenS: Stmt[], elseS: Stmt[]): Stmt {
|
|
7298
|
-
if (thenS.length === 0 && elseS.length > 0) {
|
|
8400
|
+
if (thenS.length === 0 && elseS.length > 0 && !keepsHelperRel(cond)) {
|
|
7299
8401
|
return { k: 'if', cond: negateCond(cond), then: elseS, else: [] };
|
|
7300
8402
|
}
|
|
7301
8403
|
return { k: 'if', cond, then: thenS, else: elseS };
|
|
@@ -7355,11 +8457,31 @@ function* inEdgeRecords(preds: Map<Block, Block[]>, b: Block): Generator<{ pred:
|
|
|
7355
8457
|
}
|
|
7356
8458
|
}
|
|
7357
8459
|
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
8460
|
+
/** THE function-level immediate post-dominators, the one definition `structure()` and its
|
|
8461
|
+
* enumeration gate `hasDivergentSharedRet` both read, so the gate's join for an `if` is the join
|
|
8462
|
+
* the follow sees. A loop no edge leaves reaches no `ret`, so plain post-dominance would hand
|
|
8463
|
+
* every block before it every block as a post-dominator, and an `if` ahead of it an arbitrary
|
|
8464
|
+
* join. Its header ENDS paths as a `ret` does: the region holding the loop ends there
|
|
8465
|
+
* (`emitForever`). `forest` is the caller's, when it already has one. */
|
|
8466
|
+
export function functionPostDominators(
|
|
8467
|
+
fn: Fn,
|
|
8468
|
+
forest: { byHeader: ReadonlyMap<Block, NaturalLoop> } = analyzeLoops(fn, dominators(fn)),
|
|
8469
|
+
): Map<Block, Block | null> {
|
|
8470
|
+
const exitless = [...forest.byHeader.values()].filter((nl) => nl.exitEdges.length === 0).map((nl) => nl.header);
|
|
8471
|
+
return postDominators(fn, undefined, undefined, new Set(exitless));
|
|
8472
|
+
}
|
|
8473
|
+
|
|
8474
|
+
// Immediate post-dominators. EXIT is represented as `null`; ret-blocks post-lead to it, and so do
|
|
8475
|
+
// the blocks of `ends` besides their own successors. Over the subgraph `keep` induces when given,
|
|
8476
|
+
// every member of which must still reach a `ret` inside it — or, with `onward`, over only the edges
|
|
8477
|
+
// it names between members of `keep` (and to EXIT, where it names `null`): a block it names no edge
|
|
7361
8478
|
// from ends its paths, post-dominated by nothing but itself.
|
|
7362
|
-
function postDominators(
|
|
8479
|
+
function postDominators(
|
|
8480
|
+
fn: Fn,
|
|
8481
|
+
keep?: ReadonlySet<Block>,
|
|
8482
|
+
onward?: (b: Block) => (Block | null)[],
|
|
8483
|
+
ends?: ReadonlySet<Block>,
|
|
8484
|
+
): Map<Block, Block | null> {
|
|
7363
8485
|
const blocks = keep ? fn.blocks.filter((b) => keep.has(b)) : fn.blocks;
|
|
7364
8486
|
const nodes: (Block | null)[] = [null, ...blocks];
|
|
7365
8487
|
const succ = (b: Block): (Block | null)[] => {
|
|
@@ -7367,7 +8489,11 @@ function postDominators(fn: Fn, keep?: ReadonlySet<Block>, onward?: (b: Block) =
|
|
|
7367
8489
|
return onward(b);
|
|
7368
8490
|
}
|
|
7369
8491
|
const term = b.ops[b.ops.length - 1];
|
|
7370
|
-
|
|
8492
|
+
if (term.opcode === 'ret') {
|
|
8493
|
+
return [null];
|
|
8494
|
+
}
|
|
8495
|
+
const onwards = successorsOf(b).filter((s) => keep === undefined || keep.has(s));
|
|
8496
|
+
return ends?.has(b) === true ? [...onwards, null] : onwards;
|
|
7371
8497
|
};
|
|
7372
8498
|
const pdom = new Map<Block | null, Set<Block | null>>();
|
|
7373
8499
|
pdom.set(null, new Set([null]));
|