@asmlift/core 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/rank.ts
CHANGED
|
@@ -21,15 +21,17 @@
|
|
|
21
21
|
import { cBackend } from './backend/c';
|
|
22
22
|
import {
|
|
23
23
|
assertDerefsTyped,
|
|
24
|
+
assertFrameUnionsViewed,
|
|
25
|
+
assertLocalStorageStaysInteger,
|
|
24
26
|
assertLocalsWritten,
|
|
25
27
|
assertNoOrphanedLocals,
|
|
26
28
|
assertPlacementSurvives,
|
|
29
|
+
assertPostIncrUnshared,
|
|
27
30
|
assertResolved,
|
|
28
31
|
} from './contracts';
|
|
29
32
|
import type { AsmData } from './frontend/asmdata';
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import { Fn, defOpMap } from './ir/core';
|
|
33
|
+
import { hasSetupArgsNarrowing, narrowToSetupArgs, setupArgsDiscardsPassedResult } from './frontend/ssa';
|
|
34
|
+
import { Fn, Value, defOpMap } from './ir/core';
|
|
33
35
|
import { T } from './ir/types';
|
|
34
36
|
import { verify } from './ir/verify';
|
|
35
37
|
import { advancedBases } from './l3/advance';
|
|
@@ -42,6 +44,7 @@ import type { Gate } from './l3/gates';
|
|
|
42
44
|
import type { HoistPlacement } from './l3/hoist';
|
|
43
45
|
import { homeSplitTag, homeSplitWithholds, splitHomeBases } from './l3/homesplit';
|
|
44
46
|
import { inlinableConstBases, inlineConstBases } from './l3/inlinebase';
|
|
47
|
+
import { integerCells } from './l3/intcell';
|
|
45
48
|
import { mulFirstSums } from './l3/mulfirst';
|
|
46
49
|
import { nearBaseClusters } from './l3/nearbase';
|
|
47
50
|
import { spellOperandMembers } from './l3/offmember';
|
|
@@ -52,15 +55,18 @@ import { reindexWalks } from './l3/reindex';
|
|
|
52
55
|
import { hoistScopedBases } from './l3/scopebase';
|
|
53
56
|
import { sinkInitsToFirstUse } from './l3/sinkinit';
|
|
54
57
|
import type { SymbolRef } from './l3/symbol-refs';
|
|
58
|
+
import { signedOverUnsignedCompare } from './l3/typing';
|
|
55
59
|
import { type UnreduceResult, unreduceAccumulators } from './l3/unreduce';
|
|
56
60
|
import { deviceVolatileClaims, volatilePtrLocals, volatileSubsetCandidates } from './l3/volatileptr';
|
|
57
61
|
import { volatileValueLocals } from './l3/volatileval';
|
|
58
62
|
import { volatileDeviceStores } from './l3/volstore';
|
|
59
63
|
import { zeroSubNegates } from './l3/zerosub';
|
|
60
64
|
import { RewritePattern } from './pattern/engine';
|
|
61
|
-
import { applyIdiomPatterns, raiseRecovered, structureChecked } from './pipeline';
|
|
62
|
-
import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
65
|
+
import { applyIdiomPatterns, liftStamped, raiseRecovered, stampBehaviorsFor, structureChecked } from './pipeline';
|
|
66
|
+
import { type FnProto, type Prototypes, declaresVoidReturn, prototypesFromSymbols } from './proto';
|
|
67
|
+
import { type ContextGlobals, splitOwnContext } from './proto-context';
|
|
63
68
|
import { inferGlobalArrays, orderLicensedGlobals, sameDerivedShape } from './raise/globalshape';
|
|
69
|
+
import { OFFSET_NAME_PASS } from './raise/offsetnames';
|
|
64
70
|
import { runPreRecovery } from './raise/pre-recovery';
|
|
65
71
|
import { recoverTypes } from './raise/recover';
|
|
66
72
|
import { sinkStoreTails } from './raise/tailsink';
|
|
@@ -68,6 +74,7 @@ import {
|
|
|
68
74
|
type RefusedDeclarationReason,
|
|
69
75
|
bareGlobalAccessFacts,
|
|
70
76
|
bareGlobalSymbols,
|
|
77
|
+
initAddressSymbols,
|
|
71
78
|
makeRefCollector,
|
|
72
79
|
} from './rank-declare';
|
|
73
80
|
import {
|
|
@@ -80,13 +87,16 @@ import {
|
|
|
80
87
|
SIGNEDNESS,
|
|
81
88
|
STACKED_SUBSETS,
|
|
82
89
|
STRUCTURE_VARIATIONS,
|
|
90
|
+
StackedMemberThrew,
|
|
83
91
|
type StructureVariation,
|
|
84
92
|
UNFOLDED_HOISTS,
|
|
85
93
|
applyStacked,
|
|
86
94
|
createdLocals,
|
|
95
|
+
firstFired,
|
|
87
96
|
sameBases,
|
|
88
97
|
} from './rank-variations';
|
|
89
|
-
import {
|
|
98
|
+
import { type Stillborn, stillbornVerdict } from './stillborn';
|
|
99
|
+
import { DefaultAcceptsMemo, DefaultAcceptsMemoMisuse, hasDivergentSharedRet } from './structure/structure';
|
|
90
100
|
import {
|
|
91
101
|
type SymbolInfo,
|
|
92
102
|
type SymbolMap,
|
|
@@ -96,7 +106,13 @@ import {
|
|
|
96
106
|
symbolsByName,
|
|
97
107
|
} from './symbols';
|
|
98
108
|
import { type TargetDescription, structureOptionsFor } from './target';
|
|
99
|
-
import {
|
|
109
|
+
import {
|
|
110
|
+
type SubjectVariationName,
|
|
111
|
+
type Variation,
|
|
112
|
+
type VariationName,
|
|
113
|
+
offeredOn,
|
|
114
|
+
withSubject,
|
|
115
|
+
} from './variation-tokens';
|
|
100
116
|
|
|
101
117
|
/** Pin every SCALAR entry param (index not in `ptrIdx`) to the candidate signedness, before
|
|
102
118
|
* recovery. Answers whether any param was PINNABLE — not whether its type moved: which of the
|
|
@@ -106,16 +122,33 @@ import { type SubjectVariationName, type Variation, offeredOn, withSubject } fro
|
|
|
106
122
|
* A param NARROWED by raise/paramwidth.ts is not pinnable: the extension it was narrowed at states
|
|
107
123
|
* the signedness as well as the width — agbcc's shift pair by its `asr`/`lsr`, PPC's `extsb`/`extsh`
|
|
108
124
|
* by the opcode — so there is no question for the signedness variation to put to the differ, and pinning would
|
|
109
|
-
* widen it back to 32 bits.
|
|
125
|
+
* widen it back to 32 bits.
|
|
126
|
+
*
|
|
127
|
+
* Nor is a param NOTHING READS. Its declared signedness reaches no instruction, so the two
|
|
128
|
+
* candidates would differ only in the signature's text and compile to the same object. The frontend
|
|
129
|
+
* mints such params on purpose: an argument register below the highest one read keeps its slot
|
|
130
|
+
* (`frontend/ssa.ts` mintArgSlotHoles). Counting one as pinnable would double the fan of every
|
|
131
|
+
* function whose only scalar params are unread. */
|
|
110
132
|
function pinScalarParams(fn: Fn, signed: boolean, ptrIdx: Set<number>): boolean {
|
|
133
|
+
const read = new Set<Value>();
|
|
134
|
+
for (const b of fn.blocks) {
|
|
135
|
+
for (const op of b.ops) {
|
|
136
|
+
op.operands.forEach((v) => read.add(v));
|
|
137
|
+
op.successors.forEach((s) => s.args.forEach((v) => read.add(v)));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
111
140
|
let pinnable = false;
|
|
112
141
|
fn.blocks[0].params.forEach((p, i) => {
|
|
113
|
-
if (ptrIdx.has(i)) {
|
|
142
|
+
if (ptrIdx.has(i) || !read.has(p)) {
|
|
114
143
|
return;
|
|
115
144
|
}
|
|
116
|
-
|
|
145
|
+
// AT THE PARAMETER'S OWN WIDTH. The pin answers a question the asm does not — whether the
|
|
146
|
+
// source declared this parameter signed — and a 64-bit parameter has that question too; what
|
|
147
|
+
// it does not have is a spare 32 bits to lose to a constant here. A width the machine already
|
|
148
|
+
// narrowed (a declared `u8`) is not a scalar this variation re-decides, so it is left alone.
|
|
149
|
+
if (p.type.kind === 'unknown' || (p.type.kind === 'int' && p.type.width >= 32)) {
|
|
117
150
|
pinnable = true;
|
|
118
|
-
p.type =
|
|
151
|
+
p.type = T.int(p.type.width, signed);
|
|
119
152
|
}
|
|
120
153
|
});
|
|
121
154
|
return pinnable;
|
|
@@ -133,6 +166,10 @@ export interface EnumerateOptions {
|
|
|
133
166
|
asmData?: AsmData;
|
|
134
167
|
/** address→symbol map (symbols.ts) — same contract as DecompileOptions.symbols */
|
|
135
168
|
symbols?: SymbolMap;
|
|
169
|
+
/** same contract as DecompileOptions.contextGlobals */
|
|
170
|
+
contextGlobals?: ContextGlobals;
|
|
171
|
+
/** same contract as DecompileOptions.ownContextProto */
|
|
172
|
+
ownContextProto?: FnProto;
|
|
136
173
|
/** Called when a respell variation THROWS or fails a boundary contract, so the failure is visible
|
|
137
174
|
* instead of the candidate silently not existing. Enumeration continues either way — the default
|
|
138
175
|
* candidate is unaffected — but a variation that never fires because it always throws is a defect,
|
|
@@ -220,15 +257,50 @@ export interface Candidate {
|
|
|
220
257
|
*
|
|
221
258
|
* The third admission ground, and the narrowest. A respell variation must preserve semantics by
|
|
222
259
|
* construction (the POLICY note at the respell site), because on a nonmatch row the best
|
|
223
|
-
* spelling is what the user is shown.
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
260
|
+
* spelling is what the user is shown. Two spellings cannot meet that bar from inside the pass,
|
|
261
|
+
* each for its own fact (`PROOF_OBLIGATIONS` states them):
|
|
262
|
+
*
|
|
263
|
+
* • `l3/unreduce.ts` moves a memory read into a loop whose stores are all device registers, and
|
|
264
|
+
* on this board a device store can make the DEVICE write ordinary memory (a DMA trigger),
|
|
265
|
+
* which no gate over the C can rule out;
|
|
266
|
+
* • `l3/intcell.ts` spells a global as the integer a project may have declared it, where a
|
|
267
|
+
* pointer declaration scales the offset it adds, and no declaration is in the asm.
|
|
268
|
+
*
|
|
269
|
+
* A third obligation is judged on the finished tree rather than declared by a variation
|
|
270
|
+
* (`signedUnsignedCompare` below):
|
|
271
|
+
*
|
|
272
|
+
* • `/signed-cmp`, or a respell over its tree, spells an unsigned compare over signed-rendering
|
|
273
|
+
* operands, which compiles to a signed compare unless the operands are non-negative, and the
|
|
274
|
+
* compiler's proof of that is not in the asm.
|
|
275
|
+
*
|
|
276
|
+
* What settles all three instead is the object: a candidate that assembles to the target's own
|
|
277
|
+
* bytes IS the program, whatever a gate could have proved. So the spelling is offered, scored, and then
|
|
278
|
+
* either wins on proof or is WITHHELD — never shown as a best-effort answer. Both ranking drivers
|
|
279
|
+
* ask `withheldReason`, so neither can publish what the other would not. */
|
|
231
280
|
matchOnly?: true;
|
|
281
|
+
/** The tree this source was emitted from spells some unsigned compare so that C evaluates it
|
|
282
|
+
* signed (l3/typing.ts `signedOverUnsignedCompare`, judged on the finished tree), which makes it
|
|
283
|
+
* `matchOnly` and names the fact it owes. Set for whichever variation got it there: emission
|
|
284
|
+
* under `/signed-cmp`, or a respell that substituted into a compare emission had pinned. It is
|
|
285
|
+
* the only place this obligation is decided: a pass that rebuilds a compare spreads the node to
|
|
286
|
+
* keep its `unsignedCmp` evidence (l3/regspell.ts), a merge that keeps one of two equal nodes
|
|
287
|
+
* keeps the evidence of both (l3/ast.ts `unionEvidence`), or drops it only where its own gate proved
|
|
288
|
+
* the new spelling compares the same values (l3/initfirst.ts `swapSide`, l3/reindex.ts). */
|
|
289
|
+
signedUnsignedCompare?: true;
|
|
290
|
+
/** This candidate's lift is `/setup-args` and drops an earlier callee's result that a call through
|
|
291
|
+
* a register is passed (frontend/ssa.ts `setupArgsDiscardsPassedResult`). It loses every score
|
|
292
|
+
* tie (compareScored): a tie refutes neither reading, and a shorter spelling must not win by
|
|
293
|
+
* dropping a value the callee may read. */
|
|
294
|
+
discardsPassedResult?: true;
|
|
295
|
+
/** This candidate's lift is `/narrow-param` and declares a parameter narrow that no prototype
|
|
296
|
+
* declares (raise/pre-recovery.ts `onUndeclaredWidth`). It loses every score tie
|
|
297
|
+
* (compareScored): a tie refutes neither declaration, and the narrow one changes the bytes of
|
|
298
|
+
* every prototyped caller, which this function's score cannot see. */
|
|
299
|
+
guessesParamWidth?: true;
|
|
300
|
+
/** This candidate's lift is `/far-cast` and spells a far member store through a cast of its
|
|
301
|
+
* base. It loses every score tie (compareScored): a tie means the compiler built the member's
|
|
302
|
+
* object too, and the member is the spelling that names the field. */
|
|
303
|
+
farStoreCast?: true;
|
|
232
304
|
}
|
|
233
305
|
/** A candidate paired with its score `S` (the injected scorer's result shape — must carry `.score`). */
|
|
234
306
|
export interface Scored<S> extends Candidate {
|
|
@@ -261,10 +333,20 @@ export interface WithheldCandidate {
|
|
|
261
333
|
why: string;
|
|
262
334
|
}
|
|
263
335
|
|
|
336
|
+
/** A candidate that was NEVER COMPILED: the fan was declared stillborn (stillborn.ts) before its
|
|
337
|
+
* turn came. Kept apart from `dropped`, which means "the scorer refused it" — nothing refused
|
|
338
|
+
* this one, and a count that folded the two would report compiles that never ran. */
|
|
339
|
+
export interface NotCompiledCandidate {
|
|
340
|
+
variations: readonly string[];
|
|
341
|
+
/** the text the stop bet the compiler would refuse: a stop is re-placed whenever it changes, and
|
|
342
|
+
* the benchmark keys its STOP report on it (`fanNotCompiledDigest`) */
|
|
343
|
+
source: string;
|
|
344
|
+
}
|
|
345
|
+
|
|
264
346
|
/** EVERY candidate refused — `rankBy` has no ranked result to return, so it throws this.
|
|
265
347
|
*
|
|
266
|
-
* The
|
|
267
|
-
* nothing scored,
|
|
348
|
+
* The lists ride on the error, and that is the point of having a class at all: on a row where
|
|
349
|
+
* nothing scored, the three of them ARE the whole fan, and a bare `Error` discards it. A caller that
|
|
268
350
|
* prints one candidate's failure (the `cause`) is showing the LAST spelling the scorer refused,
|
|
269
351
|
* which is neither the first nor a representative one — the benchmark's own noncompile rows have
|
|
270
352
|
* up to a thousand siblings behind that single line. The MESSAGE is load-bearing and must stay
|
|
@@ -273,11 +355,20 @@ export interface WithheldCandidate {
|
|
|
273
355
|
export class NoScorableCandidateError extends Error {
|
|
274
356
|
readonly dropped: DroppedCandidate[];
|
|
275
357
|
readonly withheld: WithheldCandidate[];
|
|
276
|
-
|
|
358
|
+
/** empty unless the fan was declared stillborn; `dropped` is then only what WAS compiled */
|
|
359
|
+
readonly notCompiled: NotCompiledCandidate[];
|
|
360
|
+
constructor(
|
|
361
|
+
message: string,
|
|
362
|
+
dropped: DroppedCandidate[],
|
|
363
|
+
withheld: WithheldCandidate[],
|
|
364
|
+
notCompiled: NotCompiledCandidate[],
|
|
365
|
+
options?: ErrorOptions,
|
|
366
|
+
) {
|
|
277
367
|
super(message, options);
|
|
278
368
|
this.name = 'NoScorableCandidateError';
|
|
279
369
|
this.dropped = dropped;
|
|
280
370
|
this.withheld = withheld;
|
|
371
|
+
this.notCompiled = notCompiled;
|
|
281
372
|
}
|
|
282
373
|
}
|
|
283
374
|
|
|
@@ -301,7 +392,7 @@ export interface RankedResult<S> {
|
|
|
301
392
|
candidates: Scored<S>[]; // sorted best (lowest) first
|
|
302
393
|
/** candidates whose scoreFn threw — empty when every spelling built */
|
|
303
394
|
dropped: DroppedCandidate[];
|
|
304
|
-
/** candidates withheld for want of a byte-exact proof — empty unless a `matchOnly`
|
|
395
|
+
/** candidates withheld for want of a byte-exact proof — empty unless a `matchOnly` candidate scored a nonmatch */
|
|
305
396
|
withheld: WithheldCandidate[];
|
|
306
397
|
}
|
|
307
398
|
|
|
@@ -309,14 +400,31 @@ export interface RankedResult<S> {
|
|
|
309
400
|
* drivers over one enumeration (this module's sync `rankBy` and the webapp's async await-loop),
|
|
310
401
|
* and a filter written twice is how they come to publish different answers. Null ⇒ publish.
|
|
311
402
|
*
|
|
312
|
-
* `score === 0` is objdiff's byte-exact match (
|
|
403
|
+
* `score === 0` is objdiff's byte-exact match (@match-kit/scoring states the equivalence), which is
|
|
313
404
|
* why a bare `.score` suffices and the generic needs no `match` field. */
|
|
314
405
|
export function withheldReason<S extends { score: number }>(c: Candidate, score: S): string | null {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
406
|
+
if (c.matchOnly !== true || score.score === 0) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
const facts = c.variations.flatMap((v) => PROOF_OBLIGATIONS.get(v) ?? []);
|
|
410
|
+
if (c.signedUnsignedCompare === true) {
|
|
411
|
+
facts.push(SIGNED_COMPARE_OBLIGATION);
|
|
412
|
+
}
|
|
413
|
+
const fact = facts.length > 0 ? facts.join(' and on ') : 'a fact no gate over the C can settle';
|
|
414
|
+
return `this spelling rests on ${fact}; only a byte-exact score proves it`;
|
|
318
415
|
}
|
|
319
416
|
|
|
417
|
+
/** The fact each proof-carrying variation (`needsProof`) rests on, as `withheldReason` publishes it. */
|
|
418
|
+
const PROOF_OBLIGATIONS: ReadonlyMap<string, string> = new Map<VariationName, string>([
|
|
419
|
+
['unreduce', 'a device-behaviour fact no gate over the C can settle'],
|
|
420
|
+
['int-cell', 'the project declaring the global an integer, which the assembly does not show'],
|
|
421
|
+
]);
|
|
422
|
+
|
|
423
|
+
/** The fact `Candidate.signedUnsignedCompare` rests on. Keyed by the flag, not by `/signed-cmp`:
|
|
424
|
+
* that name is on candidates that owe nothing (a compare over a `u32` operand) and missing from
|
|
425
|
+
* respells that do. */
|
|
426
|
+
const SIGNED_COMPARE_OBLIGATION = "the compiler having proved every unsigned compare's operands non-negative";
|
|
427
|
+
|
|
320
428
|
/** What a respell variation hands `respell`: its tree, or — when the variation cannot establish the
|
|
321
429
|
* candidate's semantics from inside the pass — the tree paired with that fact. `undefined`/`null`
|
|
322
430
|
* is a decline. */
|
|
@@ -380,6 +488,8 @@ interface TreeSource {
|
|
|
380
488
|
deviceVolatile?: number;
|
|
381
489
|
/** see `Candidate.matchOnly` — set by a variation that cannot establish its own semantics */
|
|
382
490
|
matchOnly?: true;
|
|
491
|
+
/** see `Candidate.signedUnsignedCompare` */
|
|
492
|
+
signedUnsignedCompare?: true;
|
|
383
493
|
}
|
|
384
494
|
|
|
385
495
|
/** Emit the DISTINCT type/branch-sense candidates for `name` — PURE, no scoring.
|
|
@@ -407,15 +517,17 @@ export function enumerateCandidates(
|
|
|
407
517
|
let lastEmitError: unknown = null;
|
|
408
518
|
// Same merge as `decompile`: the project's DWARF signatures fill in what the caller did not
|
|
409
519
|
// state, so both the annotate pass and the ranked candidates reason about one prototype table.
|
|
410
|
-
const
|
|
411
|
-
const
|
|
520
|
+
const ownContext = splitOwnContext(opts.prototypes, name, opts.ownContextProto);
|
|
521
|
+
const prototypes = prototypesFromSymbols(opts.symbols, ownContext.prototypes ?? {});
|
|
412
522
|
const baseOpts = {
|
|
413
|
-
...structureOptionsFor(target, prototypes[name]
|
|
523
|
+
...structureOptionsFor(target, declaresVoidReturn(prototypes[name]), prototypes),
|
|
414
524
|
// See the same line in pipeline.ts: a backend that cannot print switch fall-through must not
|
|
415
525
|
// be handed a tree carrying one, because its refusal costs the whole candidate (and, when
|
|
416
526
|
// every candidate carries it, the whole row).
|
|
417
527
|
spellSwitchFallthrough: backend.spellsSwitchFallthrough,
|
|
528
|
+
spellSwitchBreak: backend.spellsSwitchBreak,
|
|
418
529
|
...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
|
|
530
|
+
...(opts.contextGlobals ? { contextGlobals: opts.contextGlobals } : {}),
|
|
419
531
|
};
|
|
420
532
|
// Branch-sense is a differ-ranked VARIATION, the same class as param signedness: a divergent `if`
|
|
421
533
|
// can be spelled with either sense (`if (c) A else B` vs `if (!c) B else A`), and which one the
|
|
@@ -537,12 +649,23 @@ export function enumerateCandidates(
|
|
|
537
649
|
// it buys is that a lift-time change which splits them enumerates both settings rather than
|
|
538
650
|
// silently dropping one, the failure nothing reports.
|
|
539
651
|
let sharedLiftTreeOwned = false;
|
|
652
|
+
let sharedLiftExtendedReaders = false;
|
|
653
|
+
let sharedLiftFoldedStore = false;
|
|
540
654
|
// Shared lift: recover ONCE with no signedness pin, to learn which entry params are
|
|
541
655
|
// pointers/aggregates so they are excluded from the signedness variation (see NO_PIN_KINDS). One
|
|
542
656
|
// extra lift+recover, no compile. (The shared lift deliberately stops after recoverTypes — it only reads the param KINDS, so
|
|
543
657
|
// the totality contract / return-sinking of the full spine are not run on it.)
|
|
544
|
-
const sharedLift =
|
|
545
|
-
|
|
658
|
+
const sharedLift = liftStamped(
|
|
659
|
+
name,
|
|
660
|
+
asm,
|
|
661
|
+
target,
|
|
662
|
+
prototypes,
|
|
663
|
+
opts.asmData,
|
|
664
|
+
opts.symbols,
|
|
665
|
+
opts.contextGlobals,
|
|
666
|
+
undefined,
|
|
667
|
+
ownContext.ownContextProto,
|
|
668
|
+
);
|
|
546
669
|
// The ARRAY SHAPES the input assembly evidences (raise/globalshape.ts), for the DECLARATION
|
|
547
670
|
// half. Read off the shared lift's LIFTED form — before the fold below and the tower rewrite it —
|
|
548
671
|
// because the base-materialization order the derivation's licence reads does not survive them.
|
|
@@ -557,6 +680,16 @@ export function enumerateCandidates(
|
|
|
557
680
|
sharedLiftTreeOwned = true;
|
|
558
681
|
},
|
|
559
682
|
},
|
|
683
|
+
paramWidth: {
|
|
684
|
+
onExtendedReaders: () => {
|
|
685
|
+
sharedLiftExtendedReaders = true;
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
displaced: {
|
|
689
|
+
onFoldedStore: () => {
|
|
690
|
+
sharedLiftFoldedStore = true;
|
|
691
|
+
},
|
|
692
|
+
},
|
|
560
693
|
});
|
|
561
694
|
recoverTypes(sharedLift);
|
|
562
695
|
const ptrIdx = new Set<number>(
|
|
@@ -581,18 +714,47 @@ export function enumerateCandidates(
|
|
|
581
714
|
// shared lift's `onTreeOwned` hook has had its chance to fire. Called any earlier it would report a confident
|
|
582
715
|
// `false` for a function that owns a tree. As a `const` below that call, an early call is a TDZ
|
|
583
716
|
// ReferenceError instead — a wrong answer traded for a loud one.
|
|
584
|
-
|
|
717
|
+
//
|
|
718
|
+
// `/narrow-param`'s gate is read off the same lift the same way: the width pass reports a
|
|
719
|
+
// parameter the variation's lift would narrow (raise/pre-recovery.ts `ParamWidthOptions`). So is
|
|
720
|
+
// `/far-cast`'s: the displaced-access fold reports a store it moved into a struct.
|
|
721
|
+
const liftReportsIn = (
|
|
722
|
+
symbols: typeof opts.symbols,
|
|
723
|
+
): { treeOwned: boolean; extendedReaders: boolean; foldedStore: boolean } => {
|
|
585
724
|
if (symbols === opts.symbols) {
|
|
586
|
-
return
|
|
725
|
+
return {
|
|
726
|
+
treeOwned: sharedLiftTreeOwned,
|
|
727
|
+
extendedReaders: sharedLiftExtendedReaders,
|
|
728
|
+
foldedStore: sharedLiftFoldedStore,
|
|
729
|
+
};
|
|
587
730
|
}
|
|
588
|
-
const p =
|
|
589
|
-
|
|
731
|
+
const p = liftStamped(
|
|
732
|
+
name,
|
|
733
|
+
asm,
|
|
734
|
+
target,
|
|
735
|
+
prototypes,
|
|
736
|
+
opts.asmData,
|
|
737
|
+
symbols,
|
|
738
|
+
opts.contextGlobals,
|
|
739
|
+
undefined,
|
|
740
|
+
ownContext.ownContextProto,
|
|
741
|
+
);
|
|
590
742
|
applyIdiomPatterns(p, target, opts.patterns);
|
|
591
|
-
|
|
743
|
+
const seen = { treeOwned: false, extendedReaders: false, foldedStore: false };
|
|
592
744
|
runPreRecovery(p, target, () => verify(p), prototypes[name], {
|
|
593
745
|
shortCircuit: {
|
|
594
746
|
onTreeOwned: () => {
|
|
595
|
-
seen = true;
|
|
747
|
+
seen.treeOwned = true;
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
paramWidth: {
|
|
751
|
+
onExtendedReaders: () => {
|
|
752
|
+
seen.extendedReaders = true;
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
displaced: {
|
|
756
|
+
onFoldedStore: () => {
|
|
757
|
+
seen.foldedStore = true;
|
|
596
758
|
},
|
|
597
759
|
},
|
|
598
760
|
});
|
|
@@ -800,6 +962,14 @@ export function enumerateCandidates(
|
|
|
800
962
|
s.ptrElems &&
|
|
801
963
|
s.declRank &&
|
|
802
964
|
STRUCTURE_VARIATIONS.every((variation) => !s[variation.flag]);
|
|
965
|
+
/** Does a failure ABORT THE ROW, rather than drop the setting that threw? Only on the default
|
|
966
|
+
* candidate's own path: no variation named by the symbol-map setting or the lift setting —
|
|
967
|
+
* `settingVariations` is both, as the candidate spells them — and, where a structure setting is
|
|
968
|
+
* named, the default one. Anywhere else the failure is a dropped variation, reported through
|
|
969
|
+
* `onEnumerationError` under the same names. The lift, the raise and the structuring all ask
|
|
970
|
+
* this one predicate, so a setting that names a new variation is seen by all three. */
|
|
971
|
+
const abortsRow = (settingVariations: readonly Variation[], s?: StructureSetting): boolean =>
|
|
972
|
+
settingVariations.length === 0 && (s === undefined || isDefaultSetting(s));
|
|
803
973
|
|
|
804
974
|
const seen = new Map<string, Candidate>();
|
|
805
975
|
const seenTrees = new Set<string>();
|
|
@@ -840,14 +1010,18 @@ export function enumerateCandidates(
|
|
|
840
1010
|
// every OTHER name in the function, so supplying more information made the tool strictly
|
|
841
1011
|
// worse. A union cannot — each name is declared by whichever half knows more about it.
|
|
842
1012
|
// SCOPE: `declSymbols` is used ONLY here. It must never reach `opts.symbols`/`baseOpts.symbols`
|
|
843
|
-
// or `
|
|
1013
|
+
// or `liftStamped` — feeding it to the lift would turn on pool promotion, interior
|
|
844
1014
|
// attribution and the `/raw-globals` variation, which is a different (and source-moving) change.
|
|
845
1015
|
// THREE halves now, in increasing authority: the name-only pool/reloc symbols, the array shapes
|
|
846
1016
|
// the asm evidences for them (raise/globalshape.ts — an `extern u16 gTbl[];` where the bare
|
|
847
1017
|
// spelling needs one, and the declaration a candidate spelling `gTbl[i]` cannot compile
|
|
848
1018
|
// without), and the project map, which knows more than either.
|
|
849
1019
|
const mapSymbols = baseOpts.symbols;
|
|
850
|
-
const targetSymbols = new Map<string, SymbolInfo>([
|
|
1020
|
+
const targetSymbols = new Map<string, SymbolInfo>([
|
|
1021
|
+
...initAddressSymbols(sharedLift),
|
|
1022
|
+
...bareGlobalSymbols(sharedLift),
|
|
1023
|
+
...sharedLiftShapes,
|
|
1024
|
+
]);
|
|
851
1025
|
const declSymbols = new Map<string, SymbolInfo>([...targetSymbols, ...(mapSymbols ?? [])]);
|
|
852
1026
|
// The four per-enumeration constants named at the seam rather than captured across 60 lines of
|
|
853
1027
|
// closure (rank-declare.ts states why they belong on one object).
|
|
@@ -856,7 +1030,12 @@ export function enumerateCandidates(
|
|
|
856
1030
|
accessFacts,
|
|
857
1031
|
mapSymbols,
|
|
858
1032
|
targetNames: new Set(targetSymbols.keys()),
|
|
1033
|
+
// The MERGED table, the same one the frontend was handed above, so the declaration a candidate
|
|
1034
|
+
// carries and the lift that produced it cannot state different things about a callee.
|
|
1035
|
+
prototypes,
|
|
1036
|
+
target,
|
|
859
1037
|
refuse,
|
|
1038
|
+
own: Object.hasOwn(prototypes, name) ? prototypes[name] : ownContext.ownContextProto,
|
|
860
1039
|
});
|
|
861
1040
|
// THE RESPELL SET, as a function whose PARAMETER LIST is the invariant the tree skip below
|
|
862
1041
|
// rests on: every source here is a pure function of the structured tree and this call's own
|
|
@@ -880,6 +1059,14 @@ export function enumerateCandidates(
|
|
|
880
1059
|
// of `/volatile` — a candidate that did not fail and is still in the fan. The order
|
|
881
1060
|
// is the candidates' own (`[pf.name, ...sp.variations]`), so a reported name and an enumerated
|
|
882
1061
|
// candidate's variations name the same candidate the same way.
|
|
1062
|
+
//
|
|
1063
|
+
// THE COMPARE-SIGNEDNESS PROOF, asked of every tree a source is emitted from rather than of the
|
|
1064
|
+
// passes that build them. Emission pins each unsigned compare it lowers (structure.ts), but a
|
|
1065
|
+
// respell that substitutes into a compare — `/unmerge` putting an arm's `s16` global where the
|
|
1066
|
+
// pinned `u32` local stood, against an `s32` parameter — re-renders it signed. So the finished
|
|
1067
|
+
// tree is judged here, and a signed rendering of an `icmp_u*` is `matchOnly` whoever produced it.
|
|
1068
|
+
const cmpProof = (tree: SFn): { matchOnly?: true; signedUnsignedCompare?: true } =>
|
|
1069
|
+
signedOverUnsignedCompare(tree) ? { matchOnly: true, signedUnsignedCompare: true } : {};
|
|
883
1070
|
const respellTree = (sfn: SFn, preRespellVariations: readonly Variation[] = []): TreeSources => {
|
|
884
1071
|
// The walk→index respell variation (l3/reindex.ts) is a THIRD variation on the same footing as
|
|
885
1072
|
// signedness and branch sense: whether the source spelled `*p; p++` or `arr[i]` is
|
|
@@ -897,7 +1084,7 @@ export function enumerateCandidates(
|
|
|
897
1084
|
// Pascal backend loud-declines). Refusing EVERY tree is still loud — the empty-enumeration
|
|
898
1085
|
// check at the end raises the last refusal.
|
|
899
1086
|
try {
|
|
900
|
-
sources.push({ variations: [], source: backend.emit(sfn), ...refsOf(sfn), ...volOf(sfn) });
|
|
1087
|
+
sources.push({ variations: [], source: backend.emit(sfn), ...refsOf(sfn), ...volOf(sfn), ...cmpProof(sfn) });
|
|
901
1088
|
} catch (e) {
|
|
902
1089
|
reportThrow(preRespellVariations, e);
|
|
903
1090
|
return { sources, emit: { error: e } };
|
|
@@ -923,9 +1110,11 @@ export function enumerateCandidates(
|
|
|
923
1110
|
// numeric-address pointer local — the joint spelling is reachable from neither variation
|
|
924
1111
|
// alone, each composition narrows /volatile to that variation's own locals (volatilePtrLocals'
|
|
925
1112
|
// `only`), and each needed a row to demand it. The STACKED variations (STACKED_VARIATIONS) are
|
|
926
|
-
// derived onto EVERY source: statement order/shape
|
|
927
|
-
//
|
|
928
|
-
//
|
|
1113
|
+
// derived onto EVERY source: statement order/shape, and the width a narrow local is declared
|
|
1114
|
+
// at, are each orthogonal to what any respell variation outside that table changes — the same
|
|
1115
|
+
// kind of independent dimension as signedness —
|
|
1116
|
+
// so they are crossed with every source rather than paired (two members of the table may
|
|
1117
|
+
// compete for one node, and its order decides; rank-variations.ts); a third blanket composition needs
|
|
929
1118
|
// the same argument, not just a row. And a specific PAIRING is admitted on one of two
|
|
930
1119
|
// grounds, never on "it might help". FIRST, a row demands the joint spelling AND that
|
|
931
1120
|
// spelling is reachable from neither variation alone: /livebase × /indexed, × /sinkinit,
|
|
@@ -991,14 +1180,25 @@ export function enumerateCandidates(
|
|
|
991
1180
|
assertResolved(alt);
|
|
992
1181
|
assertDerefsTyped(alt);
|
|
993
1182
|
assertLocalsWritten(alt);
|
|
1183
|
+
assertPostIncrUnshared(alt);
|
|
994
1184
|
assertNoOrphanedLocals(sfn, alt);
|
|
995
|
-
|
|
1185
|
+
assertLocalStorageStaysInteger(sfn, alt);
|
|
1186
|
+
assertFrameUnionsViewed(alt);
|
|
1187
|
+
sources.push({
|
|
1188
|
+
variations,
|
|
1189
|
+
source: backend.emit(alt),
|
|
1190
|
+
...refsOf(alt),
|
|
1191
|
+
...volOf(alt),
|
|
1192
|
+
...proof,
|
|
1193
|
+
...cmpProof(alt),
|
|
1194
|
+
});
|
|
996
1195
|
// STACKED variations, derived onto EVERY source (the POLICY note above carries the
|
|
997
|
-
// admission argument). Each is a statement-order/shape
|
|
998
|
-
//
|
|
1196
|
+
// admission argument). Each is a statement-order/shape or declaration-width fact
|
|
1197
|
+
// orthogonal to the respell variations outside the table; subsets compose in the fixed
|
|
1198
|
+
// table order, which decides between two members that compete for one node. A stacked variation that
|
|
999
1199
|
// never fires declines and costs nothing.
|
|
1000
1200
|
if (!alreadyShaped) {
|
|
1001
|
-
// A shape
|
|
1201
|
+
// A shape may REORDER statements, and it is derived after a variation has placed its defs —
|
|
1002
1202
|
// so the placement is re-checked on the shaped tree (contracts.ts). Differential: judged
|
|
1003
1203
|
// only where the unshaped tree already satisfied the walk, so a variation whose placement
|
|
1004
1204
|
// it never described is not dropped on the strength of a model that does not apply.
|
|
@@ -1018,18 +1218,24 @@ export function enumerateCandidates(
|
|
|
1018
1218
|
// which moves only const or pure-read assigns and so cannot lift a read of a base local
|
|
1019
1219
|
// above its init.
|
|
1020
1220
|
const minted = createdLocals(sfn, alt);
|
|
1221
|
+
const firedSets = new Set<string>();
|
|
1021
1222
|
for (const subset of STACKED_SUBSETS) {
|
|
1022
1223
|
// ONE TRY PER SHAPE — a shape is its own candidate and fails as its own candidate.
|
|
1023
1224
|
// Sharing the respell variation's outer try would let a throw deriving one subset
|
|
1024
1225
|
// discard every later one, under that variation's name, which names no shape.
|
|
1025
|
-
|
|
1226
|
+
// A throw past applyStacked is reported under the fired set, the candidate's own name.
|
|
1227
|
+
let shapeVariations: readonly Variation[] = [];
|
|
1026
1228
|
try {
|
|
1027
1229
|
const shaped = applyStacked(subset, alt);
|
|
1028
|
-
if (shaped !== null) {
|
|
1230
|
+
if (shaped !== null && firstFired(firedSets, shaped.variations)) {
|
|
1231
|
+
shapeVariations = shaped.variations;
|
|
1029
1232
|
assertResolved(shaped.out);
|
|
1030
1233
|
assertDerefsTyped(shaped.out);
|
|
1031
1234
|
assertLocalsWritten(shaped.out);
|
|
1235
|
+
assertPostIncrUnshared(shaped.out);
|
|
1032
1236
|
assertNoOrphanedLocals(alt, shaped.out);
|
|
1237
|
+
assertLocalStorageStaysInteger(alt, shaped.out);
|
|
1238
|
+
assertFrameUnionsViewed(shaped.out);
|
|
1033
1239
|
assertPlacementSurvives(alt, shaped.out, minted);
|
|
1034
1240
|
sources.push({
|
|
1035
1241
|
variations: [...variations, ...shaped.variations],
|
|
@@ -1038,10 +1244,12 @@ export function enumerateCandidates(
|
|
|
1038
1244
|
...volOf(shaped.out),
|
|
1039
1245
|
// a shape derived from a proof-gated spelling inherits the requirement
|
|
1040
1246
|
...proof,
|
|
1247
|
+
...cmpProof(shaped.out),
|
|
1041
1248
|
});
|
|
1042
1249
|
}
|
|
1043
1250
|
} catch (e) {
|
|
1044
|
-
|
|
1251
|
+
const step = e instanceof StackedMemberThrew ? e.variations : shapeVariations;
|
|
1252
|
+
reportThrow([...preRespellVariations, ...variations, ...step], e);
|
|
1045
1253
|
}
|
|
1046
1254
|
}
|
|
1047
1255
|
}
|
|
@@ -1057,19 +1265,21 @@ export function enumerateCandidates(
|
|
|
1057
1265
|
// `/argbase` — name a call's argument bases before the call (l3/argbase.ts). A variation on the
|
|
1058
1266
|
// same footing as the others: the default inline spelling stays in the list, so the differ
|
|
1059
1267
|
// referees and this can never cost a match.
|
|
1268
|
+
const firedSets = new Set<string>();
|
|
1060
1269
|
for (const subset of STACKED_SUBSETS) {
|
|
1061
1270
|
// the truthful variations need the pass to RUN first, so this bypasses respell's
|
|
1062
1271
|
// variations-then-thunk shape: same try posture, variations from the fired members
|
|
1063
1272
|
try {
|
|
1064
1273
|
const shaped = applyStacked(subset, sfn);
|
|
1065
|
-
if (shaped !== null) {
|
|
1274
|
+
if (shaped !== null && firstFired(firedSets, shaped.variations)) {
|
|
1066
1275
|
// the ONE call whose variations already name shapes — say so, rather than making `respell`
|
|
1067
1276
|
// read them back out of the variations it was handed
|
|
1068
1277
|
respell(shaped.variations, () => shaped.out, true);
|
|
1069
1278
|
}
|
|
1070
1279
|
} catch (e) {
|
|
1071
|
-
//
|
|
1072
|
-
|
|
1280
|
+
// `respell` reports its own throws, so only a member can throw here
|
|
1281
|
+
const step = e instanceof StackedMemberThrew ? e.variations : subset.map((x) => x.name);
|
|
1282
|
+
reportThrow([...preRespellVariations, ...step], e);
|
|
1073
1283
|
}
|
|
1074
1284
|
}
|
|
1075
1285
|
respell(['argbase'], () => materializeArgBases(sfn));
|
|
@@ -1143,9 +1353,9 @@ export function enumerateCandidates(
|
|
|
1143
1353
|
// only ties `/vol-store`'s 27, and RT's 32 beats the admitted standalone `/unreduce`'s 35, so
|
|
1144
1354
|
// "worse than an admitted variation" would exclude neither.
|
|
1145
1355
|
//
|
|
1146
|
-
// WHAT THE STANDALONE LINES COST, since
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1356
|
+
// WHAT THE STANDALONE LINES COST, since the two variations rarely win an artifact row ALONE —
|
|
1357
|
+
// `apps/benchmark/test/census.test.ts` names every `/unreduce` and `/ptr-field` winner outside
|
|
1358
|
+
// a `/vol-store` pairing, and fails on one it does not name. They are kept because a
|
|
1149
1359
|
// variation has to be able to LOSE on its own terms: the admission posture (compareScored orders
|
|
1150
1360
|
// by score) is what makes a wrong respelled source harmless, and it is only observable when the
|
|
1151
1361
|
// single-variation candidate is in the fan —
|
|
@@ -1666,6 +1876,17 @@ export function enumerateCandidates(
|
|
|
1666
1876
|
// Which placement the source used is not derivable from the asm, so both are emitted and the
|
|
1667
1877
|
// differ referees.
|
|
1668
1878
|
respell(['sinkinit'], () => sinkInitsToFirstUse(sfn));
|
|
1879
|
+
// `/int-cell` — every pointer cell spelled as the integer a project may have declared it,
|
|
1880
|
+
// `g = g + K` and no conversions (l3/intcell.ts). The default's `g = (void *)((u8 *)g + K)` is
|
|
1881
|
+
// right under a pointer declaration of g and rejected by CodeWarrior under an integer one, where
|
|
1882
|
+
// this is the source; which the project declared is not in the asm, so it is published only at
|
|
1883
|
+
// a byte-exact score.
|
|
1884
|
+
respell(['int-cell'], () =>
|
|
1885
|
+
integerCells(sfn, (n) => {
|
|
1886
|
+
const shape = mapSymbols?.get(n)?.shape;
|
|
1887
|
+
return shape !== undefined && shape !== 'scalar';
|
|
1888
|
+
}),
|
|
1889
|
+
);
|
|
1669
1890
|
// the register-copy variation (l3/regspell.ts): 0–3 results (base; tail assign-back reusing
|
|
1670
1891
|
// the dead value var; tail assign-back into a fresh var — the tail decision is allocator-
|
|
1671
1892
|
// ambiguous, so both are ranked).
|
|
@@ -1707,8 +1928,13 @@ export function enumerateCandidates(
|
|
|
1707
1928
|
{ variations: ['raw-globals'], symbols: undefined },
|
|
1708
1929
|
]
|
|
1709
1930
|
: [{ variations: [] }];
|
|
1931
|
+
// The map's POINTER declarations go to the raw setting all the same: a raw name is still a
|
|
1932
|
+
// pointer in the project's file, so arithmetic on its value scales unless spelled in bytes.
|
|
1933
|
+
const pointerGlobals = new Set(
|
|
1934
|
+
[...(baseOpts.symbols?.entries() ?? [])].filter(([, info]) => info.shape === 'pointer').map(([n]) => n),
|
|
1935
|
+
);
|
|
1710
1936
|
for (const [symbolIndex, symbolSetting] of symbolSettings.entries()) {
|
|
1711
|
-
const symbolSettingOpts = symbolSetting.symbols ? baseOpts : { ...baseOpts, symbols: undefined };
|
|
1937
|
+
const symbolSettingOpts = symbolSetting.symbols ? baseOpts : { ...baseOpts, symbols: undefined, pointerGlobals };
|
|
1712
1938
|
// `/no-bitfield` names a spelling the MAP makes available, so it has no inhabitant on the
|
|
1713
1939
|
// symbol-map setting that structures without one: structure() normalizes `spellBitfieldMembers`
|
|
1714
1940
|
// to false when `symbols` is absent, so both settings structure the identical tree whatever
|
|
@@ -1728,7 +1954,23 @@ export function enumerateCandidates(
|
|
|
1728
1954
|
const usableStructureSettings = symbolSetting.symbols
|
|
1729
1955
|
? structureSettings
|
|
1730
1956
|
: structureSettings.filter((s) => s.bitfields && s.ptrElems && (s.declRank || rawDerivesRank));
|
|
1731
|
-
|
|
1957
|
+
// THE `/raw-globals` SETTING IS A VARIATION, so a lift that declines without the map drops it
|
|
1958
|
+
// and keeps the map's candidates (`abortsRow`). The two lifts can disagree on whether a
|
|
1959
|
+
// function lifts at all: a name only the map places is, without it, a pointer the frame-object
|
|
1960
|
+
// audit cannot bound. This is the setting's first lift, and every later one is the same lift
|
|
1961
|
+
// again.
|
|
1962
|
+
let treeOwnedFold: boolean;
|
|
1963
|
+
let extendedReaders: boolean;
|
|
1964
|
+
let foldedStore: boolean;
|
|
1965
|
+
try {
|
|
1966
|
+
({ treeOwned: treeOwnedFold, extendedReaders, foldedStore } = liftReportsIn(symbolSetting.symbols));
|
|
1967
|
+
} catch (e) {
|
|
1968
|
+
if (abortsRow(symbolSetting.variations)) {
|
|
1969
|
+
throw e;
|
|
1970
|
+
}
|
|
1971
|
+
reportThrow(symbolSetting.variations, e);
|
|
1972
|
+
continue;
|
|
1973
|
+
}
|
|
1732
1974
|
// The signedness variation DECLINES where the pin has nothing to pin. `pinScalarParams` writes only
|
|
1733
1975
|
// over an entry param still `unknown`/`int` that is not one of the recovered pointers/
|
|
1734
1976
|
// aggregates `ptrIdx` excludes; where no param is left, the second pass re-lifts, re-raises and
|
|
@@ -1744,7 +1986,22 @@ export function enumerateCandidates(
|
|
|
1744
1986
|
if (cand.signed && !pinnable) {
|
|
1745
1987
|
break;
|
|
1746
1988
|
}
|
|
1747
|
-
|
|
1989
|
+
let tailSlot = false;
|
|
1990
|
+
const base = liftStamped(
|
|
1991
|
+
name,
|
|
1992
|
+
asm,
|
|
1993
|
+
target,
|
|
1994
|
+
prototypes,
|
|
1995
|
+
opts.asmData,
|
|
1996
|
+
symbolSetting.symbols,
|
|
1997
|
+
opts.contextGlobals,
|
|
1998
|
+
{
|
|
1999
|
+
onTailSlot: () => {
|
|
2000
|
+
tailSlot = true;
|
|
2001
|
+
},
|
|
2002
|
+
},
|
|
2003
|
+
ownContext.ownContextProto,
|
|
2004
|
+
);
|
|
1748
2005
|
// `/setup-args` — pass a prototype-less callee only what the CALLING BLOCK set up; which of
|
|
1749
2006
|
// the two readings the source spelled is genuinely ambiguous, and frontend/ssa.ts
|
|
1750
2007
|
// narrowToSetupArgs carries the argument for why the differ is what settles it.
|
|
@@ -1810,11 +2067,66 @@ export function enumerateCandidates(
|
|
|
1810
2067
|
{ variations: ['setup-args'], narrow: true },
|
|
1811
2068
|
]
|
|
1812
2069
|
: [{ variations: [], narrow: false }];
|
|
2070
|
+
// `/narrow-param` — declare a parameter every reader of which is an extension at the widest
|
|
2071
|
+
// reader's width (raise/paramwidth.ts, EVERY READER AN EXTENSION). A lift variation for the
|
|
2072
|
+
// reason `/connective` is: the raise mutates in place, and the parameter's width is what
|
|
2073
|
+
// recovery and every later stage read. Enumerated only where this symbol-map setting's lift
|
|
2074
|
+
// reports such a parameter; crossed with the other two, and the dedup collapses what agrees.
|
|
2075
|
+
const paramSettings: { variations: readonly Variation[]; extendedReaders: boolean }[] = extendedReaders
|
|
2076
|
+
? [
|
|
2077
|
+
{ variations: [], extendedReaders: false },
|
|
2078
|
+
{ variations: ['narrow-param'], extendedReaders: true },
|
|
2079
|
+
]
|
|
2080
|
+
: [{ variations: [], extendedReaders: false }];
|
|
2081
|
+
// `/kept-slot` — keep the delay slot of a MIPS `b`/`j` whose slot ends a common tail both arms
|
|
2082
|
+
// spell, where the default lift retargets it to the copy before its target and merges the
|
|
2083
|
+
// arms (frontend/mips.ts `normaliseBranchLikely`: the two are one object, and which the source
|
|
2084
|
+
// wrote is the differ's to say). A LIFT variation because the rewrite is the CFG every later
|
|
2085
|
+
// stage reads. Enumerated only where this setting's default lift reports such a slot; crossed
|
|
2086
|
+
// with the others, and the dedup collapses what agrees. An IDO reading: GCC 2.7.2 and KMC
|
|
2087
|
+
// cross-jump a tail both arms spell before they fill a slot, so they leave no such tail.
|
|
2088
|
+
const tailSettings: { variations: readonly Variation[]; keepTailSlots: boolean }[] = tailSlot
|
|
2089
|
+
? [
|
|
2090
|
+
{ variations: [], keepTailSlots: false },
|
|
2091
|
+
{ variations: ['kept-slot'], keepTailSlots: true },
|
|
2092
|
+
]
|
|
2093
|
+
: [{ variations: [], keepTailSlots: false }];
|
|
2094
|
+
// `/far-cast` — keep each store the displaced-access fold would move into a struct on the
|
|
2095
|
+
// address it was reached through (raise/structs.ts `foldDisplacedAccesses`). On agbcc the
|
|
2096
|
+
// member store and the cast store are two objects (target.ts `farMemberStoreOrder`), and a
|
|
2097
|
+
// hoisted pointer the source held builds the cast's. Elsewhere a folded store stops aliasing
|
|
2098
|
+
// the loads beside it and a held pointer is spelled as members, which KMC, GCC 2.7.2 and mwcc
|
|
2099
|
+
// build into other objects. So which one the source wrote is the differ's to say, on every
|
|
2100
|
+
// target. A LIFT variation because the fold decides which struct the stores join, which
|
|
2101
|
+
// recovery and every later stage read. Enumerated only where this setting's lift folded a
|
|
2102
|
+
// store.
|
|
2103
|
+
const castSettings: { variations: readonly Variation[]; keepStores: boolean }[] =
|
|
2104
|
+
foldedStore && offeredOn(target, ['far-cast'])
|
|
2105
|
+
? [
|
|
2106
|
+
{ variations: [], keepStores: false },
|
|
2107
|
+
{ variations: ['far-cast'], keepStores: true },
|
|
2108
|
+
]
|
|
2109
|
+
: [{ variations: [], keepStores: false }];
|
|
1813
2110
|
const liftSettings = narrowSettings.flatMap((l) =>
|
|
1814
|
-
connectiveSettings.
|
|
2111
|
+
connectiveSettings.flatMap((c) =>
|
|
2112
|
+
paramSettings.flatMap((w) =>
|
|
2113
|
+
tailSettings.flatMap((k) =>
|
|
2114
|
+
castSettings.map((f) => ({
|
|
2115
|
+
...l,
|
|
2116
|
+
...c,
|
|
2117
|
+
...w,
|
|
2118
|
+
...k,
|
|
2119
|
+
...f,
|
|
2120
|
+
variations: [...l.variations, ...c.variations, ...w.variations, ...k.variations, ...f.variations],
|
|
2121
|
+
})),
|
|
2122
|
+
),
|
|
2123
|
+
),
|
|
2124
|
+
),
|
|
1815
2125
|
);
|
|
1816
2126
|
for (const liftSetting of liftSettings) {
|
|
1817
2127
|
let fn: Fn;
|
|
2128
|
+
let discardsPassedResult = false;
|
|
2129
|
+
let guessesParamWidth = false;
|
|
1818
2130
|
let inferredSymbols = new Map<string, SymbolInfo>();
|
|
1819
2131
|
let orderLicensed: ReadonlySet<string> = new Set<string>();
|
|
1820
2132
|
try {
|
|
@@ -1823,10 +2135,21 @@ export function enumerateCandidates(
|
|
|
1823
2135
|
fn =
|
|
1824
2136
|
liftSetting.variations.length === 0
|
|
1825
2137
|
? base
|
|
1826
|
-
:
|
|
2138
|
+
: liftStamped(
|
|
2139
|
+
name,
|
|
2140
|
+
asm,
|
|
2141
|
+
target,
|
|
2142
|
+
prototypes,
|
|
2143
|
+
opts.asmData,
|
|
2144
|
+
symbolSetting.symbols,
|
|
2145
|
+
opts.contextGlobals,
|
|
2146
|
+
{ keepTailSlots: liftSetting.keepTailSlots },
|
|
2147
|
+
ownContext.ownContextProto,
|
|
2148
|
+
);
|
|
1827
2149
|
if (liftSetting.narrow && !narrowToSetupArgs(fn)) {
|
|
1828
2150
|
continue; // nothing to cut after all — the default lift's own candidates already cover it
|
|
1829
2151
|
}
|
|
2152
|
+
discardsPassedResult = liftSetting.narrow && setupArgsDiscardsPassedResult(fn);
|
|
1830
2153
|
verify(fn);
|
|
1831
2154
|
// This lift's OWN array-shape evidence, off its own lifted fn (a symbol map promotes
|
|
1832
2155
|
// numeric pool words to `gaddr`, so the `/raw-globals` setting genuinely answers differently).
|
|
@@ -1855,6 +2178,13 @@ export function enumerateCandidates(
|
|
|
1855
2178
|
// declaration. A map that names the symbol an array takes the access to a bare `var`
|
|
1856
2179
|
// base anyway, which carries no licence: the two never meet.
|
|
1857
2180
|
orderLicensed = orderLicensedGlobals(fn, target);
|
|
2181
|
+
// …and the NAMES the map holds for the addresses this lift built by arithmetic off a
|
|
2182
|
+
// named one (raise/offsetnames.ts). Per lift setting, off that setting's own map: the
|
|
2183
|
+
// `/raw-globals` sibling has no map and so no name to reach. AFTER the two readings, and
|
|
2184
|
+
// the staleness that leaves them is priced in pipeline.ts's copy of this note.
|
|
2185
|
+
if (symbolSetting.symbols) {
|
|
2186
|
+
OFFSET_NAME_PASS.run(fn, symbolSetting.symbols, stampBehaviorsFor(target, opts.contextGlobals));
|
|
2187
|
+
}
|
|
1858
2188
|
for (const [n, si] of [...inferredSymbols]) {
|
|
1859
2189
|
if (baseOpts.symbols?.has(n) === true || !sameDerivedShape(declSymbols.get(n), si)) {
|
|
1860
2190
|
inferredSymbols.delete(n);
|
|
@@ -1875,18 +2205,28 @@ export function enumerateCandidates(
|
|
|
1875
2205
|
},
|
|
1876
2206
|
},
|
|
1877
2207
|
prototypes[name],
|
|
1878
|
-
{
|
|
2208
|
+
{
|
|
2209
|
+
shortCircuit: { foldTreeOwned: liftSetting.connective },
|
|
2210
|
+
paramWidth: {
|
|
2211
|
+
extendedReaders: liftSetting.extendedReaders,
|
|
2212
|
+
onUndeclaredWidth: () => {
|
|
2213
|
+
guessesParamWidth = true;
|
|
2214
|
+
},
|
|
2215
|
+
},
|
|
2216
|
+
displaced: { keepStores: liftSetting.keepStores },
|
|
2217
|
+
},
|
|
1879
2218
|
);
|
|
1880
2219
|
} catch (e) {
|
|
1881
2220
|
// THE DEFAULT CARRIES NO VARIATION, by construction: every lift variation appends one, so
|
|
1882
|
-
//
|
|
2221
|
+
// an empty list is the only spelling of "no lift variation is on" that stays correct
|
|
1883
2222
|
// when a fourth is added — the same reason the structure half below reads its table
|
|
1884
2223
|
// instead of naming its flags.
|
|
1885
|
-
|
|
2224
|
+
const settingVariations = [...liftSetting.variations, ...symbolSetting.variations];
|
|
2225
|
+
if (abortsRow(settingVariations)) {
|
|
1886
2226
|
throw e; // the default lift keeps its behavior: a raising failure aborts the row
|
|
1887
2227
|
}
|
|
1888
2228
|
// A dropped variation, never an aborted enumeration — the same posture as `respell`.
|
|
1889
|
-
reportThrow(
|
|
2229
|
+
reportThrow(settingVariations, e);
|
|
1890
2230
|
continue;
|
|
1891
2231
|
}
|
|
1892
2232
|
// THE SHARED-TAIL VARIATIONS: the same raised fn, structured again with `followEarlyReturns`,
|
|
@@ -1894,7 +2234,8 @@ export function enumerateCandidates(
|
|
|
1894
2234
|
// - `/shared-ret` is the follow ALONE, on the fn as raised. Some divergent `if` shares a
|
|
1895
2235
|
// `ret` the compiler left in place (`synthetic:gcseinner`).
|
|
1896
2236
|
// - `/shared-tail` is the follow after `sinkStoreTails` has rewritten the fn in place, which
|
|
1897
|
-
// is safe because `structure()`
|
|
2237
|
+
// is safe because `structure()` writes nothing on `fn` but the `declared` stamps, which it
|
|
2238
|
+
// re-derives from its map on every call, so the earlier passes are done with it.
|
|
1898
2239
|
// It is enumerated only where the sink changed something AND some divergent `if` of the
|
|
1899
2240
|
// SUNK fn shares a `ret`. That is the sink's price gate: without it, a tail copied into
|
|
1900
2241
|
// arms no `if` shares adds a candidate spelling it in each — 204 synthetic candidates on 8
|
|
@@ -1930,6 +2271,8 @@ export function enumerateCandidates(
|
|
|
1930
2271
|
// `droppedDefault` is the DEFAULT pass's drops. Each shared-tail pass reads it and keeps its
|
|
1931
2272
|
// own drops in a copy, so one pass's structuring failure never removes the other's candidate.
|
|
1932
2273
|
const droppedDefault = new Set<string>();
|
|
2274
|
+
// What `assertDefaultAccepts` learned about this fn, shared by every setting structured from it.
|
|
2275
|
+
let acceptsMemo = new DefaultAcceptsMemo(fn);
|
|
1933
2276
|
for (const pass of ['default', 'follow', 'sink'] as const) {
|
|
1934
2277
|
if (pass === 'follow' && !hasDivergentSharedRet(fn)) {
|
|
1935
2278
|
continue;
|
|
@@ -1938,11 +2281,13 @@ export function enumerateCandidates(
|
|
|
1938
2281
|
let sunk: boolean;
|
|
1939
2282
|
try {
|
|
1940
2283
|
sunk = sinkStoreTails(fn);
|
|
2284
|
+
// The sink rewrites fn in place, so nothing the memo learned before it holds after it.
|
|
2285
|
+
acceptsMemo = new DefaultAcceptsMemo(fn);
|
|
1941
2286
|
if (sunk) {
|
|
1942
2287
|
verify(fn);
|
|
1943
2288
|
}
|
|
1944
2289
|
} catch (e) {
|
|
1945
|
-
reportThrow([...liftSetting.variations, 'shared-tail'], e);
|
|
2290
|
+
reportThrow([...liftSetting.variations, 'shared-tail', ...symbolSetting.variations], e);
|
|
1946
2291
|
break;
|
|
1947
2292
|
}
|
|
1948
2293
|
// Unsunk, this fn is the `/shared-ret` pass's again.
|
|
@@ -1996,30 +2341,39 @@ export function enumerateCandidates(
|
|
|
1996
2341
|
dropped.add(key);
|
|
1997
2342
|
continue;
|
|
1998
2343
|
}
|
|
1999
|
-
// structure()
|
|
2000
|
-
// senses structure the same
|
|
2344
|
+
// structure() produces a fresh SFn and writes nothing on `fn` but the `declared` stamps,
|
|
2345
|
+
// which it re-derives from its map on every call, so both branch senses structure the same
|
|
2346
|
+
// recovered function without re-lifting.
|
|
2001
2347
|
let sfn: SFn;
|
|
2002
2348
|
try {
|
|
2003
|
-
sfn = structureChecked(
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2349
|
+
sfn = structureChecked(
|
|
2350
|
+
fn,
|
|
2351
|
+
{
|
|
2352
|
+
...symbolSettingOpts,
|
|
2353
|
+
...(inferredSymbols.size ? { inferredSymbols } : {}),
|
|
2354
|
+
...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
|
|
2355
|
+
preserveDivergentBranchSense: s.sense,
|
|
2356
|
+
negateJoinedBranchSense: s.join ? !defSense : defSense,
|
|
2357
|
+
...(s.flipSites ? { branchSenseFlipSites: s.flipSites } : {}),
|
|
2358
|
+
anchorConstCopies: s.anchor,
|
|
2359
|
+
anchorLoopEntryConsts: s.entry,
|
|
2360
|
+
spellBitfieldMembers: s.bitfields,
|
|
2361
|
+
spellPtrMemberElements: s.ptrElems,
|
|
2362
|
+
spellDeclaredSubscripts: s.declRank,
|
|
2363
|
+
...STRUCTURE_VARIATIONS.reduce(
|
|
2364
|
+
(acc, variation) => ({ ...acc, ...variation.options(s[variation.flag]) }),
|
|
2365
|
+
{},
|
|
2366
|
+
),
|
|
2367
|
+
...(alternative ? { followEarlyReturns: true } : {}),
|
|
2368
|
+
},
|
|
2369
|
+
undefined,
|
|
2370
|
+
acceptsMemo,
|
|
2371
|
+
);
|
|
2021
2372
|
} catch (e) {
|
|
2022
|
-
if (
|
|
2373
|
+
if (e instanceof DefaultAcceptsMemoMisuse) {
|
|
2374
|
+
throw e; // a memo wired to the wrong fn is a defect here, never a dropped variation
|
|
2375
|
+
}
|
|
2376
|
+
if (abortsRow([...liftVariations, ...symbolSetting.variations], s)) {
|
|
2023
2377
|
throw e; // the default lift's default setting keeps its behavior: a failure aborts the row
|
|
2024
2378
|
}
|
|
2025
2379
|
// Recorded for EVERY dropped setting: a candidate with more variations on looks its siblings
|
|
@@ -2027,7 +2381,7 @@ export function enumerateCandidates(
|
|
|
2027
2381
|
dropped.add(key);
|
|
2028
2382
|
// an anchored setting that fails structuring or its contracts is a dropped variation, never
|
|
2029
2383
|
// an aborted enumeration — same rule as respell below
|
|
2030
|
-
reportThrow([...liftVariations, ...s.variations], e);
|
|
2384
|
+
reportThrow([...liftVariations, ...s.variations, ...symbolSetting.variations], e);
|
|
2031
2385
|
continue;
|
|
2032
2386
|
}
|
|
2033
2387
|
// A TREE another structure setting already produced. `respellTree` reads the tree and this
|
|
@@ -2092,7 +2446,10 @@ export function enumerateCandidates(
|
|
|
2092
2446
|
assertResolved(made);
|
|
2093
2447
|
assertDerefsTyped(made);
|
|
2094
2448
|
assertLocalsWritten(made);
|
|
2449
|
+
assertPostIncrUnshared(made);
|
|
2095
2450
|
assertNoOrphanedLocals(sfn, made);
|
|
2451
|
+
assertLocalStorageStaysInteger(sfn, made);
|
|
2452
|
+
assertFrameUnionsViewed(made);
|
|
2096
2453
|
// A backend refusal on this REWRITTEN tree is not a refusal of the row's own
|
|
2097
2454
|
// tree, so it never becomes the row's stated cause: `TreeSources.emit` is dropped
|
|
2098
2455
|
// here and only the call over the row's own tree above records one.
|
|
@@ -2153,6 +2510,16 @@ export function enumerateCandidates(
|
|
|
2153
2510
|
// whichever route the enumeration happened to walk first.
|
|
2154
2511
|
if (sp.matchOnly === undefined) {
|
|
2155
2512
|
delete dup.matchOnly;
|
|
2513
|
+
delete dup.signedUnsignedCompare;
|
|
2514
|
+
}
|
|
2515
|
+
if (!discardsPassedResult) {
|
|
2516
|
+
delete dup.discardsPassedResult;
|
|
2517
|
+
}
|
|
2518
|
+
if (!guessesParamWidth) {
|
|
2519
|
+
delete dup.guessesParamWidth;
|
|
2520
|
+
}
|
|
2521
|
+
if (!liftSetting.keepStores) {
|
|
2522
|
+
delete dup.farStoreCast;
|
|
2156
2523
|
}
|
|
2157
2524
|
continue;
|
|
2158
2525
|
}
|
|
@@ -2175,7 +2542,11 @@ export function enumerateCandidates(
|
|
|
2175
2542
|
preference: symbolIndex,
|
|
2176
2543
|
...(sp.symbolRefs ? { symbolRefs: sp.symbolRefs } : {}),
|
|
2177
2544
|
...(sp.deviceVolatile ? { deviceVolatile: sp.deviceVolatile } : {}),
|
|
2178
|
-
...(sp.matchOnly ? { matchOnly:
|
|
2545
|
+
...(sp.matchOnly ? { matchOnly: true as const } : {}),
|
|
2546
|
+
...(sp.signedUnsignedCompare ? { signedUnsignedCompare: true as const } : {}),
|
|
2547
|
+
...(discardsPassedResult ? { discardsPassedResult: true as const } : {}),
|
|
2548
|
+
...(guessesParamWidth ? { guessesParamWidth: true as const } : {}),
|
|
2549
|
+
...(liftSetting.keepStores ? { farStoreCast: true as const } : {}),
|
|
2179
2550
|
};
|
|
2180
2551
|
seen.set(source, made);
|
|
2181
2552
|
out.push(made);
|
|
@@ -2200,36 +2571,75 @@ export function enumerateCandidates(
|
|
|
2200
2571
|
/** Score each candidate with the injected `scoreFn` and rank by score (lowest first). A candidate
|
|
2201
2572
|
* whose `scoreFn` throws — e.g. its C failed to compile — is SKIPPED so it cannot sink a sibling
|
|
2202
2573
|
* that compiles and matches; only if EVERY candidate fails is the failure surfaced. Synchronous:
|
|
2203
|
-
* the scorer must be sync (the cli/Node objdiff path).
|
|
2204
|
-
* its
|
|
2205
|
-
*
|
|
2574
|
+
* the scorer must be sync (the cli/Node objdiff path). An async driver scores on its own schedule
|
|
2575
|
+
* and then ranks through here against its memoized outcomes (the pooled CLI driver, the webapp).
|
|
2576
|
+
*
|
|
2577
|
+
* A STILLBORN fan (stillborn.ts) is not compiled to the end: `scoreFn` is called for the default
|
|
2578
|
+
* candidate and the probes, and the rest ride on the thrown error as `notCompiled`. A `scoreFn`
|
|
2579
|
+
* that wants the rule to read its refusals throws `CompilerRejection`; any other throw ranks the
|
|
2580
|
+
* whole fan. */
|
|
2206
2581
|
export function rankBy<S extends { score: number; rows?: number }>(
|
|
2207
2582
|
candidates: Candidate[],
|
|
2208
2583
|
symbol: string,
|
|
2209
2584
|
scoreFn: (source: string, symbol: string, candidate: Candidate) => S,
|
|
2210
2585
|
): RankedResult<S> {
|
|
2586
|
+
const outcomes = new Map<number, { score: S } | { thrown: unknown }>();
|
|
2587
|
+
const outcomeAt = (i: number): { score: S } | { thrown: unknown } => {
|
|
2588
|
+
let outcome = outcomes.get(i);
|
|
2589
|
+
if (outcome === undefined) {
|
|
2590
|
+
try {
|
|
2591
|
+
outcome = { score: scoreFn(candidates[i].source, symbol, candidates[i]) };
|
|
2592
|
+
} catch (e) {
|
|
2593
|
+
outcome = { thrown: e };
|
|
2594
|
+
}
|
|
2595
|
+
outcomes.set(i, outcome);
|
|
2596
|
+
}
|
|
2597
|
+
return outcome;
|
|
2598
|
+
};
|
|
2599
|
+
const stillborn = stillbornVerdict(candidates, (i) => {
|
|
2600
|
+
const outcome = outcomeAt(i);
|
|
2601
|
+
return 'score' in outcome ? 'compiled' : outcome;
|
|
2602
|
+
});
|
|
2603
|
+
if (stillborn !== null) {
|
|
2604
|
+
const thrownAt = (i: number): unknown => {
|
|
2605
|
+
const outcome = outcomeAt(i);
|
|
2606
|
+
if ('score' in outcome) {
|
|
2607
|
+
throw new Error(`internal: a stillborn fan holds a candidate that scored (${i})`);
|
|
2608
|
+
}
|
|
2609
|
+
return outcome.thrown;
|
|
2610
|
+
};
|
|
2611
|
+
throw new NoScorableCandidateError(
|
|
2612
|
+
`no scorable candidate for '${symbol}': ${stillbornNote(stillborn, candidates.length)}\n` +
|
|
2613
|
+
`The default candidate's compile: ${fullMessage(thrownAt(0))}`,
|
|
2614
|
+
stillborn.compiled.map((i) => ({ variations: candidates[i].variations, error: firstLine(thrownAt(i)) })),
|
|
2615
|
+
[],
|
|
2616
|
+
stillborn.notCompiled.map((i) => ({ variations: candidates[i].variations, source: candidates[i].source })),
|
|
2617
|
+
{ cause: thrownAt(0) },
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2211
2620
|
const results: (Scored<S> & { order: number })[] = [];
|
|
2212
2621
|
const dropped: DroppedCandidate[] = []; // candidates that failed to build; only fatal if ALL do
|
|
2213
2622
|
const withheld: WithheldCandidate[] = []; // candidates that built but did not earn publication
|
|
2214
2623
|
let lastScoreErr: unknown = null;
|
|
2215
2624
|
candidates.forEach((c, order) => {
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
variations: c.variations,
|
|
2222
|
-
score: score.score,
|
|
2223
|
-
...(score.rows === undefined ? {} : { rows: score.rows }),
|
|
2224
|
-
why,
|
|
2225
|
-
});
|
|
2226
|
-
return;
|
|
2227
|
-
}
|
|
2228
|
-
results.push({ ...c, order, score });
|
|
2229
|
-
} catch (e) {
|
|
2230
|
-
lastScoreErr = e;
|
|
2231
|
-
dropped.push({ variations: c.variations, error: firstLine(e) });
|
|
2625
|
+
const outcome = outcomeAt(order);
|
|
2626
|
+
if ('thrown' in outcome) {
|
|
2627
|
+
lastScoreErr = outcome.thrown;
|
|
2628
|
+
dropped.push({ variations: c.variations, error: firstLine(outcome.thrown) });
|
|
2629
|
+
return;
|
|
2232
2630
|
}
|
|
2631
|
+
const { score } = outcome;
|
|
2632
|
+
const why = withheldReason(c, score);
|
|
2633
|
+
if (why !== null) {
|
|
2634
|
+
withheld.push({
|
|
2635
|
+
variations: c.variations,
|
|
2636
|
+
score: score.score,
|
|
2637
|
+
...(score.rows === undefined ? {} : { rows: score.rows }),
|
|
2638
|
+
why,
|
|
2639
|
+
});
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
results.push({ ...c, order, score });
|
|
2233
2643
|
});
|
|
2234
2644
|
if (results.length === 0) {
|
|
2235
2645
|
// Naming the withheld count matters here: "no scorable candidate" with a null cause reads as a
|
|
@@ -2240,7 +2650,7 @@ export function rankBy<S extends { score: number; rows?: number }>(
|
|
|
2240
2650
|
// above stays one line each — it is a roster, not a diagnosis.
|
|
2241
2651
|
const why =
|
|
2242
2652
|
lastScoreErr !== null ? fullMessage(lastScoreErr) : `${withheld.length} candidate(s) withheld, none scored`;
|
|
2243
|
-
throw new NoScorableCandidateError(`no scorable candidate for '${symbol}': ${why}`, dropped, withheld, {
|
|
2653
|
+
throw new NoScorableCandidateError(`no scorable candidate for '${symbol}': ${why}`, dropped, withheld, [], {
|
|
2244
2654
|
cause: lastScoreErr,
|
|
2245
2655
|
});
|
|
2246
2656
|
}
|
|
@@ -2248,15 +2658,37 @@ export function rankBy<S extends { score: number; rows?: number }>(
|
|
|
2248
2658
|
return { winner: results[0], candidates: results.map(({ order: _order, ...c }) => c), dropped, withheld };
|
|
2249
2659
|
}
|
|
2250
2660
|
|
|
2251
|
-
/**
|
|
2252
|
-
*
|
|
2253
|
-
*
|
|
2254
|
-
*
|
|
2661
|
+
/** The sentence a stillborn fan's error OPENS on: how much of the fan was never compiled, on what
|
|
2662
|
+
* evidence, and the errors each half's compiled candidates kept. The DEFAULT candidate's own
|
|
2663
|
+
* diagnostic — the spelling the rule is anchored on, rather than whichever was refused last —
|
|
2664
|
+
* follows it, so that a printer bounding a long diagnostic keeps the verdict. */
|
|
2665
|
+
function stillbornNote(stillborn: Stillborn, fan: number): string {
|
|
2666
|
+
const tally = new Map<string, number>();
|
|
2667
|
+
for (const m of stillborn.messages) {
|
|
2668
|
+
tally.set(m, (tally.get(m) ?? 0) + 1);
|
|
2669
|
+
}
|
|
2670
|
+
const shared = [...tally].map(([m, n]) => ` ${m}${n > 1 ? ` (x${n})` : ''}`).join('\n');
|
|
2671
|
+
return (
|
|
2672
|
+
`${stillborn.notCompiled.length} of ${fan} candidates were NOT COMPILED: in each signedness half, the default ` +
|
|
2673
|
+
`candidate and one probe per variation (${stillborn.compiled.length} compiled) were all rejected, every probe ` +
|
|
2674
|
+
`keeping one of its default's errors:\n${shared}`
|
|
2675
|
+
);
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
/** THE candidate ordering — score, then semantics, then preference, then readability, then enumeration
|
|
2679
|
+
* order. Exported because it is asked outside `rankBy` too — the benchmark's sweep driver and
|
|
2680
|
+
* fan runner order what they read back — and a second copy would let the same input produce two
|
|
2681
|
+
* different winners.
|
|
2255
2682
|
*
|
|
2256
2683
|
* SCORE dominates absolutely: the differ is the fitness function, and a tie means the variation
|
|
2257
2684
|
* that separates these two candidates did not change the bytes — so everything below only chooses what
|
|
2258
2685
|
* the READER sees, and can never cost a match.
|
|
2259
2686
|
*
|
|
2687
|
+
* A DISCARDED PASSED RESULT next (`Candidate.discardsPassedResult`): equal bytes refute neither
|
|
2688
|
+
* reading, so the one that drops a value the callee may read wins only where it scores strictly
|
|
2689
|
+
* better. A GUESSED PARAMETER WIDTH (`Candidate.guessesParamWidth`) likewise: a narrow declaration
|
|
2690
|
+
* no prototype supplied changes its callers' bytes, which no score here reads.
|
|
2691
|
+
*
|
|
2260
2692
|
* PREFERENCE next: a named symbol-map spelling beats its `/raw-globals` sibling at equal bytes.
|
|
2261
2693
|
*
|
|
2262
2694
|
* DEVICE VOLATILITY next: at equal bytes, the spelling that qualifies a DEVICE REGISTER
|
|
@@ -2274,7 +2706,14 @@ export function rankBy<S extends { score: number; rows?: number }>(
|
|
|
2274
2706
|
* with no score and no outcome moving. That is a judgement about the source rather than a
|
|
2275
2707
|
* measurement of it — the differ never refereed those six — and it is the same judgement this
|
|
2276
2708
|
* term was declared to make, taken on the same evidence. What it must never do is change WHICH
|
|
2277
|
-
* candidates exist; that stays an admission question, one variation at a time.
|
|
2709
|
+
* candidates exist; that stays an admission question, one variation at a time. The CAST COUNT
|
|
2710
|
+
* below does the same for `/narrow-decl`, which drops one cast per local it narrows and so wins
|
|
2711
|
+
* every tie against the candidate it is derived onto.
|
|
2712
|
+
*
|
|
2713
|
+
* A FAR STORE SPELLED THROUGH A CAST (`Candidate.farStoreCast`) next: where `/far-cast` ties the
|
|
2714
|
+
* member spelling, the compiler built the member's object too, and the member names the field. Its
|
|
2715
|
+
* `(u8 *)` base is no scalar cast and its line count is no higher, so neither term below would
|
|
2716
|
+
* prefer the member on its own.
|
|
2278
2717
|
*
|
|
2279
2718
|
* CAST COUNT next, and only WITHIN a preference. A wrong signedness pin is what manufactures casts —
|
|
2280
2719
|
* the C backend has to cast a shift operand back to the signedness the machine op needs, so
|
|
@@ -2293,20 +2732,22 @@ export function rankBy<S extends { score: number; rows?: number }>(
|
|
|
2293
2732
|
* result deterministic. Spelled explicitly rather than leaning on Array#sort's stability, which
|
|
2294
2733
|
* would make each preference an accident of two unrelated decisions.
|
|
2295
2734
|
*
|
|
2296
|
-
* WHAT NO TERM HERE WEIGHS: a comparison's rendered SIGNEDNESS.
|
|
2297
|
-
*
|
|
2298
|
-
*
|
|
2299
|
-
*
|
|
2300
|
-
*
|
|
2301
|
-
* comparator carries; `deviceVolatile` is the shape such a term would take. */
|
|
2735
|
+
* WHAT NO TERM HERE WEIGHS: a comparison's rendered SIGNEDNESS. It needs none: every tree a
|
|
2736
|
+
* source is emitted from is judged for an unsigned compare that renders signed
|
|
2737
|
+
* (`Candidate.signedUnsignedCompare`), whichever variation spelled it so, and such a source is
|
|
2738
|
+
* `matchOnly` — on a nonmatch it is withheld before this comparator sees it, whatever it scores,
|
|
2739
|
+
* and on a match both spellings are the program. */
|
|
2302
2740
|
export function compareScored<S extends { score: number }>(
|
|
2303
2741
|
a: Candidate & { score: S; order: number },
|
|
2304
2742
|
b: Candidate & { score: S; order: number },
|
|
2305
2743
|
): number {
|
|
2306
2744
|
return (
|
|
2307
2745
|
a.score.score - b.score.score ||
|
|
2746
|
+
(a.discardsPassedResult ? 1 : 0) - (b.discardsPassedResult ? 1 : 0) ||
|
|
2747
|
+
(a.guessesParamWidth ? 1 : 0) - (b.guessesParamWidth ? 1 : 0) ||
|
|
2308
2748
|
a.preference - b.preference ||
|
|
2309
2749
|
(b.deviceVolatile ?? 0) - (a.deviceVolatile ?? 0) ||
|
|
2750
|
+
(a.farStoreCast ? 1 : 0) - (b.farStoreCast ? 1 : 0) ||
|
|
2310
2751
|
castCount(a.source) - castCount(b.source) ||
|
|
2311
2752
|
lineCount(a.source) - lineCount(b.source) ||
|
|
2312
2753
|
a.order - b.order
|