@asmlift/core 0.6.0 → 0.7.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 +2 -2
- package/package.json +1 -1
- package/src/backend/cfamily.ts +39 -11
- package/src/contracts.ts +60 -11
- package/src/frontend/ssa.ts +1 -1
- package/src/frontend/thumb.ts +2 -2
- package/src/ir/alias.ts +24 -0
- package/src/ir/core.ts +8 -0
- package/src/ir/opcodes.ts +43 -7
- package/src/ir/simplify.ts +1 -1
- package/src/l3/address.ts +2 -2
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +4 -4
- package/src/l3/ast.ts +65 -21
- package/src/l3/basecse.ts +48 -28
- package/src/l3/coalesce.ts +9 -9
- package/src/l3/gates.ts +75 -1
- package/src/l3/hoist.ts +1 -1
- package/src/l3/homesplit.ts +13 -13
- package/src/l3/initfirst.ts +3 -3
- package/src/l3/inlinebase.ts +16 -16
- package/src/l3/mentions.ts +68 -5
- package/src/l3/mulfirst.ts +3 -3
- package/src/l3/nearbase.ts +4 -4
- package/src/l3/offmember.ts +5 -5
- package/src/l3/parkfirst.ts +6 -6
- package/src/l3/pollguard.ts +3 -3
- package/src/l3/ptrfield.ts +4 -4
- package/src/l3/regspell.ts +8 -8
- package/src/l3/reindex.ts +22 -17
- package/src/l3/scopebase.ts +28 -25
- package/src/l3/sinkinit.ts +7 -7
- package/src/l3/slotorder.ts +3 -3
- package/src/l3/storage.ts +1 -1
- package/src/l3/tailmerge.ts +2 -2
- package/src/l3/typing.ts +3 -3
- package/src/l3/unmerge.ts +483 -59
- package/src/l3/unreduce.ts +13 -13
- package/src/l3/volatileptr.ts +11 -11
- package/src/l3/volatileval.ts +11 -11
- package/src/l3/volstore.ts +16 -16
- package/src/l3/zerosub.ts +6 -6
- package/src/pattern/engine.ts +4 -4
- package/src/pipeline.ts +17 -5
- package/src/proto.ts +2 -2
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +32 -12
- package/src/raise/gvn.ts +2 -2
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +4 -4
- package/src/raise/narrowlocal.ts +18 -2
- package/src/raise/paramwidth.ts +24 -2
- package/src/raise/pre-recovery.ts +90 -25
- package/src/raise/retsink.ts +381 -15
- package/src/raise/shortcircuit.ts +595 -34
- package/src/raise/structs.ts +4 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +4 -4
- package/src/{rank-axes.ts → rank-variations.ts} +319 -189
- package/src/rank.ts +1148 -803
- package/src/structure/analysis.ts +87 -90
- package/src/structure/bitfields.ts +130 -30
- package/src/structure/globalaccess.ts +30 -4
- package/src/structure/namecoalesce.ts +32 -13
- package/src/structure/structure.ts +1415 -200
- package/src/structure/switch-recover.ts +100 -7
- package/src/symbols.ts +127 -6
- package/src/target.ts +155 -35
- package/src/trace.ts +1 -1
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
package/src/rank.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// from asm alone (is this value signed or unsigned? which branch sense did the source spell?).
|
|
3
3
|
// Rather than guess, asmlift emits a small set of CANDIDATES and lets an external differ score
|
|
4
4
|
// pick the winner — the differ is the fitness function; types/branch-sense are differ-ranked
|
|
5
|
-
//
|
|
5
|
+
// variations, not asserted truths.
|
|
6
6
|
//
|
|
7
|
-
// This module owns only the PURE half: producing the distinct
|
|
7
|
+
// This module owns only the PURE half: producing the distinct candidates. It has NO
|
|
8
8
|
// scorer (that stays out of @asmlift/core, which is browser-pure). `rankBy` takes an INJECTED
|
|
9
9
|
// scoreFn, so the same enumeration feeds the cli's Node/objdiff scorer and the webapp's
|
|
10
10
|
// wasm/objdiff scorer alike.
|
|
@@ -12,20 +12,27 @@
|
|
|
12
12
|
// THREE SIBLING FILES, one job each — never a `rank/` directory, which beside `rank.ts` is a
|
|
13
13
|
// resolver trap:
|
|
14
14
|
// rank.ts this file: the enumeration DRIVER and the two ranking drivers over it.
|
|
15
|
-
// rank-
|
|
16
|
-
// base-CSE
|
|
15
|
+
// rank-variations.ts the TABLES the driver walks — structure variations, stacked and pre-respell
|
|
16
|
+
// variations, the base-CSE hoist rosters. Their DECLARATION ORDER is published behaviour
|
|
17
17
|
// (`compareScored` breaks a score tie by enumeration order), so reordering one
|
|
18
18
|
// is a behaviour change and never a tidy-up.
|
|
19
19
|
// rank-declare.ts the DECLARATION half: what a candidate's own asm says about the globals it
|
|
20
20
|
// names, and which of those names a declaration must refuse to claim.
|
|
21
21
|
import { cBackend } from './backend/c';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
assertDerefsTyped,
|
|
24
|
+
assertLocalsWritten,
|
|
25
|
+
assertNoOrphanedLocals,
|
|
26
|
+
assertPlacementSurvives,
|
|
27
|
+
assertResolved,
|
|
28
|
+
} from './contracts';
|
|
23
29
|
import type { AsmData } from './frontend/asmdata';
|
|
24
30
|
import { frontendFor } from './frontend/registry';
|
|
25
31
|
import { hasSetupArgsNarrowing, narrowToSetupArgs } from './frontend/ssa';
|
|
26
32
|
import { Fn, defOpMap } from './ir/core';
|
|
27
33
|
import { T } from './ir/types';
|
|
28
34
|
import { verify } from './ir/verify';
|
|
35
|
+
import { advancedBases } from './l3/advance';
|
|
29
36
|
import { materializeArgBases } from './l3/argbase';
|
|
30
37
|
import type { LanguageBackend, SFn } from './l3/ast';
|
|
31
38
|
import { type BaseKey, admittedBases, hoistBaseLocals } from './l3/basecse';
|
|
@@ -55,39 +62,49 @@ import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
|
55
62
|
import { inferGlobalArrays, orderLicensedGlobals, sameDerivedShape } from './raise/globalshape';
|
|
56
63
|
import { runPreRecovery } from './raise/pre-recovery';
|
|
57
64
|
import { recoverTypes } from './raise/recover';
|
|
58
|
-
import {
|
|
59
|
-
BASEFOLD_ADMISSIONS,
|
|
60
|
-
type BaseAdmission,
|
|
61
|
-
LIVEBASE_ADMISSIONS,
|
|
62
|
-
NO_PIN_KINDS,
|
|
63
|
-
ORDERBASE_ADMISSIONS,
|
|
64
|
-
PRE_FAN_PRODUCTS,
|
|
65
|
-
SHAPE_SUBSETS,
|
|
66
|
-
SIGN_CANDS,
|
|
67
|
-
STRUCTURING_AXES,
|
|
68
|
-
type StructuringAxis,
|
|
69
|
-
UNFOLDED_ADMISSIONS,
|
|
70
|
-
applyShapes,
|
|
71
|
-
createdLocals,
|
|
72
|
-
sameBases,
|
|
73
|
-
} from './rank-axes';
|
|
65
|
+
import { sinkStoreTails } from './raise/tailsink';
|
|
74
66
|
import {
|
|
75
67
|
type RefusedDeclarationReason,
|
|
76
68
|
bareGlobalAccessFacts,
|
|
77
69
|
bareGlobalSymbols,
|
|
78
70
|
makeRefCollector,
|
|
79
71
|
} from './rank-declare';
|
|
80
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
BASEFOLD_HOISTS,
|
|
74
|
+
type BaseHoist,
|
|
75
|
+
LIVEBASE_HOISTS,
|
|
76
|
+
NO_PIN_KINDS,
|
|
77
|
+
ORDERBASE_HOISTS,
|
|
78
|
+
PRE_RESPELL_VARIATIONS,
|
|
79
|
+
SIGNEDNESS,
|
|
80
|
+
STACKED_SUBSETS,
|
|
81
|
+
STRUCTURE_VARIATIONS,
|
|
82
|
+
type StructureVariation,
|
|
83
|
+
UNFOLDED_HOISTS,
|
|
84
|
+
applyStacked,
|
|
85
|
+
createdLocals,
|
|
86
|
+
sameBases,
|
|
87
|
+
} from './rank-variations';
|
|
88
|
+
import { hasDivergentSharedRet } from './structure/structure';
|
|
89
|
+
import {
|
|
90
|
+
type SymbolInfo,
|
|
91
|
+
type SymbolMap,
|
|
92
|
+
arrayInnerExtents,
|
|
93
|
+
declaresBitfields,
|
|
94
|
+
isPtrField,
|
|
95
|
+
symbolsByName,
|
|
96
|
+
} from './symbols';
|
|
81
97
|
import { type TargetDescription, structureOptionsFor } from './target';
|
|
98
|
+
import { type SubjectVariationName, type Variation, offeredOn, withSubject } from './variation-tokens';
|
|
82
99
|
|
|
83
100
|
/** Pin every SCALAR entry param (index not in `ptrIdx`) to the candidate signedness, before
|
|
84
101
|
* recovery. Answers whether any param was PINNABLE — not whether its type moved: which of the
|
|
85
|
-
* two passes writes first is an accident of enumeration order, and the
|
|
86
|
-
* a param can be written at all.
|
|
102
|
+
* two passes writes first is an accident of enumeration order, and the two signedness candidates
|
|
103
|
+
* differ exactly where a param can be written at all.
|
|
87
104
|
*
|
|
88
105
|
* A param NARROWED by raise/paramwidth.ts is not pinnable: the extension it was narrowed at states
|
|
89
106
|
* the signedness as well as the width — agbcc's shift pair by its `asr`/`lsr`, PPC's `extsb`/`extsh`
|
|
90
|
-
* by the opcode — so there is no question for the
|
|
107
|
+
* by the opcode — so there is no question for the signedness variation to put to the differ, and pinning would
|
|
91
108
|
* widen it back to 32 bits. */
|
|
92
109
|
function pinScalarParams(fn: Fn, signed: boolean, ptrIdx: Set<number>): boolean {
|
|
93
110
|
let pinnable = false;
|
|
@@ -115,21 +132,29 @@ export interface EnumerateOptions {
|
|
|
115
132
|
asmData?: AsmData;
|
|
116
133
|
/** address→symbol map (symbols.ts) — same contract as DecompileOptions.symbols */
|
|
117
134
|
symbols?: SymbolMap;
|
|
118
|
-
/** Called when a
|
|
119
|
-
* instead of the candidate silently not existing. Enumeration continues either way — the
|
|
120
|
-
*
|
|
121
|
-
* without this it looks identical to a
|
|
122
|
-
|
|
135
|
+
/** Called when a respell variation THROWS or fails a boundary contract, so the failure is visible
|
|
136
|
+
* instead of the candidate silently not existing. Enumeration continues either way — the default
|
|
137
|
+
* candidate is unaffected — but a variation that never fires because it always throws is a defect,
|
|
138
|
+
* and without this it looks identical to a variation that correctly declined.
|
|
139
|
+
*
|
|
140
|
+
* `variations` is NOT a candidate's name: it lists what the throwing step itself applied, in
|
|
141
|
+
* name order. A lift or structure setting reports its lift and structure variations
|
|
142
|
+
* (`['defsite']`). A respell or pre-respell step reports only its pre-respell and respell
|
|
143
|
+
* variations (`['unmerge', 'vol-slot']`), never the lift, structure, signedness or symbol-map
|
|
144
|
+
* variations of the tree it ran on. So one failure is reported once per signedness, symbol-map
|
|
145
|
+
* setting and structured tree it recurs on, each time with the same list, and `[]` means a
|
|
146
|
+
* structured tree's own source could not be spelled. */
|
|
147
|
+
onEnumerationError?: (variations: readonly string[], error: string) => void;
|
|
123
148
|
/** Called once per (name, reason) when the declaration synthesis REFUSES a name the tree
|
|
124
149
|
* references (see `RefusedDeclarationReason`). The name then stays undeclared and the
|
|
125
150
|
* candidate fails loudly in a self-declared world — this is what lets the consumer say which
|
|
126
151
|
* undeclared name was asmlift's own refusal rather than a symbol it never saw. */
|
|
127
152
|
onRefusedDeclaration?: (name: string, reason: RefusedDeclarationReason) => void;
|
|
128
|
-
/** Called with the
|
|
129
|
-
* function has no inhabitant for it, so the
|
|
153
|
+
/** Called with the variation's name (`defsite`) each time a STRUCTURE variation's shared gate says this
|
|
154
|
+
* function has no inhabitant for it, so the alternative is never enumerated.
|
|
130
155
|
*
|
|
131
156
|
* The two callbacks below report the enumeration's two SILENT candidate-deleting sites, and
|
|
132
|
-
* they exist for `
|
|
157
|
+
* they exist for `onEnumerationError`'s reason read one level up: a candidate that was never
|
|
133
158
|
* enumerated is indistinguishable, from outside, from one the differ simply did not pick, and
|
|
134
159
|
* nothing else in the pipeline reports it. A gate that has stopped firing and a gate that
|
|
135
160
|
* correctly declines on every corpus row look identical without this.
|
|
@@ -140,22 +165,29 @@ export interface EnumerateOptions {
|
|
|
140
165
|
* Nothing shipped passes either one, so a channel that had stopped firing would be invisible in
|
|
141
166
|
* exactly the way the channel exists to prevent. `test/enumerate-signals.test.ts` pins that both
|
|
142
167
|
* reach a caller. */
|
|
143
|
-
|
|
144
|
-
/** Called once per
|
|
145
|
-
* dedup, which is where most of the cross's factors of two go. See `
|
|
168
|
+
onVariationGated?: (variation: string) => void;
|
|
169
|
+
/** Called once per structure setting whose structured tree an earlier setting already produced —
|
|
170
|
+
* the tree dedup, which is where most of the cross's factors of two go. See `onVariationGated` for why both
|
|
146
171
|
* are here rather than on the result. */
|
|
147
172
|
onTreeDeduped?: () => void;
|
|
173
|
+
/** PROBE (`ASMLIFT_PERSITE_SENSE`, wired in the cli): fork the two per-FUNCTION branch-sense
|
|
174
|
+
* booleans into one bit per SITE, crossing every sense setting with all 2^n masks over the first
|
|
175
|
+
* `n` sense sites. Costs a factor of 2^n on the whole fan, which is the measurement — see the
|
|
176
|
+
* per-site sense measurement in the enumeration below. 0/absent = the shipped per-function sense. */
|
|
177
|
+
perSiteSenseBits?: number;
|
|
148
178
|
}
|
|
149
179
|
|
|
150
|
-
/** One distinct candidate
|
|
151
|
-
* def-site anchoring × bitfield spelling × symbol
|
|
180
|
+
/** One distinct candidate — one combination of variations (signedness × branch sense ×
|
|
181
|
+
* def-site anchoring × bitfield spelling × symbol map, plus the respell variations) —
|
|
152
182
|
* emitted to source. */
|
|
153
183
|
export interface Candidate {
|
|
154
|
-
|
|
184
|
+
/** the variations this candidate applied, in enumeration order — signedness first. This list IS
|
|
185
|
+
* the candidate's name; `joinVariations` prints it. */
|
|
186
|
+
variations: readonly string[];
|
|
155
187
|
source: string;
|
|
156
|
-
/** Which PREFERENCE
|
|
157
|
-
* named spellings, 1 = their `/raw-globals` siblings). Enumeration emits the
|
|
158
|
-
* preference order, and a lower
|
|
188
|
+
/** Which PREFERENCE this candidate carries — the symbol-map setting's index (0 = the map's own
|
|
189
|
+
* named spellings, 1 = their `/raw-globals` siblings). Enumeration emits the settings in
|
|
190
|
+
* preference order, and a lower preference WINS a score tie: when both compile to the same bytes the
|
|
159
191
|
* reader should get `gCounter.field`, not a byte offset off a hoisted `(u8 *)` base.
|
|
160
192
|
*
|
|
161
193
|
* Carried structurally rather than left to enumeration order because the readability tie-break
|
|
@@ -163,12 +195,12 @@ export interface Candidate {
|
|
|
163
195
|
* thing. Ranking a named spelling against a raw-address one on cast count is not a readability
|
|
164
196
|
* comparison at all — the raw form's `(u8 *)` base is not counted, so it would win by
|
|
165
197
|
* construction, trading named struct fields for anonymous byte offsets. */
|
|
166
|
-
|
|
198
|
+
preference: number;
|
|
167
199
|
/** the DECLARABLE VALUE references this candidate's tree contains — what the scoring layer's
|
|
168
200
|
* declaration synthesis renders. DERIVED, never carried: computed once from the exact tree
|
|
169
201
|
* this candidate's source was emitted from, at the moment the candidate is finalized
|
|
170
202
|
* (l3/symbol-refs.ts — no pipeline stage caches refs, so they cannot go stale). Present on
|
|
171
|
-
* EVERY
|
|
203
|
+
* EVERY candidate that names such symbols — including '/raw-globals', whose tree still
|
|
172
204
|
* names pool/reloc-derived globals (it only drops the map's shaped SPELLINGS).
|
|
173
205
|
*
|
|
174
206
|
* PRESENT WITHOUT A MAP TOO: a name is read out of the asm's own literal pool or relocation,
|
|
@@ -185,7 +217,7 @@ export interface Candidate {
|
|
|
185
217
|
deviceVolatile?: number;
|
|
186
218
|
/** PUBLISHABLE ONLY WHERE THE DIFFER PROVES IT — a byte-exact score, nothing else.
|
|
187
219
|
*
|
|
188
|
-
* The third admission ground, and the narrowest. A
|
|
220
|
+
* The third admission ground, and the narrowest. A respell variation must preserve semantics by
|
|
189
221
|
* construction (the POLICY note at the respell site), because on a nonmatch row the best
|
|
190
222
|
* spelling is what the user is shown. One spelling cannot meet that bar from inside the pass:
|
|
191
223
|
* `l3/unreduce.ts` moves a memory read into a loop whose stores are all device registers, and
|
|
@@ -206,7 +238,7 @@ export interface Scored<S> extends Candidate {
|
|
|
206
238
|
* scoring harness that shows only the surviving sibling reports a clean win over a hidden
|
|
207
239
|
* failure. */
|
|
208
240
|
export interface DroppedCandidate {
|
|
209
|
-
|
|
241
|
+
variations: readonly string[];
|
|
210
242
|
/** the scorer's first error line (a compiler diagnostic, usually) */
|
|
211
243
|
error: string;
|
|
212
244
|
}
|
|
@@ -216,18 +248,59 @@ export interface DroppedCandidate {
|
|
|
216
248
|
* compiled fine and simply did not earn publication is a different fact, and folding the two
|
|
217
249
|
* would make the `[dropped]` line report compile failures that never happened. */
|
|
218
250
|
export interface WithheldCandidate {
|
|
219
|
-
|
|
251
|
+
variations: readonly string[];
|
|
220
252
|
score: number;
|
|
253
|
+
/** the denominator that score was measured against — objdiff's row count for THIS candidate's
|
|
254
|
+
* alignment, so it moves with the spelling. Present whenever the injected scorer supplies one
|
|
255
|
+
* (the cli and webapp objdiff scorers both do); a scorer whose result carries no `rows` leaves
|
|
256
|
+
* it absent rather than inventing a scale. A withheld score is read across runs exactly like a
|
|
257
|
+
* published one, and a bare numerator there invites the same subtraction on a scale that moved. */
|
|
258
|
+
rows?: number;
|
|
221
259
|
/** one line: why publication needed a proof this score did not supply */
|
|
222
260
|
why: string;
|
|
223
261
|
}
|
|
224
262
|
|
|
263
|
+
/** EVERY candidate refused — `rankBy` has no ranked result to return, so it throws this.
|
|
264
|
+
*
|
|
265
|
+
* The two lists ride on the error, and that is the point of having a class at all: on a row where
|
|
266
|
+
* nothing scored, `dropped` IS the whole fan, and a bare `Error` discards it. A caller that
|
|
267
|
+
* prints one candidate's failure (the `cause`) is showing the LAST spelling the scorer refused,
|
|
268
|
+
* which is neither the first nor a representative one — the benchmark's own noncompile rows have
|
|
269
|
+
* up to a thousand siblings behind that single line. The MESSAGE is load-bearing and must stay
|
|
270
|
+
* byte-identical: `bench fidelity` matches it verbatim to recognise a reproduced noncompile
|
|
271
|
+
* row. */
|
|
272
|
+
export class NoScorableCandidateError extends Error {
|
|
273
|
+
readonly dropped: DroppedCandidate[];
|
|
274
|
+
readonly withheld: WithheldCandidate[];
|
|
275
|
+
constructor(message: string, dropped: DroppedCandidate[], withheld: WithheldCandidate[], options?: ErrorOptions) {
|
|
276
|
+
super(message, options);
|
|
277
|
+
this.name = 'NoScorableCandidateError';
|
|
278
|
+
this.dropped = dropped;
|
|
279
|
+
this.withheld = withheld;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** EVERY spelling refused BY THE BACKEND, before anything was compiled — `enumerateCandidates`
|
|
284
|
+
* has no fan to return, so it throws this.
|
|
285
|
+
*
|
|
286
|
+
* It is a sibling of `NoScorableCandidateError` and a DIFFERENT fact, which is the whole reason
|
|
287
|
+
* it is a class: nothing was scored there because nothing COMPILED, and nothing was scored here
|
|
288
|
+
* because nothing was ever spelled. A surface that cannot tell the two apart prints one of them
|
|
289
|
+
* under the other's name. The message is load-bearing too
|
|
290
|
+
* (`packages/core/test/rank-backend-decline.test.ts` matches it). */
|
|
291
|
+
export class NoSpellableCandidateError extends Error {
|
|
292
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
293
|
+
super(message, options);
|
|
294
|
+
this.name = 'NoSpellableCandidateError';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
225
298
|
export interface RankedResult<S> {
|
|
226
|
-
|
|
299
|
+
winner: Scored<S>; // lowest score
|
|
227
300
|
candidates: Scored<S>[]; // sorted best (lowest) first
|
|
228
301
|
/** candidates whose scoreFn threw — empty when every spelling built */
|
|
229
302
|
dropped: DroppedCandidate[];
|
|
230
|
-
/** candidates withheld for want of a byte-exact proof — empty unless a `matchOnly`
|
|
303
|
+
/** candidates withheld for want of a byte-exact proof — empty unless a `matchOnly` variation fired */
|
|
231
304
|
withheld: WithheldCandidate[];
|
|
232
305
|
}
|
|
233
306
|
|
|
@@ -243,15 +316,15 @@ export function withheldReason<S extends { score: number }>(c: Candidate, score:
|
|
|
243
316
|
: null;
|
|
244
317
|
}
|
|
245
318
|
|
|
246
|
-
/** What a
|
|
319
|
+
/** What a respell variation hands `respell`: its tree, or — when the variation cannot establish the
|
|
247
320
|
* candidate's semantics from inside the pass — the tree paired with that fact. `undefined`/`null`
|
|
248
321
|
* is a decline. */
|
|
249
|
-
type
|
|
322
|
+
type RespellResult = SFn | { sfn: SFn; needsProof: boolean } | null | undefined;
|
|
250
323
|
|
|
251
|
-
/** REQUIRE-ALL composition of
|
|
252
|
-
* from one
|
|
324
|
+
/** REQUIRE-ALL composition of respell variations, and the ONE place a proof obligation crosses
|
|
325
|
+
* from one variation to the next.
|
|
253
326
|
*
|
|
254
|
-
* `
|
|
327
|
+
* `RespellResult` is a union, so a hand-written composition can spell the obligation away by
|
|
255
328
|
* accident and stay type-correct: `return pointerFields(u.sfn);` in place of
|
|
256
329
|
* `return { sfn: t, needsProof: u.needsProof };` compiles, passes tsc and passes every suite,
|
|
257
330
|
* and publishes as asmlift's answer a spelling that was supposed to be withheld unless byte-exact.
|
|
@@ -259,14 +332,14 @@ type LeverResult = SFn | { sfn: SFn; needsProof: boolean } | null | undefined;
|
|
|
259
332
|
* touches the flag.
|
|
260
333
|
*
|
|
261
334
|
* The obligation is MONOTONE, which is what lets it be an `or`: it says "no gate over this C can
|
|
262
|
-
* settle the fact this spelling rests on", and a later
|
|
335
|
+
* settle the fact this spelling rests on", and a later respell variation cannot settle a fact about an
|
|
263
336
|
* earlier one. `/ptr-field` re-types a field and never moves a read, so it carries `/unreduce`'s
|
|
264
337
|
* obligation through unchanged rather than discharging it.
|
|
265
338
|
*
|
|
266
339
|
* REQUIRE-ALL, never skip-on-decline: one declining stage declines the whole composition, so the
|
|
267
|
-
*
|
|
268
|
-
* on, and the reason it rejects `
|
|
269
|
-
export function
|
|
340
|
+
* candidate's variations always name exactly the ones that fired. That is the property the pairing site turns
|
|
341
|
+
* on, and the reason it rejects `applyStacked` — see the POLICY note there. */
|
|
342
|
+
export function composeRespellVariations(sfn: SFn, stages: readonly ((s: SFn) => RespellResult)[]): RespellResult {
|
|
270
343
|
let cur = sfn;
|
|
271
344
|
let needsProof = false;
|
|
272
345
|
for (const stage of stages) {
|
|
@@ -280,42 +353,43 @@ export function composeLevers(sfn: SFn, stages: readonly ((s: SFn) => LeverResul
|
|
|
280
353
|
return needsProof ? { sfn: cur, needsProof } : cur;
|
|
281
354
|
}
|
|
282
355
|
|
|
283
|
-
/** What one `
|
|
284
|
-
* backend declined the tree it was handed.
|
|
356
|
+
/** What one `respellTree` call produced: the sources it emitted, plus the DEFAULT source's emit refusal
|
|
357
|
+
* where the backend declined the tree it was handed.
|
|
285
358
|
*
|
|
286
359
|
* RETURNED rather than written to the enumeration's shared `lastEmitError`, because only ONE
|
|
287
|
-
* caller may record one. `
|
|
360
|
+
* caller may record one. `respellTree` runs over the row's own tree and over each pre-respell variation's
|
|
288
361
|
* REWRITTEN tree, and a backend refusal of a rewrite is not a refusal of the row's spelling —
|
|
289
362
|
* letting it reach `lastEmitError` would put the wrong cause on the row's "no spellable
|
|
290
|
-
* candidate" throw. With the value returned, the
|
|
291
|
-
* does not, which is the rule made structural instead of saved and restored around the call.
|
|
363
|
+
* candidate" throw. With the value returned, the caller over the row's own tree records and the
|
|
364
|
+
* pre-respell caller does not, which is the rule made structural instead of saved and restored around the call.
|
|
292
365
|
*
|
|
293
|
-
* A DISCRIMINATED FIELD, not a nullable error: a
|
|
366
|
+
* A DISCRIMINATED FIELD, not a nullable error: a variation that throws a falsy value is still
|
|
294
367
|
* recorded, where `?? ` would read it as "nothing was thrown". */
|
|
295
|
-
interface
|
|
296
|
-
|
|
368
|
+
interface TreeSources {
|
|
369
|
+
sources: TreeSource[];
|
|
297
370
|
emit?: { error: unknown };
|
|
298
371
|
}
|
|
299
372
|
|
|
300
|
-
/** One emitted
|
|
373
|
+
/** One source emitted from a structured tree: the variations that produced it,
|
|
301
374
|
* the rendered source, and the tree-derived facts `compareScored` ranks by. */
|
|
302
|
-
interface
|
|
303
|
-
|
|
375
|
+
interface TreeSource {
|
|
376
|
+
variations: readonly Variation[];
|
|
304
377
|
source: string;
|
|
305
378
|
symbolRefs?: SymbolRef[];
|
|
306
379
|
deviceVolatile?: number;
|
|
307
|
-
/** see `Candidate.matchOnly` — set by a
|
|
380
|
+
/** see `Candidate.matchOnly` — set by a variation that cannot establish its own semantics */
|
|
308
381
|
matchOnly?: true;
|
|
309
382
|
}
|
|
310
383
|
|
|
311
|
-
/** Emit the DISTINCT type/branch-sense
|
|
384
|
+
/** Emit the DISTINCT type/branch-sense candidates for `name` — PURE, no scoring.
|
|
312
385
|
* It differs from `decompile()` in exactly two arguments to the shared spine — the signedness
|
|
313
386
|
* pin, injected between pre-recovery and recoverTypes via the `beforeRecover` hook, and the
|
|
314
387
|
* `pre.shortCircuit` connective owner (the raiseRecovered call below states both).
|
|
315
388
|
* Duplicate sources are collapsed so the scorer never
|
|
316
|
-
* recompiles an identical spelling, and the
|
|
317
|
-
* than once per
|
|
318
|
-
* already
|
|
389
|
+
* recompiles an identical spelling, and the respell set runs once per distinct STRUCTURED TREE
|
|
390
|
+
* rather than once per structure setting — a structure variation inert on this function reaches a
|
|
391
|
+
* tree an earlier setting already produced, and every respell variation is a pure function of that
|
|
392
|
+
* tree. */
|
|
319
393
|
export function enumerateCandidates(
|
|
320
394
|
name: string,
|
|
321
395
|
asm: string,
|
|
@@ -323,6 +397,10 @@ export function enumerateCandidates(
|
|
|
323
397
|
opts: EnumerateOptions = {},
|
|
324
398
|
): Candidate[] {
|
|
325
399
|
const backend = opts.backend ?? cBackend;
|
|
400
|
+
/** Report a setting that THREW through `onEnumerationError`, as the variations it names (none for
|
|
401
|
+
* the function's own tree). */
|
|
402
|
+
const reportThrow = (variations: readonly Variation[], e: unknown): void =>
|
|
403
|
+
opts.onEnumerationError?.(variations, firstLine(e));
|
|
326
404
|
/** The last refusal from a backend asked to spell a tree — what the empty-enumeration check
|
|
327
405
|
* below reports, so "this backend can spell nothing here" names its reason. */
|
|
328
406
|
let lastEmitError: unknown = null;
|
|
@@ -338,7 +416,7 @@ export function enumerateCandidates(
|
|
|
338
416
|
spellSwitchFallthrough: backend.spellsSwitchFallthrough,
|
|
339
417
|
...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
|
|
340
418
|
};
|
|
341
|
-
// Branch-sense is a differ-ranked
|
|
419
|
+
// Branch-sense is a differ-ranked VARIATION, the same class as param signedness: a divergent `if`
|
|
342
420
|
// can be spelled with either sense (`if (c) A else B` vs `if (!c) B else A`), and which one the
|
|
343
421
|
// source compiler emitted is genuinely ambiguous from asm. There is no safe global heuristic
|
|
344
422
|
// (`ifor` wants positive, `simpleif` wants negated, `diamond` wants positive) — emit BOTH senses
|
|
@@ -346,134 +424,165 @@ export function enumerateCandidates(
|
|
|
346
424
|
// worse; it only wins where the flip matches.
|
|
347
425
|
const defSense = baseOpts.preserveDivergentBranchSense ?? true;
|
|
348
426
|
// `/defsite` — def-site-anchored constant merge copies (structure.ts anchorConstCopies) — is a
|
|
349
|
-
//
|
|
427
|
+
// structure variation on the same footing as branch sense: where the asm materialized a merge
|
|
350
428
|
// constant is placement evidence, but whether the SOURCE spelled it there is genuinely
|
|
351
429
|
// ambiguous, so both placements are emitted and the differ referees. Crossed with branch sense
|
|
352
430
|
// (an anchored copy empties an arm, which is exactly what changes which sense wins); the dedup
|
|
353
|
-
// below collapses every
|
|
431
|
+
// below collapses every candidate the anchoring left unchanged.
|
|
354
432
|
//
|
|
355
433
|
// `/defsite/loop-entry` widens it to a LOOP HEADER's entry const (`int s = 0;` above the guard
|
|
356
|
-
// rather than on the edge into the loop). Its own
|
|
434
|
+
// rather than on the edge into the loop). Its own setting rather than a widening of `/defsite`
|
|
357
435
|
// because it is a SECOND placement decision: a function carrying both kinds of anchorable const
|
|
358
436
|
// has THREE spellings, and folding the two decisions into one boolean would delete the middle
|
|
359
437
|
// one — measured on klonoa's TransitionSelfRemoveFadeIn, where 448 of the 896 sources `/defsite`
|
|
360
438
|
// reaches became unreachable. Enumerated as a CHAIN (none ⊂ plain ⊂ plain + entry) rather than a
|
|
361
|
-
// 2×2 cross: the fourth
|
|
439
|
+
// 2×2 cross: the fourth setting costs another quarter of the whole fan — the anchor dimension
|
|
362
440
|
// multiplies everything below it — and no row has been shown to need it.
|
|
363
441
|
//
|
|
364
|
-
// The
|
|
365
|
-
// (`bitfields`, `ptrElems`, `declRank` and the anchor pair) start from one record, so
|
|
366
|
-
//
|
|
442
|
+
// The spelling booleans hand-carried outside `STRUCTURE_VARIATIONS`
|
|
443
|
+
// (`bitfields`, `ptrElems`, `declRank` and the anchor pair) start from one record, so the
|
|
444
|
+
// default setting lives in one place instead of six literals that can disagree. Each entry
|
|
367
445
|
// states only what it VARIES — which is the whole content of the chain above.
|
|
368
|
-
const
|
|
369
|
-
const senseAnchor = [
|
|
370
|
-
{ ...
|
|
371
|
-
{ ...
|
|
372
|
-
{ ...
|
|
373
|
-
{ ...
|
|
374
|
-
{ ...
|
|
375
|
-
{
|
|
446
|
+
const STRUCTURE_DEFAULTS = { anchor: false, entry: false, bitfields: true, ptrElems: true, declRank: true };
|
|
447
|
+
const senseAnchor: (typeof STRUCTURE_DEFAULTS & { variations: readonly Variation[]; sense: boolean })[] = [
|
|
448
|
+
{ ...STRUCTURE_DEFAULTS, variations: [], sense: defSense },
|
|
449
|
+
{ ...STRUCTURE_DEFAULTS, variations: ['flip-branch'], sense: !defSense },
|
|
450
|
+
{ ...STRUCTURE_DEFAULTS, variations: ['defsite'], sense: defSense, anchor: true },
|
|
451
|
+
{ ...STRUCTURE_DEFAULTS, variations: ['flip-branch', 'defsite'], sense: !defSense, anchor: true },
|
|
452
|
+
{ ...STRUCTURE_DEFAULTS, variations: ['defsite', 'loop-entry'], sense: defSense, anchor: true, entry: true },
|
|
453
|
+
{
|
|
454
|
+
...STRUCTURE_DEFAULTS,
|
|
455
|
+
variations: ['flip-branch', 'defsite', 'loop-entry'],
|
|
456
|
+
sense: !defSense,
|
|
457
|
+
anchor: true,
|
|
458
|
+
entry: true,
|
|
459
|
+
},
|
|
376
460
|
];
|
|
377
461
|
// `/flip-join` — the JOINED-if sibling of `/flip-branch` (structure.ts
|
|
378
462
|
// negateJoinedBranchSense): a reconverging two-armed if reads the same fall-through-is-then
|
|
379
463
|
// layout evidence the divergent case does, so the DEFAULT sense is the divergent one's and
|
|
380
|
-
// this
|
|
381
|
-
// `/flip-branch` still moves only divergent ifs and the two
|
|
382
|
-
// therefore names a sense RELATIVE to the target's default: a
|
|
383
|
-
// a
|
|
464
|
+
// this variation emits the other. Read off the TARGET's sense, not this candidate's `s.sense`, so
|
|
465
|
+
// `/flip-branch` still moves only divergent ifs and the two variations stay independent. The name
|
|
466
|
+
// therefore names a sense RELATIVE to the target's default: a candidate's variations quoted from a
|
|
467
|
+
// log identify a candidate only together with the tree that produced it, which is what the `[asmlift source
|
|
384
468
|
// <commit>]` stamp on the `[ranked]` line is for (docs/ranked-repro.md).
|
|
385
469
|
// Crossed with the pair above. The two senses are two different sources wherever a two-armed
|
|
386
470
|
// joined `if` exists at all — agbcc emits different bytes for the arms-swapped spelling — and
|
|
387
|
-
// all three things that invert the polarity are per-SITE where this
|
|
471
|
+
// all three things that invert the polarity are per-SITE where this variation is per-function, so no
|
|
388
472
|
// per-function predicate decides it: a short-circuit fold choosing the orientation, a
|
|
389
473
|
// conditional branch relayed past Thumb's ±256-byte reach, and a rotated loop's zero-trip guard,
|
|
390
|
-
// where the `if` is the compiler's own and no source sense exists to be faithful to. The
|
|
391
|
-
//
|
|
474
|
+
// where the `if` is the compiler's own and no source sense exists to be faithful to. The FIRST
|
|
475
|
+
// of the three is now decided per site rather than enumerated — `/site-sense` (rank-variations.ts)
|
|
476
|
+
// reads the orientation the fold records — and this variation stays because the other two are not.
|
|
477
|
+
// The third
|
|
478
|
+
// is what keeps the residue on targets that have neither: rows still win on the variation under
|
|
392
479
|
// gcc2.7.2 / gcc2.7.2kmc / mwcc, with no `short-circuit` tag and no Thumb branch range to
|
|
393
480
|
// explain them, and most of those carry `loop`. A function with no two-armed joined if emits identical
|
|
394
481
|
// source and the dedup collapses it before any compile.
|
|
395
|
-
const
|
|
482
|
+
const senseOnly = [
|
|
396
483
|
...senseAnchor.map((s) => ({ ...s, join: false })),
|
|
397
|
-
...senseAnchor.map((s)
|
|
484
|
+
...senseAnchor.map((s): typeof s & { join: boolean } => ({
|
|
485
|
+
...s,
|
|
486
|
+
variations: [...s.variations, 'flip-join'],
|
|
487
|
+
join: true,
|
|
488
|
+
})),
|
|
398
489
|
];
|
|
490
|
+
// THE PER-SITE SENSE MEASUREMENT, a structure variation never enumerated by default: every mask
|
|
491
|
+
// over the sense sites, crossed with the whole fan. It exists to price the fork the two booleans above cannot express
|
|
492
|
+
// — 2^n on a function with n sites, which is the number a decidable predicate would replace —
|
|
493
|
+
// and to say whether the target's configuration is REACHABLE at all. Not enumerated unless the
|
|
494
|
+
// caller asks; `structure()`'s own `branchSenseFlipSites` is the seam it drives.
|
|
495
|
+
const maskSites = (m: number): ReadonlySet<number> => new Set([...Array(32).keys()].filter((i) => (m >> i) & 1));
|
|
496
|
+
const senseMasks = Array.from({ length: 1 << (opts.perSiteSenseBits ?? 0) }, (_, m) => m);
|
|
497
|
+
const baseSense = senseMasks.flatMap((m) =>
|
|
498
|
+
senseOnly.map((s) => ({
|
|
499
|
+
...s,
|
|
500
|
+
...(m === 0 ? {} : { variations: [...s.variations, withSubject('sense', String(m))] }),
|
|
501
|
+
// Always present, `undefined` at mask 0: an optional key added on one arm of a ternary would
|
|
502
|
+
// make the two arms different object TYPES, and the list is what the whole fan spreads from.
|
|
503
|
+
flipSites: m === 0 ? undefined : maskSites(m),
|
|
504
|
+
})),
|
|
505
|
+
);
|
|
399
506
|
// `/no-bitfield` — keep the honest shift spelling where the map would name a bitfield member.
|
|
400
507
|
// The named read recompiles at the DECLARATION's access width; where that diverges from the
|
|
401
508
|
// asm's load width, the shifts are the spelling that matches — so both are emitted and the
|
|
402
509
|
// differ referees. Enumerated only when the map carries any bitfield member at all (checked
|
|
403
510
|
// below), so the 2× cross is paid exactly by the functions it can help; the dedup collapses
|
|
404
|
-
// every
|
|
405
|
-
const
|
|
406
|
-
opts.symbols !== undefined &&
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
: baseSense;
|
|
511
|
+
// every candidate where no fold fired.
|
|
512
|
+
const bitfieldSettings =
|
|
513
|
+
opts.symbols !== undefined && declaresBitfields(opts.symbols)
|
|
514
|
+
? [
|
|
515
|
+
...baseSense,
|
|
516
|
+
...baseSense.map((s): typeof s => ({ ...s, variations: [...s.variations, 'no-bitfield'], bitfields: false })),
|
|
517
|
+
]
|
|
518
|
+
: baseSense;
|
|
413
519
|
// `/connective`'s enumeration gate, read off the pass's OWN refusal rather than from a second
|
|
414
520
|
// copy of its matcher: the fold reports every site where the PAIRWISE comparison-tree refusal is
|
|
415
521
|
// the ONE thing stopping it — asked after `sameArgs` and the negatability check, so a report
|
|
416
522
|
// means a candidate that DIFFERS, not a refusal merely reached — and a function with none has no
|
|
417
|
-
// inhabitant for the
|
|
523
|
+
// inhabitant for the variation.
|
|
418
524
|
//
|
|
419
|
-
// PER SYMBOL
|
|
525
|
+
// PER SYMBOL-MAP SETTING, on a lift of its OWN, for the reason the `/setup-args` gate below states
|
|
420
526
|
// for itself: no lift may be governed by a fact measured on a different one. The pin and
|
|
421
527
|
// `/setup-args` cannot move this answer — neither a parameter's type nor a call's argument list
|
|
422
528
|
// moves a `cond_br` — but a SYMBOL MAP can, by lifting a pool-loaded comparison constant as a
|
|
423
529
|
// `gaddr` the const-test test then does not read. Measured once and NOT re-derived since: over
|
|
424
530
|
// the real rows that lifted, 21 sites mapped and 21 raw with no per-row divergence. Read that as
|
|
425
|
-
// the REASON the gate is asked per
|
|
531
|
+
// the REASON the gate is asked per symbol-map setting, not as a fact about today's corpus — it carries no
|
|
426
532
|
// commit stamp. The SYNTHETIC tier is inside that comparison rather than exempt from it: 9 of
|
|
427
533
|
// its 770 rows carry a map (`SynthSpec.symbols`) in the committed artifact, and that count moves
|
|
428
534
|
// every time a map row is added, so re-derive it rather than carrying this one forward. So this
|
|
429
535
|
// buys no candidate; what
|
|
430
|
-
// it buys is that a lift-time change which splits them enumerates both
|
|
536
|
+
// it buys is that a lift-time change which splits them enumerates both settings rather than
|
|
431
537
|
// silently dropping one, the failure nothing reports.
|
|
432
|
-
let
|
|
433
|
-
//
|
|
434
|
-
// so they are excluded from the signedness
|
|
435
|
-
// compile. (The
|
|
538
|
+
let sharedLiftTreeOwned = false;
|
|
539
|
+
// Shared lift: recover ONCE with no signedness pin, to learn which entry params are
|
|
540
|
+
// pointers/aggregates so they are excluded from the signedness variation (see NO_PIN_KINDS). One
|
|
541
|
+
// extra lift+recover, no compile. (The shared lift deliberately stops after recoverTypes — it only reads the param KINDS, so
|
|
436
542
|
// the totality contract / return-sinking of the full spine are not run on it.)
|
|
437
|
-
const
|
|
438
|
-
verify(
|
|
543
|
+
const sharedLift = frontend.lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
|
|
544
|
+
verify(sharedLift);
|
|
439
545
|
// The ARRAY SHAPES the input assembly evidences (raise/globalshape.ts), for the DECLARATION
|
|
440
|
-
// half. Read off the
|
|
546
|
+
// half. Read off the shared lift's LIFTED form — before the fold below and the tower rewrite it —
|
|
441
547
|
// because the base-materialization order the derivation's licence reads does not survive them.
|
|
442
|
-
//
|
|
443
|
-
// The candidate half reads its OWN lift (each symbol
|
|
444
|
-
//
|
|
445
|
-
const
|
|
446
|
-
applyIdiomPatterns(
|
|
447
|
-
runPreRecovery(
|
|
548
|
+
// Derived from the shared lift like `accessFacts` beside it, and for the same reason: it is a
|
|
549
|
+
// lift-time fact. The candidate half reads its OWN lift (each symbol-map setting lifts
|
|
550
|
+
// differently), just as the structure variations do.
|
|
551
|
+
const sharedLiftShapes = inferGlobalArrays(sharedLift, target);
|
|
552
|
+
applyIdiomPatterns(sharedLift, target, opts.patterns);
|
|
553
|
+
runPreRecovery(sharedLift, target, () => verify(sharedLift), prototypes[name], {
|
|
448
554
|
shortCircuit: {
|
|
449
555
|
onTreeOwned: () => {
|
|
450
|
-
|
|
556
|
+
sharedLiftTreeOwned = true;
|
|
451
557
|
},
|
|
452
558
|
},
|
|
453
559
|
});
|
|
454
|
-
recoverTypes(
|
|
455
|
-
const ptrIdx = new Set<number>(
|
|
560
|
+
recoverTypes(sharedLift);
|
|
561
|
+
const ptrIdx = new Set<number>(
|
|
562
|
+
sharedLift.blocks[0].params.flatMap((p, i) => (NO_PIN_KINDS.has(p.type.kind) ? [i] : [])),
|
|
563
|
+
);
|
|
456
564
|
// Access facts for name-only symbol declarations (see bareGlobalAccessFacts) — derived once
|
|
457
|
-
// from the
|
|
565
|
+
// from the shared lift: widths/offsets are lift-time facts, identical across every candidate.
|
|
458
566
|
// Ungated on `opts.symbols`: map-less candidates now carry name-only refs too (see
|
|
459
567
|
// `bareGlobalSymbols`), and these facts are their declarations' WIDTH AUTHORITY — without them
|
|
460
568
|
// every map-less decl would be the `extern u32` fallback and a bare `gCell = x` would compile
|
|
461
569
|
// to `str` where the target says `strh`. One IR walk; on a function with no `gaddr` at all
|
|
462
570
|
// (every synthetic corpus row) it returns the same empty map the gate used to hand back.
|
|
463
|
-
const accessFacts = bareGlobalAccessFacts(
|
|
571
|
+
const accessFacts = bareGlobalAccessFacts(sharedLift);
|
|
464
572
|
//
|
|
465
|
-
// The MAPPED
|
|
466
|
-
// reuse, not inheritance. Only a
|
|
467
|
-
// so the price is one per `/raw-globals`
|
|
573
|
+
// The MAPPED setting reads it off the shared lift below, itself a lift in exactly that
|
|
574
|
+
// configuration — reuse, not inheritance. Only a setting lifting under DIFFERENT symbols pays a
|
|
575
|
+
// lift of its own, so the price is one per `/raw-globals` setting and zero on a map-less row,
|
|
576
|
+
// never per candidate.
|
|
468
577
|
//
|
|
469
|
-
// DECLARED AFTER THE
|
|
470
|
-
// precondition: the
|
|
471
|
-
// `onTreeOwned` hook has had its chance to fire. Called any earlier it would report a confident
|
|
578
|
+
// DECLARED AFTER THE SHARED LIFT'S OWN `runPreRecovery` ON PURPOSE, and that placement is the
|
|
579
|
+
// memo's precondition: the mapped case returns `sharedLiftTreeOwned`, which is only the answer once the
|
|
580
|
+
// shared lift's `onTreeOwned` hook has had its chance to fire. Called any earlier it would report a confident
|
|
472
581
|
// `false` for a function that owns a tree. As a `const` below that call, an early call is a TDZ
|
|
473
582
|
// ReferenceError instead — a wrong answer traded for a loud one.
|
|
474
583
|
const treeOwnedIn = (symbols: typeof opts.symbols): boolean => {
|
|
475
584
|
if (symbols === opts.symbols) {
|
|
476
|
-
return
|
|
585
|
+
return sharedLiftTreeOwned;
|
|
477
586
|
}
|
|
478
587
|
const p = frontend.lift(name, asm, target, prototypes, opts.asmData, symbols);
|
|
479
588
|
verify(p);
|
|
@@ -505,66 +614,66 @@ export function enumerateCandidates(
|
|
|
505
614
|
// (`/no-bitfield` above still asks the MAP rather than the function; narrowing it would be this
|
|
506
615
|
// same edit against a different measurement. Its cross is censused below beside this one.)
|
|
507
616
|
//
|
|
508
|
-
// Where it DOES reach, the cross is the honest price of an
|
|
509
|
-
// the corpus's largest fan it is large: `kleod:ProcessInputAndUpdateEntities` enumerates
|
|
510
|
-
// candidates of which 23,040 carry this
|
|
617
|
+
// Where it DOES reach, the cross is the honest price of an alternative the differ has to referee,
|
|
618
|
+
// and on the corpus's largest fan it is large: `kleod:ProcessInputAndUpdateEntities` enumerates
|
|
619
|
+
// 58,752 candidates of which 23,040 carry this variation, so removing it leaves 35,712 — a factor of 1.65,
|
|
511
620
|
// not a doubling. A ROUNDER NUMBER IS NOT A SAFER ONE: re-measure rather than reaching for a
|
|
512
621
|
// vaguer word. The instrument is `decompileRanked`'s own enumeration, and a direct
|
|
513
622
|
// `enumerateCandidates` call from a standalone script is NOT it (an ESM/CJS duplicate of this
|
|
514
623
|
// module answers 544 where the harness answers 952 on `SetupBG3WindowOverlay`).
|
|
515
624
|
//
|
|
516
|
-
// The two
|
|
517
|
-
// `
|
|
518
|
-
// families' counts double-counts the overlap: on that same row 12,672 of the 23,040 carry BOTH
|
|
519
|
-
//
|
|
520
|
-
//
|
|
625
|
+
// The two variations are also NESTED rather than independent — `ptrElemSettings` is built by doubling
|
|
626
|
+
// `bitfieldSettings`, so this variation's candidates include the `/no-bitfield` ones and adding the
|
|
627
|
+
// two families' counts double-counts the overlap: on that same row 12,672 of the 23,040 carry BOTH
|
|
628
|
+
// variations, which is half of `/no-bitfield`'s own 25,344. A per-family price read off either
|
|
629
|
+
// variation alone therefore double-counts more than half of this row's cross.
|
|
521
630
|
//
|
|
522
|
-
// EXACTLY ONE
|
|
523
|
-
// match at 0. READ THAT ONE, NOT A ZERO: the
|
|
631
|
+
// EXACTLY ONE WINNER IN THE ARTIFACT CARRIES `/no-ptr-elem` — `synthetic:ptrelem:agbcc`,
|
|
632
|
+
// match at 0. READ THAT ONE, NOT A ZERO: the variation is two-sided where it fires. Compile the byte
|
|
524
633
|
// spelling and the element spelling of the SAME address with the klonoa checkout's own agbcc,
|
|
525
|
-
// lift each back with that project's own map, and the
|
|
634
|
+
// lift each back with that project's own map, and the `/no-ptr-elem` candidate is the ONLY one that matches the
|
|
526
635
|
// byte target while the default is the only one that matches the element target — on a constant
|
|
527
636
|
// element offset, at one element in, at a pointee width of 1, and on a STORE.
|
|
528
|
-
// `cli/test/matching/ptr-elem-
|
|
637
|
+
// `cli/test/matching/ptr-elem-variation.test.ts` is that measurement, and deleting the `ptrElemSettings`
|
|
529
638
|
// cross turns 8 of its 13 assertions red — the four BYTE-target ones (each scoring 1
|
|
530
|
-
// rather than 0) and the four that check the
|
|
639
|
+
// rather than 0) and the four that check the variation is enumerated at all — while its four
|
|
531
640
|
// ELEMENT-target ones stay green, which is the two-sidedness itself. A low count over the REAL
|
|
532
641
|
// tier counts something else: klonoa's map declares a sized pointer member at ONE address, and
|
|
533
642
|
// every decompiled caller of it happens to be written in the element form.
|
|
534
643
|
//
|
|
535
644
|
// WHERE IT GENUINELY DOES NOT REACH, measured on the same probes: at element offset ZERO the two
|
|
536
|
-
//
|
|
645
|
+
// spellings emit the IDENTICAL source (`((u16 *)gSym.pMap)[a0]`, because with no constant left there
|
|
537
646
|
// is nothing for the byte form to spell differently), the tree dedup collapses the pair — 10
|
|
538
|
-
// candidates, not 12 — and neither
|
|
539
|
-
// spelling, not a refusal of this
|
|
647
|
+
// candidates, not 12 — and neither matches a byte-form target. That is an open gap in the
|
|
648
|
+
// spelling, not a refusal of this variation.
|
|
540
649
|
//
|
|
541
650
|
// STATE THE DENOMINATOR, AND DERIVE IT FROM REACH RATHER THAN FROM CO-OCCURRENCE — the same
|
|
542
651
|
// distinction the paragraph above draws about the gate, applied to the gate's own price.
|
|
543
652
|
// "Offered only where a symbol map exists" is true and useless: all 252 real rows carry a map,
|
|
544
|
-
// so that framing hands back 151
|
|
653
|
+
// so that framing hands back 151 rows with a winner, which is the map-wide `some` this gate was
|
|
545
654
|
// written to avoid. The gate is per-FUNCTION, so census the FUNCTIONS. Enumerating every real
|
|
546
655
|
// case (candidates only, `ASMLIFT_CANDCACHE=0`, the harness's own inputs) and counting rows with
|
|
547
656
|
// any surviving `/no-ptr-elem` candidate: TWO — `kleod:ProcessInputAndUpdateEntities` (23040 of
|
|
548
|
-
// its 58752) and `kleod:SetupBG3WindowOverlay` (128 of 952), and only the first
|
|
549
|
-
//
|
|
550
|
-
// COUNTS ARE NOT — they move with every fan-widening
|
|
551
|
-
// quoting these. So the REAL tier's "0
|
|
657
|
+
// its 58752) and `kleod:SetupBG3WindowOverlay` (128 of 952), and only the first has a
|
|
658
|
+
// winner at all, the second being `noncompile`. THE TWO-ROW REACH IS STABLE AND THE
|
|
659
|
+
// COUNTS ARE NOT — they move with every fan-widening variation, so re-run the census rather than
|
|
660
|
+
// quoting these. So the REAL tier's "0 winners carry it" is 0 of ONE here, not 0 of 151 and not
|
|
552
661
|
// 0 of the artifact's row count. That census enumerates 155 of the 252 real rows — the
|
|
553
|
-
// 151
|
|
662
|
+
// 151 with a winner plus its 4 `noncompile` rows — and the 97 it cannot enumerate are
|
|
554
663
|
// exactly the rows the artifact declines. What makes "no row is LOST" a proof rather than a
|
|
555
|
-
// sample is the soundness rule instead: this
|
|
664
|
+
// sample is the soundness rule instead: this variation only ADDS candidates, so a row whose winner
|
|
556
665
|
// does not carry it cannot move when it is removed. The same census prices `/no-bitfield`: it
|
|
557
666
|
// survives dedup on FIVE real rows — `ProcessInputAndUpdateEntities` 25344, `CountCollectedGems`
|
|
558
667
|
// 192, `UpdateWorldMapNodeAnim` 168, `UpdateHUDCounterDisplay` 96, `CopyBGScrollTiles` 4 — every
|
|
559
|
-
// one of them a row
|
|
668
|
+
// one of them a row with a winner, and none of the five wins under it. So its map-wide
|
|
560
669
|
// enumeration gate buys a candidate cross on 5 functions and the dedup collapses it everywhere
|
|
561
670
|
// else.
|
|
562
671
|
//
|
|
563
|
-
// WHERE THE
|
|
564
|
-
// carries rows that hand asmlift a map (`SynthSpec.symbols`), and both
|
|
672
|
+
// WHERE THE WINNERS ARE, since the REAL tier has none for either variation: the SYNTHETIC tier
|
|
673
|
+
// carries rows that hand asmlift a map (`SynthSpec.symbols`), and both variations win on one —
|
|
565
674
|
// `/no-bitfield` on `bfwordread` and `bfwordwrite`, `/no-ptr-elem` on `ptrelem`, each a match at
|
|
566
|
-
// 0 that becomes a NONMATCH when its own
|
|
567
|
-
// AND ITS COMMIT: say which tier a count is over, and re-derive it rather than carrying it
|
|
675
|
+
// 0 that becomes a NONMATCH when its own variation is ablated. A CENSUS OF WINNERS' VARIATIONS IS
|
|
676
|
+
// SCOPED TO ITS TIER AND ITS COMMIT: say which tier a count is over, and re-derive it rather than carrying it
|
|
568
677
|
// forward.
|
|
569
678
|
// The name-keyed map `baseOpts` already built, not a second `symbolsByName` walk over the same
|
|
570
679
|
// input: the function is deterministic and unmemoized, nothing in packages/core mutates a
|
|
@@ -573,7 +682,7 @@ export function enumerateCandidates(
|
|
|
573
682
|
const byName = baseOpts.symbols;
|
|
574
683
|
const fnHasSizedPtrFields =
|
|
575
684
|
byName !== undefined &&
|
|
576
|
-
[...bareGlobalSymbols(
|
|
685
|
+
[...bareGlobalSymbols(sharedLift).keys()].some((n) => {
|
|
577
686
|
const i = byName.get(n);
|
|
578
687
|
return (
|
|
579
688
|
i !== undefined &&
|
|
@@ -582,9 +691,16 @@ export function enumerateCandidates(
|
|
|
582
691
|
)
|
|
583
692
|
);
|
|
584
693
|
});
|
|
585
|
-
const
|
|
586
|
-
? [
|
|
587
|
-
|
|
694
|
+
const ptrElemSettings = fnHasSizedPtrFields
|
|
695
|
+
? [
|
|
696
|
+
...bitfieldSettings,
|
|
697
|
+
...bitfieldSettings.map((s): typeof s => ({
|
|
698
|
+
...s,
|
|
699
|
+
variations: [...s.variations, 'no-ptr-elem'],
|
|
700
|
+
ptrElems: false,
|
|
701
|
+
})),
|
|
702
|
+
]
|
|
703
|
+
: bitfieldSettings;
|
|
588
704
|
// `/flat-rank` — spell a multidimensional global's access as the FLAT byte arithmetic
|
|
589
705
|
// (`*(u16 *)((r << 11) + (i << 1) + (u32)&g)`) where the default recovers the map's declared
|
|
590
706
|
// subscripts (`g[r][i]`). The recovery's evidence is a term at the declared ROW stride, and that
|
|
@@ -600,7 +716,7 @@ export function enumerateCandidates(
|
|
|
600
716
|
// reaches the IR as a `gaddr`. `arrayInnerExtents` is the recovery's own rank test, called here
|
|
601
717
|
// rather than re-spelled, so the gate cannot be narrower than the rule it gates. It is still a
|
|
602
718
|
// superset — it does not know the access WIDTH, and it cannot know whether any residual carries
|
|
603
|
-
// a row term — so where the
|
|
719
|
+
// a row term — so where the variation changes nothing the tree dedup below collapses the pair and the
|
|
604
720
|
// fan does not grow. OVER THE ARTIFACT'S 957 ROWS: 10 name such a symbol at all — 9 of them in
|
|
605
721
|
// their winning `symbolsUsed`, the tenth (`kleod:SetupBG3WindowOverlay`) in a source its row
|
|
606
722
|
// cannot compile, which is why the count is taken off the emitted sources and not off
|
|
@@ -613,74 +729,90 @@ export function enumerateCandidates(
|
|
|
613
729
|
// project map and the shapes the asm evidences, so a MAP-LESS function whose own strides nest
|
|
614
730
|
// (`synthetic:tblrank2:agbcc`) now spells `gPtrTbl[a0][a1]` by default while its flat sibling
|
|
615
731
|
// `*(s32 *)((a1 << 2) + (a0 << 3) + (u32)&gPtrTbl)` — a genuinely different tree — was
|
|
616
|
-
// enumerated nowhere.
|
|
732
|
+
// enumerated nowhere. A variation exists BECAUSE the asm underdetermines the question; supplying the
|
|
617
733
|
// rank from a new place does not make it determined, and nothing reports a candidate that was
|
|
618
734
|
// never enumerated. Map first, exactly as everywhere else: a name the map knows is answered by
|
|
619
735
|
// the map.
|
|
620
|
-
const derivedOrMapped = (n: string): SymbolInfo | undefined => byName?.get(n) ??
|
|
621
|
-
const fnNamesMultidimArray = [...bareGlobalSymbols(
|
|
736
|
+
const derivedOrMapped = (n: string): SymbolInfo | undefined => byName?.get(n) ?? sharedLiftShapes.get(n);
|
|
737
|
+
const fnNamesMultidimArray = [...bareGlobalSymbols(sharedLift).keys()].some((n) => {
|
|
622
738
|
const i = derivedOrMapped(n);
|
|
623
739
|
return i !== undefined && i.shape === 'array' && (arrayInnerExtents(i)?.length ?? 0) > 0;
|
|
624
740
|
});
|
|
625
|
-
const
|
|
626
|
-
? [
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
741
|
+
const declRankSettings = fnNamesMultidimArray
|
|
742
|
+
? [
|
|
743
|
+
...ptrElemSettings,
|
|
744
|
+
...ptrElemSettings.map((s): typeof s => ({
|
|
745
|
+
...s,
|
|
746
|
+
variations: [...s.variations, 'flat-rank'],
|
|
747
|
+
declRank: false,
|
|
748
|
+
})),
|
|
749
|
+
]
|
|
750
|
+
: ptrElemSettings;
|
|
751
|
+
// The structure-variation chain, derived from STRUCTURE_VARIATIONS: each admitted variation doubles
|
|
752
|
+
// the list, the settings without it first — order is load-bearing for the dropped-default skip
|
|
753
|
+
// below (every default sibling enumerates before the alternative that applies the variation, so
|
|
754
|
+
// an alternative's stripped-key lookup always finds a sibling that has already run or been
|
|
755
|
+
// condemned). Each variation's rationale lives on its table
|
|
756
|
+
// entry; both settings are always emitted and the differ referees, never a fixed default — the
|
|
757
|
+
// dedup below collapses a pair wherever the variation changed nothing.
|
|
758
|
+
const sharedLiftDefs = defOpMap(sharedLift);
|
|
759
|
+
type StructureSetting = (typeof ptrElemSettings)[number] & Record<StructureVariation['flag'], boolean>;
|
|
760
|
+
/** Every structure variation OFF — seeded from the table so an added variation is one table entry and not a second
|
|
637
761
|
* hand-edited literal, in table order like everything else derived from it.
|
|
638
762
|
*
|
|
639
|
-
* WHAT THE CAST CANNOT CATCH: a `
|
|
763
|
+
* WHAT THE CAST CANNOT CATCH: a `StructureVariation['flag']` union member with NO table entry.
|
|
640
764
|
* `Object.fromEntries` types its result by the key type it was handed, not by the union, so the
|
|
641
765
|
* assertion is taken on trust where the hand-written literal was checked. Such a member is
|
|
642
|
-
* inert either way — every reader of these flags iterates `
|
|
766
|
+
* inert either way — every reader of these flags iterates `STRUCTURE_VARIATIONS`, so a flag with no
|
|
643
767
|
* entry is never read — but it stops being a type error and becomes an absent field. */
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
opts.onAxisGated?.(ax.suffix);
|
|
768
|
+
const allStructureVariationsOff = Object.fromEntries(
|
|
769
|
+
STRUCTURE_VARIATIONS.map((variation) => [variation.flag, false]),
|
|
770
|
+
) as Record<StructureVariation['flag'], boolean>;
|
|
771
|
+
let structureSettings: StructureSetting[] = declRankSettings.map((s) => ({ ...s, ...allStructureVariationsOff }));
|
|
772
|
+
for (const variation of STRUCTURE_VARIATIONS) {
|
|
773
|
+
if (variation.sharedGate !== undefined && !variation.sharedGate(sharedLift, sharedLiftDefs)) {
|
|
774
|
+
opts.onVariationGated?.(variation.name);
|
|
652
775
|
continue;
|
|
653
776
|
}
|
|
654
|
-
|
|
655
|
-
...
|
|
656
|
-
...
|
|
777
|
+
structureSettings = [
|
|
778
|
+
...structureSettings,
|
|
779
|
+
...structureSettings.map((s) => {
|
|
780
|
+
const on: StructureSetting = { ...s, variations: [...s.variations, variation.name] };
|
|
781
|
+
on[variation.flag] = true;
|
|
782
|
+
return on;
|
|
783
|
+
}),
|
|
657
784
|
];
|
|
658
785
|
}
|
|
659
|
-
/** Is this
|
|
660
|
-
* at the
|
|
661
|
-
* rather than that one
|
|
786
|
+
/** Is this a setting where no structure variation is on, other than `/flip-branch`? The default-setting abort guard's
|
|
787
|
+
* other half: at the default LIFT setting a failure here aborts the row, because it says the lift
|
|
788
|
+
* is broken rather than that one variation cannot spell this tree.
|
|
662
789
|
*
|
|
663
|
-
* `s.
|
|
664
|
-
* the
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
const
|
|
669
|
-
!s.anchor &&
|
|
790
|
+
* `s.variations.length === 0` IS NOT THE SAME TEST, which is why this is a named predicate rather than
|
|
791
|
+
* the length check it looks like. `/flip-branch` names a branch sense RELATIVE to the target's
|
|
792
|
+
* default, so both of its senses pass this test: the flipped one carries a variation, and `sense` is
|
|
793
|
+
* not among the flags read here. `/flip-join` does not pass, because `join` is one of the five
|
|
794
|
+
* hand-carried booleans below. The table's own flags decide, plus those five. */
|
|
795
|
+
const isDefaultSetting = (s: StructureSetting): boolean =>
|
|
796
|
+
!s.anchor &&
|
|
797
|
+
!s.join &&
|
|
798
|
+
s.bitfields &&
|
|
799
|
+
s.ptrElems &&
|
|
800
|
+
s.declRank &&
|
|
801
|
+
STRUCTURE_VARIATIONS.every((variation) => !s[variation.flag]);
|
|
670
802
|
|
|
671
803
|
const seen = new Map<string, Candidate>();
|
|
672
804
|
const seenTrees = new Set<string>();
|
|
673
|
-
/** the
|
|
674
|
-
const
|
|
805
|
+
/** the pre-respell variations' own tree dedup — see their loop for why it is not `seenTrees` */
|
|
806
|
+
const seenPreRespell = new Set<string>();
|
|
675
807
|
const out: Candidate[] = [];
|
|
676
808
|
// The map-derived VALUE references one emitted tree contains, applied at every point a candidate
|
|
677
809
|
// is finalized and derived from the tree that candidate emitted. No pipeline stage carries refs
|
|
678
810
|
// (SFn has no such field), so a future l3 pass that rewrites the tree can never leave a stale ref
|
|
679
811
|
// behind: whatever tree reaches emit is the tree the refs describe, by construction. Collected
|
|
680
|
-
// against the FULL name-keyed map for EVERY
|
|
812
|
+
// against the FULL name-keyed map for EVERY candidate — the '/raw-globals' sibling drops
|
|
681
813
|
// the map's shaped SPELLINGS, but its tree still NAMES pool/reloc-derived globals (ARM
|
|
682
814
|
// `.word gSym`, MIPS `%lo(gSym)`), and those references need declarations in the self-declared
|
|
683
|
-
// scoring world exactly like the named
|
|
815
|
+
// scoring world exactly like the named candidate's (without them every raw sibling fails to compile
|
|
684
816
|
// there, and the eval-winning raw candidate becomes unreproducible outside project headers).
|
|
685
817
|
// The volatility tie-break's input, derived at the same moment as the refs and for the same
|
|
686
818
|
// reason: whatever tree reaches emit is the tree it describes. Absent on a target that declares
|
|
@@ -690,8 +822,8 @@ export function enumerateCandidates(
|
|
|
690
822
|
return n > 0 ? { deviceVolatile: n } : {};
|
|
691
823
|
};
|
|
692
824
|
// Every refusal is reported at most once per (name, reason): `refsOf` runs per CANDIDATE over
|
|
693
|
-
// the same
|
|
694
|
-
// once per
|
|
825
|
+
// the same dictionary derived from the shared lift, so without this the caller would hear the
|
|
826
|
+
// same refusal once per candidate in the fan (hundreds of times on a wide row).
|
|
695
827
|
const refusalsSeen = new Set<string>();
|
|
696
828
|
const refuse = (name: string, reason: RefusedDeclarationReason): void => {
|
|
697
829
|
if (refusalsSeen.has(`${name}\u0000${reason}`)) {
|
|
@@ -708,159 +840,171 @@ export function enumerateCandidates(
|
|
|
708
840
|
// worse. A union cannot — each name is declared by whichever half knows more about it.
|
|
709
841
|
// SCOPE: `declSymbols` is used ONLY here. It must never reach `opts.symbols`/`baseOpts.symbols`
|
|
710
842
|
// or `frontend.lift` — feeding it to the lift would turn on pool promotion, interior
|
|
711
|
-
// attribution and the `/raw-globals`
|
|
843
|
+
// attribution and the `/raw-globals` variation, which is a different (and source-moving) change.
|
|
712
844
|
// THREE halves now, in increasing authority: the name-only pool/reloc symbols, the array shapes
|
|
713
845
|
// the asm evidences for them (raise/globalshape.ts — an `extern u16 gTbl[];` where the bare
|
|
714
846
|
// spelling needs one, and the declaration a candidate spelling `gTbl[i]` cannot compile
|
|
715
847
|
// without), and the project map, which knows more than either.
|
|
716
848
|
const mapSymbols = baseOpts.symbols;
|
|
717
|
-
const declSymbols = new Map<string, SymbolInfo>([
|
|
849
|
+
const declSymbols = new Map<string, SymbolInfo>([
|
|
850
|
+
...bareGlobalSymbols(sharedLift),
|
|
851
|
+
...sharedLiftShapes,
|
|
852
|
+
...(mapSymbols ?? []),
|
|
853
|
+
]);
|
|
718
854
|
// The four per-enumeration constants named at the seam rather than captured across 60 lines of
|
|
719
855
|
// closure (rank-declare.ts states why they belong on one object).
|
|
720
856
|
const refsOf = makeRefCollector({ declSymbols, accessFacts, mapSymbols, refuse });
|
|
721
|
-
// THE
|
|
722
|
-
// rests on: every
|
|
723
|
-
// constants, so a tree an earlier
|
|
724
|
-
// already holds. Inline in that loop the invariant would be a comment asking future
|
|
725
|
-
// read `fn` or the
|
|
726
|
-
// a reviewer. The same argument l3/ast.ts's `walkExprs` header makes
|
|
727
|
-
// counts for more here: a
|
|
857
|
+
// THE RESPELL SET, as a function whose PARAMETER LIST is the invariant the tree skip below
|
|
858
|
+
// rests on: every source here is a pure function of the structured tree and this call's own
|
|
859
|
+
// constants, so a tree an earlier structure setting already produced can only re-emit sources
|
|
860
|
+
// `seen` already holds. Inline in that loop the invariant would be a comment asking future respell
|
|
861
|
+
// variations not to read `fn` or the structure flags; as a signature, a variation that needs one
|
|
862
|
+
// has to widen it in front of a reviewer. The same argument l3/ast.ts's `walkExprs` header makes
|
|
863
|
+
// for its own shape, and it counts for more here: a variation reading `fn` would not misprint a
|
|
864
|
+
// candidate, it would DELETE one,
|
|
728
865
|
// and nothing in the harness reports a candidate that was never enumerated.
|
|
729
|
-
// `
|
|
730
|
-
// reaches `
|
|
731
|
-
// every
|
|
732
|
-
// it. It exists because the pre-
|
|
733
|
-
// the
|
|
866
|
+
// `preRespellVariations` names the tree this call re-spells, and it is a diagnostic argument only: it
|
|
867
|
+
// reaches `onEnumerationError` and nothing else, so the invariant the parameter list states above —
|
|
868
|
+
// every source is a pure function of the tree and this call's own constants — is untouched by
|
|
869
|
+
// it. It exists because the pre-respell variations call this on a REWRITTEN tree, where a refusal
|
|
870
|
+
// of the default source is a refusal of the rewrite, not of the row's own tree.
|
|
734
871
|
//
|
|
735
|
-
// IT PREFIXES EVERY `
|
|
736
|
-
// the whole point rather than a detail: every one of them is reachable from both
|
|
737
|
-
//
|
|
738
|
-
// rewrite. Reported without this prefix, a refusal of `/unmerge/volatile` reads as a refusal
|
|
739
|
-
// `/volatile` — a
|
|
740
|
-
// is the
|
|
741
|
-
//
|
|
742
|
-
const
|
|
743
|
-
// The walk→index
|
|
872
|
+
// IT PREFIXES EVERY `onEnumerationError` IN THIS FUNCTION, not just the default emit's, and that is
|
|
873
|
+
// the whole point rather than a detail: every one of them is reachable from both calls, and the
|
|
874
|
+
// variations each one already carries are, on a pre-respell tree, variations applied to the
|
|
875
|
+
// rewrite. Reported without this prefix, a refusal of `/unmerge/volatile` reads as a refusal
|
|
876
|
+
// of `/volatile` — a candidate that did not fail and is still in the fan. The order
|
|
877
|
+
// is the candidates' own (`[pf.name, ...sp.variations]`), so a reported name and an enumerated
|
|
878
|
+
// candidate's variations name the same candidate the same way.
|
|
879
|
+
const respellTree = (sfn: SFn, preRespellVariations: readonly Variation[] = []): TreeSources => {
|
|
880
|
+
// The walk→index respell variation (l3/reindex.ts) is a THIRD variation on the same footing as
|
|
744
881
|
// signedness and branch sense: whether the source spelled `*p; p++` or `arr[i]` is
|
|
745
882
|
// genuinely ambiguous from asm (compilers strength-reduce the latter into the former), so
|
|
746
883
|
// when a loop re-spells, BOTH representations are emitted and the differ referees. The
|
|
747
|
-
//
|
|
884
|
+
// respelled tree passes the same boundary contracts as the default; one that fails them is
|
|
748
885
|
// dropped here — never scored, never able to win.
|
|
749
|
-
const
|
|
750
|
-
// The
|
|
751
|
-
// declines by throwing costs this tree — its
|
|
752
|
-
// never the row. The opposite posture from the
|
|
753
|
-
// row at the
|
|
886
|
+
const sources: TreeSource[] = [];
|
|
887
|
+
// The DEFAULT source takes the same posture as every respell variation below: a backend that
|
|
888
|
+
// declines by throwing costs this tree — its default source and the respelled sources built
|
|
889
|
+
// from it — never the row. The opposite posture from the STRUCTURE refusal below, which aborts
|
|
890
|
+
// the row at the default setting, and for the reason that separates them: that one says the lift is
|
|
754
891
|
// broken, this one that the target language has no spelling for a tree the lift got right
|
|
755
892
|
// (structuring is language-neutral, and the signedness pins it inserts are `cast` nodes the
|
|
756
893
|
// Pascal backend loud-declines). Refusing EVERY tree is still loud — the empty-enumeration
|
|
757
894
|
// check at the end raises the last refusal.
|
|
758
895
|
try {
|
|
759
|
-
|
|
896
|
+
sources.push({ variations: [], source: backend.emit(sfn), ...refsOf(sfn), ...volOf(sfn) });
|
|
760
897
|
} catch (e) {
|
|
761
|
-
|
|
762
|
-
return {
|
|
898
|
+
reportThrow(preRespellVariations, e);
|
|
899
|
+
return { sources, emit: { error: e } };
|
|
763
900
|
}
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
//
|
|
767
|
-
//
|
|
901
|
+
// Respell variations — each on the same footing as signedness/branch sense, each guarded:
|
|
902
|
+
// it must pass the same boundary contracts as the default AND emit (a backend that declines
|
|
903
|
+
// by throwing — Pascal loud-fails unspellable shapes — drops the candidate, never aborts the
|
|
904
|
+
// enumeration). A dropped respell variation loses nothing: the default remains.
|
|
768
905
|
//
|
|
769
|
-
// POLICY:
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
//
|
|
906
|
+
// POLICY: respell variations derive from the tree they are handed, un-respelled (its default
|
|
907
|
+
// source), only — they do not compose unless
|
|
908
|
+
// one of these compositions sanctions it, each with its own admission bar: `/volatile`
|
|
909
|
+
// narrowed onto a variation's own locals, the STACKED variations and the PAIRINGS, which all
|
|
910
|
+
// derive from or compose onto a source, plus the PRE-RESPELL variations (PRE_RESPELL_VARIATIONS,
|
|
911
|
+
// applied to the TREE before this respell set runs over it; its admission bar is stated at the
|
|
912
|
+
// table) —
|
|
913
|
+
// plus MULTI-RESULT variations: one variation whose single application has several legitimate
|
|
775
914
|
// results (which locals a coalesce merges, which pointers /volatile qualifies) emits
|
|
776
|
-
// each as its own candidate via `
|
|
777
|
-
// retained;
|
|
778
|
-
// subsets), since they add no new
|
|
779
|
-
//
|
|
780
|
-
// numeric-address pointer local — the joint spelling is reachable from neither
|
|
781
|
-
// alone, each
|
|
782
|
-
// `only`), and each needed a row to demand it. The
|
|
783
|
-
// derived onto EVERY
|
|
784
|
-
//
|
|
785
|
-
// so they
|
|
786
|
-
// same argument, not just a row. And a specific
|
|
915
|
+
// each as its own candidate via `respellEach`, capped at the variation, with the default
|
|
916
|
+
// retained; the results may also ride an already-sanctioned composition (the /livebase/volatile
|
|
917
|
+
// subsets), since they add no new variation to it.
|
|
918
|
+
// `/volatile` composes only onto a variation whose output CENTRES ON a
|
|
919
|
+
// numeric-address pointer local — the joint spelling is reachable from neither variation
|
|
920
|
+
// alone, each composition narrows /volatile to that variation's own locals (volatilePtrLocals'
|
|
921
|
+
// `only`), and each needed a row to demand it. The STACKED variations (STACKED_VARIATIONS) are
|
|
922
|
+
// derived onto EVERY source: statement order/shape is orthogonal to what any other respell
|
|
923
|
+
// variation changes — the same kind of independent dimension as signedness —
|
|
924
|
+
// so they are crossed with every source rather than paired; a third blanket composition needs
|
|
925
|
+
// the same argument, not just a row. And a specific PAIRING is admitted on one of two
|
|
787
926
|
// grounds, never on "it might help". FIRST, a row demands the joint spelling AND that
|
|
788
|
-
// spelling is reachable from neither
|
|
789
|
-
// × /nearbase and × /coalesce, plus /vol-store × /unreduce and that
|
|
927
|
+
// spelling is reachable from neither variation alone: /livebase × /indexed, × /sinkinit,
|
|
928
|
+
// × /nearbase and × /coalesce, /scopebase × /coalesce, plus /vol-store × /unreduce and that
|
|
929
|
+
// pair × /ptr-field —
|
|
790
930
|
// each with its demanding row at the respell site. (A TRIPLE is admitted on the same ground
|
|
791
931
|
// and no weaker one: it is one joint spelling with one demanding row, and the pairs BELOW it
|
|
792
932
|
// are not thereby admitted — on synthetic:dmaptrsrc the two intermediate pairs measure 27 and
|
|
793
933
|
// 32 against the triple's 0, and neither is in the fan.) SECOND, a
|
|
794
|
-
//
|
|
934
|
+
// variation COMMITS a policy the differ would otherwise never see — /nearbase × /sinkinit,
|
|
795
935
|
// where `l3/nearbase.ts` picks one of two init orderings inside the pass, so without the
|
|
796
|
-
// pairing that
|
|
797
|
-
// ground is narrower than it looks: it needs a committed
|
|
798
|
-
// existing
|
|
799
|
-
// twice. Anything else stays un-composed. A pairing is admitted for
|
|
800
|
-
// over
|
|
801
|
-
// which bases
|
|
802
|
-
// And a
|
|
803
|
-
// (a wrong candidate can never fake a score-0 match), but on a NONMATCH row the
|
|
804
|
-
// scoring source is shown to the user — a semantically-wrong
|
|
936
|
+
// pairing that decision decides a match with no candidate beside it to lose to. The second
|
|
937
|
+
// ground is narrower than it looks: it needs a committed decision INSIDE a variation with an
|
|
938
|
+
// existing variation that expresses the alternative, not a variation one could imagine wanting
|
|
939
|
+
// twice. Anything else stays un-composed. A pairing is admitted for the hoist mechanism, not per
|
|
940
|
+
// entry, so it runs over every LIVEBASE_HOISTS entry that sets `pairings`: adding an entry
|
|
941
|
+
// changes which bases get bound, not what pairing a hoist with /coalesce means.
|
|
942
|
+
// And a respell variation must PRESERVE SEMANTICS by construction: the differ referees
|
|
943
|
+
// byte-exactness (a wrong candidate can never fake a score-0 match), but on a NONMATCH row the
|
|
944
|
+
// best-scoring source is shown to the user — a semantically-wrong respelled source there is
|
|
805
945
|
// plausible-but-wrong output, the defect class this project exists to avoid. THE ONE
|
|
806
|
-
// EXCEPTION IS THE SAME RULE READ FORWARD: where a
|
|
946
|
+
// EXCEPTION IS THE SAME RULE READ FORWARD: where a variation cannot establish its semantics from
|
|
807
947
|
// inside the pass — `l3/unreduce.ts` moving a read into a loop whose device stores may make
|
|
808
948
|
// the DEVICE write memory — the spelling is marked `Candidate.matchOnly` and published ONLY
|
|
809
949
|
// at a byte-exact score, which is the clause in brackets above used as a licence instead of a
|
|
810
950
|
// consolation. It is never shown as a best-effort answer, so the nonmatch case the sentence
|
|
811
951
|
// is about cannot arise. Hence each
|
|
812
|
-
//
|
|
813
|
-
// Takes a THUNK, so the
|
|
814
|
-
// from the pass itself — rather than from the contracts or the backend — would escape and
|
|
815
|
-
// abort the whole enumeration for this row,
|
|
816
|
-
// a match. Making that structural rather than per-call-site means no
|
|
952
|
+
// variation's decline-over-approximate gates, adversarially audited.
|
|
953
|
+
// Takes a THUNK, so the variation's own computation is inside the try too. A variation that
|
|
954
|
+
// threw from the pass itself — rather than from the contracts or the backend — would escape and
|
|
955
|
+
// abort the whole enumeration for this row, default included: the one way a variation can cost
|
|
956
|
+
// a match. Making that structural rather than per-call-site means no variation can opt out.
|
|
817
957
|
//
|
|
818
|
-
// WHICH boundary contracts run here, and why it is three of the four. A
|
|
819
|
-
// tree `structureChecked` already validated, so what these re-check is what a
|
|
958
|
+
// WHICH boundary contracts run here, and why it is three of the four. A respell variation gets
|
|
959
|
+
// the tree `structureChecked` already validated, so what these re-check is what a VARIATION can
|
|
820
960
|
// break, not what structuring can. `assertResolved` and `assertDerefsTyped` catch an
|
|
821
961
|
// unspellable tree — a candidate the compiler would reject, which the harness would report
|
|
822
|
-
// as a dropped
|
|
962
|
+
// as a dropped candidate with no cause. `assertLocalsWritten` catches the one wrongness the
|
|
823
963
|
// differ REWARDS: a pass that moves or suppresses an assignment and never emits it leaves
|
|
824
964
|
// the reads standing over whatever the allocator left behind, and that candidate compiles,
|
|
825
|
-
// scores, and can win (the shape #106 shipped).
|
|
826
|
-
// l3/basecse.ts's first-use policy, l3/nearbase.ts, l3/scopebase.ts,
|
|
827
|
-
// exactly the
|
|
828
|
-
// rather than on theirs. It cost nothing when measured: 0 violations over the 34357 trees the
|
|
965
|
+
// scores, and can win (the shape #106 shipped). Respell variations that place a def —
|
|
966
|
+
// l3/sinkinit.ts, l3/basecse.ts's first-use policy, l3/nearbase.ts, l3/scopebase.ts,
|
|
967
|
+
// l3/argbase.ts — are exactly the ones that can produce it, so the check belongs on every
|
|
968
|
+
// respelled tree rather than on theirs. It cost nothing when measured: 0 violations over the 34357 trees the
|
|
829
969
|
// artifact's agbcc rows enumerated in both symbol-map configurations. A count with no commit
|
|
830
970
|
// stamp — re-run it rather than reading it as today's.
|
|
831
971
|
// `assertEffectsPreserved` is the fourth and is NOT here: it needs the L1 `fn`, and
|
|
832
|
-
// `
|
|
972
|
+
// `respellTree`'s parameter list is the invariant the tree-dedup skip rests on (see its header).
|
|
833
973
|
// Widening it for a contract is a defensible change and an argued one — not a silent import.
|
|
834
|
-
// A
|
|
974
|
+
// A respell variation returns its tree, or `{ sfn, needsProof }` when it cannot establish its own
|
|
835
975
|
// semantics from inside the pass (Candidate.matchOnly carries the argument).
|
|
836
|
-
const respell = (
|
|
976
|
+
const respell = (variations: readonly Variation[], make: () => RespellResult, alreadyShaped = false): void => {
|
|
977
|
+
if (!offeredOn(target, variations)) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
837
980
|
try {
|
|
838
981
|
const made = make();
|
|
839
982
|
if (!made) {
|
|
840
|
-
return; // the
|
|
983
|
+
return; // the variation declined to fire — no candidate, not a duplicate of the default
|
|
841
984
|
}
|
|
842
985
|
const alt = 'sfn' in made ? made.sfn : made;
|
|
843
986
|
const proof: { matchOnly?: true } = 'sfn' in made && made.needsProof ? { matchOnly: true } : {};
|
|
844
987
|
assertResolved(alt);
|
|
845
988
|
assertDerefsTyped(alt);
|
|
846
989
|
assertLocalsWritten(alt);
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
//
|
|
850
|
-
// a statement-order/shape fact orthogonal to
|
|
851
|
-
// fixed order below. A
|
|
990
|
+
assertNoOrphanedLocals(sfn, alt);
|
|
991
|
+
sources.push({ variations, source: backend.emit(alt), ...refsOf(alt), ...volOf(alt), ...proof });
|
|
992
|
+
// STACKED variations, derived onto EVERY source (the POLICY note above carries the
|
|
993
|
+
// admission argument). Each is a statement-order/shape fact orthogonal to
|
|
994
|
+
// representation; subsets compose in the fixed order below. A stacked variation that
|
|
995
|
+
// never fires declines and costs nothing.
|
|
852
996
|
if (!alreadyShaped) {
|
|
853
|
-
// A shape REORDERS statements, and it is derived after a
|
|
854
|
-
// the placement is re-checked on the shaped tree (contracts.ts). Differential: judged
|
|
855
|
-
// only where the unshaped tree already satisfied the walk, so a
|
|
856
|
-
// never described is not dropped on the strength of a model that does not apply.
|
|
997
|
+
// A shape REORDERS statements, and it is derived after a variation has placed its defs —
|
|
998
|
+
// so the placement is re-checked on the shaped tree (contracts.ts). Differential: judged
|
|
999
|
+
// only where the unshaped tree already satisfied the walk, so a variation whose placement
|
|
1000
|
+
// it never described is not dropped on the strength of a model that does not apply.
|
|
857
1001
|
//
|
|
858
|
-
// `minted` is a NAME DIFF, so for a RENAMING
|
|
859
|
-
// holds locals the
|
|
1002
|
+
// `minted` is a NAME DIFF, so for a RENAMING variation (`/regspell`, `/merge-names`) it
|
|
1003
|
+
// also holds locals the variation never PLACED. Harmless and deliberate: the differential's
|
|
860
1004
|
// early return absorbs a name the unshaped tree already fails on, and a renamed local
|
|
861
1005
|
// whose def a shape moved below a read is the same wrongness as a placed one.
|
|
862
1006
|
//
|
|
863
|
-
// KNOWN GAP on the other side of the same diff: a
|
|
1007
|
+
// KNOWN GAP on the other side of the same diff: a variation that RELOCATES a local it did not
|
|
864
1008
|
// mint contributes no name, so the shape differential does not judge it. The `scope`
|
|
865
1009
|
// placement is the one that does this — it sinks the run `structureChecked` already
|
|
866
1010
|
// committed (l3/basecse.ts judges those itself, over the placer's own report of the
|
|
@@ -870,20 +1014,21 @@ export function enumerateCandidates(
|
|
|
870
1014
|
// which moves only const or pure-read assigns and so cannot lift a read of a base local
|
|
871
1015
|
// above its init.
|
|
872
1016
|
const minted = createdLocals(sfn, alt);
|
|
873
|
-
for (const subset of
|
|
1017
|
+
for (const subset of STACKED_SUBSETS) {
|
|
874
1018
|
// ONE TRY PER SHAPE — a shape is its own candidate and fails as its own candidate.
|
|
875
|
-
// Sharing the
|
|
876
|
-
// later one, under
|
|
877
|
-
const
|
|
1019
|
+
// Sharing the respell variation's outer try would let a throw deriving one subset
|
|
1020
|
+
// discard every later one, under that variation's name, which names no shape.
|
|
1021
|
+
const shapeVariations = subset.map((x) => x.name);
|
|
878
1022
|
try {
|
|
879
|
-
const shaped =
|
|
1023
|
+
const shaped = applyStacked(subset, alt);
|
|
880
1024
|
if (shaped !== null) {
|
|
881
1025
|
assertResolved(shaped.out);
|
|
882
1026
|
assertDerefsTyped(shaped.out);
|
|
883
1027
|
assertLocalsWritten(shaped.out);
|
|
1028
|
+
assertNoOrphanedLocals(alt, shaped.out);
|
|
884
1029
|
assertPlacementSurvives(alt, shaped.out, minted);
|
|
885
|
-
|
|
886
|
-
|
|
1030
|
+
sources.push({
|
|
1031
|
+
variations: [...variations, ...shaped.variations],
|
|
887
1032
|
source: backend.emit(shaped.out),
|
|
888
1033
|
...refsOf(shaped.out),
|
|
889
1034
|
...volOf(shaped.out),
|
|
@@ -892,61 +1037,60 @@ export function enumerateCandidates(
|
|
|
892
1037
|
});
|
|
893
1038
|
}
|
|
894
1039
|
} catch (e) {
|
|
895
|
-
|
|
1040
|
+
reportThrow([...preRespellVariations, ...variations, ...shapeVariations], e);
|
|
896
1041
|
}
|
|
897
1042
|
}
|
|
898
1043
|
}
|
|
899
1044
|
} catch (e) {
|
|
900
|
-
// A throwing
|
|
901
|
-
// REPORTED, not swallowed. `dropped` (below) records only
|
|
902
|
-
// so without this a
|
|
1045
|
+
// A throwing variation, a contract failure, or an unspellable respelled tree: keep the
|
|
1046
|
+
// default. REPORTED, not swallowed. `dropped` (below) records only candidates the SCORER
|
|
1047
|
+
// refused, so without this a variation that fails here vanishes with no trace — indistinguishable
|
|
903
1048
|
// from one that correctly declined, which is exactly the hidden failure
|
|
904
1049
|
// DroppedCandidate exists to surface.
|
|
905
|
-
|
|
1050
|
+
reportThrow([...preRespellVariations, ...variations], e);
|
|
906
1051
|
}
|
|
907
1052
|
};
|
|
908
|
-
// `/argbase` — name a call's argument bases before the call (l3/argbase.ts). A
|
|
909
|
-
// same footing as the others: the
|
|
1053
|
+
// `/argbase` — name a call's argument bases before the call (l3/argbase.ts). A variation on the
|
|
1054
|
+
// same footing as the others: the default inline spelling stays in the list, so the differ
|
|
910
1055
|
// referees and this can never cost a match.
|
|
911
|
-
for (const subset of
|
|
912
|
-
// the truthful
|
|
913
|
-
//
|
|
1056
|
+
for (const subset of STACKED_SUBSETS) {
|
|
1057
|
+
// the truthful variations need the pass to RUN first, so this bypasses respell's
|
|
1058
|
+
// variations-then-thunk shape: same try posture, variations from the fired members
|
|
914
1059
|
try {
|
|
915
|
-
const shaped =
|
|
1060
|
+
const shaped = applyStacked(subset, sfn);
|
|
916
1061
|
if (shaped !== null) {
|
|
917
|
-
// the ONE call whose
|
|
918
|
-
// read
|
|
919
|
-
respell(shaped.
|
|
1062
|
+
// the ONE call whose variations already name shapes — say so, rather than making `respell`
|
|
1063
|
+
// read them back out of the variations it was handed
|
|
1064
|
+
respell(shaped.variations, () => shaped.out, true);
|
|
920
1065
|
}
|
|
921
1066
|
} catch (e) {
|
|
922
|
-
// the
|
|
923
|
-
|
|
924
|
-
opts.onLeverError?.(name + leverLabel + label, firstLine(e));
|
|
1067
|
+
// the report names the full subset — the fired set is unknown mid-throw
|
|
1068
|
+
reportThrow([...preRespellVariations, ...subset.map((x) => x.name)], e);
|
|
925
1069
|
}
|
|
926
1070
|
}
|
|
927
|
-
respell('
|
|
1071
|
+
respell(['argbase'], () => materializeArgBases(sfn));
|
|
928
1072
|
// `/zerosub` — spell a negate of a SHARED subtraction as `0 - x` (l3/zerosub.ts). gcc 2.9
|
|
929
1073
|
// folds `-(a - b)` into `(b - a)` before CSE but leaves `0 - (a - b)` as a negate of the
|
|
930
1074
|
// subtraction itself, so over a value the function also uses elsewhere the two spellings are
|
|
931
1075
|
// a computation and a register apart — and both are reachable from a real source. The differ
|
|
932
1076
|
// referees; its gate keeps it off every shape where the fold rule does not apply, which is
|
|
933
1077
|
// every operand but a shared subtraction.
|
|
934
|
-
respell('
|
|
1078
|
+
respell(['zerosub'], () => zeroSubNegates(sfn));
|
|
935
1079
|
// `/volatile` — declare a pointer local holding a NUMERIC address as pointing to volatile
|
|
936
1080
|
// data (l3/volatileptr.ts). A raw constant has no declaration anywhere, so the original
|
|
937
1081
|
// qualifier is not derivable — and it is codegen-visible (a volatile MEM is barred from
|
|
938
1082
|
// motion, which lands the allocator on different homes). Both spellings are emitted and
|
|
939
1083
|
// the differ referees.
|
|
940
|
-
respell('
|
|
1084
|
+
respell(['volatile'], () => volatilePtrLocals(sfn));
|
|
941
1085
|
// `/vol-slot` — declare a STACK-HOMED scalar local volatile (l3/volatileval.ts). The
|
|
942
1086
|
// qualifier takes away the allocator's freedom to keep the value in a callee-saved
|
|
943
1087
|
// register across a call, and which of the three ways a slot can arise (a volatile local,
|
|
944
1088
|
// an address-taken one, plain register pressure) the source used is not derivable from
|
|
945
|
-
// the asm. A DECLARATION
|
|
946
|
-
// fork): it changes nothing structure() decides, so it rides
|
|
947
|
-
// `/volatile` sibling rather than doubling every enumeration, and its frame-flag gate
|
|
1089
|
+
// the asm. A DECLARATION respell variation, not a structure variation (docs/level-tower.md's
|
|
1090
|
+
// third fork): it changes nothing structure() decides, so it rides each structured tree as `structure()`
|
|
1091
|
+
// produced it, like its `/volatile` sibling, rather than doubling every enumeration, and its frame-flag gate
|
|
948
1092
|
// costs nothing on a function with no slot.
|
|
949
|
-
respell('
|
|
1093
|
+
respell(['vol-slot'], () => volatileValueLocals(sfn));
|
|
950
1094
|
/** `/vol-store`'s pass with the target's device-register window handed over — the window that
|
|
951
1095
|
* keeps it off ordinary memory. Written once because five call sites take it. */
|
|
952
1096
|
const volStore = (from: SFn): SFn | null => volatileDeviceStores(from, target.capabilities.deviceRegisters);
|
|
@@ -957,7 +1101,7 @@ export function enumerateCandidates(
|
|
|
957
1101
|
// hoists an unpinned fixed-address store clean out of a loop (gcc/loop.c:8934), so the pinned
|
|
958
1102
|
// spelling is the only one that reproduces a device-driving loop body at all. Its window gate
|
|
959
1103
|
// is the target's own `deviceRegisters` range, which is what keeps it off ordinary memory.
|
|
960
|
-
respell('
|
|
1104
|
+
respell(['vol-store'], () => volStore(sfn));
|
|
961
1105
|
/** `/unreduce` with both halves of the device model handed over — the SPELLING range and the
|
|
962
1106
|
* MEMORY-MODEL trigger list (target.ts). Written once because three call sites take it. */
|
|
963
1107
|
const unreduced = (from: SFn): UnreduceResult | null =>
|
|
@@ -968,177 +1112,191 @@ export function enumerateCandidates(
|
|
|
968
1112
|
// reaches a preheader slot no C statement can (a compiler-created giv init is inserted after
|
|
969
1113
|
// the invariant hoist, gcc/loop.c:1151 then :1173). The scalar-value sibling of `/indexed`,
|
|
970
1114
|
// which makes the same argument for a pointer walk.
|
|
971
|
-
respell('
|
|
1115
|
+
respell(['unreduce'], () => unreduced(sfn));
|
|
972
1116
|
// `/ptr-field` — declare a recovered WORD field a pointer (l3/ptrfield.ts). raise/structs.ts
|
|
973
1117
|
// types a field from the access width alone, and on a 32-bit target `void *` fits that
|
|
974
1118
|
// evidence exactly — but not the compiler's alias analysis, which is what lets a pointer
|
|
975
1119
|
// field's load leave a loop an `s32` store pins it inside. Both are enumerated.
|
|
976
|
-
respell('
|
|
1120
|
+
respell(['ptr-field'], () => pointerFields(sfn));
|
|
977
1121
|
// `/offmember` — spell a leaf base's constant subscript as a struct MEMBER (l3/offmember.ts),
|
|
978
1122
|
// so the offset stays in the load's displacement instead of folding into the pool literal.
|
|
979
1123
|
// The SECOND source of the shape `/basefold` already reads: that row answers the same
|
|
980
1124
|
// evidence with a named base, this one with an aggregate member, and the two are different C
|
|
981
|
-
// and different register pressure. Offered only where the target declares the fold
|
|
982
|
-
// PPC put the addend in the instruction by
|
|
983
|
-
// it there, exactly as with
|
|
984
|
-
|
|
985
|
-
respell('/offmember', () => spellOperandMembers(sfn));
|
|
986
|
-
}
|
|
1125
|
+
// and different register pressure. Offered only where the target declares the fold (its registry
|
|
1126
|
+
// entry's target gate, which `respell` asks): MIPS and PPC put the addend in the instruction by
|
|
1127
|
+
// construction, so nothing there says a member put it there, exactly as with BASEFOLD_HOISTS.
|
|
1128
|
+
respell(['offmember'], () => spellOperandMembers(sfn));
|
|
987
1129
|
// The `/vol-store` × `/unreduce` PAIRING — row-demanded (synthetic:dmafill), and the joint
|
|
988
|
-
// spelling is reachable from neither
|
|
1130
|
+
// spelling is reachable from neither variation alone: pinning the stores keeps three of them in
|
|
989
1131
|
// the loop body, which is what makes the loop's register pressure — and so the placement of
|
|
990
1132
|
// the induction init — observable at all. Alone the two score 19 and 34 against the row's own
|
|
991
1133
|
// 30; together, 0. The TRIPLE adds `/ptr-field` for synthetic:dmaptrsrc, whose closed form
|
|
992
1134
|
// reads a struct field the un-reduce puts back inside the loop: 27 · 35 · 42 alone, 0
|
|
993
1135
|
// together. The intermediate pairs are not admitted, and the reason is that NO ROW DEMANDS
|
|
994
1136
|
// ONE — neither could win where they are reachable: compiled on synthetic:dmaptrsrc, VT TIES
|
|
995
|
-
// `/vol-store`'s 27 and RT LOSES to it at 32.
|
|
996
|
-
//
|
|
997
|
-
//
|
|
998
|
-
//
|
|
1137
|
+
// `/vol-store`'s 27 and RT LOSES to it at 32. Rank a pair against the BEST already-admitted
|
|
1138
|
+
// candidate (on synthetic:dmaptrsrc the triple, at 0), not against any admitted variation: VT
|
|
1139
|
+
// only ties `/vol-store`'s 27, and RT's 32 beats the admitted standalone `/unreduce`'s 35, so
|
|
1140
|
+
// "worse than an admitted variation" would exclude neither.
|
|
999
1141
|
//
|
|
1000
|
-
// WHAT THE STANDALONE LINES COST, since neither of the two
|
|
1142
|
+
// WHAT THE STANDALONE LINES COST, since neither of the two variations ever wins an artifact row
|
|
1001
1143
|
// ALONE — every `/unreduce` and `/ptr-field` winner rides inside a `/vol-store` pairing, which
|
|
1002
|
-
// is the property `apps/benchmark/test/census.test.ts` asserts
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
1005
|
-
//
|
|
1144
|
+
// is the property `apps/benchmark/test/census.test.ts` asserts. They are kept because a
|
|
1145
|
+
// variation has to be able to LOSE on its own terms: the admission posture (compareScored orders
|
|
1146
|
+
// by score) is what makes a wrong respelled source harmless, and it is only observable when the
|
|
1147
|
+
// single-variation candidate is in the fan —
|
|
1006
1148
|
// `synthetic:dmastride` exists to show exactly that for `/unreduce`, at 33 against its match.
|
|
1007
1149
|
//
|
|
1008
1150
|
// AND THE SUBSET APPLIER IS NOT THE RIGHT MECHANISM HERE, though it looks like it: rebuilding
|
|
1009
|
-
// this as a
|
|
1010
|
-
// `
|
|
1011
|
-
// one of the three declines. That is the property the
|
|
1151
|
+
// this as a STACKED_SUBSETS-style table would admit VT and RT by construction, because
|
|
1152
|
+
// `applyStacked` is SKIP-ON-DECLINE and would emit "everything that fired" on any tree where
|
|
1153
|
+
// one of the three declines. That is the property the stacked variations are designed around and
|
|
1012
1154
|
// the one the pairing policy forbids — a pair reaches the fan only when a row demands it.
|
|
1013
1155
|
//
|
|
1014
|
-
// Both compose through `
|
|
1156
|
+
// Both compose through `composeRespellVariations`, which carries `/unreduce`'s proof obligation across
|
|
1015
1157
|
// the stages after it — hand-writing that carry made dropping it a type-correct edit.
|
|
1016
|
-
respell('
|
|
1017
|
-
respell('
|
|
1158
|
+
respell(['vol-store', 'unreduce'], () => composeRespellVariations(sfn, [volStore, unreduced]));
|
|
1159
|
+
respell(['vol-store', 'unreduce', 'ptr-field'], () =>
|
|
1160
|
+
composeRespellVariations(sfn, [volStore, unreduced, pointerFields]),
|
|
1161
|
+
);
|
|
1018
1162
|
// `/inlinebase` — spell a CONSTANT-address pointer local at its uses instead
|
|
1019
1163
|
// (l3/inlinebase.ts). The local is structure/analysis.ts's value home for a `const` the
|
|
1020
1164
|
// asm kept in a callee-saved register across a call; the register is real, but a constant
|
|
1021
1165
|
// re-spelled per use is CSEd back into that same one, so which the source had is not
|
|
1022
1166
|
// derivable. Its own bare-`const`-initializer gate keeps it off l3/basecse.ts's reuse
|
|
1023
|
-
// hoists, whose placement
|
|
1167
|
+
// hoists, whose placement variations already answer that question.
|
|
1024
1168
|
//
|
|
1025
|
-
// TWO
|
|
1169
|
+
// TWO RESULTS, not a composition: deleting the local also deletes the only place
|
|
1026
1170
|
// a `volatile` POINTEE could be written, and a raw address has no declaration anywhere
|
|
1027
1171
|
// else to carry it. So the qualified spelling is emitted too, `/volatile` narrowed to
|
|
1028
|
-
// exactly the locals this
|
|
1172
|
+
// exactly the locals this variation deletes. Usually the bytes separate them and the score
|
|
1029
1173
|
// decides (11 against 12 on pokeemerald:EReader_Reset), but where the compiler was not
|
|
1030
1174
|
// exploiting the non-volatility they are byte-identical — as they are on that row's
|
|
1031
1175
|
// WINNING shape, the one that also qualifies the slot — and `compareScored`'s device-
|
|
1032
|
-
// volatility term picks the qualified
|
|
1176
|
+
// volatility term picks the qualified candidate, 0x4000208 being REG_IME.
|
|
1033
1177
|
//
|
|
1034
1178
|
// COST — it fires broadly: on 33 of the 69 klonoa functions that lift with no symbol map
|
|
1035
1179
|
// (a symbol-map sweep sees fewer, since an absolute pool constant lifts to a `gaddr`
|
|
1036
1180
|
// there). Both outputs together add 766 candidates over 47058, +1.6%, and up to +67% on
|
|
1037
1181
|
// one function (EntityPositionFromLevelTable) — the same class of price the enumeration
|
|
1038
|
-
// already pays for `/volatile`, and cheaper than
|
|
1039
|
-
// be — the
|
|
1182
|
+
// already pays for `/volatile`, and cheaper than a structure variation over the same question
|
|
1183
|
+
// would be — the decision the variation's header argues. `/vol-slot` adds nothing at all there: no
|
|
1040
1184
|
// klonoa function reaches its frame gate.
|
|
1041
1185
|
const inlineVolatile = (): SFn | null => {
|
|
1042
1186
|
const only = new Set(inlinableConstBases(sfn));
|
|
1043
1187
|
const q = only.size ? volatilePtrLocals(sfn, only) : null;
|
|
1044
1188
|
return q ? inlineConstBases(q) : null;
|
|
1045
1189
|
};
|
|
1046
|
-
respell('
|
|
1047
|
-
respell('
|
|
1190
|
+
respell(['inlinebase', 'volatile'], inlineVolatile);
|
|
1191
|
+
respell(['inlinebase'], () => inlineConstBases(sfn));
|
|
1048
1192
|
// The `/inlinebase` × `/vol-slot` PAIRING — row-demanded, and the joint spelling is
|
|
1049
|
-
// reachable from neither
|
|
1193
|
+
// reachable from neither variation alone: on pokeemerald:EReader_Reset the default scores 11,
|
|
1050
1194
|
// `/inlinebase` alone 11 and `/vol-slot` alone 2, and the pair 0. The two touch disjoint
|
|
1051
1195
|
// locals (one pointer-typed, one a scalar frame slot), so applying them in either order
|
|
1052
1196
|
// gives the same spelling — and each of `/inlinebase`'s two outputs carries it.
|
|
1053
|
-
respell('
|
|
1197
|
+
respell(['inlinebase', 'volatile', 'vol-slot'], () => {
|
|
1054
1198
|
const r = inlineVolatile();
|
|
1055
1199
|
return r ? volatileValueLocals(r) : null;
|
|
1056
1200
|
});
|
|
1057
|
-
respell('
|
|
1201
|
+
respell(['inlinebase', 'vol-slot'], () => {
|
|
1058
1202
|
const r = inlineConstBases(sfn);
|
|
1059
1203
|
return r ? volatileValueLocals(r) : null;
|
|
1060
1204
|
});
|
|
1061
1205
|
// `/scopebase` — name a reused global base at the INNERMOST scope holding its uses
|
|
1062
|
-
// (l3/scopebase.ts). Distinct from basecse's function-top hoist, which the
|
|
1206
|
+
// (l3/scopebase.ts). Distinct from basecse's function-top hoist, which the default already
|
|
1063
1207
|
// carries: this one fires exactly where that placement would extend a live range the
|
|
1064
1208
|
// original never had.
|
|
1065
|
-
// `/scopebase`, and its COALESCED
|
|
1209
|
+
// `/scopebase`, and its COALESCED results. Which locals a register allocator shared is not
|
|
1066
1210
|
// derivable from the tree — on the row this was built for the two legal merges score 18 and
|
|
1067
1211
|
// 40 against a no-merge 21, so committing to one by declaration order costs 19 points and
|
|
1068
|
-
// discards the winner. Every
|
|
1069
|
-
// `/regcopy` does for its allocator-ambiguous tail
|
|
1212
|
+
// discards the winner. Every result is emitted and the differ referees, exactly as
|
|
1213
|
+
// `/regcopy` does for its allocator-ambiguous tail decision.
|
|
1070
1214
|
//
|
|
1071
|
-
// POLICY NOTE:
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1215
|
+
// POLICY NOTE: this is a PAIRING, `/scopebase` × `/coalesce`, admitted for the row above, which
|
|
1216
|
+
// demands the joint spelling (18 against the no-merge 21). `respellEach` makes the coalesce's
|
|
1217
|
+
// results from the tree `/scopebase` produced, in the one place that knows the hoist happened.
|
|
1218
|
+
// Unlike the `/livebase` × `/coalesce` pairings it takes `coalesceCandidates`' whole merge set,
|
|
1219
|
+
// not the arm-disjoint subset. The name lists both variations because both were applied. A
|
|
1220
|
+
// candidate's variations name what was applied, not a route a deletion must remove: these
|
|
1221
|
+
// results are minted by their own `respellEach` call, so deleting `respell(['scopebase'], …)` does
|
|
1222
|
+
// not delete them. The un-coalesced `/scopebase` stays in the list, so nothing is lost.
|
|
1075
1223
|
//
|
|
1076
1224
|
// EVERY pass invocation stays INSIDE a thunk — see the paragraph above on why a pass that
|
|
1077
|
-
// runs outside `respell`'s try is the one way a
|
|
1225
|
+
// runs outside `respell`'s try is the one way a variation can cost a match. `respellEach` re-runs
|
|
1078
1226
|
// the hoist per candidate, which is pure and cheap, rather than caching it outside the guard.
|
|
1079
|
-
respell('
|
|
1227
|
+
respell(['scopebase'], () => hoistScopedBases(sfn));
|
|
1080
1228
|
// `/regionbase` — the same pass under its second region rule: a base the source spells inside N
|
|
1081
|
-
// disjoint regions becomes N locals, one per region, rather than one at function scope. A
|
|
1082
|
-
// beside `/scopebase`, not a replacement for it: both spellings and the un-hoisted
|
|
1229
|
+
// disjoint regions becomes N locals, one per region, rather than one at function scope. A VARIATION
|
|
1230
|
+
// beside `/scopebase`, not a replacement for it: both spellings and the un-hoisted default stay
|
|
1083
1231
|
// in the list, so the differ settles which allocation the original had.
|
|
1084
1232
|
const regionbase = (): SFn | null => hoistScopedBases(sfn, { regions: 'per-region' });
|
|
1085
|
-
respell('
|
|
1086
|
-
// …and its `/volatile`
|
|
1087
|
-
//
|
|
1088
|
-
//
|
|
1089
|
-
// reference spells it `vu32 *dmaRegs`; without the
|
|
1090
|
-
// with is published UNqualified, and `compareScored`'s `deviceVolatile` term — which prefers
|
|
1091
|
-
// the qualified
|
|
1233
|
+
respell(['regionbase'], regionbase);
|
|
1234
|
+
// …and its `/volatile` composition, narrowed to exactly the locals this variation mints — the
|
|
1235
|
+
// same composition `/livebase` and `/inlinebase` already carry, for the same reason. The shape
|
|
1236
|
+
// this variation exists for is a DEVICE base (the DMA block at 0x040000D4), and the project's own
|
|
1237
|
+
// reference spells it `vu32 *dmaRegs`; without the composition every region local this variation
|
|
1238
|
+
// wins with is published UNqualified, and `compareScored`'s `deviceVolatile` term — which prefers
|
|
1239
|
+
// the qualified candidate on a tie — never sees a qualified candidate to prefer. It is a candidate like
|
|
1092
1240
|
// any other where the qualifier costs bytes, and the differ referees.
|
|
1093
1241
|
const regionVolatile = (): SFn | null => {
|
|
1094
1242
|
const r = regionbase();
|
|
1095
1243
|
return r ? volatilePtrLocals(r, createdLocals(sfn, r)) : null;
|
|
1096
1244
|
};
|
|
1097
|
-
respell('
|
|
1098
|
-
// …and the `/vol-store` triple, the pairing this
|
|
1245
|
+
respell(['regionbase', 'volatile'], regionVolatile);
|
|
1246
|
+
// …and the `/vol-store` triple, the pairing this variation is the first to inhabit (see
|
|
1099
1247
|
// l3/volstore.ts, where the two qualifiers' reach over a tree's OWN locals is disjoint).
|
|
1100
|
-
// `/volatile` qualifies a pointer LOCAL and `/vol-store` a STORE SITE, and this
|
|
1248
|
+
// `/volatile` qualifies a pointer LOCAL and `/vol-store` a STORE SITE, and this variation leaves
|
|
1101
1249
|
// both in one function: it homes the regions holding two or more direct uses and leaves every
|
|
1102
1250
|
// other spelling of the same device address inline. On `synthetic:dmascope` that residue is
|
|
1103
1251
|
// the write to REG_DMA0CNT that STARTS the transfer, and without the triple it is published
|
|
1104
1252
|
// bare beside three `volatile s32 *` region locals.
|
|
1105
|
-
respell('
|
|
1253
|
+
respell(['regionbase', 'volatile', 'vol-store'], () => {
|
|
1106
1254
|
const v = regionVolatile();
|
|
1107
1255
|
return v ? volStore(v) : null;
|
|
1108
1256
|
});
|
|
1109
|
-
|
|
1110
|
-
|
|
1257
|
+
/** One candidate per result of a multi-result variation, `name` applied to the result's own
|
|
1258
|
+
* subject after `prefix`. */
|
|
1259
|
+
const respellEach = (
|
|
1260
|
+
prefix: readonly Variation[],
|
|
1261
|
+
name: SubjectVariationName,
|
|
1111
1262
|
from: () => SFn | null | undefined,
|
|
1112
|
-
|
|
1263
|
+
resultsOf: (s: SFn) => { merged: string; sfn: SFn }[] = coalesceCandidates,
|
|
1113
1264
|
): void => {
|
|
1114
|
-
|
|
1265
|
+
if (!offeredOn(target, [...prefix, name])) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
let results: { variations: readonly Variation[]; sfn: SFn }[] = [];
|
|
1115
1269
|
try {
|
|
1116
1270
|
const base = from();
|
|
1117
|
-
|
|
1271
|
+
results = (base ? resultsOf(base) : []).map((c) => ({
|
|
1272
|
+
variations: [...prefix, withSubject(name, c.merged)],
|
|
1273
|
+
sfn: c.sfn,
|
|
1274
|
+
}));
|
|
1118
1275
|
} catch (e) {
|
|
1119
|
-
|
|
1276
|
+
reportThrow([...preRespellVariations, ...prefix, name], e);
|
|
1120
1277
|
return;
|
|
1121
1278
|
}
|
|
1122
|
-
for (const c of
|
|
1123
|
-
respell(
|
|
1279
|
+
for (const c of results) {
|
|
1280
|
+
respell(c.variations, () => c.sfn);
|
|
1124
1281
|
}
|
|
1125
1282
|
};
|
|
1126
|
-
|
|
1127
|
-
|
|
1283
|
+
respellEach(['scopebase'], 'coalesce', () => hoistScopedBases(sfn));
|
|
1284
|
+
respellEach([], 'coalesce', () => sfn);
|
|
1128
1285
|
// `/volatile`'s per-local SUBSETS: which pointers the source declared volatile is
|
|
1129
1286
|
// per-pointer knowledge (an MMIO block and a plain RAM table sit side by side, and
|
|
1130
1287
|
// qualifying the table blocks the read collapse its region wants), so each proper
|
|
1131
1288
|
// non-empty subset is its own candidate — the same alternative-OUTPUTS mechanism as the
|
|
1132
|
-
// coalesce merges, not a
|
|
1133
|
-
// ≤3 cap). The all-qualifiers form is plain `/volatile` above; the livebase
|
|
1134
|
-
// subsets ride below with
|
|
1135
|
-
|
|
1136
|
-
|
|
1289
|
+
// coalesce merges, not a pairing (l3/volatileptr.ts volatileSubsetCandidates carries the
|
|
1290
|
+
// ≤3 cap). The all-qualifiers form is plain `/volatile` above; the `/livebase/volatile`
|
|
1291
|
+
// composition's subsets ride below with that composition's own `only` scope.
|
|
1292
|
+
respellEach(
|
|
1293
|
+
[],
|
|
1294
|
+
'volatile',
|
|
1137
1295
|
() => sfn,
|
|
1138
1296
|
(s) => volatileSubsetCandidates(s),
|
|
1139
1297
|
);
|
|
1140
|
-
respell('
|
|
1141
|
-
respell('
|
|
1298
|
+
respell(['indexed'], () => reindexWalks(sfn));
|
|
1299
|
+
respell(['indexed', 'volatile'], () => {
|
|
1142
1300
|
const kept = new Set<string>();
|
|
1143
1301
|
const r = reindexWalks(sfn, kept);
|
|
1144
1302
|
return r ? volatilePtrLocals(r, kept) : null;
|
|
@@ -1147,47 +1305,46 @@ export function enumerateCandidates(
|
|
|
1147
1305
|
// LIVEBASE_GATES): its `loop` and `repeated-const-offset` rules predict re-materialization,
|
|
1148
1306
|
// and an MMIO poll (store then re-read the same fixed offset while it spins) is the shape
|
|
1149
1307
|
// where the prediction is wrong — the compiler holds ONE base register across stores, the
|
|
1150
|
-
// loop, and the read-back. The
|
|
1151
|
-
// hoist-nothing result means the
|
|
1152
|
-
// One family per
|
|
1153
|
-
//
|
|
1154
|
-
// `/unfolded`
|
|
1155
|
-
// declares the array-shape fork, so a target with neither is
|
|
1156
|
-
// and nothing else. The
|
|
1157
|
-
//
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
...
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
];
|
|
1308
|
+
// loop, and the read-back. The default already carries every base those rules admit, so a
|
|
1309
|
+
// hoist-nothing result means the variation has nothing to add and declines.
|
|
1310
|
+
// One family per hoist; a hoist binding exactly what an earlier hoist bound is the same
|
|
1311
|
+
// source under different variations, so it declines for that too. `/basefold`'s TWO hoists and
|
|
1312
|
+
// `/unfolded` stay on the roster where the target declares the fold, and `/orderbase` where it
|
|
1313
|
+
// declares the array-shape fork (each registry entry's target gate), so a target with neither is
|
|
1314
|
+
// offered the two `/livebase` hoists and nothing else. The array-shape fork is the opt-in
|
|
1315
|
+
// raise/globalshape.ts carries: with it off nothing is stamped, so `order-licensed` would refuse
|
|
1316
|
+
// every key anyway and the filter only saves the census.
|
|
1317
|
+
const hoists: readonly BaseHoist[] = [
|
|
1318
|
+
...LIVEBASE_HOISTS,
|
|
1319
|
+
...BASEFOLD_HOISTS,
|
|
1320
|
+
...UNFOLDED_HOISTS,
|
|
1321
|
+
...ORDERBASE_HOISTS,
|
|
1322
|
+
].filter((h) => offeredOn(target, h.variations));
|
|
1166
1323
|
// AND THE SAME SKIP KEYED ON THE LICENCE ITSELF WOULD BUY NOTHING, which is worth a paragraph
|
|
1167
|
-
// because this
|
|
1168
|
-
// on the lifted fn, so the
|
|
1324
|
+
// because this hoist is where the next reader will propose it. `orderLicensedGlobals` is decidable
|
|
1325
|
+
// on the lifted fn, so the hoist could also be dropped wherever THAT set is empty. It would be
|
|
1169
1326
|
// sound, and it would be inert, for the same one reason: an empty licence stamps no
|
|
1170
1327
|
// `baseOrdered` (structure.ts `stampOrderedBases`), so `order-licensed` refuses every key, so
|
|
1171
|
-
// `hoist` returns null and this
|
|
1328
|
+
// `hoist` returns null and this hoist's three emission sites — two `respell`s and the `respellEach`
|
|
1172
1329
|
// whose generator fans over volatile SUBSETS, so the third is a set and not one spelling — emit
|
|
1173
|
-
// nothing. Which GENERALIZES to every
|
|
1174
|
-
// where the
|
|
1330
|
+
// nothing. Which GENERALIZES to every variation carrying a licence: a skip like it is sound exactly
|
|
1331
|
+
// where the variation would have emitted no candidate, so a sound one shrinks the fan by zero, so it
|
|
1175
1332
|
// removes no COMPILE, and one compile per candidate is where a ranked run's cost is; what it
|
|
1176
1333
|
// saves is one `admittedBases` walk per tree. `docs/level-tower.md` carries the general form.
|
|
1177
1334
|
// Measured on klonoa's `LoadBGTilemapData`, the checkout function whose 112,896-candidate fan
|
|
1178
|
-
// raises the question: the licence is empty on every lift
|
|
1335
|
+
// raises the question: the licence is empty on every lift setting of BOTH symbol-map configurations —
|
|
1179
1336
|
// four named symbols DO reach the licence table map-ful and the ADDRESS gates refuse all four,
|
|
1180
1337
|
// so "the pool spells no names" is not the reason — and the skip fires on every tree there and
|
|
1181
1338
|
// removes not one candidate.
|
|
1182
1339
|
//
|
|
1183
1340
|
// IF IT IS EVER BUILT ANYWAY, IT IS `orderLicensedGlobals(fn, target)` READ AT THE SITE BELOW
|
|
1184
|
-
// that hands `orderLicensed` to the structuring call, PER LIFT
|
|
1341
|
+
// that hands `orderLicensed` to the structuring call, PER LIFT SETTING — never per function,
|
|
1185
1342
|
// and never either of the two predicates standing beside it in that same loop. All three wrong
|
|
1186
1343
|
// readings delete the SAME four live candidates on `sub_806800C` in the sa3 checkout, in BOTH
|
|
1187
|
-
//
|
|
1344
|
+
// symbol-map configurations: `unsigned/setup-args/orderbase` and its `/flip-join`, `/derived-home` and
|
|
1188
1345
|
// `/flip-join/derived-home` siblings.
|
|
1189
|
-
// · PER FUNCTION — `/setup-args` narrows the lift and can license a name the
|
|
1190
|
-
// not, so the first
|
|
1346
|
+
// · PER FUNCTION — `/setup-args` narrows the lift and can license a name the default lift does
|
|
1347
|
+
// not, so the first lift setting's answer is not the function's.
|
|
1191
1348
|
// · `inferGlobalArrays`, seven lines above the licence call and off the same `fn` — a
|
|
1192
1349
|
// documented strict SUBSET (`raise/globalshape.ts`), and measurably empty on functions
|
|
1193
1350
|
// where the licence is not, several of them carrying `/orderbase` candidates.
|
|
@@ -1197,17 +1354,17 @@ export function enumerateCandidates(
|
|
|
1197
1354
|
// tower destroys the order evidence, but the tempting next step — "so it reads empty
|
|
1198
1355
|
// everywhere and the skip is free" — is FALSE: it reads NON-empty on a function whose
|
|
1199
1356
|
// `/orderbase` candidates it then deletes anyway. Firing less often is not a defence.
|
|
1200
|
-
// A per-row
|
|
1357
|
+
// A per-row variations/source diff catches the last two, and CANNOT catch the first. Both of those
|
|
1201
1358
|
// delete `unsigned/orderbase` off `synthetic:bgarr:agbcc`, the exact source that row publishes
|
|
1202
|
-
// as its score-0 MATCH — a row carrying no symbol map, so its single
|
|
1203
|
-
// actually runs. The per-function reading needs `/setup-args` AND `/orderbase` in ONE
|
|
1204
|
-
// and NO published winner
|
|
1359
|
+
// as its score-0 MATCH — a row carrying no symbol map, so its single configuration is the one the
|
|
1360
|
+
// gate actually runs. The per-function reading needs `/setup-args` AND `/orderbase` in ONE
|
|
1361
|
+
// candidate's variations, and NO published winner carries both — so a green corpus gate is evidence about two of
|
|
1205
1362
|
// these readings and none at all about the third. That property is the gate, not a count:
|
|
1206
1363
|
// apps/benchmark/test/census.test.ts asserts it over the committed artifact.
|
|
1207
1364
|
|
|
1208
|
-
// The CENSUS is a pure function of (this tree, that table) and every
|
|
1209
|
-
// earlier
|
|
1210
|
-
// number of
|
|
1365
|
+
// The CENSUS is a pure function of (this tree, that table) and every hoist asks for every
|
|
1366
|
+
// earlier hoist's, from thunks each composition re-invokes — quadratic in the roster, times the
|
|
1367
|
+
// number of compositions. Memoized on the gate table's identity. The value is a list of key
|
|
1211
1368
|
// STRINGS whose two readers here only compare and count it, so a memo hit shares no tree.
|
|
1212
1369
|
const censuses = new Map<readonly Gate<BaseKey>[], readonly string[]>();
|
|
1213
1370
|
const census = (g: readonly Gate<BaseKey>[]): readonly string[] => {
|
|
@@ -1219,14 +1376,14 @@ export function enumerateCandidates(
|
|
|
1219
1376
|
censuses.set(g, v);
|
|
1220
1377
|
return v;
|
|
1221
1378
|
};
|
|
1222
|
-
/** Does an EARLIER
|
|
1223
|
-
* that
|
|
1379
|
+
/** Does an EARLIER hoist already bind exactly `bound` at this placement? Then this hoist is
|
|
1380
|
+
* that hoist's source under different variations and declines.
|
|
1224
1381
|
*
|
|
1225
1382
|
* Same bases in the same POSITION is the same spelling; the same bases somewhere else is not,
|
|
1226
1383
|
* which is why the placement is a conjunct and not an afterthought. `rows` is the slice's
|
|
1227
1384
|
* own list, so the two readers scope it differently — the roster hoist asks over the whole
|
|
1228
|
-
*
|
|
1229
|
-
* must never drop a withhold no other
|
|
1385
|
+
* hoist roster, the homesplit pairing over the PAIRED hoists only, because a skip there
|
|
1386
|
+
* must never drop a withhold no other hoist enumerates. Captures `census`, so a repeated table
|
|
1230
1387
|
* costs one memo lookup rather than a second walk. */
|
|
1231
1388
|
const shadowedByEarlier = (
|
|
1232
1389
|
rows: readonly { placement: HoistPlacement; gates: readonly Gate<BaseKey>[] }[],
|
|
@@ -1234,32 +1391,33 @@ export function enumerateCandidates(
|
|
|
1234
1391
|
placement: HoistPlacement,
|
|
1235
1392
|
bound: readonly string[],
|
|
1236
1393
|
): boolean => rows.slice(0, i).some((r) => r.placement === placement && sameBases(bound, census(r.gates)));
|
|
1237
|
-
const livebases =
|
|
1394
|
+
const livebases = hoists.map(({ variations, gates, placement, pairings }, i) => {
|
|
1238
1395
|
const hoist = (): SFn | null => {
|
|
1239
1396
|
const bound = census(gates);
|
|
1240
1397
|
if (bound.length === 0) {
|
|
1241
1398
|
return null;
|
|
1242
1399
|
}
|
|
1243
|
-
return shadowedByEarlier(
|
|
1400
|
+
return shadowedByEarlier(hoists, i, placement, bound) ? null : hoistBaseLocals(sfn, gates, placement);
|
|
1244
1401
|
};
|
|
1245
1402
|
const volatiles = (): SFn | null => {
|
|
1246
1403
|
const r = hoist();
|
|
1247
1404
|
return r ? volatilePtrLocals(r, createdLocals(sfn, r)) : null;
|
|
1248
1405
|
};
|
|
1249
|
-
return {
|
|
1406
|
+
return { variations, hoist, volatiles, pairings, gates, placement };
|
|
1250
1407
|
});
|
|
1251
|
-
// THE PLACEMENT DIFFERENTIAL, one composition inwards. `respell` re-checks a
|
|
1252
|
-
// placement across the
|
|
1253
|
-
// are the same hazard in the same file and are outside it, because the
|
|
1254
|
-
// INSIDE one `make()` thunk and the intermediate tree never reaches
|
|
1255
|
-
// def-MOVING pass (`sinkInitsToFirstUse`, `nearBaseClusters`,
|
|
1256
|
-
// tree a PLACING
|
|
1408
|
+
// THE PLACEMENT DIFFERENTIAL, one composition inwards. `respell` re-checks a variation's
|
|
1409
|
+
// placement across the stacked variations derived onto it; the variation-on-variation
|
|
1410
|
+
// compositions below are the same hazard in the same file and are outside it, because the
|
|
1411
|
+
// composition happens INSIDE one `make()` thunk and the intermediate tree never reaches
|
|
1412
|
+
// `respell`'s check. A def-MOVING pass (`sinkInitsToFirstUse`, `nearBaseClusters`,
|
|
1413
|
+
// `reindexWalks`) running on a tree a PLACING variation built can move a def below a use exactly
|
|
1414
|
+
// as a shape can. Same
|
|
1257
1415
|
// differential, so a placement neither pass can model is not judged either way, and the throw
|
|
1258
1416
|
// lands inside the thunk — a reported, dropped candidate.
|
|
1259
1417
|
//
|
|
1260
1418
|
// BOTH SIDES' minted locals, because the mover MINTS TOO: `nearBaseClusters` creates the
|
|
1261
1419
|
// cluster base it then places, and `reindexWalks` creates the induction variable, so the
|
|
1262
|
-
// outer
|
|
1420
|
+
// outer variation's name diff alone is empty for a standalone mover and a strict subset for a
|
|
1263
1421
|
// composition — the mover's own stranding of its own local walks straight through. Judging a
|
|
1264
1422
|
// name the BEFORE tree does not carry keeps the differential honest rather than turning it
|
|
1265
1423
|
// absolute: a name absent from `before` is never read there, so that walk passes and only the
|
|
@@ -1270,59 +1428,59 @@ export function enumerateCandidates(
|
|
|
1270
1428
|
}
|
|
1271
1429
|
return after;
|
|
1272
1430
|
};
|
|
1273
|
-
// Every
|
|
1431
|
+
// Every composition below runs over the hoists a demanding row earned, never the whole roster.
|
|
1274
1432
|
const paired = livebases.filter((l) => l.pairings);
|
|
1275
|
-
for (const {
|
|
1276
|
-
respell(
|
|
1277
|
-
respell(
|
|
1278
|
-
|
|
1433
|
+
for (const { variations, hoist, volatiles } of livebases) {
|
|
1434
|
+
respell(variations, hoist);
|
|
1435
|
+
respell([...variations, 'volatile'], volatiles);
|
|
1436
|
+
respellEach(variations, 'volatile', hoist, (r) => volatileSubsetCandidates(r, createdLocals(sfn, r)));
|
|
1279
1437
|
}
|
|
1280
|
-
// The livebase × indexed PAIRINGS
|
|
1281
|
-
//
|
|
1438
|
+
// The livebase × indexed PAIRINGS (see POLICY): row-demanded, and the joint spelling is
|
|
1439
|
+
// reachable from neither variation alone (the
|
|
1282
1440
|
// frame-copy + DMA shape).
|
|
1283
|
-
for (const {
|
|
1284
|
-
respell(
|
|
1441
|
+
for (const { variations, hoist, volatiles } of paired) {
|
|
1442
|
+
respell([...variations, 'indexed'], () => {
|
|
1285
1443
|
const r = hoist();
|
|
1286
1444
|
return r ? survives(r, reindexWalks(r)) : null;
|
|
1287
1445
|
});
|
|
1288
|
-
respell(
|
|
1446
|
+
respell([...variations, 'volatile', 'indexed'], () => {
|
|
1289
1447
|
const r = volatiles();
|
|
1290
1448
|
return r ? survives(r, reindexWalks(r)) : null;
|
|
1291
1449
|
});
|
|
1292
1450
|
}
|
|
1293
1451
|
// The livebase × sinkinit PAIRINGS — the same admission again: row-demanded
|
|
1294
|
-
// (kleod:DecompressDma), and the joint spelling is reachable from neither
|
|
1295
|
-
// bases whose placement moves the row are the ones only this
|
|
1452
|
+
// (kleod:DecompressDma, on kl-eod-decomp's source, before 2026-09-13), and the joint spelling is reachable from neither variation alone. The
|
|
1453
|
+
// bases whose placement moves the row are the ones only this variation's ablation binds, and
|
|
1296
1454
|
// `/sinkinit` alone reads the DEFAULT hoist's head, which does not carry them.
|
|
1297
|
-
for (const {
|
|
1298
|
-
respell(
|
|
1455
|
+
for (const { variations, hoist, volatiles } of paired) {
|
|
1456
|
+
respell([...variations, 'sinkinit'], () => {
|
|
1299
1457
|
const r = hoist();
|
|
1300
1458
|
return r ? survives(r, sinkInitsToFirstUse(r)) : null;
|
|
1301
1459
|
});
|
|
1302
|
-
respell(
|
|
1460
|
+
respell([...variations, 'volatile', 'sinkinit'], () => {
|
|
1303
1461
|
const r = volatiles();
|
|
1304
1462
|
return r ? survives(r, sinkInitsToFirstUse(r)) : null;
|
|
1305
1463
|
});
|
|
1306
1464
|
}
|
|
1307
|
-
// The livebase x homesplit PAIRINGS
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1465
|
+
// The livebase x homesplit PAIRINGS, row-demanded (see POLICY; synthetic:dmapoll): ONE base
|
|
1466
|
+
// kept at the head and a SECOND split per region, which neither variation spells alone because
|
|
1467
|
+
// each applies its own policy to every base it binds: compiled
|
|
1310
1468
|
// against that row's own object, the score reaches 0 only where the two policies land on
|
|
1311
1469
|
// DIFFERENT bases, and every uniform choice is worse. The endpoint figures live in
|
|
1312
1470
|
// l3/homesplit.ts, which is the measurement's one home, along with why this is a PIPE and
|
|
1313
1471
|
// never a merge.
|
|
1314
1472
|
//
|
|
1315
|
-
// WHICH key is withheld is not derivable, so every admitted key is its own candidate,
|
|
1316
|
-
//
|
|
1317
|
-
// `HOMESPLIT_FAN_GATES`' `homesplit-fan-cap` is what bounds the
|
|
1318
|
-
// ADDITIVE, like every
|
|
1319
|
-
// un-hoisted
|
|
1473
|
+
// WHICH key is withheld is not derivable, so every admitted key is its own candidate, with that
|
|
1474
|
+
// key as the variation's SUBJECT — a candidate's variations are its identity, and one name over
|
|
1475
|
+
// two withholds names two programs. `HOMESPLIT_FAN_GATES`' `homesplit-fan-cap` is what bounds the
|
|
1476
|
+
// pairing. ADDITIVE, like every variation here: `/livebase-block`, `/regionbase`, `/scopebase`
|
|
1477
|
+
// and the un-hoisted default all stay in the list, which is what keeps `synthetic:dmaflat` — where the
|
|
1320
1478
|
// composed spelling scores 13 against its own 0 — at MATCH.
|
|
1321
|
-
for (const [i, {
|
|
1479
|
+
for (const [i, { variations, gates, placement }] of paired.entries()) {
|
|
1322
1480
|
const bound = census(gates);
|
|
1323
|
-
// The ROSTER's dedup, which `hoist` applies to every other
|
|
1324
|
-
// for itself: every pairing piped from a shadowed
|
|
1325
|
-
//
|
|
1481
|
+
// The ROSTER's dedup, which `hoist` applies to every other composition and this loop has to
|
|
1482
|
+
// ask for itself: every pairing piped from a shadowed hoist is that hoist's source under
|
|
1483
|
+
// different variations too. Asked over the PAIRED hoists only — the earlier hoist runs the identical pipe and
|
|
1326
1484
|
// emits the identical source. Without it both run and `seen` collapses the pair afterwards,
|
|
1327
1485
|
// having paid a head hoist, region plan, rewrite and emit for each.
|
|
1328
1486
|
if (shadowedByEarlier(paired, i, placement, bound)) {
|
|
@@ -1332,7 +1490,7 @@ export function enumerateCandidates(
|
|
|
1332
1490
|
// rules read the key count and nothing else, so inside the pipe they would cost that whole
|
|
1333
1491
|
// pipe to report a fact this loop already holds.
|
|
1334
1492
|
for (const key of homeSplitWithholds(bound)) {
|
|
1335
|
-
const
|
|
1493
|
+
const homesplitVariations = [...variations, withSubject('homesplit', homeSplitTag(key))];
|
|
1336
1494
|
const homesplit = (): SFn | null => {
|
|
1337
1495
|
const p = splitHomeBases(sfn, {
|
|
1338
1496
|
gates,
|
|
@@ -1346,9 +1504,9 @@ export function enumerateCandidates(
|
|
|
1346
1504
|
const r = homesplit();
|
|
1347
1505
|
return r ? volatilePtrLocals(r, createdLocals(sfn, r)) : null;
|
|
1348
1506
|
};
|
|
1349
|
-
respell(
|
|
1350
|
-
respell(
|
|
1351
|
-
respell(
|
|
1507
|
+
respell(homesplitVariations, homesplit);
|
|
1508
|
+
respell([...homesplitVariations, 'volatile'], homesplitVolatile);
|
|
1509
|
+
respell([...homesplitVariations, 'volatile', 'vol-store'], () => {
|
|
1352
1510
|
const v = homesplitVolatile();
|
|
1353
1511
|
return v ? volStore(v) : null;
|
|
1354
1512
|
});
|
|
@@ -1357,7 +1515,7 @@ export function enumerateCandidates(
|
|
|
1357
1515
|
// `/mulfirst` — product-first commutative sums (l3/mulfirst.ts): IDO/mwcc schedule the
|
|
1358
1516
|
// independent operand's load above the product's mflo/mullw, so def order re-spells a
|
|
1359
1517
|
// product-first source as load-first. Both orders are emitted; the differ referees.
|
|
1360
|
-
respell('
|
|
1518
|
+
respell(['mulfirst'], () => mulFirstSums(sfn));
|
|
1361
1519
|
// `/nearbase` — neighbor absolute addresses derive from one shared base local
|
|
1362
1520
|
// (l3/nearbase.ts): one object's cells anchored as separate pool constants re-spell as
|
|
1363
1521
|
// offsets off its lowest address, within the target's declared derivation reach. Both
|
|
@@ -1380,144 +1538,213 @@ export function enumerateCandidates(
|
|
|
1380
1538
|
const r = near(base);
|
|
1381
1539
|
return r ? survives(r, sinkInitsToFirstUse(r)) : null;
|
|
1382
1540
|
};
|
|
1383
|
-
respell('
|
|
1384
|
-
respell('
|
|
1541
|
+
respell(['nearbase'], () => near(sfn));
|
|
1542
|
+
respell(['nearbase', 'sinkinit'], () => nearSunk(sfn));
|
|
1543
|
+
// `/advance` — a pointer local the source MOVED between two accesses (l3/advance.ts), read off
|
|
1544
|
+
// the `add` the target performed on an address register that already held an address it used.
|
|
1545
|
+
//
|
|
1546
|
+
// THE `/volatile` COMPOSITION IS THE ONE THAT PAYS, and both halves are measured on
|
|
1547
|
+
// `kleod:StreamCmd_SetWindowRegs:agbcc`. Against the INDEXED spelling of the same minted local
|
|
1548
|
+
// the advance buys nothing — agbcc folds `p = p + 1; *p` back into `strh [r3, #2]`, so
|
|
1549
|
+
// `/advance` and `/nearbase` both score 15/23 there — and against the qualified one it is the
|
|
1550
|
+
// match: `/advance/volatile` 0/22, because `volatile` bars that fold and leaves the `add` the
|
|
1551
|
+
// target records.
|
|
1552
|
+
//
|
|
1553
|
+
// WHY THE CONJUNCTION IS A VARIATION AND NOT A COMPILER BEHAVIOR, since the four corners in
|
|
1554
|
+
// test/advance.test.ts's header read as a FUNCTION from the asm: on agbcc a surviving
|
|
1555
|
+
// `adds r3,#2` between two accesses through one address register is produced by exactly one of
|
|
1556
|
+
// the four sources, so volatility and the advance are both determined once the stamp is there.
|
|
1557
|
+
// What the mapping is a function OF is one compiler — agbcc — and the accesses that carry the
|
|
1558
|
+
// stamp: every access agbcc DID fold carries no stamp, so an always-on rewrite gated by a compiler
|
|
1559
|
+
// behavior would never see them, but nothing says the next compiler's fold has the same shape. It stays a variation for
|
|
1560
|
+
// as long as agbcc is the only target that reaches the stamp (the corpus census below), and the
|
|
1561
|
+
// day a second one does, `compilerBehaviors` is where this belongs rather than a variation.
|
|
1562
|
+
//
|
|
1563
|
+
// THE PLAIN `/advance` IS GATED ON THE COMPILER BEHAVIOUR IT IS INERT UNDER — its registry entry's
|
|
1564
|
+
// target gate names a `compilerBehaviors` flag, as `foldsConstAddrOffset` keys `/offmember` above,
|
|
1565
|
+
// but with the POLARITY REVERSED: that flag admits a variation where the compiler folds, this one
|
|
1566
|
+
// withholds one. agbcc FOLDS the advance back (`compilerBehaviors.foldsPointerAdvance`, its four
|
|
1567
|
+
// compiled corners in test/advance.test.ts's header), so the plain spelling emits the same stores
|
|
1568
|
+
// as the indexed one this roster already offers, and it never wins on a row that reaches it:
|
|
1569
|
+
// `/advance` 15/23 against this row's 0/22 match, and it LOSES outright on the other four — `offhi_split`
|
|
1570
|
+
// 33/64 vs 12/61 · `offhi_fused` 31/63 vs 0/58 · `dma_fill_uninit` 76/114 vs 0/103 ·
|
|
1571
|
+
// `volwalk` 5/7 vs 0/7 (2026-09-12).
|
|
1572
|
+
//
|
|
1573
|
+
// THOSE `/advance` HALVES ARE NOT REPRODUCIBLE BY A BARE `bench fan`, because this withhold is
|
|
1574
|
+
// what removes them from every agbcc fan. The falsifying command is the ablation: flip
|
|
1575
|
+
// `foldsPointerAdvance` to `false` in target.ts and re-run `pnpm bench fan <sym>` on the five
|
|
1576
|
+
// rows — the plain `/advance` reappears at the scores above, or these numbers are wrong.
|
|
1577
|
+
//
|
|
1578
|
+
// ABSENT ⇒ FALSY ⇒ THE VARIATION SHIPS, so every compiler whose pair nobody has compiled keeps
|
|
1579
|
+
// exactly the coverage it had: a compiler that does not fold `p = p + 1; *p` back would score
|
|
1580
|
+
// the spelling apart, and that is the case the variation exists for. `/advance/volatile` is not
|
|
1581
|
+
// gated — `volatile` is what bars the fold, and that composition is this row's match.
|
|
1582
|
+
//
|
|
1583
|
+
// NO `/vol-store` COMPOSITION. That variation pins a store whose WHOLE ADDRESS is a device constant,
|
|
1584
|
+
// and this one has just replaced those constants with a local — so on the shape `/advance`
|
|
1585
|
+
// fires for, the pair reaches only whatever OTHER const-addressed device store the function
|
|
1586
|
+
// still has, which no row on the corpus has beside an advanced chain. A pairing with no
|
|
1587
|
+
// inhabitant is candidates without a row behind them.
|
|
1588
|
+
//
|
|
1589
|
+
// AND NO `/nearbase` OR `/livebase` PAIRING, which is a different answer from the one those two
|
|
1590
|
+
// give each other ("each variation's constants are invisible to the other's model"). Here they are
|
|
1591
|
+
// not: all three mint a local for a const address, and `l3/advance.ts` needs its members to
|
|
1592
|
+
// still BE const-addressed accesses (`cellAddress`), which is exactly what a nearbase cluster
|
|
1593
|
+
// or a livebase hoist has already replaced. Running `/advance` on such a tree is a NO-REACH,
|
|
1594
|
+
// not a decline — the same hazard `l3/nearbase.ts` records for committed base-CSE, which
|
|
1595
|
+
// `/advance` inherits: where `structureChecked` has already hoisted the chain's pool word, the
|
|
1596
|
+
// members arrive as a `var` base and this pass enumerates nothing at all.
|
|
1597
|
+
//
|
|
1598
|
+
// AND IT IS MAP-LESS ONLY, which is what caps its reach: with a symbol map the pool word
|
|
1599
|
+
// promotes to `®_WININ` and `l3/address.ts`'s `cellAddress` answers null, so every
|
|
1600
|
+
// `/advance` candidate on this row carries `/raw-globals` (`bench fan
|
|
1601
|
+
// kleod:StreamCmd_SetWindowRegs:agbcc --enumerate`, 2026-09-12: 17 candidates, the one
|
|
1602
|
+
// advanced candidate `unsigned/advance/volatile/raw-globals`). A capability that reads a CONST
|
|
1603
|
+
// address does not survive the symbol-map direction unless `cellAddress` learns the promoted
|
|
1604
|
+
// form; test/advance.test.ts records that at the row it exists for.
|
|
1605
|
+
const advance = (): SFn | null => survives(sfn, advancedBases(sfn));
|
|
1606
|
+
respell(['advance'], advance);
|
|
1607
|
+
respell(['advance', 'volatile'], () => {
|
|
1608
|
+
const a = advance();
|
|
1609
|
+
return a ? volatilePtrLocals(a, createdLocals(sfn, a)) : null;
|
|
1610
|
+
});
|
|
1385
1611
|
// The livebase × nearbase PAIRINGS — the same admission as livebase × indexed above:
|
|
1386
1612
|
// the volatile triple is the row-demanded one, and the joint spelling is reachable from
|
|
1387
|
-
// neither
|
|
1388
|
-
// function — each
|
|
1613
|
+
// neither variation alone (a neighbor-cell object and a multi-index MMIO block in one
|
|
1614
|
+
// function — each variation's constants are invisible to the other's model); the plain
|
|
1389
1615
|
// sibling rides for symmetry with /livebase/indexed.
|
|
1390
|
-
for (const {
|
|
1391
|
-
respell(
|
|
1392
|
-
respell(
|
|
1393
|
-
respell(
|
|
1394
|
-
respell(
|
|
1616
|
+
for (const { variations, hoist, volatiles } of paired) {
|
|
1617
|
+
respell([...variations, 'nearbase'], () => near(hoist()));
|
|
1618
|
+
respell([...variations, 'volatile', 'nearbase'], () => near(volatiles()));
|
|
1619
|
+
respell([...variations, 'nearbase', 'sinkinit'], () => nearSunk(hoist()));
|
|
1620
|
+
respell([...variations, 'volatile', 'nearbase', 'sinkinit'], () => nearSunk(volatiles()));
|
|
1395
1621
|
}
|
|
1396
1622
|
// The livebase × coalesce PAIRINGS — same admission again: the volatile triple is the
|
|
1397
|
-
// row-demanded one, the joint spelling reachable from neither
|
|
1623
|
+
// row-demanded one, the joint spelling reachable from neither variation alone (an MMIO base
|
|
1398
1624
|
// worth homing and a counter shared across both arms of one if, in one function); the
|
|
1399
1625
|
// plain sibling rides for symmetry.
|
|
1400
1626
|
// ARM-DISJOINT merges only: the demanding row's shared counter is that class, and the
|
|
1401
|
-
// span-model merges already ride the plain /coalesce
|
|
1627
|
+
// span-model merges already ride the plain /coalesce variation — pairing them too would
|
|
1402
1628
|
// multiply candidates with no row behind it.
|
|
1403
|
-
for (const {
|
|
1404
|
-
|
|
1405
|
-
|
|
1629
|
+
for (const { variations, hoist, volatiles } of paired) {
|
|
1630
|
+
respellEach(variations, 'coalesce', hoist, armDisjointCandidates);
|
|
1631
|
+
respellEach([...variations, 'volatile'], 'coalesce', volatiles, armDisjointCandidates);
|
|
1406
1632
|
}
|
|
1407
1633
|
// `/parkfirst` — incoming-argument parks lead the entry prefix (l3/parkfirst.ts): the
|
|
1408
1634
|
// park's `mov` lifts to pure SSA aliasing, so its position is unrecoverable and the
|
|
1409
1635
|
// default order is emission's. Both orders are emitted; the differ referees.
|
|
1410
|
-
respell('
|
|
1636
|
+
respell(['parkfirst'], () => parkParamsFirst(sfn));
|
|
1411
1637
|
// `/sinkinit` — each leading pointer-base init sinks to its own first use (l3/sinkinit.ts):
|
|
1412
1638
|
// the base hoist places every init at the head of the body, which keeps the base live across
|
|
1413
1639
|
// everything above its first use and can cost a callee-saved register the original avoided.
|
|
1414
1640
|
// Which placement the source used is not derivable from the asm, so both are emitted and the
|
|
1415
1641
|
// differ referees.
|
|
1416
|
-
respell('
|
|
1417
|
-
// the register-copy
|
|
1418
|
-
// the dead value var; tail assign-back into a fresh var — the tail
|
|
1642
|
+
respell(['sinkinit'], () => sinkInitsToFirstUse(sfn));
|
|
1643
|
+
// the register-copy variation (l3/regspell.ts): 0–3 results (base; tail assign-back reusing
|
|
1644
|
+
// the dead value var; tail assign-back into a fresh var — the tail decision is allocator-
|
|
1419
1645
|
// ambiguous, so both are ranked).
|
|
1420
1646
|
//
|
|
1421
|
-
//
|
|
1647
|
+
// NAMED BY THE TAIL THE RESULT CARRIES, NEVER BY ITS INDEX. The reuse tail exists only
|
|
1422
1648
|
// where R1 fired, so the list is 1, 2 or 3 long and the fresh tail sits at no fixed position;
|
|
1423
|
-
// an index-keyed
|
|
1424
|
-
// — the dead-var-reuse name on the one spelling that has no dead var — and `
|
|
1649
|
+
// an index-keyed table names the fresh spelling `/regcopy-ret` on every R1-less function
|
|
1650
|
+
// — the dead-var-reuse name on the one spelling that has no dead var — and `winnerVariations` is
|
|
1425
1651
|
// what every census in this repo counts, `bench diff` included. The exhaustive record is the
|
|
1426
1652
|
// pin: a new tail kind is a type error here rather than a silent `/regcopy-3`.
|
|
1427
1653
|
// `cli/test/matching/regspell-candidate.test.ts` holds the correspondence.
|
|
1428
|
-
const
|
|
1429
|
-
none: '
|
|
1430
|
-
reuse: '
|
|
1431
|
-
fresh: '
|
|
1654
|
+
const REGCOPY_VARIATION: Record<RegcopyTail, Variation> = {
|
|
1655
|
+
none: 'regcopy',
|
|
1656
|
+
reuse: withSubject('regcopy', 'ret'),
|
|
1657
|
+
fresh: withSubject('regcopy', 'ret-fresh'),
|
|
1432
1658
|
};
|
|
1433
|
-
registerishSpellings(sfn).forEach((alt) => respell(
|
|
1434
|
-
return {
|
|
1659
|
+
registerishSpellings(sfn).forEach((alt) => respell([REGCOPY_VARIATION[alt.tail]], () => alt.sfn));
|
|
1660
|
+
return { sources };
|
|
1435
1661
|
};
|
|
1436
|
-
// The SYMBOL-MAP spelling is itself a ranked
|
|
1662
|
+
// The SYMBOL-MAP spelling is itself a ranked VARIATION on the same footing as signedness/branch
|
|
1437
1663
|
// sense: naming a global changes agbcc's codegen (the eager-load effect), and which side
|
|
1438
1664
|
// byte-wins is genuinely per-function — the dogfood's landed matches split between extern
|
|
1439
1665
|
// spellings and raw-address macros. So when a map is present the raw-global spelling is ALSO
|
|
1440
1666
|
// enumerated ('/raw-globals') and the differ referees; the dedup below collapses the pair
|
|
1441
1667
|
// wherever the map changed nothing, so this never scores worse than either side alone.
|
|
1442
1668
|
//
|
|
1443
|
-
// Does the `/raw-globals`
|
|
1444
|
-
// map does not answer for — the only ones a map-less structuring ever sees — because
|
|
1445
|
-
// exactly the
|
|
1446
|
-
//
|
|
1447
|
-
// too: this only ADDS an
|
|
1448
|
-
|
|
1669
|
+
// Does the `/raw-globals` setting have a RANK of its own to spell? Read off the DERIVED shapes the
|
|
1670
|
+
// map does not answer for — the only ones a map-less structuring ever sees — because those are
|
|
1671
|
+
// exactly the shapes `/flat-rank`'s decline just below is about. A superset of what the raw
|
|
1672
|
+
// setting's own lift derives (it is read off the shared lift's), for the reason the
|
|
1673
|
+
// structure-variation gate above is one too: this only ADDS an alternative, and where it changes
|
|
1674
|
+
// nothing the tree dedup collapses it.
|
|
1675
|
+
const rawDerivesRank = [...sharedLiftShapes].some(
|
|
1449
1676
|
([n, i]) => byName?.get(n) === undefined && (arrayInnerExtents(i)?.length ?? 0) > 0,
|
|
1450
1677
|
);
|
|
1451
|
-
const
|
|
1678
|
+
const symbolSettings: { variations: readonly Variation[]; symbols?: typeof opts.symbols }[] = opts.symbols
|
|
1452
1679
|
? [
|
|
1453
|
-
{
|
|
1454
|
-
{
|
|
1680
|
+
{ variations: [], symbols: opts.symbols },
|
|
1681
|
+
{ variations: ['raw-globals'], symbols: undefined },
|
|
1455
1682
|
]
|
|
1456
|
-
: [{
|
|
1457
|
-
for (const [
|
|
1458
|
-
const
|
|
1683
|
+
: [{ variations: [] }];
|
|
1684
|
+
for (const [symbolIndex, symbolSetting] of symbolSettings.entries()) {
|
|
1685
|
+
const symbolSettingOpts = symbolSetting.symbols ? baseOpts : { ...baseOpts, symbols: undefined };
|
|
1459
1686
|
// `/no-bitfield` names a spelling the MAP makes available, so it has no inhabitant on the
|
|
1460
|
-
//
|
|
1461
|
-
// when `symbols` is absent, so both
|
|
1462
|
-
// declines to build the second
|
|
1687
|
+
// symbol-map setting that structures without one: structure() normalizes `spellBitfieldMembers`
|
|
1688
|
+
// to false when `symbols` is absent, so both settings structure the identical tree whatever
|
|
1689
|
+
// reads it. This declines to build the second one rather than leaving the tree skip to collapse it, which is
|
|
1463
1690
|
// worth 512 of LoadBGTilemapData's 1536 structurings under docs/ranked-repro.md's flags.
|
|
1464
1691
|
// Declining is not pruning — same posture as the signedness decline below, and the same
|
|
1465
1692
|
// candidate list; bitfield-members.test.ts pins the normalization the decline rests on.
|
|
1466
1693
|
// …and `/no-ptr-elem` names a spelling only the MAP makes available, for the same reason:
|
|
1467
|
-
// structure() normalizes `spellPtrMemberElements` to false without `symbols`, so both
|
|
1468
|
-
// structure the identical tree on the raw
|
|
1694
|
+
// structure() normalizes `spellPtrMemberElements` to false without `symbols`, so both settings
|
|
1695
|
+
// structure the identical tree on the raw setting. `/flat-rank` IS NOT a third such spelling,
|
|
1469
1696
|
// and the difference is why its decline is asked of the EVIDENCE and not of the map: the
|
|
1470
1697
|
// declared subscripts come off a render context structure() builds from the UNION of the map
|
|
1471
|
-
// and the shapes this function's own strides evidence (raise/globalshape.ts), so the raw
|
|
1698
|
+
// and the shapes this function's own strides evidence (raise/globalshape.ts), so the raw setting
|
|
1472
1699
|
// derives a rank of its own. The decline therefore stands only where no derived shape carries
|
|
1473
|
-
// a rank for that
|
|
1474
|
-
// identical tree.
|
|
1475
|
-
const
|
|
1476
|
-
?
|
|
1477
|
-
:
|
|
1478
|
-
const treeOwnedFold = treeOwnedIn(
|
|
1479
|
-
// The signedness
|
|
1700
|
+
// a rank for that setting to spell — the condition under which both settings really do structure
|
|
1701
|
+
// the identical tree.
|
|
1702
|
+
const usableStructureSettings = symbolSetting.symbols
|
|
1703
|
+
? structureSettings
|
|
1704
|
+
: structureSettings.filter((s) => s.bitfields && s.ptrElems && (s.declRank || rawDerivesRank));
|
|
1705
|
+
const treeOwnedFold = treeOwnedIn(symbolSetting.symbols);
|
|
1706
|
+
// The signedness variation DECLINES where the pin has nothing to pin. `pinScalarParams` writes only
|
|
1480
1707
|
// over an entry param still `unknown`/`int` that is not one of the recovered pointers/
|
|
1481
1708
|
// aggregates `ptrIdx` excludes; where no param is left, the second pass re-lifts, re-raises and
|
|
1482
1709
|
// re-structures a function BYTE-IDENTICAL to the first, reaching a tree the first pass already
|
|
1483
1710
|
// spelled. Declining is not pruning: the candidate list is the same list, reached without
|
|
1484
1711
|
// building the duplicates. What the decline saves is therefore invisible in the candidates —
|
|
1485
|
-
//
|
|
1712
|
+
// signedness-variation.test.ts counts LIFTS, the one reading of the enumeration that it moves.
|
|
1486
1713
|
//
|
|
1487
|
-
// Read off the pin's OWN call, per symbol
|
|
1488
|
-
// map and answers for itself, so no lift is governed by a fact measured on a different one.
|
|
1714
|
+
// Read off the pin's OWN call, per symbol-map setting — the `/raw-globals` setting lifts without
|
|
1715
|
+
// the map and answers for itself, so no lift is governed by a fact measured on a different one.
|
|
1489
1716
|
let pinnable = false;
|
|
1490
|
-
for (const cand of
|
|
1717
|
+
for (const cand of SIGNEDNESS) {
|
|
1491
1718
|
if (cand.signed && !pinnable) {
|
|
1492
1719
|
break;
|
|
1493
1720
|
}
|
|
1494
|
-
const base = frontend.lift(name, asm, target, prototypes, opts.asmData,
|
|
1721
|
+
const base = frontend.lift(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
|
|
1495
1722
|
// `/setup-args` — pass a prototype-less callee only what the CALLING BLOCK set up; which of
|
|
1496
1723
|
// the two readings the source spelled is genuinely ambiguous, and frontend/ssa.ts
|
|
1497
1724
|
// narrowToSetupArgs carries the argument for why the differ is what settles it.
|
|
1498
1725
|
//
|
|
1499
|
-
// A LIFT
|
|
1500
|
-
//
|
|
1501
|
-
// changes the IR every
|
|
1726
|
+
// A LIFT VARIATION, in the same position in a candidate's variations as the signedness pin and
|
|
1727
|
+
// the symbol-map setting — not a respell variation under the POLICY note below. Dropping an
|
|
1728
|
+
// argument changes the IR every structure variation then reads: the value the argument carried loses a
|
|
1502
1729
|
// consumer, so what materializes changes with it, and a row whose callee arities are GUESSED
|
|
1503
1730
|
// can need the narrowed lift to reach a spelling neither side reaches alone —
|
|
1504
1731
|
// `kleod:ReadKeyInput` did, until its manifest declared those arities to asmlift as its own
|
|
1505
|
-
// `ctx` already declared them to m2c; it now matches on the
|
|
1506
|
-
// `unsigned/derived-home`, enumerating no
|
|
1507
|
-
// Only
|
|
1508
|
-
// downstream emits the
|
|
1509
|
-
// arity records nothing and enumerates no
|
|
1510
|
-
//
|
|
1732
|
+
// `ctx` already declared them to m2c; it now matches on the default lift, at
|
|
1733
|
+
// `unsigned/derived-home`, enumerating no alternative lift at all.
|
|
1734
|
+
// Only sources the narrowing actually changed reach a compiler: one that changes nothing
|
|
1735
|
+
// downstream emits the default source and the dedup collapses it, and a DECLARED
|
|
1736
|
+
// arity records nothing and enumerates no alternative lift at all. What survives the dedup is
|
|
1737
|
+
// the variation's real price, and it is not free: it added 1201 distinct candidates, all of
|
|
1511
1738
|
// them in the 13 rows whose narrowing changes anything downstream. Quoted as a DELTA with no
|
|
1512
1739
|
// denominator, because the agbcc row count it was taken over has moved since — measured
|
|
1513
1740
|
// before those six kleod rows declared their callee arities,
|
|
1514
|
-
// and declaring one takes its row out of
|
|
1741
|
+
// and declaring one takes its row out of that count.
|
|
1515
1742
|
//
|
|
1516
1743
|
// `/connective` — spell a same-scrutinee const-test chain as `x == 0 || x == 2` rather than
|
|
1517
1744
|
// leaving it to switch recovery. They are mutually exclusive within one raise
|
|
1518
1745
|
// (raise/shortcircuit.ts's REFUSALS note has the mechanism: a folded `logic_or` is not the
|
|
1519
1746
|
// `icmp` switch-recover.ts requires), so no predicate settles it — the differ does.
|
|
1520
|
-
// Enumerated only where THIS
|
|
1747
|
+
// Enumerated only where THIS LIFT SETTING's lift reports the PAIRWISE refusal, which a handful of
|
|
1521
1748
|
// corpus rows do.
|
|
1522
1749
|
//
|
|
1523
1750
|
// WHAT IT IS *NOT* FOR: the shared-arm spelling `switch (x) { case 0: case 2: … }`. That is
|
|
@@ -1527,71 +1754,75 @@ export function enumerateCandidates(
|
|
|
1527
1754
|
// IDO 64 bytes each and one md5). A second group parts them: agbcc 20 against 16, the switch
|
|
1528
1755
|
// building a balanced `bgt` dispatch where the chain tests sequentially; IDO 80 bytes each,
|
|
1529
1756
|
// different bytes. So on a recovered MULTI-GROUP switch the connective is a genuine second
|
|
1530
|
-
// spelling, and this
|
|
1757
|
+
// spelling, and this variation is the only thing that reaches it.
|
|
1531
1758
|
//
|
|
1532
|
-
// WHERE IT IS WORTH 0 POINTS IT IS STILL NOT WORTH NOTHING, and the two
|
|
1759
|
+
// WHERE IT IS WORTH 0 POINTS IT IS STILL NOT WORTH NOTHING, and the two kinds of row differ.
|
|
1533
1760
|
// On `kleod:ProcessInputAndUpdateEntities` the grouping alone reaches the same score the
|
|
1534
|
-
//
|
|
1535
|
-
// site `gUnk_030034C0 == 0 || gUnk_030034C0 == 2` — so deleting the
|
|
1536
|
-
// SOURCE. It moves the SCORE on the other
|
|
1761
|
+
// variation reaches with it, yet the published winner there carries `/connective` and spells its
|
|
1762
|
+
// site `gUnk_030034C0 == 0 || gUnk_030034C0 == 2` — so deleting the variation moves that row's
|
|
1763
|
+
// SOURCE. It moves the SCORE on the other kind of row, where switch recovery declined
|
|
1537
1764
|
// ENTIRELY and the tree came out as nested `if`s: `kleod:CountCollectedGems` and
|
|
1538
1765
|
// `kleod:CheckWorldCompletion`, neither with a `switch` at all. Telling the two apart needs
|
|
1539
1766
|
// an L3 fact (did recovery produce a grouped arm?) at a raise-level hook, which is a level
|
|
1540
1767
|
// inversion; the fan is the price instead. NO ABLATION PAIR IS QUOTED HERE: the artifact
|
|
1541
|
-
// carries only the with-
|
|
1768
|
+
// carries only the with-variation score, so half a refreshed pair would manufacture a delta
|
|
1542
1769
|
// across two bases — re-run the ablation to price it.
|
|
1543
1770
|
//
|
|
1544
|
-
// It
|
|
1771
|
+
// It is a LIFT variation because the raise mutates in place: a second raise policy needs
|
|
1545
1772
|
// its own copy of the lifted fn, exactly as `/setup-args` needs one to narrow. Crossed with
|
|
1546
1773
|
// `/setup-args` rather than nested under it — dropping a call argument and choosing this
|
|
1547
1774
|
// shape are independent, and the four combinations dedup down to whatever the trees differ on.
|
|
1548
|
-
const
|
|
1775
|
+
const connectiveSettings: { variations: readonly Variation[]; connective: boolean }[] = treeOwnedFold
|
|
1549
1776
|
? [
|
|
1550
|
-
{
|
|
1551
|
-
{
|
|
1777
|
+
{ variations: [], connective: false },
|
|
1778
|
+
{ variations: ['connective'], connective: true },
|
|
1552
1779
|
]
|
|
1553
|
-
: [{
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1780
|
+
: [{ variations: [], connective: false }];
|
|
1781
|
+
const narrowSettings: { variations: readonly Variation[]; narrow: boolean }[] = hasSetupArgsNarrowing(base)
|
|
1782
|
+
? [
|
|
1783
|
+
{ variations: [], narrow: false },
|
|
1784
|
+
{ variations: ['setup-args'], narrow: true },
|
|
1785
|
+
]
|
|
1786
|
+
: [{ variations: [], narrow: false }];
|
|
1787
|
+
const liftSettings = narrowSettings.flatMap((l) =>
|
|
1788
|
+
connectiveSettings.map((c) => ({ ...l, ...c, variations: [...l.variations, ...c.variations] })),
|
|
1789
|
+
);
|
|
1790
|
+
for (const liftSetting of liftSettings) {
|
|
1563
1791
|
let fn: Fn;
|
|
1564
1792
|
let inferredSymbols = new Map<string, SymbolInfo>();
|
|
1565
1793
|
let orderLicensed: ReadonlySet<string> = new Set<string>();
|
|
1566
1794
|
try {
|
|
1567
|
-
// A
|
|
1568
|
-
// flags here would leave a fourth
|
|
1569
|
-
fn =
|
|
1570
|
-
|
|
1571
|
-
|
|
1795
|
+
// A SETTING THAT NAMES ANY VARIATION IS WHAT NEEDS ITS OWN COPY, the catch below's spelling: naming the
|
|
1796
|
+
// flags here would leave a fourth lift variation sharing the default's already-mutated `base`.
|
|
1797
|
+
fn =
|
|
1798
|
+
liftSetting.variations.length === 0
|
|
1799
|
+
? base
|
|
1800
|
+
: frontend.lift(name, asm, target, prototypes, opts.asmData, symbolSetting.symbols);
|
|
1801
|
+
if (liftSetting.narrow && !narrowToSetupArgs(fn)) {
|
|
1802
|
+
continue; // nothing to cut after all — the default lift's own candidates already cover it
|
|
1572
1803
|
}
|
|
1573
1804
|
verify(fn);
|
|
1574
|
-
// This
|
|
1575
|
-
// numeric pool words to `gaddr`, so the `/raw-globals`
|
|
1805
|
+
// This lift's OWN array-shape evidence, off its own lifted fn (a symbol map promotes
|
|
1806
|
+
// numeric pool words to `gaddr`, so the `/raw-globals` setting genuinely answers differently).
|
|
1576
1807
|
//
|
|
1577
1808
|
// NEVER A NAME THE PROJECT MAP KNOWS, and the filter is here rather than left to
|
|
1578
|
-
// structure()'s map-first lookup because THE `/raw-globals`
|
|
1579
|
-
// AND DECLARES WITH ONE. `declSymbols` is
|
|
1580
|
-
// name it knows), so on an asm whose pool NAMES its globals the raw
|
|
1809
|
+
// structure()'s map-first lookup because THE `/raw-globals` SETTING STRUCTURES WITH NO MAP
|
|
1810
|
+
// AND DECLARES WITH ONE. `declSymbols` is derived from the shared lift and map-last (the map
|
|
1811
|
+
// wins every name it knows), so on an asm whose pool NAMES its globals the raw setting could spell a
|
|
1581
1812
|
// subscript off THIS function's strides — `gFoo[i][j]`, inner extent 2 — while the
|
|
1582
1813
|
// declaration beside it came from the map — `extern u32 gFoo[][8];` — and the emitted C
|
|
1583
1814
|
// would stride by 8. Compiling, and the wrong address. One name the map describes is
|
|
1584
|
-
// one name this derivation does not claim, on either
|
|
1815
|
+
// one name this derivation does not claim, on either setting.
|
|
1585
1816
|
//
|
|
1586
1817
|
// AND NEVER A SHAPE THE DECLARATION BLOCK WILL NOT CARRY, which is the same hazard one
|
|
1587
|
-
// step further out. `declSymbols` is built ONCE, off the
|
|
1588
|
-
// per
|
|
1589
|
-
// does NOT know, the map-precedence delete above says nothing and the raw
|
|
1818
|
+
// step further out. `declSymbols` is built ONCE, off the shared lift; this map is built
|
|
1819
|
+
// per lift, off that lift's own. Where the two lifts disagree about a name the map
|
|
1820
|
+
// does NOT know, the map-precedence delete above says nothing and the raw setting would
|
|
1590
1821
|
// again spell from one shape and declare from another. So the test is not "the map
|
|
1591
1822
|
// knows this name" but "whatever will be DECLARED for this name says the same thing" —
|
|
1592
1823
|
// a name the two answer differently keeps the cast form, which needs no declaration.
|
|
1593
1824
|
inferredSymbols = inferGlobalArrays(fn, target);
|
|
1594
|
-
// The ORDER half, off the same
|
|
1825
|
+
// The ORDER half, off the same lift. NO map-precedence filter, and the
|
|
1595
1826
|
// asymmetry is the point: a shape is a DECLARATION, so a name the map describes must
|
|
1596
1827
|
// not be spelled from this function's strides — a licence declares nothing, and the
|
|
1597
1828
|
// spelling it enables keeps the cast (`(T *)&gSym`), which is byte-correct under any
|
|
@@ -1618,205 +1849,311 @@ export function enumerateCandidates(
|
|
|
1618
1849
|
},
|
|
1619
1850
|
},
|
|
1620
1851
|
prototypes[name],
|
|
1621
|
-
{ shortCircuit: { foldTreeOwned:
|
|
1852
|
+
{ shortCircuit: { foldTreeOwned: liftSetting.connective } },
|
|
1622
1853
|
);
|
|
1623
1854
|
} catch (e) {
|
|
1624
|
-
// THE
|
|
1625
|
-
//
|
|
1626
|
-
// when a fourth is added — the same reason the
|
|
1855
|
+
// THE DEFAULT CARRIES NO VARIATION, by construction: every lift variation appends one, so
|
|
1856
|
+
// `variations.length === 0` is the only spelling of "no lift variation is on" that stays correct
|
|
1857
|
+
// when a fourth is added — the same reason the structure half below reads its table
|
|
1627
1858
|
// instead of naming its flags.
|
|
1628
|
-
if (
|
|
1629
|
-
throw e; // the
|
|
1859
|
+
if (liftSetting.variations.length === 0) {
|
|
1860
|
+
throw e; // the default lift keeps its behavior: a raising failure aborts the row
|
|
1630
1861
|
}
|
|
1631
|
-
// A dropped
|
|
1632
|
-
|
|
1862
|
+
// A dropped variation, never an aborted enumeration — the same posture as `respell`.
|
|
1863
|
+
reportThrow(liftSetting.variations, e);
|
|
1633
1864
|
continue;
|
|
1634
1865
|
}
|
|
1635
|
-
//
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1640
|
-
//
|
|
1641
|
-
//
|
|
1642
|
-
//
|
|
1643
|
-
//
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1866
|
+
// THE SHARED-TAIL VARIATIONS: the same raised fn, structured again with `followEarlyReturns`,
|
|
1867
|
+
// in two passes after the default one.
|
|
1868
|
+
// - `/shared-ret` is the follow ALONE, on the fn as raised. Some divergent `if` shares a
|
|
1869
|
+
// `ret` the compiler left in place (`synthetic:gcseinner`).
|
|
1870
|
+
// - `/shared-tail` is the follow after `sinkStoreTails` has rewritten the fn in place, which
|
|
1871
|
+
// is safe because `structure()` never mutates `fn`, so the earlier passes are done with it.
|
|
1872
|
+
// It is enumerated only where the sink changed something AND some divergent `if` of the
|
|
1873
|
+
// SUNK fn shares a `ret`. That is the sink's price gate: without it, a tail copied into
|
|
1874
|
+
// arms no `if` shares adds a candidate spelling it in each — 204 synthetic candidates on 8
|
|
1875
|
+
// rows (`gcsedup`, `gcseinnerdup`, `armexpr`, `mergeu16`, `ladder4`, `ladder5`,
|
|
1876
|
+
// `ladidx2`, `revlad5s`), every one of them MATCH today.
|
|
1877
|
+
// They are lift variations rather than structure variations because the sink is an IR
|
|
1878
|
+
// rewrite, and they run here rather than in pipeline.ts's spine because that costs no second
|
|
1879
|
+
// lift. Neither is the default: the same IR comes from both sources (raise/tailsink.ts), and
|
|
1880
|
+
// the follow alone as a default costs `synthetic:sw_fallguard:ido7.1` 13/19 → 19/23 and
|
|
1881
|
+
// `synthetic:gcseflat:agbcc` 19/53 → 22/54.
|
|
1882
|
+
//
|
|
1883
|
+
// TWO BITS, NOT ONE, because the sink can DELETE the follow. A store tail that is itself the
|
|
1884
|
+
// `ret` both sides of an `if` reach is copied into its `br` sources, and then no shared `ret`
|
|
1885
|
+
// is left. `synthetic:gcsejoin:agbcc` is ordinary loop-free C of that shape, and it scored
|
|
1886
|
+
// 7/37 while the follow was tried only behind the sink. No per-tail predicate on this IR picks
|
|
1887
|
+
// between the two: refusing to sink a tail that is already a follow costs
|
|
1888
|
+
// `synthetic:gcseflat:agbcc` 0/49 → 19/53 and `synthetic:gcsearms6:agbcc` 0/84 → 23/87, and
|
|
1889
|
+
// those two need a tail that is BOTH. So the follow-alone candidate is enumerated wherever
|
|
1890
|
+
// the fn as raised has a follow, sunk or not. Its price over a single bundled follow-and-sink
|
|
1891
|
+
// pass is +76 synthetic candidates, on those two rows, and +0 real: where the sink does not
|
|
1892
|
+
// fire only the `/shared-ret` pass runs, and on every real row where it fires the fn as
|
|
1893
|
+
// raised has no follow.
|
|
1894
|
+
//
|
|
1895
|
+
// EACH BIT IS PER LIFT, not per site: the `/shared-tail` pass sinks every store tail
|
|
1896
|
+
// and follows every divergent `if` at once, so a function with two sites gets the both-on
|
|
1897
|
+
// candidate only. Counted over both tiers (`ProcessInputAndUpdateEntities` excepted), three
|
|
1898
|
+
// functions carry two of something: `synthetic:maskchain:agbcc` two follow sites,
|
|
1899
|
+
// `synthetic:gcsearms6:agbcc` two sunk tails — both MATCH — and
|
|
1900
|
+
// `pokeemerald:SetMauvilleOldManLanguage:agbcc` two sunk tails after which no `if` shares a
|
|
1901
|
+
// `ret`, so the price gate above refuses and its fan is unchanged. Every other function
|
|
1902
|
+
// carries at most one of each.
|
|
1903
|
+
//
|
|
1904
|
+
// `droppedDefault` is the DEFAULT pass's drops. Each shared-tail pass reads it and keeps its
|
|
1905
|
+
// own drops in a copy, so one pass's structuring failure never removes the other's candidate.
|
|
1906
|
+
const droppedDefault = new Set<string>();
|
|
1907
|
+
for (const pass of ['default', 'follow', 'sink'] as const) {
|
|
1908
|
+
if (pass === 'follow' && !hasDivergentSharedRet(fn)) {
|
|
1654
1909
|
continue;
|
|
1655
1910
|
}
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
spellDeclaredSubscripts: s.declRank,
|
|
1671
|
-
...STRUCTURING_AXES.reduce((acc, ax) => ({ ...acc, ...ax.options(s[ax.flag]) }), {}),
|
|
1672
|
-
});
|
|
1673
|
-
} catch (e) {
|
|
1674
|
-
if (lv.suffix === '' && isBaseAxisPoint(s)) {
|
|
1675
|
-
throw e; // the base lift's base axes keep their behavior: a failure aborts the row
|
|
1911
|
+
if (pass === 'sink') {
|
|
1912
|
+
let sunk: boolean;
|
|
1913
|
+
try {
|
|
1914
|
+
sunk = sinkStoreTails(fn);
|
|
1915
|
+
if (sunk) {
|
|
1916
|
+
verify(fn);
|
|
1917
|
+
}
|
|
1918
|
+
} catch (e) {
|
|
1919
|
+
reportThrow([...liftSetting.variations, 'shared-tail'], e);
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
// Unsunk, this fn is the `/shared-ret` pass's again.
|
|
1923
|
+
if (!sunk || !hasDivergentSharedRet(fn)) {
|
|
1924
|
+
break;
|
|
1676
1925
|
}
|
|
1677
|
-
// Recorded for EVERY dropped variant: a candidate with more axes on looks its siblings
|
|
1678
|
-
// up by stripping one axis at a time, and the stripped key can itself carry the other.
|
|
1679
|
-
droppedPrimary.add(s.suffix);
|
|
1680
|
-
// an anchored variant that fails structuring or its contracts is a dropped lever, never
|
|
1681
|
-
// an aborted enumeration — same rule as respell below
|
|
1682
|
-
opts.onLeverError?.(name + lv.suffix + s.suffix, firstLine(e));
|
|
1683
|
-
continue;
|
|
1684
1926
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
//
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1927
|
+
const alternative = pass !== 'default';
|
|
1928
|
+
const dropped = alternative ? new Set(droppedDefault) : droppedDefault;
|
|
1929
|
+
const liftVariations: readonly Variation[] =
|
|
1930
|
+
pass === 'follow'
|
|
1931
|
+
? [...liftSetting.variations, 'shared-ret']
|
|
1932
|
+
: pass === 'sink'
|
|
1933
|
+
? [...liftSetting.variations, 'shared-tail']
|
|
1934
|
+
: liftSetting.variations;
|
|
1935
|
+
// the per-lift gates, on THIS lift's fn — see the table doc
|
|
1936
|
+
const offForThisLift = STRUCTURE_VARIATIONS.filter(
|
|
1937
|
+
(variation) => variation.perLiftGate !== undefined && !variation.perLiftGate(fn),
|
|
1938
|
+
);
|
|
1939
|
+
const settingsForLift = usableStructureSettings.filter((s) =>
|
|
1940
|
+
offForThisLift.every((variation) => !s[variation.flag]),
|
|
1941
|
+
);
|
|
1942
|
+
// `/merge-names` combinations whose un-merged sibling was DROPPED. `structure()` already
|
|
1943
|
+
// refuses to let the variation unlock a function the default declines, but it can only see its own
|
|
1944
|
+
// refusals — a boundary contract fails out here, in `structureChecked`. Without this a
|
|
1945
|
+
// `/reread-globals/merge-names` candidate could ship where plain `/reread-globals` did not,
|
|
1946
|
+
// which is the same trade one level up. `structureSettings` puts each `mergeNames:false` sibling
|
|
1947
|
+
// first, so the entry is always recorded before its merged sibling is reached.
|
|
1700
1948
|
//
|
|
1701
|
-
// The
|
|
1702
|
-
// `
|
|
1703
|
-
//
|
|
1704
|
-
//
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1949
|
+
// The shared-tail passes read the DEFAULT pass's set as well: neither `X/shared-ret` nor
|
|
1950
|
+
// `X/shared-tail` ever ships where `X` was dropped. The follow and the sink each give the
|
|
1951
|
+
// structurer a shape it can accept where the default declined — sound, but the same
|
|
1952
|
+
// trade one level up again.
|
|
1953
|
+
for (const s of settingsForLift) {
|
|
1954
|
+
const key = s.variations.join('/');
|
|
1955
|
+
if (alternative && droppedDefault.has(key)) {
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
if (
|
|
1959
|
+
STRUCTURE_VARIATIONS.some(
|
|
1960
|
+
(variation) =>
|
|
1961
|
+
variation.strip &&
|
|
1962
|
+
s[variation.flag] &&
|
|
1963
|
+
dropped.has(s.variations.filter((v) => v !== variation.name).join('/')),
|
|
1964
|
+
)
|
|
1965
|
+
) {
|
|
1966
|
+
// A SKIPPED setting is recorded exactly like a dropped one, or the closure would not be
|
|
1967
|
+
// transitive: with plain X dropped and X/inplace skipped-but-unrecorded,
|
|
1968
|
+
// X/inplace/merge-names would find neither stripped key and run — shipping a
|
|
1969
|
+
// candidate carrying two variations where its ancestor failed the boundary contracts.
|
|
1970
|
+
dropped.add(key);
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
// structure() reads `fn` and produces a fresh SFn (it does not mutate `fn`), so both branch
|
|
1974
|
+
// senses structure the same recovered function without re-lifting.
|
|
1975
|
+
let sfn: SFn;
|
|
1728
1976
|
try {
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
// A backend refusal on this REWRITTEN tree is not a refusal of the row's own
|
|
1748
|
-
// spelling, so it never becomes the row's stated cause: `FanResult.emit` is dropped
|
|
1749
|
-
// here and only the primary call above records one.
|
|
1750
|
-
//
|
|
1751
|
-
// It is reported instead through `onLeverError` under `pf.suffix`, which is what
|
|
1752
|
-
// `fanOut`'s second argument is for: a primary emit refusal does not THROW —
|
|
1753
|
-
// `fanOut` returns it — so the `catch` below never sees it, and under the bare
|
|
1754
|
-
// function name it would read as a refusal of the primary spelling while the lever's
|
|
1755
|
-
// whole half of the fan was deleted.
|
|
1756
|
-
const fanned = fanOut(made, pf.suffix).spellings;
|
|
1757
|
-
for (const sp of fanned) {
|
|
1758
|
-
spellings.push({ ...sp, suffix: `${pf.suffix}${sp.suffix}` });
|
|
1759
|
-
}
|
|
1977
|
+
sfn = structureChecked(fn, {
|
|
1978
|
+
...symbolSettingOpts,
|
|
1979
|
+
...(inferredSymbols.size ? { inferredSymbols } : {}),
|
|
1980
|
+
...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
|
|
1981
|
+
preserveDivergentBranchSense: s.sense,
|
|
1982
|
+
negateJoinedBranchSense: s.join ? !defSense : defSense,
|
|
1983
|
+
...(s.flipSites ? { branchSenseFlipSites: s.flipSites } : {}),
|
|
1984
|
+
anchorConstCopies: s.anchor,
|
|
1985
|
+
anchorLoopEntryConsts: s.entry,
|
|
1986
|
+
spellBitfieldMembers: s.bitfields,
|
|
1987
|
+
spellPtrMemberElements: s.ptrElems,
|
|
1988
|
+
spellDeclaredSubscripts: s.declRank,
|
|
1989
|
+
...STRUCTURE_VARIATIONS.reduce(
|
|
1990
|
+
(acc, variation) => ({ ...acc, ...variation.options(s[variation.flag]) }),
|
|
1991
|
+
{},
|
|
1992
|
+
),
|
|
1993
|
+
...(alternative ? { followEarlyReturns: true } : {}),
|
|
1994
|
+
});
|
|
1760
1995
|
} catch (e) {
|
|
1761
|
-
|
|
1996
|
+
if (liftVariations.length === 0 && isDefaultSetting(s)) {
|
|
1997
|
+
throw e; // the default lift's default setting keeps its behavior: a failure aborts the row
|
|
1998
|
+
}
|
|
1999
|
+
// Recorded for EVERY dropped setting: a candidate with more variations on looks its siblings
|
|
2000
|
+
// up by stripping one variation at a time, and the stripped key can itself carry the other.
|
|
2001
|
+
dropped.add(key);
|
|
2002
|
+
// an anchored setting that fails structuring or its contracts is a dropped variation, never
|
|
2003
|
+
// an aborted enumeration — same rule as respell below
|
|
2004
|
+
reportThrow([...liftVariations, ...s.variations], e);
|
|
2005
|
+
continue;
|
|
1762
2006
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
//
|
|
1767
|
-
//
|
|
1768
|
-
//
|
|
1769
|
-
//
|
|
1770
|
-
//
|
|
1771
|
-
//
|
|
1772
|
-
// THE PUBLISHED LABEL IS THEREFORE NOT AN ATTRIBUTION, and every argument in this tree
|
|
1773
|
-
// that counts winning labels is unsound to exactly that extent. The label kept is the
|
|
1774
|
-
// FIRST route's; the later routes are discarded, silently and by design. Adding one
|
|
1775
|
-
// roster row renamed 21 agbcc rows whose emitted source sets were byte-identical —
|
|
1776
|
-
// a CANDIDATE-SET census (whole fan unchanged, some candidate relabelled), which is a
|
|
1777
|
-
// different population from a WINNING-label census: over published winners the same
|
|
1778
|
-
// row moved 5 labels, 2 of them renames.
|
|
2007
|
+
// A TREE another structure setting already produced. `respellTree` reads the tree and this
|
|
2008
|
+
// call's own constants, nothing that varies per setting — its signature is the argument —
|
|
2009
|
+
// so a repeated tree can only re-emit sources `seen` already holds: the candidate list,
|
|
2010
|
+
// its order and its variations are exactly the ones the whole fan produces, reached
|
|
2011
|
+
// without re-deriving forty passes. A structure variation is INERT on most functions
|
|
2012
|
+
// (nothing to re-read, no bitfield member, no joined if), and an inert one is a factor of
|
|
2013
|
+
// two in the cross that changes nothing: on the klonoa checkout's `LoadBGTilemapData`
|
|
2014
|
+
// under docs/ranked-repro.md's flags, 640 of 1024 structure settings (62.5%) re-derive a
|
|
2015
|
+
// tree an earlier one already emitted.
|
|
1779
2016
|
//
|
|
1780
|
-
//
|
|
1781
|
-
//
|
|
1782
|
-
//
|
|
1783
|
-
//
|
|
1784
|
-
//
|
|
1785
|
-
// look the same from here; only the emitted SOURCE tells them apart (`bench diff`
|
|
1786
|
-
// publishes that field, `bench regression` does not).
|
|
2017
|
+
// Keyed on the JSON text, in a Set of STRINGS — a value comparison, so it can never
|
|
2018
|
+
// merge two trees the way a hash could. Its one direction of error is a MISS (a
|
|
2019
|
+
// differing key order re-runs a respell set whose sources then dedup as they do today), and
|
|
2020
|
+
// the property that rules the other direction out — that the text determines the tree —
|
|
2021
|
+
// is pinned by rank-tree-key.test.ts rather than assumed.
|
|
1787
2022
|
//
|
|
1788
|
-
//
|
|
1789
|
-
//
|
|
1790
|
-
//
|
|
1791
|
-
//
|
|
1792
|
-
//
|
|
1793
|
-
//
|
|
1794
|
-
//
|
|
1795
|
-
//
|
|
1796
|
-
//
|
|
1797
|
-
//
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
// The same TEXT, reached twice. `matchOnly` is a property of the DERIVATION and the
|
|
1802
|
-
// published artifact is the text, so a spelling some sound route also produces is a
|
|
1803
|
-
// proven one however the first route reached it — clear the flag rather than keeping
|
|
1804
|
-
// whichever route the enumeration happened to walk first.
|
|
1805
|
-
if (sp.matchOnly === undefined) {
|
|
1806
|
-
delete dup.matchOnly;
|
|
1807
|
-
}
|
|
2023
|
+
// The key therefore spans EVIDENCE fields too, `index.operandOff` among them, which
|
|
2024
|
+
// `exprEquals` deliberately ignores (l3/ast.ts). The two are right to disagree: two
|
|
2025
|
+
// trees identical but for that field denote the same cells, so a CSE may collapse them,
|
|
2026
|
+
// and they admit different bases under `BASEFOLD_GATES`, so a fan may not. Dropping it
|
|
2027
|
+
// from the key would be the direction the paragraph above rules out. It carries a
|
|
2028
|
+
// DISPLACEMENT rather than a presence flag, so it can split two trees that print the
|
|
2029
|
+
// same subscript off different addends — re-priced when it widened, over klonoa's
|
|
2030
|
+
// `LoadBGTilemapData` under docs/ranked-repro.md's flags: 66816 candidates either way,
|
|
2031
|
+
// and all 66816 `[score]` lines identical. It splits 0 keys, so the miss it can cause
|
|
2032
|
+
// has no inhabitant.
|
|
2033
|
+
const treeKey = JSON.stringify(sfn);
|
|
2034
|
+
if (seenTrees.has(treeKey)) {
|
|
2035
|
+
opts.onTreeDeduped?.();
|
|
1808
2036
|
continue;
|
|
1809
2037
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
2038
|
+
seenTrees.add(treeKey);
|
|
2039
|
+
// The row's OWN tree, so this is the one call whose backend refusal is the row's cause.
|
|
2040
|
+
const own = respellTree(sfn);
|
|
2041
|
+
const sources = own.sources;
|
|
2042
|
+
if (own.emit) {
|
|
2043
|
+
lastEmitError = own.emit.error;
|
|
2044
|
+
}
|
|
2045
|
+
// The PRE-RESPELL variations (PRE_RESPELL_VARIATIONS, the last composition the POLICY note
|
|
2046
|
+
// names): rewrite the TREE, then run the whole respell set over the result, so every
|
|
2047
|
+
// respell variation derives from the rewrite instead of composing onto it. The gate is the pass's
|
|
2048
|
+
// own decline; the contracts are `respell`'s three, for `respell`'s reasons.
|
|
2049
|
+
for (const pf of PRE_RESPELL_VARIATIONS) {
|
|
2050
|
+
try {
|
|
2051
|
+
const made = pf.apply(sfn);
|
|
2052
|
+
if (made === null) {
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
// The SAME tree dedup the row's own tree above gets, and for the same reason: `respellTree`
|
|
2056
|
+
// is a pure function of the tree, so running it again on a tree it has already run on buys nothing
|
|
2057
|
+
// and makes the row's quoted fan cost a number that is partly duplicates. A SEPARATE
|
|
2058
|
+
// set, not `seenTrees`: adding a rewritten tree there would let it skip a later
|
|
2059
|
+
// STRUCTURED tree that happens to equal it, and that tree's own pre-respell output —
|
|
2060
|
+
// which nothing has computed — would go with it.
|
|
2061
|
+
const madeKey = JSON.stringify(made);
|
|
2062
|
+
if (seenPreRespell.has(madeKey)) {
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
seenPreRespell.add(madeKey);
|
|
2066
|
+
assertResolved(made);
|
|
2067
|
+
assertDerefsTyped(made);
|
|
2068
|
+
assertLocalsWritten(made);
|
|
2069
|
+
assertNoOrphanedLocals(sfn, made);
|
|
2070
|
+
// A backend refusal on this REWRITTEN tree is not a refusal of the row's own
|
|
2071
|
+
// tree, so it never becomes the row's stated cause: `TreeSources.emit` is dropped
|
|
2072
|
+
// here and only the call over the row's own tree above records one.
|
|
2073
|
+
//
|
|
2074
|
+
// It is reported instead through `onEnumerationError` under `pf.name`, which is what
|
|
2075
|
+
// `respellTree`'s second argument is for: a default emit refusal does not THROW —
|
|
2076
|
+
// `respellTree` returns it — so the `catch` below never sees it, and reported with no
|
|
2077
|
+
// variations it would read as a refusal of the row's default source while the
|
|
2078
|
+
// variation's whole half of the fan was deleted.
|
|
2079
|
+
const respelled = respellTree(made, [pf.name]).sources;
|
|
2080
|
+
for (const sp of respelled) {
|
|
2081
|
+
sources.push({ ...sp, variations: [pf.name, ...sp.variations] });
|
|
2082
|
+
}
|
|
2083
|
+
} catch (e) {
|
|
2084
|
+
reportThrow([pf.name], e);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
for (const sp of sources) {
|
|
2088
|
+
const source = sp.source;
|
|
2089
|
+
// Collapse a candidate whose source is identical (a function with no divergent `if`
|
|
2090
|
+
// structures the same either way): no point scoring a duplicate. Deduping the
|
|
2091
|
+
// WHOLE emitted set (not just scored survivors) is equivalent — an identical source
|
|
2092
|
+
// scores identically, so it can never change the winner — and it keeps the candidate set to
|
|
2093
|
+
// the genuinely distinct sources.
|
|
2094
|
+
//
|
|
2095
|
+
// A CANDIDATE'S PUBLISHED VARIATIONS ARE THEREFORE NOT AN ATTRIBUTION, and every argument
|
|
2096
|
+
// in this tree that counts winners' variations is unsound to exactly that extent. The
|
|
2097
|
+
// variations kept are the FIRST route's; the later routes are discarded, silently and by
|
|
2098
|
+
// design. Adding one hoist renamed candidates on 21 agbcc rows whose emitted source sets
|
|
2099
|
+
// were byte-identical — a CANDIDATE-SET census (whole fan unchanged, some candidate
|
|
2100
|
+
// renamed), which counts something different from a census of WINNERS: over published
|
|
2101
|
+
// winners the same hoist moved 5 names, 2 of them renames.
|
|
2102
|
+
//
|
|
2103
|
+
// AND A CENSUS OF VARIATIONS CANNOT EVEN SEPARATE A RENAME FROM A CHANGED SOURCE. Of those 5
|
|
2104
|
+
// winners, THREE changed the source they publish — `synthetic:unfoldpark`
|
|
2105
|
+
// (402 → 397 bytes, score 9 → 0), `kleod:ConfigureEntityBehavior` (3677 → 3993,
|
|
2106
|
+
// 233 → 230) and `synthetic:livepark` (337 → 346, both MATCH) — while
|
|
2107
|
+
// `synthetic:foldpark` and `kleod:DecompressDma` (kl-eod-decomp's source, before 2026-09-13) were byte-identical renames. The two
|
|
2108
|
+
// look the same from here; only the emitted SOURCE tells them apart (`bench diff`
|
|
2109
|
+
// publishes that field, `bench regression` does not).
|
|
2110
|
+
//
|
|
2111
|
+
// So "N rows win under this family" bounds nothing: a family can win zero winners and
|
|
2112
|
+
// still be the only route to a source, and a family can win five and have introduced
|
|
2113
|
+
// three. Price a family by ABLATING it and re-running the rows
|
|
2114
|
+
// (LIVEBASE_BLOCK_GATES carries the recipe); a zero census is not a death certificate,
|
|
2115
|
+
// and a nonzero one is not a mechanism.
|
|
2116
|
+
// THE SEAM FIX IS BOOKED AND NOT BUILT: keep the losing routes on the surviving
|
|
2117
|
+
// candidate (`variations` plus an `alsoReachedBy`) and a census by mechanism
|
|
2118
|
+
// becomes one. It is not free — every consumer that reads `variations` as the derivation
|
|
2119
|
+
// would have to say which it means, and the published `winnerVariations` must not
|
|
2120
|
+
// change — so build it when a round needs the census, not before. Until then the only
|
|
2121
|
+
// sound census is an ablation.
|
|
2122
|
+
const dup = seen.get(source);
|
|
2123
|
+
if (dup !== undefined) {
|
|
2124
|
+
// The same TEXT, reached twice. `matchOnly` is a property of the DERIVATION and the
|
|
2125
|
+
// published artifact is the text, so a spelling some sound route also produces is a
|
|
2126
|
+
// proven one however the first route reached it — clear the flag rather than keeping
|
|
2127
|
+
// whichever route the enumeration happened to walk first.
|
|
2128
|
+
if (sp.matchOnly === undefined) {
|
|
2129
|
+
delete dup.matchOnly;
|
|
2130
|
+
}
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
const variations: readonly Variation[] = [
|
|
2134
|
+
cand.variation,
|
|
2135
|
+
...liftVariations,
|
|
2136
|
+
...s.variations,
|
|
2137
|
+
...sp.variations,
|
|
2138
|
+
...symbolSetting.variations,
|
|
2139
|
+
];
|
|
2140
|
+
// `respell`, `respellEach` and the hoist roster ask `offeredOn` before a pass runs; this
|
|
2141
|
+
// asks it of the whole name, so a mint site that skips the question is an enumeration
|
|
2142
|
+
// error and the drawer's target line stays what enumeration does.
|
|
2143
|
+
if (!offeredOn(target, variations)) {
|
|
2144
|
+
throw new Error(`'${variations.join('/')}' is withheld on ${target.compiler} but was enumerated`);
|
|
2145
|
+
}
|
|
2146
|
+
const made: Candidate = {
|
|
2147
|
+
variations,
|
|
2148
|
+
source,
|
|
2149
|
+
preference: symbolIndex,
|
|
2150
|
+
...(sp.symbolRefs ? { symbolRefs: sp.symbolRefs } : {}),
|
|
2151
|
+
...(sp.deviceVolatile ? { deviceVolatile: sp.deviceVolatile } : {}),
|
|
2152
|
+
...(sp.matchOnly ? { matchOnly: sp.matchOnly } : {}),
|
|
2153
|
+
};
|
|
2154
|
+
seen.set(source, made);
|
|
2155
|
+
out.push(made);
|
|
2156
|
+
}
|
|
1820
2157
|
}
|
|
1821
2158
|
}
|
|
1822
2159
|
}
|
|
@@ -1826,9 +2163,10 @@ export function enumerateCandidates(
|
|
|
1826
2163
|
// candidate; all of them together is the row, and it stays LOUD — the alternative is a caller
|
|
1827
2164
|
// ranking an empty list and reporting no match for a function nothing ever tried to spell.
|
|
1828
2165
|
if (out.length === 0) {
|
|
1829
|
-
throw new
|
|
1830
|
-
|
|
1831
|
-
|
|
2166
|
+
throw new NoSpellableCandidateError(
|
|
2167
|
+
`no spellable candidate for '${name}': ${firstLine(lastEmitError ?? 'no candidate produced')}`,
|
|
2168
|
+
{ cause: lastEmitError },
|
|
2169
|
+
);
|
|
1832
2170
|
}
|
|
1833
2171
|
return out;
|
|
1834
2172
|
}
|
|
@@ -1839,27 +2177,32 @@ export function enumerateCandidates(
|
|
|
1839
2177
|
* the scorer must be sync (the cli/Node objdiff path). The webapp scores asynchronously and does
|
|
1840
2178
|
* its own await-loop over `enumerateCandidates`, reusing this module's `Candidate`/`RankedResult`
|
|
1841
2179
|
* types but not this driver. */
|
|
1842
|
-
export function rankBy<S extends { score: number }>(
|
|
2180
|
+
export function rankBy<S extends { score: number; rows?: number }>(
|
|
1843
2181
|
candidates: Candidate[],
|
|
1844
2182
|
symbol: string,
|
|
1845
2183
|
scoreFn: (source: string, symbol: string, candidate: Candidate) => S,
|
|
1846
2184
|
): RankedResult<S> {
|
|
1847
2185
|
const results: (Scored<S> & { order: number })[] = [];
|
|
1848
|
-
const dropped: DroppedCandidate[] = []; //
|
|
1849
|
-
const withheld: WithheldCandidate[] = []; //
|
|
2186
|
+
const dropped: DroppedCandidate[] = []; // candidates that failed to build; only fatal if ALL do
|
|
2187
|
+
const withheld: WithheldCandidate[] = []; // candidates that built but did not earn publication
|
|
1850
2188
|
let lastScoreErr: unknown = null;
|
|
1851
2189
|
candidates.forEach((c, order) => {
|
|
1852
2190
|
try {
|
|
1853
2191
|
const score = scoreFn(c.source, symbol, c);
|
|
1854
2192
|
const why = withheldReason(c, score);
|
|
1855
2193
|
if (why !== null) {
|
|
1856
|
-
withheld.push({
|
|
2194
|
+
withheld.push({
|
|
2195
|
+
variations: c.variations,
|
|
2196
|
+
score: score.score,
|
|
2197
|
+
...(score.rows === undefined ? {} : { rows: score.rows }),
|
|
2198
|
+
why,
|
|
2199
|
+
});
|
|
1857
2200
|
return;
|
|
1858
2201
|
}
|
|
1859
2202
|
results.push({ ...c, order, score });
|
|
1860
2203
|
} catch (e) {
|
|
1861
2204
|
lastScoreErr = e;
|
|
1862
|
-
dropped.push({
|
|
2205
|
+
dropped.push({ variations: c.variations, error: firstLine(e) });
|
|
1863
2206
|
}
|
|
1864
2207
|
});
|
|
1865
2208
|
if (results.length === 0) {
|
|
@@ -1867,22 +2210,24 @@ export function rankBy<S extends { score: number }>(
|
|
|
1867
2210
|
// scorer failure, and a list that was entirely proof-gated is a different thing entirely.
|
|
1868
2211
|
const why =
|
|
1869
2212
|
lastScoreErr !== null ? firstLine(lastScoreErr) : `${withheld.length} candidate(s) withheld, none scored`;
|
|
1870
|
-
throw new
|
|
2213
|
+
throw new NoScorableCandidateError(`no scorable candidate for '${symbol}': ${why}`, dropped, withheld, {
|
|
2214
|
+
cause: lastScoreErr,
|
|
2215
|
+
});
|
|
1871
2216
|
}
|
|
1872
2217
|
results.sort(compareScored);
|
|
1873
|
-
return {
|
|
2218
|
+
return { winner: results[0], candidates: results.map(({ order: _order, ...c }) => c), dropped, withheld };
|
|
1874
2219
|
}
|
|
1875
2220
|
|
|
1876
|
-
/** THE candidate ordering — score, then preference
|
|
2221
|
+
/** THE candidate ordering — score, then preference, then readability, then enumeration
|
|
1877
2222
|
* order. Exported because there are TWO drivers over the same enumeration (this module's sync
|
|
1878
2223
|
* `rankBy` for the Node/objdiff scorer, and the webapp's async await-loop for the wasm one), and
|
|
1879
2224
|
* a per-driver copy would let the same input produce two different winners.
|
|
1880
2225
|
*
|
|
1881
|
-
* SCORE dominates absolutely: the differ is the fitness function, and a tie means the
|
|
1882
|
-
* separates these two
|
|
2226
|
+
* SCORE dominates absolutely: the differ is the fitness function, and a tie means the variation
|
|
2227
|
+
* that separates these two candidates did not change the bytes — so everything below only chooses what
|
|
1883
2228
|
* the READER sees, and can never cost a match.
|
|
1884
2229
|
*
|
|
1885
|
-
*
|
|
2230
|
+
* PREFERENCE next: a named symbol-map spelling beats its `/raw-globals` sibling at equal bytes.
|
|
1886
2231
|
*
|
|
1887
2232
|
* DEVICE VOLATILITY next: at equal bytes, the spelling that qualifies a DEVICE REGISTER
|
|
1888
2233
|
* (`capabilities.deviceRegisters`) is the one to publish. A dropped `volatile` on an MMIO cell is
|
|
@@ -1891,17 +2236,17 @@ export function rankBy<S extends { score: number }>(
|
|
|
1891
2236
|
* window rather than counting the word, because outside it the qualifier is a claim about
|
|
1892
2237
|
* ordinary memory that the asm does not support — over the bench, counting the word
|
|
1893
2238
|
* alone decides twelve rows and only two of them touch a device address. A declared term rather
|
|
1894
|
-
* than an enumeration order, which an unrelated
|
|
2239
|
+
* than an enumeration order, which an unrelated variation's candidates can slide between.
|
|
1895
2240
|
*
|
|
1896
2241
|
* IT IS A PREFERENCE, AND EVERY NEW MINTER INHERITS IT. `deviceVolatileClaims` only ever ADDS a
|
|
1897
|
-
* claim, so any
|
|
1898
|
-
* `/vol-store` joined the roster, six rows changed their published `
|
|
2242
|
+
* claim, so any variation that qualifies a device access wins its own tie by construction: when
|
|
2243
|
+
* `/vol-store` joined the roster, six rows changed their published `winnerVariations` and `source`
|
|
1899
2244
|
* with no score and no outcome moving. That is a judgement about the source rather than a
|
|
1900
2245
|
* measurement of it — the differ never refereed those six — and it is the same judgement this
|
|
1901
2246
|
* term was declared to make, taken on the same evidence. What it must never do is change WHICH
|
|
1902
|
-
* candidates exist; that stays an admission question, one
|
|
2247
|
+
* candidates exist; that stays an admission question, one variation at a time.
|
|
1903
2248
|
*
|
|
1904
|
-
* CAST COUNT next, and only WITHIN a
|
|
2249
|
+
* CAST COUNT next, and only WITHIN a preference. A wrong signedness pin is what manufactures casts —
|
|
1905
2250
|
* the C backend has to cast a shift operand back to the signedness the machine op needs, so
|
|
1906
2251
|
* pinning `u32` on a genuinely-signed parameter buys `s32 f(u32 a0) { return (s32)a0 >> a1; }`
|
|
1907
2252
|
* for the same bytes as `s32 f(s32 a0) { return a0 >> a1; }`. Before the backend synthesized that
|
|
@@ -1918,10 +2263,10 @@ export function rankBy<S extends { score: number }>(
|
|
|
1918
2263
|
* result deterministic. Spelled explicitly rather than leaning on Array#sort's stability, which
|
|
1919
2264
|
* would make each preference an accident of two unrelated decisions.
|
|
1920
2265
|
*
|
|
1921
|
-
* WHAT NO TERM HERE WEIGHS: a comparison's rendered SIGNEDNESS. `/uns-cmp`'s whole
|
|
2266
|
+
* WHAT NO TERM HERE WEIGHS: a comparison's rendered SIGNEDNESS. `/uns-cmp`'s whole output is
|
|
1922
2267
|
* that polarity, and it carries in a DECLARED TYPE rather than a cast — `castCount` reads 0 on
|
|
1923
2268
|
* both sides of the tie it loses — so at equal bytes enumeration order decides, and a rival
|
|
1924
|
-
*
|
|
2269
|
+
* variation's candidate can publish a signed compare where the asm's is unsigned. Weighing it needs
|
|
1925
2270
|
* the candidate's own SFn and the icmp facts it was structured from, neither of which this
|
|
1926
2271
|
* comparator carries; `deviceVolatile` is the shape such a term would take. */
|
|
1927
2272
|
export function compareScored<S extends { score: number }>(
|
|
@@ -1930,7 +2275,7 @@ export function compareScored<S extends { score: number }>(
|
|
|
1930
2275
|
): number {
|
|
1931
2276
|
return (
|
|
1932
2277
|
a.score.score - b.score.score ||
|
|
1933
|
-
a.
|
|
2278
|
+
a.preference - b.preference ||
|
|
1934
2279
|
(b.deviceVolatile ?? 0) - (a.deviceVolatile ?? 0) ||
|
|
1935
2280
|
castCount(a.source) - castCount(b.source) ||
|
|
1936
2281
|
lineCount(a.source) - lineCount(b.source) ||
|
|
@@ -1946,7 +2291,7 @@ function lineCount(source: string): number {
|
|
|
1946
2291
|
|
|
1947
2292
|
/** Scalar casts in a candidate's rendered source — the readability tie-break above.
|
|
1948
2293
|
*
|
|
1949
|
-
* A WITHIN-
|
|
2294
|
+
* A WITHIN-PREFERENCE tie-break over two spellings of ONE function, and deliberately NARROWER than
|
|
1950
2295
|
* the published readability metric (apps/benchmark/src/eval/quality.ts `casts`): it counts the
|
|
1951
2296
|
* decomp SCALAR typedef vocabulary only — `(u8)` … `(s32)` — so a pointer, struct or C-keyword
|
|
1952
2297
|
* cast is not read as noise, those being structural spellings a candidate does not choose.
|
|
@@ -1966,7 +2311,7 @@ function castCount(source: string): number {
|
|
|
1966
2311
|
return all - addr;
|
|
1967
2312
|
}
|
|
1968
2313
|
|
|
1969
|
-
/** First line of whatever a
|
|
2314
|
+
/** First line of whatever a variation, a backend or the scorer threw — the compiler's own diagnostic,
|
|
1970
2315
|
* not a stack. TOTAL on any value, including a non-Error throw, so no caller has to re-spell the
|
|
1971
2316
|
* `instanceof` test; a caller wanting a word for "nothing was thrown" supplies it at the call. */
|
|
1972
2317
|
function firstLine(e: unknown): string {
|