@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/trace.ts
CHANGED
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
// trail), browser-pure. Runs the SAME stage sequence as pipeline.ts and captures a TraceReport:
|
|
3
3
|
// per-stage IR dumps (each post-verify) and per-pattern before/after events. Scoring lives on
|
|
4
4
|
// the other side of the seam: @asmlift/cli/report enriches this into the full DecompileReport
|
|
5
|
-
// (objdiff score, per-pattern score deltas
|
|
5
|
+
// (objdiff score, per-pattern score deltas over the `probeSource` hook's sources, ranked candidates) when a
|
|
6
6
|
// target object is available; the web playground renders the TraceReport as-is.
|
|
7
7
|
import { cBackend } from './backend/c';
|
|
8
8
|
import type { CodegenProfile } from './codegen-flags';
|
|
9
9
|
import type { AsmData } from './frontend/asmdata';
|
|
10
|
-
import { frontendFor } from './frontend/registry';
|
|
11
10
|
import type { Fn } from './ir/core';
|
|
12
11
|
import { print } from './ir/print';
|
|
13
12
|
import { verify } from './ir/verify';
|
|
14
13
|
import type { LanguageBackend } from './l3/ast';
|
|
15
14
|
import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
|
|
16
|
-
import { type OnGap, raiseRecovered, structureChecked, stubResult } from './pipeline';
|
|
17
|
-
import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
15
|
+
import { type OnGap, liftStamped, raiseRecovered, stampBehaviorsFor, structureChecked, stubResult } from './pipeline';
|
|
16
|
+
import { type FnProto, type Prototypes, declaresVoidReturn, prototypesFromSymbols } from './proto';
|
|
17
|
+
import { type ContextGlobals, splitOwnContext } from './proto-context';
|
|
18
18
|
import { assumedShapes, inferGlobalArrays, orderLicensedGlobals } from './raise/globalshape';
|
|
19
|
+
import { OFFSET_NAME_PASS } from './raise/offsetnames';
|
|
19
20
|
import { type SymbolInfo, type SymbolMap, symbolsByName } from './symbols';
|
|
20
21
|
import { type ResolvedTarget, type TargetDescription, type ToolchainId, structureOptionsFor } from './target';
|
|
21
22
|
|
|
@@ -39,9 +40,9 @@ export interface PatternEvent {
|
|
|
39
40
|
hits: number;
|
|
40
41
|
beforeIr: string;
|
|
41
42
|
afterIr: string;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
/** The source a `probeSource` hook emitted at this pattern's boundaries (the cli report scores them). */
|
|
44
|
+
sourceBefore?: string;
|
|
45
|
+
sourceAfter?: string;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export interface TraceReport {
|
|
@@ -92,15 +93,18 @@ export interface TraceOptions {
|
|
|
92
93
|
prototypes?: Prototypes; // header facts (callee arities + void-ness), keyed by symbol
|
|
93
94
|
asmData?: AsmData; // data-section side table (Regime-B jump tables), as in decompile()
|
|
94
95
|
symbols?: SymbolMap; // address→symbol map (symbols.ts), as in decompile(); absent ⇒ inert
|
|
96
|
+
contextGlobals?: ContextGlobals; // the declaration context's globals, as in decompile()
|
|
97
|
+
ownContextProto?: FnProto; // the context's own entry, as in decompile()
|
|
95
98
|
onGap?: OnGap; // "strict" (default) | "annotate", as in decompile()
|
|
96
|
-
/**
|
|
97
|
-
* pattern N's after-
|
|
99
|
+
/** Source probe at pattern boundaries: the source asmlift would emit there, which the cli report
|
|
100
|
+
* scores. One call per boundary: pattern N's after-source is pattern N+1's before-source. Absent ⇒
|
|
101
|
+
* the source fields stay unset.
|
|
98
102
|
*
|
|
99
103
|
* THE DERIVED ARRAY SHAPES TRAVEL WITH THE CLONE: they are read once off the lift
|
|
100
104
|
* (`stage:globalshape`) and change the default spelling of every indexed global, so a probe
|
|
101
105
|
* that structures without them attributes its per-pattern delta to a source the main path does
|
|
102
106
|
* not emit. */
|
|
103
|
-
|
|
107
|
+
probeSource?: (fn: Fn, inferredSymbols: Map<string, SymbolInfo>) => string | undefined;
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
// The report's per-pass trace stage id + title, keyed by the shared PreRecoveryPass.id. Kept HERE
|
|
@@ -114,6 +118,12 @@ const PRE_RECOVERY_TRACE: Record<string, { stage: string; title: (result: number
|
|
|
114
118
|
const: { stage: 'stage:const', title: () => 'Const materialize (lui;ori → one 32-bit const)' },
|
|
115
119
|
magicdiv: { stage: 'stage:magicdiv', title: () => 'Magic-number division recovery (mulh/mulhu → sdiv/udiv)' },
|
|
116
120
|
softdiv: { stage: 'stage:softdiv', title: () => 'Soft-division lower (bl __divsi3 → division op)' },
|
|
121
|
+
floathelpers: {
|
|
122
|
+
stage: 'stage:floathelpers',
|
|
123
|
+
title: () => 'Soft-float lower (bl __adddf3 → float op, a declared double, a literal)',
|
|
124
|
+
},
|
|
125
|
+
widehelpers: { stage: 'stage:widehelpers', title: () => '64-bit helper lower (bl __muldi3 → 64-bit op)' },
|
|
126
|
+
pairparams: { stage: 'stage:pairparams', title: () => 'Parameter pairs (two argument registers → one s64)' },
|
|
117
127
|
arrays: { stage: 'stage:legalize', title: (r) => `Array legalize (${r} scaled access(es) → aload/astore)` },
|
|
118
128
|
structs: { stage: 'stage:structs', title: () => 'Struct-pointer recovery (access-pattern evidence)' },
|
|
119
129
|
shortcircuit: { stage: 'stage:shortcircuit', title: () => 'Short-circuit recovery (boolean && / ||)' },
|
|
@@ -182,14 +192,24 @@ function traceTower(
|
|
|
182
192
|
// Merged exactly as pipeline.ts does: the project's own DWARF signatures fill in what the caller
|
|
183
193
|
// did not state. A trace that lifted from a different table would explain a run that never
|
|
184
194
|
// happened.
|
|
185
|
-
const
|
|
186
|
-
const
|
|
195
|
+
const ownContext = splitOwnContext(opts.prototypes, name, opts.ownContextProto);
|
|
196
|
+
const prototypes = prototypesFromSymbols(opts.symbols, ownContext.prototypes ?? {});
|
|
197
|
+
const returnsVoid = declaresVoidReturn(prototypes[name]);
|
|
187
198
|
const trace: StageTrace[] = [];
|
|
188
199
|
const patternEvents: PatternEvent[] = [];
|
|
189
200
|
|
|
190
201
|
// (1) lift → typed-SSA IR
|
|
191
|
-
const fn =
|
|
192
|
-
|
|
202
|
+
const fn = liftStamped(
|
|
203
|
+
name,
|
|
204
|
+
asm,
|
|
205
|
+
target,
|
|
206
|
+
prototypes,
|
|
207
|
+
opts.asmData,
|
|
208
|
+
opts.symbols,
|
|
209
|
+
opts.contextGlobals,
|
|
210
|
+
undefined,
|
|
211
|
+
ownContext.ownContextProto,
|
|
212
|
+
);
|
|
193
213
|
trace.push({ id: 'stage:lift', title: 'Lift (ISA frontend → typed-SSA IR)', irDump: irDump(fn), verified: true });
|
|
194
214
|
// The array shapes the assembly evidences, off the LIFTED fn — same reading, same reason, as
|
|
195
215
|
// pipeline.ts's runTower: the fold and the tower below destroy the order the licence reads.
|
|
@@ -212,13 +232,30 @@ function traceTower(
|
|
|
212
232
|
.join('; '),
|
|
213
233
|
});
|
|
214
234
|
|
|
215
|
-
//
|
|
235
|
+
// The names the map holds for addresses this machine code built by arithmetic off a named one
|
|
236
|
+
// (raise/offsetnames.ts), off the same lifted fn and for the same reason as pipeline.ts's
|
|
237
|
+
// runTower.
|
|
238
|
+
const named = opts.symbols
|
|
239
|
+
? OFFSET_NAME_PASS.run(fn, opts.symbols, stampBehaviorsFor(target, opts.contextGlobals))
|
|
240
|
+
: [];
|
|
241
|
+
trace.push({
|
|
242
|
+
id: 'stage:offsetnames',
|
|
243
|
+
title: 'Name a walked-to address the symbol map knows',
|
|
244
|
+
irDump: irDump(fn),
|
|
245
|
+
verified: true,
|
|
246
|
+
note:
|
|
247
|
+
named.length === 0
|
|
248
|
+
? 'no address built off a named one lands on a symbol the map names'
|
|
249
|
+
: `${named.length} address${named.length === 1 ? '' : 'es'} named: ${named.join(', ')}`,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// (2) idiom fold (capability-gated), with an optional probed source per pattern boundary —
|
|
216
253
|
// the SAME default set as decompile()/decompileRanked
|
|
217
254
|
const active = (opts.patterns ?? DEFAULT_IDIOM_PATTERNS).filter((p) => patternApplies(p, target));
|
|
218
|
-
// Probe economy: ONE probe per pattern boundary — pattern N's after-
|
|
219
|
-
// before-
|
|
220
|
-
//
|
|
221
|
-
let
|
|
255
|
+
// Probe economy: ONE probe per pattern boundary — pattern N's after-source IS pattern N+1's
|
|
256
|
+
// before-source (the state is identical), and each probe costs a full clone + tower. Zero-hit
|
|
257
|
+
// patterns emit NO event (the IR is unchanged).
|
|
258
|
+
let sourceBefore = opts.probeSource?.(fn, inferredSymbols);
|
|
222
259
|
for (const p of active) {
|
|
223
260
|
const beforeIr = irDump(fn);
|
|
224
261
|
const hits = applyPattern(fn, p, target);
|
|
@@ -228,18 +265,17 @@ function traceTower(
|
|
|
228
265
|
continue;
|
|
229
266
|
}
|
|
230
267
|
const afterIr = irDump(fn);
|
|
231
|
-
const
|
|
268
|
+
const sourceAfter = opts.probeSource?.(fn, inferredSymbols);
|
|
232
269
|
patternEvents.push({
|
|
233
270
|
id: `pattern:${p.id}`,
|
|
234
271
|
patternId: p.id,
|
|
235
272
|
hits,
|
|
236
273
|
beforeIr,
|
|
237
274
|
afterIr,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
scoreDelta: scoreBefore !== undefined && scoreAfter !== undefined ? scoreAfter - scoreBefore : undefined,
|
|
275
|
+
sourceBefore,
|
|
276
|
+
sourceAfter,
|
|
241
277
|
});
|
|
242
|
-
|
|
278
|
+
sourceBefore = sourceAfter;
|
|
243
279
|
}
|
|
244
280
|
if (active.length) {
|
|
245
281
|
trace.push({
|
|
@@ -282,10 +318,10 @@ function traceTower(
|
|
|
282
318
|
irDump: irDump(fn),
|
|
283
319
|
verified: true,
|
|
284
320
|
}),
|
|
285
|
-
|
|
321
|
+
afterFold: (fold) =>
|
|
286
322
|
trace.push({
|
|
287
|
-
id:
|
|
288
|
-
title:
|
|
323
|
+
id: `stage:${fold.id}`,
|
|
324
|
+
title: fold.title,
|
|
289
325
|
irDump: irDump(fn),
|
|
290
326
|
verified: true,
|
|
291
327
|
}),
|
|
@@ -297,10 +333,12 @@ function traceTower(
|
|
|
297
333
|
// spelled as a loud ASMLIFT_ERROR marker (annotate) — same onGap option as decompile()
|
|
298
334
|
const mapSymbols = opts.symbols ? symbolsByName(opts.symbols) : undefined;
|
|
299
335
|
const sfn = structureChecked(fn, {
|
|
300
|
-
...structureOptionsFor(target, returnsVoid),
|
|
336
|
+
...structureOptionsFor(target, returnsVoid, prototypes),
|
|
301
337
|
spellSwitchFallthrough: (opts.backend ?? cBackend).spellsSwitchFallthrough,
|
|
338
|
+
spellSwitchBreak: (opts.backend ?? cBackend).spellsSwitchBreak,
|
|
302
339
|
onGap: opts.onGap ?? 'strict',
|
|
303
340
|
...(mapSymbols ? { symbols: mapSymbols } : {}),
|
|
341
|
+
...(opts.contextGlobals ? { contextGlobals: opts.contextGlobals } : {}),
|
|
304
342
|
...(inferredSymbols.size ? { inferredSymbols } : {}),
|
|
305
343
|
...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
|
|
306
344
|
});
|
|
@@ -64,6 +64,11 @@ export const READER_WORDS: readonly ReaderWord[] = [
|
|
|
64
64
|
word: 'withheld',
|
|
65
65
|
meaning: 'A candidate that compiled and scored, but was refused publication for want of a byte-exact proof.',
|
|
66
66
|
},
|
|
67
|
+
{
|
|
68
|
+
word: 'not compiled',
|
|
69
|
+
meaning:
|
|
70
|
+
"A candidate never handed to the scorer: its fan was stillborn — in each signedness half, the default candidate and one probe per variation were all rejected, every probe keeping one of its default's errors, so the rest was not compiled.",
|
|
71
|
+
},
|
|
67
72
|
];
|
|
68
73
|
|
|
69
74
|
export interface VariationKindDefinition {
|
|
@@ -91,7 +96,7 @@ export const VARIATION_KIND_DEFINITIONS: { readonly [K in VariationKind]: Variat
|
|
|
91
96
|
title: 'Structure',
|
|
92
97
|
meaning:
|
|
93
98
|
'How the checked control flow becomes C: which way a test reads, where a loop starts, what reaches a merge (the point where two paths meet), where a value is kept, and how a read or a compare is spelled.',
|
|
94
|
-
examples: ['flip-branch', 'defsite', 'loop-entry', 'reread-globals', '
|
|
99
|
+
examples: ['flip-branch', 'defsite', 'loop-entry', 'reread-globals', 'signed-cmp'],
|
|
95
100
|
},
|
|
96
101
|
respell: {
|
|
97
102
|
title: 'Respell',
|
|
@@ -195,6 +200,18 @@ export const TARGET_BEHAVIOR_READINGS: { readonly [B in GatingBehavior]: TargetB
|
|
|
195
200
|
'the span is how far one instruction can add to a base register, a fact of the instruction set that no pair of spellings decides',
|
|
196
201
|
},
|
|
197
202
|
},
|
|
203
|
+
pointerIntConversionIsFree: {
|
|
204
|
+
reads: 'builds a word global read or stored through a pointer conversion into the integer spelling',
|
|
205
|
+
witness: {
|
|
206
|
+
compiler: 'agbcc',
|
|
207
|
+
unit: 'extern u32 g; extern u32 gLimit; void sink(void); u32 example(u32 a) { @ }',
|
|
208
|
+
spellings: [
|
|
209
|
+
'*(s32 *)g = a; g = (void *)((u8 *)g + 4); sink(); return (gLimit ^ (u32)g) < 1;',
|
|
210
|
+
'*(s32 *)g = a; g = g + 4; sink(); return (gLimit ^ g) < 1;',
|
|
211
|
+
],
|
|
212
|
+
compiles: 'same',
|
|
213
|
+
},
|
|
214
|
+
},
|
|
198
215
|
foldsPointerAdvance: {
|
|
199
216
|
reads: 'folds a stepped pointer back into an offset load',
|
|
200
217
|
witness: {
|
|
@@ -279,7 +296,7 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
|
|
|
279
296
|
after: 's32 example(u32 a0, u32 a1)',
|
|
280
297
|
},
|
|
281
298
|
implementedIn: RANK,
|
|
282
|
-
seeAlso: ['signed', '
|
|
299
|
+
seeAlso: ['signed', 'signed-cmp'],
|
|
283
300
|
},
|
|
284
301
|
signed: {
|
|
285
302
|
title: 'Signed parameters',
|
|
@@ -354,6 +371,31 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
|
|
|
354
371
|
implementedIn: 'packages/core/src/raise/shortcircuit.ts',
|
|
355
372
|
seeAlso: ['site-sense'],
|
|
356
373
|
},
|
|
374
|
+
'narrow-param': {
|
|
375
|
+
title: 'Parameter declared at the width every use extends it to',
|
|
376
|
+
summary: 'a parameter read only through narrowing casts is declared narrow, though the prologue does not widen it',
|
|
377
|
+
detail:
|
|
378
|
+
'A parameter the function only ever reads through narrowing casts can be declared at 32 bits and cast ' +
|
|
379
|
+
'at each use, or declared at the widest cast and read bare. Every use is the same value either way, ' +
|
|
380
|
+
'and where the compiler widens it at each use rather than on entry, the assembly does not say which ' +
|
|
381
|
+
'one the source declared. This lift declares it narrow, and loses a tie when no prototype declares the width.',
|
|
382
|
+
compilerBehavior:
|
|
383
|
+
'agbcc widens an `s16` parameter at each use when every use extends it, and then the two declarations ' +
|
|
384
|
+
'differ only in how the registers around them are allocated: a sibling `u8` parameter is copied ' +
|
|
385
|
+
'before it is widened under the narrow one.',
|
|
386
|
+
offeredWhen: {
|
|
387
|
+
when: 'Some parameter is read only through 8- or 16-bit casts or masks, and the default lift left it 32 bits wide.',
|
|
388
|
+
decidedBy: { symbol: 'runPreRecovery', file: 'packages/core/src/raise/pre-recovery.ts' },
|
|
389
|
+
gates: ['PARAM_READER_WIDTH_GATES'],
|
|
390
|
+
},
|
|
391
|
+
example: {
|
|
392
|
+
compiler: 'agbcc',
|
|
393
|
+
unit: 'extern u8 *gM; void f3(u8 *, u16, u8); void f2(u8 *, u8);\n@',
|
|
394
|
+
before: 'void example(s32 a, u8 b) { u8 *p = gM; f3(p + (s16)a * 4, (u16)a, b); f2(p + b * 8, b); }',
|
|
395
|
+
after: 'void example(s16 a, u8 b) { u8 *p = gM; f3(p + a * 4, (u16)a, b); f2(p + b * 8, b); }',
|
|
396
|
+
},
|
|
397
|
+
implementedIn: 'packages/core/src/raise/paramwidth.ts',
|
|
398
|
+
},
|
|
357
399
|
'shared-ret': {
|
|
358
400
|
title: 'Shared code after an early return',
|
|
359
401
|
summary: 'code both arms of an `if` reach before returning is written once, after the `if`',
|
|
@@ -403,6 +445,63 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
|
|
|
403
445
|
implementedIn: 'packages/core/src/raise/tailsink.ts',
|
|
404
446
|
seeAlso: ['shared-ret'],
|
|
405
447
|
},
|
|
448
|
+
'kept-slot': {
|
|
449
|
+
title: 'Each arm keeps its copy of a shared tail',
|
|
450
|
+
summary: "a MIPS jump's delay slot that ends a tail both arms spell stays in its own arm",
|
|
451
|
+
detail:
|
|
452
|
+
'IDO, KMC and GCC 2.7.2 fill the delay slot of a `b` or `j` with a copy of the first word at its ' +
|
|
453
|
+
'target, so by default the jump is read as landing one word earlier and the two arms meet there. ' +
|
|
454
|
+
'Where the word before the jump also matches the word before that copy, the two arms may instead ' +
|
|
455
|
+
'have been written out twice, each ending in the same instructions. This lift keeps the slot in its ' +
|
|
456
|
+
'own arm, so each arm keeps the whole tail it spells.',
|
|
457
|
+
compilerBehavior:
|
|
458
|
+
'The two readings are one object: the slot runs before the target on its one edge either way. ' +
|
|
459
|
+
'Which arms the source wrote out is something only the differ can tell. The question arises on IDO ' +
|
|
460
|
+
'alone: GCC 2.7.2 and KMC cross-jump a tail both arms spell into one copy at the join before they ' +
|
|
461
|
+
'fill any slot, so their output has no tail written twice (`mergenarrow` builds with no `b` on either).',
|
|
462
|
+
offeredWhen: {
|
|
463
|
+
when: 'The MIPS lift reported a `b`/`j` whose slot ends a common tail of two or more words, and retargeted it.',
|
|
464
|
+
decidedBy: { symbol: 'enumerateCandidates', file: RANK },
|
|
465
|
+
},
|
|
466
|
+
example: {
|
|
467
|
+
compiler: 'ido',
|
|
468
|
+
unit: 'void example(s32 *out, s32 a, s32 b, s32 c) { @ }',
|
|
469
|
+
before: 's32 v; if (c) { v = (a + b) << 16; } else { v = (a - b) << 16; } out[0] = v >> 16;',
|
|
470
|
+
after: 's16 v; if (c) { v = a + b; } else { v = a - b; } out[0] = v;',
|
|
471
|
+
note: 'the shape of `synthetic:mergenarrow`',
|
|
472
|
+
},
|
|
473
|
+
implementedIn: 'packages/core/src/frontend/mips.ts',
|
|
474
|
+
},
|
|
475
|
+
'far-cast': {
|
|
476
|
+
title: 'Store past the displacement range through a cast',
|
|
477
|
+
summary: 'a store to a member past the instruction offset range is spelled through a cast of the base',
|
|
478
|
+
detail:
|
|
479
|
+
'A member past the offset an instruction can carry is reached through an address the machine adds up ' +
|
|
480
|
+
'first, and by default that access joins its struct and is spelled as the member. A store there could ' +
|
|
481
|
+
'equally have been written through a cast of the base, `*((u8 *)p + 38) = v`, or through a pointer the ' +
|
|
482
|
+
'source held, and on some compilers that is a different object. This lift keeps every such store on ' +
|
|
483
|
+
'the address it was reached through; loads still join the struct.',
|
|
484
|
+
compilerBehavior:
|
|
485
|
+
'agbcc evaluates the right-hand side of a member store before it adds up the address, and the address ' +
|
|
486
|
+
'of a cast store before the right-hand side, so the two spellings allocate and order registers ' +
|
|
487
|
+
"differently; a hoisted `u8 *d = &p->f38; *d = v;` builds the cast spelling's object. Elsewhere the " +
|
|
488
|
+
'member and the cast compile alike one access at a time, but a folded store no longer aliases the ' +
|
|
489
|
+
'loads beside it, so the lift moves those loads past it, and a pointer the source held is spelled as ' +
|
|
490
|
+
'members: KMC and GCC 2.7.2 build two loads written before a store and the same loads written after ' +
|
|
491
|
+
'it into different objects, and two of the three mwcc builds allocate a held pointer and the member ' +
|
|
492
|
+
'spelling different registers.',
|
|
493
|
+
offeredWhen: {
|
|
494
|
+
when: 'The default lift folded a store through a materialised `base + K` address into its struct.',
|
|
495
|
+
decidedBy: { symbol: 'enumerateCandidates', file: RANK },
|
|
496
|
+
},
|
|
497
|
+
example: {
|
|
498
|
+
compiler: 'agbcc',
|
|
499
|
+
unit: 'struct S { s32 a; u8 pad[34]; u8 f38; };\nvoid example(struct S *p, u8 *q) { @ }',
|
|
500
|
+
before: 'p->f38 = *q; p->a = 0;',
|
|
501
|
+
after: '*((u8 *)p + 38) = *q; p->a = 0;',
|
|
502
|
+
},
|
|
503
|
+
implementedIn: 'packages/core/src/raise/structs.ts',
|
|
504
|
+
},
|
|
406
505
|
|
|
407
506
|
// ── structure ───────────────────────────────────────────────────────────────────────────────
|
|
408
507
|
'flip-branch': {
|
|
@@ -729,14 +828,42 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
|
|
|
729
828
|
implementedIn: ANALYSIS,
|
|
730
829
|
seeAlso: ['expr-home', 'fresh-merge'],
|
|
731
830
|
},
|
|
732
|
-
'
|
|
733
|
-
title: '
|
|
734
|
-
summary: '
|
|
831
|
+
'escape-home': {
|
|
832
|
+
title: 'Narrowed value named where it was narrowed',
|
|
833
|
+
summary: 'a widening or narrowing whose readers are all elsewhere is computed once, at its own place',
|
|
735
834
|
detail:
|
|
736
|
-
'
|
|
737
|
-
'
|
|
738
|
-
'
|
|
739
|
-
|
|
835
|
+
'A value narrowed or widened from another, read two or more times and never in the place it was ' +
|
|
836
|
+
'computed, gets a local there — the register the machine narrowed into once and every later block ' +
|
|
837
|
+
'read. By default the cast is written out again at each reader.',
|
|
838
|
+
offeredWhen: {
|
|
839
|
+
when: "A symbol-map setting's own lift narrows or widens a value whose two or more readers are all elsewhere.",
|
|
840
|
+
decidedBy: { symbol: 'hasEscapingExtension', file: ANALYSIS },
|
|
841
|
+
},
|
|
842
|
+
example: {
|
|
843
|
+
compiler: 'agbcc',
|
|
844
|
+
unit: CALLS + 's32 example(void) { @ }',
|
|
845
|
+
before:
|
|
846
|
+
's32 v; u32 n; u32 i; n = 0; ' +
|
|
847
|
+
'do { i = 0; do { v = f(); i = i + 1; } while (i <= 63); } while ((u16)v != 0 && n++ <= 9); ' +
|
|
848
|
+
'return (u16)v;',
|
|
849
|
+
after:
|
|
850
|
+
's32 v; s32 t; u32 n; u32 i; n = 0; ' +
|
|
851
|
+
'do { i = 0; do { v = f(); t = (u16)v; i = i + 1; } while (i <= 63); } while (t != 0 && n++ <= 9); ' +
|
|
852
|
+
'return t;',
|
|
853
|
+
},
|
|
854
|
+
implementedIn: ANALYSIS,
|
|
855
|
+
seeAlso: ['expr-home', 'derived-home'],
|
|
856
|
+
},
|
|
857
|
+
'signed-cmp': {
|
|
858
|
+
title: 'Unsigned compare spelled signed',
|
|
859
|
+
summary: 'an unsigned compare drops the `(u32)` cast the default gives an operand that does not already say so',
|
|
860
|
+
detail:
|
|
861
|
+
'By default an unsigned comparison takes a `(u32)` cast on an operand the rendering does not already ' +
|
|
862
|
+
'make unsigned, and a declaration claimed by both kinds of use becomes `u32` when nothing under it ' +
|
|
863
|
+
'needs signed. This writes the comparison without them, as a signed one. The source may have done so: ' +
|
|
864
|
+
'a compiler emits an unsigned compare for a signed one where it proved both sides non-negative, and ' +
|
|
865
|
+
'asmlift can prove less than the compiler can. Where the sides can be negative it computes something ' +
|
|
866
|
+
'else, so it is published only when the candidate matches byte for byte; otherwise it is withheld.',
|
|
740
867
|
compilerBehavior:
|
|
741
868
|
'A compiler emits an unsigned branch from a signed compare only where it proved both sides non-negative.',
|
|
742
869
|
offeredWhen: {
|
|
@@ -746,8 +873,8 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
|
|
|
746
873
|
example: {
|
|
747
874
|
compiler: 'agbcc',
|
|
748
875
|
unit: CALLS + 'void example(s32 a, s32 b) { @ A(); }',
|
|
749
|
-
before: 'if (a < b)',
|
|
750
|
-
after: 'if (
|
|
876
|
+
before: 'if ((u32)a < b)',
|
|
877
|
+
after: 'if (a < b)',
|
|
751
878
|
},
|
|
752
879
|
implementedIn: STRUCTURE,
|
|
753
880
|
seeAlso: ['unsigned', 'signed'],
|
|
@@ -792,9 +919,13 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
|
|
|
792
919
|
},
|
|
793
920
|
example: {
|
|
794
921
|
compiler: 'agbcc',
|
|
922
|
+
// The callees are PROTOTYPED here, unlike `CALLS`: `bl f` sees `c` read in r2 and a0 untouched
|
|
923
|
+
// in r0, which a pass-through `f(a0, a1, c)` would leave the same way, so an undeclared `f`
|
|
924
|
+
// declines (frontend/ssa.ts `fallbackArgc` refuses the gap). The lift reads these declarations
|
|
925
|
+
// off the unit as shown.
|
|
795
926
|
unit:
|
|
796
|
-
|
|
797
|
-
's32 example(s32 a0, s32 a1, s32 c) { s32 v1; s32 v2; if (c) { v1 = f(); v2 = g(); } else { @ } return h(v1, v2); }',
|
|
927
|
+
's32 f(void); s32 g(void); s32 h(s32, s32);\n' +
|
|
928
|
+
's32 example(s32 a0, s32 a1, s32 c) { s32 v1; s32 v2; if (c) { v1 = f() + 1; v2 = g(); } else { @ } return h(v1, v2); }',
|
|
798
929
|
before: 'v2 = a1; v1 = a0;',
|
|
799
930
|
after: 'v1 = a0; v2 = a1;',
|
|
800
931
|
note: 'the copies at the end of one path into a merge',
|
|
@@ -1564,6 +1695,105 @@ export const VARIATION_DEFINITIONS: { readonly [N in VariationName]: VariationDe
|
|
|
1564
1695
|
implementedIn: l3('pollguard'),
|
|
1565
1696
|
seeAlso: ['pollguard'],
|
|
1566
1697
|
},
|
|
1698
|
+
'narrow-decl': {
|
|
1699
|
+
title: 'Narrow value declared at its width',
|
|
1700
|
+
summary: 'a local holding one narrowed value is declared at that width instead of as an `s32` with a cast',
|
|
1701
|
+
detail:
|
|
1702
|
+
'A value narrowed once and read as a local can be spelled two ways: an `s32` assigned the narrowing ' +
|
|
1703
|
+
'cast, or a local declared at the narrow width. Every read is an `int` either way, so the value is the ' +
|
|
1704
|
+
"same; which one the source declared is not in the assembly. Applied on top of every other candidate's source.",
|
|
1705
|
+
compilerBehavior:
|
|
1706
|
+
"gcc 2.9's front end sees through the promotion of a narrow variable and not through a cast assigned " +
|
|
1707
|
+
'to an `int`, so compares, divisions and right shifts of an unsigned narrow one become unsigned; ' +
|
|
1708
|
+
'where none reads it, agbcc can still allocate registers differently. IDO, KMC gcc and gcc 2.7.2 ' +
|
|
1709
|
+
'each emit different code for the example too, and every mwcc build does for a compare of it.',
|
|
1710
|
+
offeredWhen: {
|
|
1711
|
+
when: 'An `s32` local written once, outside a `for` init, by an integer narrowed to a narrower integer, that lives in no memory.',
|
|
1712
|
+
decidedBy: { symbol: 'narrowDeclarations', file: l3('narrowdecl') },
|
|
1713
|
+
},
|
|
1714
|
+
example: {
|
|
1715
|
+
compiler: 'agbcc',
|
|
1716
|
+
unit: CALLS + 'extern u8 gA[]; extern u8 gB[]; void example(void) { @ }',
|
|
1717
|
+
before: 's32 v; v = (u8)(gA[12] - 1); gB[14] = (v & 1) + f() % (5 - v) + 1;',
|
|
1718
|
+
after: 'u8 v; v = gA[12] - 1; gB[14] = (v & 1) + f() % (5 - v) + 1;',
|
|
1719
|
+
},
|
|
1720
|
+
implementedIn: l3('narrowdecl'),
|
|
1721
|
+
seeAlso: ['narrow-load', 'narrow-read', 'derived-home', 'escape-home'],
|
|
1722
|
+
},
|
|
1723
|
+
'narrow-load': {
|
|
1724
|
+
title: 'Narrow load declared at its width',
|
|
1725
|
+
summary: 'a local holding one narrow memory read is declared at that width instead of as an `s32`',
|
|
1726
|
+
detail:
|
|
1727
|
+
'A byte or halfword read from memory into a local can be spelled two ways: an `s32` local assigned the ' +
|
|
1728
|
+
"read, or a local declared at the read's width. Every read of the local is the same value either way; " +
|
|
1729
|
+
"which one the source declared is not in the assembly. Applied on top of every other candidate's source.",
|
|
1730
|
+
compilerBehavior:
|
|
1731
|
+
'agbcc loads a narrow local where it is written and an `s32` one where it is first read, so two byte ' +
|
|
1732
|
+
'reads held across two calls are loaded at different points.',
|
|
1733
|
+
offeredWhen: {
|
|
1734
|
+
when: 'An `s32` local written once, outside a `for` init, by an element or member read of a narrower integer, in no memory.',
|
|
1735
|
+
decidedBy: { symbol: 'narrowLoadDeclarations', file: l3('narrowdecl') },
|
|
1736
|
+
},
|
|
1737
|
+
example: {
|
|
1738
|
+
compiler: 'agbcc',
|
|
1739
|
+
unit: 'void f7(u8 *, u8 *, u8, u8, s32, u8, u8);\nvoid example(u8 *p, s32 x) { @ }',
|
|
1740
|
+
before:
|
|
1741
|
+
's32 a; s32 b; a = p[100]; b = p[101]; f7(p + 4, p + 8, a, b, x, 1, 2); f7(p + 8, p + 12, a, b, x, 3, 4);',
|
|
1742
|
+
after: 'u8 a; u8 b; a = p[100]; b = p[101]; f7(p + 4, p + 8, a, b, x, 1, 2); f7(p + 8, p + 12, a, b, x, 3, 4);',
|
|
1743
|
+
},
|
|
1744
|
+
implementedIn: l3('narrowdecl'),
|
|
1745
|
+
seeAlso: ['narrow-decl', 'narrow-read'],
|
|
1746
|
+
},
|
|
1747
|
+
'narrow-read': {
|
|
1748
|
+
title: 'Narrow value declared at its width, read bare',
|
|
1749
|
+
summary: 'a local read only through one narrowing cast is declared at that width instead of at 32 bits',
|
|
1750
|
+
detail:
|
|
1751
|
+
'A value stored whole and narrowed wherever it is read can be spelled two ways: a 32-bit local cast at every ' +
|
|
1752
|
+
'read, or a local declared at the narrow width and read bare. Every read is the same value either way; ' +
|
|
1753
|
+
"which one the source declared is not in the assembly. Applied on top of every other candidate's source.",
|
|
1754
|
+
compilerBehavior:
|
|
1755
|
+
'A narrow local is extended as it is stored, where the cast extends at each read, after whatever ran in ' +
|
|
1756
|
+
'between: agbcc moves the extension of a call result held across a second call.',
|
|
1757
|
+
offeredWhen: {
|
|
1758
|
+
when: 'A 32-bit local in no memory, written by integers or calls but not in a `for` header, and narrowed by one cast at every read.',
|
|
1759
|
+
decidedBy: { symbol: 'narrowReadDeclarations', file: l3('narrowdecl') },
|
|
1760
|
+
},
|
|
1761
|
+
example: {
|
|
1762
|
+
compiler: 'agbcc',
|
|
1763
|
+
unit: 'u32 cv(u8); u16 dc(u8, u8, u8);\nu16 example(u8 *p) { @ }',
|
|
1764
|
+
before: 's32 y; s32 m; y = cv(p[0]); m = cv(p[1]); return dc((u8)y, (u8)m, (u8)cv(p[2]));',
|
|
1765
|
+
after: 'u8 y; u8 m; y = cv(p[0]); m = cv(p[1]); return dc(y, m, (u8)cv(p[2]));',
|
|
1766
|
+
},
|
|
1767
|
+
implementedIn: l3('narrowdecl'),
|
|
1768
|
+
seeAlso: ['narrow-decl', 'narrow-load'],
|
|
1769
|
+
},
|
|
1770
|
+
'int-cell': {
|
|
1771
|
+
title: 'Global stored a pointer, spelled as an integer',
|
|
1772
|
+
summary: 'a global the function stores a pointer into is written as an integer: `g = g + K`, no conversions',
|
|
1773
|
+
detail:
|
|
1774
|
+
'A global the function stores a pointer into is written as a pointer by default: `g = (void *)((u8 *)g + K)` ' +
|
|
1775
|
+
'for the byte the assembly addressed, `(u32)g` where the value is used as an integer, and a `void *` ' +
|
|
1776
|
+
'declaration. A project that declares the global an integer wrote `g = g + K` and no conversion, and a ' +
|
|
1777
|
+
'pointer declaration scales that offset instead, so which one is right depends on a declaration the ' +
|
|
1778
|
+
'assembly does not show. Published only when the candidate matches byte for byte; otherwise it is withheld.',
|
|
1779
|
+
compilerBehavior:
|
|
1780
|
+
'CodeWarrior rejects a pointer stored into an integer global; IDO allocates registers differently around ' +
|
|
1781
|
+
'a `(u32)` conversion of one.',
|
|
1782
|
+
offeredWhen: {
|
|
1783
|
+
when:
|
|
1784
|
+
'A store of a `void *` value into a global no declaration in the function types and the symbol map ' +
|
|
1785
|
+
'declares, if at all, an integer.',
|
|
1786
|
+
decidedBy: { symbol: 'integerCells', file: l3('intcell') },
|
|
1787
|
+
},
|
|
1788
|
+
example: {
|
|
1789
|
+
compiler: 'ido',
|
|
1790
|
+
unit: 'extern u32 g; extern u32 gLimit; void sink(void);\nu32 example(u32 a) { @ }',
|
|
1791
|
+
before: '*(s32 *)g = a; g = (void *)((u8 *)g + 4); sink(); return (gLimit ^ (u32)g) < 1;',
|
|
1792
|
+
after: '*(s32 *)g = a; g = g + 4; sink(); return (gLimit ^ g) < 1;',
|
|
1793
|
+
note: 'under the integer declaration the `after` spelling was written against',
|
|
1794
|
+
},
|
|
1795
|
+
implementedIn: l3('intcell'),
|
|
1796
|
+
},
|
|
1567
1797
|
|
|
1568
1798
|
// ── symbol map ──────────────────────────────────────────────────────────────────────────────
|
|
1569
1799
|
'raw-globals': {
|
package/src/variation-gates.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { UNREDUCE_GATES } from './l3/unreduce';
|
|
30
30
|
import { VOL_SLOT_GATES } from './l3/volatileval';
|
|
31
31
|
import { VOL_STORE_GATES } from './l3/volstore';
|
|
32
|
+
import { PARAM_READER_WIDTH_GATES } from './raise/paramwidth';
|
|
32
33
|
import { ARM_REREAD_GATES } from './raise/shortcircuit';
|
|
33
34
|
import { NAME_COALESCE_GATES } from './structure/namecoalesce';
|
|
34
35
|
import { FRESH_MERGE_GATES } from './structure/structure';
|
|
@@ -59,6 +60,7 @@ export const VARIATION_GATE_TABLES = {
|
|
|
59
60
|
NAME_COALESCE_GATES,
|
|
60
61
|
OFFMEMBER_GATES,
|
|
61
62
|
ORDERBASE_GATES,
|
|
63
|
+
PARAM_READER_WIDTH_GATES,
|
|
62
64
|
PTR_FIELD_GATES,
|
|
63
65
|
REGIONBASE_GATES,
|
|
64
66
|
SCOPEBASE_ELIGIBILITY,
|
package/src/variation-tokens.ts
CHANGED
|
@@ -67,8 +67,11 @@ const TOKENS = [
|
|
|
67
67
|
// lift: the assembly lifted or raised again
|
|
68
68
|
{ name: 'setup-args', variationKind: 'lift' },
|
|
69
69
|
{ name: 'connective', variationKind: 'lift' },
|
|
70
|
+
{ name: 'narrow-param', variationKind: 'lift' },
|
|
70
71
|
{ name: 'shared-ret', variationKind: 'lift' },
|
|
71
72
|
{ name: 'shared-tail', variationKind: 'lift' },
|
|
73
|
+
{ name: 'kept-slot', variationKind: 'lift' },
|
|
74
|
+
{ name: 'far-cast', variationKind: 'lift' },
|
|
72
75
|
// structure: `structure()` re-run with other options
|
|
73
76
|
{ name: 'flip-branch', variationKind: 'structure' },
|
|
74
77
|
{ name: 'defsite', variationKind: 'structure' },
|
|
@@ -85,7 +88,8 @@ const TOKENS = [
|
|
|
85
88
|
{ name: 'expr-home', variationKind: 'structure' },
|
|
86
89
|
{ name: 'derived-home', variationKind: 'structure' },
|
|
87
90
|
{ name: 'merge-home', variationKind: 'structure' },
|
|
88
|
-
{ name: '
|
|
91
|
+
{ name: 'escape-home', variationKind: 'structure' },
|
|
92
|
+
{ name: 'signed-cmp', variationKind: 'structure' },
|
|
89
93
|
{ name: 'fresh-merge', variationKind: 'structure' },
|
|
90
94
|
{ name: 'copy-defpos', variationKind: 'structure' },
|
|
91
95
|
{ name: 'site-sense', variationKind: 'structure' },
|
|
@@ -129,6 +133,14 @@ const TOKENS = [
|
|
|
129
133
|
{ name: 'initfirst', variationKind: 'respell' },
|
|
130
134
|
{ name: 'pollguard', variationKind: 'respell' },
|
|
131
135
|
{ name: 'pollread', variationKind: 'respell' },
|
|
136
|
+
{ name: 'narrow-decl', variationKind: 'respell' },
|
|
137
|
+
{ name: 'narrow-load', variationKind: 'respell' },
|
|
138
|
+
{ name: 'narrow-read', variationKind: 'respell' },
|
|
139
|
+
{
|
|
140
|
+
name: 'int-cell',
|
|
141
|
+
variationKind: 'respell',
|
|
142
|
+
target: { behavior: 'pointerIntConversionIsFree', declared: false },
|
|
143
|
+
},
|
|
132
144
|
// symbol map: the map's shaped spellings withheld; always the last part
|
|
133
145
|
{ name: 'raw-globals', variationKind: 'symbol-map' },
|
|
134
146
|
] as const satisfies readonly VariationToken[];
|
|
@@ -282,12 +294,15 @@ export function splitVariations(name: string): string[] {
|
|
|
282
294
|
}
|
|
283
295
|
|
|
284
296
|
/** How many of one fan's candidates carry one variation. `candidates` counts the whole fan —
|
|
285
|
-
* scored, dropped and
|
|
286
|
-
* count, each absent when 0.
|
|
297
|
+
* scored, dropped, withheld and never compiled alike — and `dropped`, `withheld` and
|
|
298
|
+
* `notCompiled` are the parts of that count the ranking did not score, each absent when 0.
|
|
299
|
+
* `notCompiled` is not a refusal: nothing was compiled (core stillborn.ts), and a tally that
|
|
300
|
+
* folded it into `dropped` would report compile failures that never ran. */
|
|
287
301
|
export interface VariationTally {
|
|
288
302
|
candidates: number;
|
|
289
303
|
dropped?: number;
|
|
290
304
|
withheld?: number;
|
|
305
|
+
notCompiled?: number;
|
|
291
306
|
}
|
|
292
307
|
|
|
293
308
|
interface NamedCandidate {
|
|
@@ -299,7 +314,7 @@ interface NamedCandidate {
|
|
|
299
314
|
* (`coalesce-v0-v1` and `coalesce-v2-v3` are both `coalesce`), and a candidate counts once under
|
|
300
315
|
* each name it carries however many subjects it applies it to.
|
|
301
316
|
*
|
|
302
|
-
* `fan` is ranking's
|
|
317
|
+
* `fan` is ranking's four-way partition, which puts every enumerated candidate in exactly one
|
|
303
318
|
* list, so each signedness entry's `candidates` sums with the other's to the fan size.
|
|
304
319
|
*
|
|
305
320
|
* A tally, not a factorisation: enumeration gates prune the fan, so the counts do not multiply to
|
|
@@ -309,6 +324,7 @@ export function tallyFanVariations(fan: {
|
|
|
309
324
|
candidates: readonly NamedCandidate[];
|
|
310
325
|
dropped: readonly NamedCandidate[];
|
|
311
326
|
withheld: readonly NamedCandidate[];
|
|
327
|
+
notCompiled: readonly NamedCandidate[];
|
|
312
328
|
}): Record<string, VariationTally> {
|
|
313
329
|
const registeredName = new Map<string, string>();
|
|
314
330
|
const nameOf = (part: string): string => {
|
|
@@ -319,13 +335,13 @@ export function tallyFanVariations(fan: {
|
|
|
319
335
|
}
|
|
320
336
|
return name;
|
|
321
337
|
};
|
|
322
|
-
const counts = new Map<string, { candidates: number; dropped: number; withheld: number }>();
|
|
323
|
-
const add = (list: readonly NamedCandidate[], refusal: 'dropped' | 'withheld' | undefined): void => {
|
|
338
|
+
const counts = new Map<string, { candidates: number; dropped: number; withheld: number; notCompiled: number }>();
|
|
339
|
+
const add = (list: readonly NamedCandidate[], refusal: 'dropped' | 'withheld' | 'notCompiled' | undefined): void => {
|
|
324
340
|
for (const c of list) {
|
|
325
341
|
for (const name of new Set(c.variations.map(nameOf))) {
|
|
326
342
|
let n = counts.get(name);
|
|
327
343
|
if (n === undefined) {
|
|
328
|
-
n = { candidates: 0, dropped: 0, withheld: 0 };
|
|
344
|
+
n = { candidates: 0, dropped: 0, withheld: 0, notCompiled: 0 };
|
|
329
345
|
counts.set(name, n);
|
|
330
346
|
}
|
|
331
347
|
n.candidates++;
|
|
@@ -338,6 +354,7 @@ export function tallyFanVariations(fan: {
|
|
|
338
354
|
add(fan.candidates, undefined);
|
|
339
355
|
add(fan.dropped, 'dropped');
|
|
340
356
|
add(fan.withheld, 'withheld');
|
|
357
|
+
add(fan.notCompiled, 'notCompiled');
|
|
341
358
|
const kindIndex = (name: string): number => VARIATION_KINDS.indexOf(variationToken(name).variationKind);
|
|
342
359
|
const names = [...counts.keys()].sort((a, b) => kindIndex(a) - kindIndex(b) || (a < b ? -1 : a > b ? 1 : 0));
|
|
343
360
|
return Object.fromEntries(
|
|
@@ -349,6 +366,7 @@ export function tallyFanVariations(fan: {
|
|
|
349
366
|
candidates: n.candidates,
|
|
350
367
|
...(n.dropped ? { dropped: n.dropped } : {}),
|
|
351
368
|
...(n.withheld ? { withheld: n.withheld } : {}),
|
|
369
|
+
...(n.notCompiled ? { notCompiled: n.notCompiled } : {}),
|
|
352
370
|
},
|
|
353
371
|
];
|
|
354
372
|
}),
|