@asmlift/core 0.10.0 → 0.11.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
package/src/rank-variations.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
ORDERBASE_GATES,
|
|
25
25
|
UNFOLDED_GATES,
|
|
26
26
|
} from './l3/basecse';
|
|
27
|
+
import { jumpToCaseEnds } from './l3/caseendgoto';
|
|
27
28
|
import type { Gate } from './l3/gates';
|
|
28
29
|
import type { HoistPlacement } from './l3/hoist';
|
|
29
30
|
import { initFirstGuards } from './l3/initfirst';
|
|
@@ -55,7 +56,11 @@ import type { VariationName } from './variation-tokens';
|
|
|
55
56
|
* that setting's own lifted fn (a map-lifted shared lift spells const bases as gaddr, which would
|
|
56
57
|
* blind the /raw-globals siblings — the /addr-home lesson). `strip` opts the variation into the
|
|
57
58
|
* dropped-sibling closure: a candidate that applies it is skipped when its default sibling (the
|
|
58
|
-
* same setting without it) failed the boundary contracts.
|
|
59
|
+
* same setting without it) failed the boundary contracts. `offWitness` is the exact gate where
|
|
60
|
+
* only the structuring itself knows whether the variation can change the tree: the OFF sibling
|
|
61
|
+
* (the same setting without it, structured earlier in the same pass) runs with that hook, and a
|
|
62
|
+
* setting whose OFF sibling never fired it is skipped, since it would build that sibling's tree
|
|
63
|
+
* again. Which of these entries `structure()`'s
|
|
59
64
|
* `assertDefaultAccepts` guard does NOT reset — the ones that cannot unlock a function the default
|
|
60
65
|
* declines, and so carry a written argument in place of the guard — is stated ONCE, beside that
|
|
61
66
|
* guard in structure.ts, and derived from the guard's own reset list in
|
|
@@ -74,11 +79,15 @@ export interface StructureVariation {
|
|
|
74
79
|
| 'signedCmp'
|
|
75
80
|
| 'freshMerge'
|
|
76
81
|
| 'copyDefPos'
|
|
77
|
-
| 'siteSense'
|
|
82
|
+
| 'siteSense'
|
|
83
|
+
| 'tailCopies'
|
|
84
|
+
| 'forContinue'
|
|
85
|
+
| 'caseGoto';
|
|
78
86
|
name: VariationName;
|
|
79
87
|
options: (on: boolean) => Parameters<typeof structureChecked>[1];
|
|
80
88
|
sharedGate?: (sharedLift: Fn, defs: Map<Value, Op>) => boolean;
|
|
81
89
|
perLiftGate?: (fn: Fn) => boolean;
|
|
90
|
+
offWitness?: 'onTailFallerKept' | 'onForOffered' | 'onCaseCopied';
|
|
82
91
|
strip: boolean;
|
|
83
92
|
}
|
|
84
93
|
export const STRUCTURE_VARIATIONS: readonly StructureVariation[] = [
|
|
@@ -376,6 +385,80 @@ export const STRUCTURE_VARIATIONS: readonly StructureVariation[] = [
|
|
|
376
385
|
),
|
|
377
386
|
strip: false,
|
|
378
387
|
},
|
|
388
|
+
// `/tail-copies` — WHICH ARM KEEPS A CROSS-JUMPED TAIL (structure.ts `copyEveryTailEntrant`,
|
|
389
|
+
// switch-recover.ts `copySharedFallTargets`). A compiler that cross-jumps keeps one copy of the
|
|
390
|
+
// code two or more jump-table arms end in, so those arms all enter one sibling arm's body. The
|
|
391
|
+
// default writes the body back into every entrant but the one the arm-order policy puts directly
|
|
392
|
+
// above the sibling, which keeps its fall-through; this writes it into every entrant. Which arm
|
|
393
|
+
// the source wrote the tail under is not in the asm, and one compiler answers both ways at one
|
|
394
|
+
// set of flags: on KMC `-O2` and gcc 2.7.2 `-O1`, `synthetic:jt_xjcase` (case 1 written above
|
|
395
|
+
// case 2 and falling into it) recompiles only from the default, while the same arms written
|
|
396
|
+
// out of case order (`case 3`, `case 2`, `case 1`) are 24 objdump lines off from the default
|
|
397
|
+
// and 4 from this. So the differ referees it per row, like `/copy-defpos`.
|
|
398
|
+
//
|
|
399
|
+
// Gated EXACTLY, by its OFF sibling's structuring (`offWitness`): the two settings read
|
|
400
|
+
// different exits only where an entrant is left falling into a cross-jumped sibling
|
|
401
|
+
// (`copySharedFallTargets`' `onFallerKept`), so where the OFF sibling never left one the ON setting
|
|
402
|
+
// is not structured at all. No question of the raised fn answers that ahead of the pass: a block
|
|
403
|
+
// two arms reach is no sibling where it is the switch's merge (nearly every switch's post-switch
|
|
404
|
+
// code), and two entrants can reach their sibling through one shared cross-jumped block, so one
|
|
405
|
+
// in-edge. In the strip closure, and reset by `assertDefaultAccepts`: where the target language
|
|
406
|
+
// has no fall-through (Pascal) the default can still decline on the one fall-through it keeps,
|
|
407
|
+
// and copying every entrant would structure what the default refused.
|
|
408
|
+
{
|
|
409
|
+
flag: 'tailCopies',
|
|
410
|
+
name: 'tail-copies',
|
|
411
|
+
options: (on) => ({ copyEveryTailEntrant: on }),
|
|
412
|
+
offWitness: 'onTailFallerKept',
|
|
413
|
+
strip: true,
|
|
414
|
+
},
|
|
415
|
+
// `/for-continue` — A COUNTED LOOP AS `for` WHERE THE DEFAULT LIFTS IT AS `while (1)` (structure.ts
|
|
416
|
+
// `spellCountedLoopsFor`, `forPlanAt`). A loop whose latch steps one counter and tests it, entered
|
|
417
|
+
// through the same test, is one asm under both spellings, and the `for` is the one a jump-table arm
|
|
418
|
+
// inside it can `continue` and leave by `break;` after the switch. structure() reaches that `for`
|
|
419
|
+
// on its own only where every `while (1)` structuring declined (`ForRanking`); here it is ranked
|
|
420
|
+
// where the default lifts. Measured on agbcc, objdump instruction lines off the target, both lifts
|
|
421
|
+
// 0/20000 against the source in a host differential: a `for` whose five jump-table arms each
|
|
422
|
+
// `continue` or call and leave lifts as `while (1)` with the code after the loop copied into every
|
|
423
|
+
// arm at 54, and as the `for` at 27; two nested-loop probes go 81 → 68 and 55 → 44.
|
|
424
|
+
//
|
|
425
|
+
// Gated EXACTLY, by its OFF sibling's structuring (`offWitness`): only a `while (1)` loop that passes
|
|
426
|
+
// every one of `forPlanAt`'s checks can change, and the OFF sibling reports meeting one
|
|
427
|
+
// (`onForOffered`). In the strip closure, and reset by `assertDefaultAccepts`: a first run that
|
|
428
|
+
// spells every such loop `for` can structure a function the default (with its own reruns) refuses.
|
|
429
|
+
{
|
|
430
|
+
flag: 'forContinue',
|
|
431
|
+
name: 'for-continue',
|
|
432
|
+
options: (on) => ({ spellCountedLoopsFor: on }),
|
|
433
|
+
offWitness: 'onForOffered',
|
|
434
|
+
strip: true,
|
|
435
|
+
},
|
|
436
|
+
// `/case-goto` — TWO ARMS ENTERING ONE CASE'S BODY AS A `goto` TO ITS LABEL WHERE THE DEFAULT
|
|
437
|
+
// COPIES IT (structure.ts `jumpToSharedCases`, switch-recover.ts `jumpSharedFallTargets`). Where
|
|
438
|
+
// two or more jump-table arms enter one case arm's body, the default writes that body back into the
|
|
439
|
+
// entrants but one (`copySharedFallTargets`) and reads the goto only where every goto-free
|
|
440
|
+
// structuring of the function declines, in a switch none of them spelled goto-free
|
|
441
|
+
// (`JumpRanking`); this reads the goto first. The asm does not
|
|
442
|
+
// say which: a compiler that cross-jumps merges a source copy into the goto's bytes when the whole
|
|
443
|
+
// copy matches, and on agbcc it stops short where CSE has the arm re-use an address the copy
|
|
444
|
+
// re-forms, so the copy compiles to more code than the goto (`synthetic:sw_gotocase2`, case 1's copy
|
|
445
|
+
// re-reading `gE[i]`). The differ referees it per row, like `/tail-copies`, and a tie goes to the
|
|
446
|
+
// candidate without the goto (rank.ts `gotoCount`). It does not take `/tail-copies`' faller choice:
|
|
447
|
+
// the entrant directly above the case always falls, since a jump from it would land on the label
|
|
448
|
+
// right below it, a path its fall-through already spells — so `/tail-copies/case-goto` is no
|
|
449
|
+
// candidate of its own.
|
|
450
|
+
//
|
|
451
|
+
// Gated EXACTLY, by its OFF sibling's structuring (`offWitness`): the two settings read different
|
|
452
|
+
// exits only where the copies write a case arm's body into an entrant (`onCaseCopied`). In the
|
|
453
|
+
// strip closure, and reset by `assertDefaultAccepts`: read first, the jumps can leave a chain the
|
|
454
|
+
// copies after them structure where the default (copies, then jumps) declined.
|
|
455
|
+
{
|
|
456
|
+
flag: 'caseGoto',
|
|
457
|
+
name: 'case-goto',
|
|
458
|
+
options: (on) => ({ jumpToSharedCases: on }),
|
|
459
|
+
offWitness: 'onCaseCopied',
|
|
460
|
+
strip: true,
|
|
461
|
+
},
|
|
379
462
|
];
|
|
380
463
|
|
|
381
464
|
/** The statement-order/shape members. */
|
|
@@ -467,8 +550,18 @@ export const STACKED_VARIATIONS = [...STACKED_SHAPES, ...STACKED_WIDTHS];
|
|
|
467
550
|
* `synthetic:ladidx1`, `synthetic:ladidx2`, `synthetic:revlad5s`. `bench regression` is an
|
|
468
551
|
* OUTCOME gate over all tiers, so every one of them turns it red — but `benchmark.yml` is
|
|
469
552
|
* `workflow_dispatch`, manual, with no cron, so nothing runs that gate on a PR. The scores and the
|
|
470
|
-
* method are in `apps/benchmark/dataset/synthetic.ts`'s `/unmerge` block.
|
|
471
|
-
|
|
553
|
+
* method are in `apps/benchmark/dataset/synthetic.ts`'s `/unmerge` block.
|
|
554
|
+
*
|
|
555
|
+
* `/case-end-goto` (l3/caseendgoto.ts) is the second entry: a search loop's hit flag read where the
|
|
556
|
+
* code after its loop ends the switch arm, rewritten as the case-end goto the source wrote. Here
|
|
557
|
+
* rather than among the stacked members because the rest of the respell set must derive from the
|
|
558
|
+
* goto tree (an `initfirst` or `sinkinit` over a tree that no longer declares the flag), and here
|
|
559
|
+
* rather than as a structure variation for the cost l3/caseendgoto.ts's header gives. Each entry applies to
|
|
560
|
+
* the structured tree on its own, so `/unmerge` and `/case-end-goto` never compose. */
|
|
561
|
+
export const PRE_RESPELL_VARIATIONS: typeof STACKED_VARIATIONS = [
|
|
562
|
+
{ name: 'unmerge', apply: unmergeJoins },
|
|
563
|
+
{ name: 'case-end-goto', apply: jumpToCaseEnds },
|
|
564
|
+
];
|
|
472
565
|
|
|
473
566
|
/** Every subset of `xs`, the empty one first, each in `xs`' order. */
|
|
474
567
|
const subsets = <X>(xs: readonly X[]): X[][] =>
|
package/src/rank.ts
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
// names, and which of those names a declaration must refuse to claim.
|
|
21
21
|
import { cBackend } from './backend/c';
|
|
22
22
|
import {
|
|
23
|
+
assertCaseGotosNarrow,
|
|
23
24
|
assertDerefsTyped,
|
|
25
|
+
assertEffectsPreserved,
|
|
24
26
|
assertFrameUnionsViewed,
|
|
25
27
|
assertLocalStorageStaysInteger,
|
|
26
28
|
assertLocalsWritten,
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
assertResolved,
|
|
31
33
|
} from './contracts';
|
|
32
34
|
import type { AsmData } from './frontend/asmdata';
|
|
35
|
+
import type { CalleeBodies } from './frontend/callee-body';
|
|
33
36
|
import { hasSetupArgsNarrowing, narrowToSetupArgs, setupArgsDiscardsPassedResult } from './frontend/ssa';
|
|
34
37
|
import { Fn, Value, defOpMap } from './ir/core';
|
|
35
38
|
import { T } from './ir/types';
|
|
@@ -62,7 +65,15 @@ import { volatileValueLocals } from './l3/volatileval';
|
|
|
62
65
|
import { volatileDeviceStores } from './l3/volstore';
|
|
63
66
|
import { zeroSubNegates } from './l3/zerosub';
|
|
64
67
|
import { RewritePattern } from './pattern/engine';
|
|
65
|
-
import {
|
|
68
|
+
import {
|
|
69
|
+
applyIdiomPatterns,
|
|
70
|
+
liftStamped,
|
|
71
|
+
offsetNameBehaviorsFor,
|
|
72
|
+
raiseRecovered,
|
|
73
|
+
sourceLineCount,
|
|
74
|
+
structureChecked,
|
|
75
|
+
structureReturnTailReading,
|
|
76
|
+
} from './pipeline';
|
|
66
77
|
import { type FnProto, type Prototypes, declaresVoidReturn, prototypesFromSymbols } from './proto';
|
|
67
78
|
import { type ContextGlobals, splitOwnContext } from './proto-context';
|
|
68
79
|
import { inferGlobalArrays, orderLicensedGlobals, sameDerivedShape } from './raise/globalshape';
|
|
@@ -96,7 +107,13 @@ import {
|
|
|
96
107
|
sameBases,
|
|
97
108
|
} from './rank-variations';
|
|
98
109
|
import { type Stillborn, stillbornVerdict } from './stillborn';
|
|
99
|
-
import {
|
|
110
|
+
import {
|
|
111
|
+
DefaultAcceptsMemo,
|
|
112
|
+
DefaultAcceptsMemoMisuse,
|
|
113
|
+
FOR_BOUND_GATES,
|
|
114
|
+
StructureError,
|
|
115
|
+
hasDivergentSharedRet,
|
|
116
|
+
} from './structure/structure';
|
|
100
117
|
import {
|
|
101
118
|
type SymbolInfo,
|
|
102
119
|
type SymbolMap,
|
|
@@ -170,6 +187,8 @@ export interface EnumerateOptions {
|
|
|
170
187
|
contextGlobals?: ContextGlobals;
|
|
171
188
|
/** same contract as DecompileOptions.ownContextProto */
|
|
172
189
|
ownContextProto?: FnProto;
|
|
190
|
+
/** same contract as DecompileOptions.calleeBodies */
|
|
191
|
+
calleeBodies?: CalleeBodies;
|
|
173
192
|
/** Called when a respell variation THROWS or fails a boundary contract, so the failure is visible
|
|
174
193
|
* instead of the candidate silently not existing. Enumeration continues either way — the default
|
|
175
194
|
* candidate is unaffected — but a variation that never fires because it always throws is a defect,
|
|
@@ -492,6 +511,27 @@ interface TreeSource {
|
|
|
492
511
|
signedUnsignedCompare?: true;
|
|
493
512
|
}
|
|
494
513
|
|
|
514
|
+
/** Did `bound-plus-one-counter-wraps` CAUSE this structuring's decline? The rule fires from every
|
|
515
|
+
* attempt `forPlanAt` makes, committed or not, and on loops the thrown refusal never reaches
|
|
516
|
+
* (`synthetic:sw_gotocase_inner:ido7.1` fires it and declines on its switches' cases), so its firing
|
|
517
|
+
* alone says nothing about the decline. The question is asked the one way that answers it: the
|
|
518
|
+
* same structuring with that rule off. Where that one structures, the refusal was the rule's, and
|
|
519
|
+
* the other pin, whose counter the rule admits, can take the loop; where it still throws, the
|
|
520
|
+
* decline is the function's and the row aborts as any other default decline does. The tree the
|
|
521
|
+
* ablated run builds is discarded — it is the `for` the rule refuses as wrong under this pin.
|
|
522
|
+
* Not holding every pinnable decline instead: that would let the signed pin past any structure
|
|
523
|
+
* refusal that reads a declared type, not only this one, and re-lift every declined row. */
|
|
524
|
+
function declinesOnCounterSign(fn: Fn, opts: Parameters<typeof structureChecked>[1]): boolean {
|
|
525
|
+
try {
|
|
526
|
+
structureChecked(fn, opts, {
|
|
527
|
+
forBoundGates: FOR_BOUND_GATES.filter((g) => g.id !== 'bound-plus-one-counter-wraps'),
|
|
528
|
+
});
|
|
529
|
+
return true;
|
|
530
|
+
} catch {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
495
535
|
/** Emit the DISTINCT type/branch-sense candidates for `name` — PURE, no scoring.
|
|
496
536
|
* It differs from `decompile()` in exactly two arguments to the shared spine — the signedness
|
|
497
537
|
* pin, injected between pre-recovery and recoverTypes via the `beforeRecover` hook, and the
|
|
@@ -515,6 +555,17 @@ export function enumerateCandidates(
|
|
|
515
555
|
/** The last refusal from a backend asked to spell a tree — what the empty-enumeration check
|
|
516
556
|
* below reports, so "this backend can spell nothing here" names its reason. */
|
|
517
557
|
let lastEmitError: unknown = null;
|
|
558
|
+
/** THE UNSIGNED PIN'S DECLINE, HELD FOR THE SIGNED ONE. The default candidate's structuring aborts
|
|
559
|
+
* the row (`abortsRow`) on the premise that a structure refusal does not depend on the pin. One
|
|
560
|
+
* does: `forPlanAt`'s `bound-plus-one-counter-wraps` reads the counter's declared type, so IDO's
|
|
561
|
+
* counted loop (`synthetic:sw_gotocase2:ido7.1`) takes its `for` under the `s32` pin and not the
|
|
562
|
+
* `u32` one, and without the `for` the switch's loop exit cannot be spelled at all. The decline is
|
|
563
|
+
* held only where that rule CAUSED it (`declinesOnCounterSign`), and then the rest of the unsigned
|
|
564
|
+
* pin is skipped: its other settings would run behind a declined default, which `abortsRow` exists
|
|
565
|
+
* to forbid. The signed pin runs as it would have; a row that then produces no candidate declines
|
|
566
|
+
* with the held error. Nothing the signed pin builds is new: it is the same pass, under its own
|
|
567
|
+
* default setting and its own gates. */
|
|
568
|
+
let heldPinDecline: unknown = undefined;
|
|
518
569
|
// Same merge as `decompile`: the project's DWARF signatures fill in what the caller did not
|
|
519
570
|
// state, so both the annotate pass and the ranked candidates reason about one prototype table.
|
|
520
571
|
const ownContext = splitOwnContext(opts.prototypes, name, opts.ownContextProto);
|
|
@@ -526,6 +577,7 @@ export function enumerateCandidates(
|
|
|
526
577
|
// every candidate carries it, the whole row).
|
|
527
578
|
spellSwitchFallthrough: backend.spellsSwitchFallthrough,
|
|
528
579
|
spellSwitchBreak: backend.spellsSwitchBreak,
|
|
580
|
+
spellEarlyReturns: backend.spellsEarlyReturn,
|
|
529
581
|
...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
|
|
530
582
|
...(opts.contextGlobals ? { contextGlobals: opts.contextGlobals } : {}),
|
|
531
583
|
};
|
|
@@ -663,7 +715,7 @@ export function enumerateCandidates(
|
|
|
663
715
|
opts.asmData,
|
|
664
716
|
opts.symbols,
|
|
665
717
|
opts.contextGlobals,
|
|
666
|
-
|
|
718
|
+
{ calleeBodies: opts.calleeBodies },
|
|
667
719
|
ownContext.ownContextProto,
|
|
668
720
|
);
|
|
669
721
|
// The ARRAY SHAPES the input assembly evidences (raise/globalshape.ts), for the DECLARATION
|
|
@@ -736,7 +788,7 @@ export function enumerateCandidates(
|
|
|
736
788
|
opts.asmData,
|
|
737
789
|
symbols,
|
|
738
790
|
opts.contextGlobals,
|
|
739
|
-
|
|
791
|
+
{ calleeBodies: opts.calleeBodies },
|
|
740
792
|
ownContext.ownContextProto,
|
|
741
793
|
);
|
|
742
794
|
applyIdiomPatterns(p, target, opts.patterns);
|
|
@@ -1164,6 +1216,7 @@ export function enumerateCandidates(
|
|
|
1164
1216
|
// `assertEffectsPreserved` is the fourth and is NOT here: it needs the L1 `fn`, and
|
|
1165
1217
|
// `respellTree`'s parameter list is the invariant the tree-dedup skip rests on (see its header).
|
|
1166
1218
|
// Widening it for a contract is a defensible change and an argued one — not a silent import.
|
|
1219
|
+
// The PRE-RESPELL loop below has `fn` in scope and no such invariant, so it runs it there.
|
|
1167
1220
|
// A respell variation returns its tree, or `{ sfn, needsProof }` when it cannot establish its own
|
|
1168
1221
|
// semantics from inside the pass (Candidate.matchOnly carries the argument).
|
|
1169
1222
|
const respell = (variations: readonly Variation[], make: () => RespellResult, alreadyShaped = false): void => {
|
|
@@ -1184,6 +1237,7 @@ export function enumerateCandidates(
|
|
|
1184
1237
|
assertNoOrphanedLocals(sfn, alt);
|
|
1185
1238
|
assertLocalStorageStaysInteger(sfn, alt);
|
|
1186
1239
|
assertFrameUnionsViewed(alt);
|
|
1240
|
+
assertCaseGotosNarrow(alt);
|
|
1187
1241
|
sources.push({
|
|
1188
1242
|
variations,
|
|
1189
1243
|
source: backend.emit(alt),
|
|
@@ -1236,6 +1290,7 @@ export function enumerateCandidates(
|
|
|
1236
1290
|
assertNoOrphanedLocals(alt, shaped.out);
|
|
1237
1291
|
assertLocalStorageStaysInteger(alt, shaped.out);
|
|
1238
1292
|
assertFrameUnionsViewed(shaped.out);
|
|
1293
|
+
assertCaseGotosNarrow(shaped.out);
|
|
1239
1294
|
assertPlacementSurvives(alt, shaped.out, minted);
|
|
1240
1295
|
sources.push({
|
|
1241
1296
|
variations: [...variations, ...shaped.variations],
|
|
@@ -1982,7 +2037,7 @@ export function enumerateCandidates(
|
|
|
1982
2037
|
// Read off the pin's OWN call, per symbol-map setting — the `/raw-globals` setting lifts without
|
|
1983
2038
|
// the map and answers for itself, so no lift is governed by a fact measured on a different one.
|
|
1984
2039
|
let pinnable = false;
|
|
1985
|
-
for (const cand of SIGNEDNESS) {
|
|
2040
|
+
pins: for (const cand of SIGNEDNESS) {
|
|
1986
2041
|
if (cand.signed && !pinnable) {
|
|
1987
2042
|
break;
|
|
1988
2043
|
}
|
|
@@ -1999,6 +2054,7 @@ export function enumerateCandidates(
|
|
|
1999
2054
|
onTailSlot: () => {
|
|
2000
2055
|
tailSlot = true;
|
|
2001
2056
|
},
|
|
2057
|
+
calleeBodies: opts.calleeBodies,
|
|
2002
2058
|
},
|
|
2003
2059
|
ownContext.ownContextProto,
|
|
2004
2060
|
);
|
|
@@ -2143,7 +2199,7 @@ export function enumerateCandidates(
|
|
|
2143
2199
|
opts.asmData,
|
|
2144
2200
|
symbolSetting.symbols,
|
|
2145
2201
|
opts.contextGlobals,
|
|
2146
|
-
{ keepTailSlots: liftSetting.keepTailSlots },
|
|
2202
|
+
{ keepTailSlots: liftSetting.keepTailSlots, calleeBodies: opts.calleeBodies },
|
|
2147
2203
|
ownContext.ownContextProto,
|
|
2148
2204
|
);
|
|
2149
2205
|
if (liftSetting.narrow && !narrowToSetupArgs(fn)) {
|
|
@@ -2181,9 +2237,10 @@ export function enumerateCandidates(
|
|
|
2181
2237
|
// …and the NAMES the map holds for the addresses this lift built by arithmetic off a
|
|
2182
2238
|
// named one (raise/offsetnames.ts). Per lift setting, off that setting's own map: the
|
|
2183
2239
|
// `/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
|
|
2240
|
+
// the staleness that leaves them is priced in pipeline.ts's copy of this note, which also
|
|
2241
|
+
// states the one-spelling-per-cell decline the pass throws from here.
|
|
2185
2242
|
if (symbolSetting.symbols) {
|
|
2186
|
-
OFFSET_NAME_PASS.run(fn, symbolSetting.symbols,
|
|
2243
|
+
OFFSET_NAME_PASS.run(fn, symbolSetting.symbols, offsetNameBehaviorsFor(target, opts.contextGlobals));
|
|
2187
2244
|
}
|
|
2188
2245
|
for (const [n, si] of [...inferredSymbols]) {
|
|
2189
2246
|
if (baseOpts.symbols?.has(n) === true || !sameDerivedShape(declSymbols.get(n), si)) {
|
|
@@ -2229,6 +2286,33 @@ export function enumerateCandidates(
|
|
|
2229
2286
|
reportThrow(settingVariations, e);
|
|
2230
2287
|
continue;
|
|
2231
2288
|
}
|
|
2289
|
+
// THE OTHER RETURN-TAIL READING, `/ret-copy` or `/ret-join`, in its own `reading` pass
|
|
2290
|
+
// between the follow and the sink. The default pass structures each setting both ways where
|
|
2291
|
+
// the join past early returns took a join somewhere (`onJoinPastReturns`), and publishes one
|
|
2292
|
+
// (pipeline.ts `structureReturnTailReading`). On a target that declares the join its default
|
|
2293
|
+
// (`joinPastReturnsByDefault`: mwcc 2.4.2 and 2.3.3) it structures the join first
|
|
2294
|
+
// and publishes the copies (`copyReturnTails`) only where they spell the function in fewer
|
|
2295
|
+
// lines, the layout shows a cross-jumped return tail, or an early return keeps a
|
|
2296
|
+
// branch-proven constant unfolded. On every other target it structures the copies first and publishes them, and the
|
|
2297
|
+
// join only where the copies refuse. This pass
|
|
2298
|
+
// enumerates the tree it did not publish, so neither reading is structured twice:
|
|
2299
|
+
// - `/ret-copy`, the copies, where the join was published. A chain of `if`s each holding
|
|
2300
|
+
// an early `return;` has no CFG join (`synthetic:retchain`), and where the early returns
|
|
2301
|
+
// cross-jump into one return tail the follow lands on that tail
|
|
2302
|
+
// (`synthetic:retchain_xj`); without the join both copy the code below every such `if`
|
|
2303
|
+
// into its arms, and a switch with a `case` that returns early likewise. The copies are
|
|
2304
|
+
// kept as a candidate because some rows' bytes are them (`synthetic:sw_fall:ido7.1`);
|
|
2305
|
+
// - `/ret-join`, the join, where the copies were published: the join copies a return tail
|
|
2306
|
+
// into each early return in its turn, and where that is the longer spelling it is kept
|
|
2307
|
+
// as a candidate beside the copies.
|
|
2308
|
+
// A setting whose structuring never took the join has one reading, and pays no candidate.
|
|
2309
|
+
// With both in the fan, the bound moves a ranked score nowhere: `compareScored` orders a
|
|
2310
|
+
// score tie by goto, cast and line counts before enumeration order.
|
|
2311
|
+
//
|
|
2312
|
+
// The shared-tail passes below structure with the copies too: their follow and sink were
|
|
2313
|
+
// ranked against the copies, their winners hold those trees, and crossing them with the join
|
|
2314
|
+
// is a reading nothing has measured.
|
|
2315
|
+
//
|
|
2232
2316
|
// THE SHARED-TAIL VARIATIONS: the same raised fn, structured again with `followEarlyReturns`,
|
|
2233
2317
|
// in two passes after the default one.
|
|
2234
2318
|
// - `/shared-ret` is the follow ALONE, on the fn as raised. Some divergent `if` shares a
|
|
@@ -2271,12 +2355,24 @@ export function enumerateCandidates(
|
|
|
2271
2355
|
// `droppedDefault` is the DEFAULT pass's drops. Each shared-tail pass reads it and keeps its
|
|
2272
2356
|
// own drops in a copy, so one pass's structuring failure never removes the other's candidate.
|
|
2273
2357
|
const droppedDefault = new Set<string>();
|
|
2358
|
+
// Per DEFAULT-pass setting whose structuring was a choice between the join past early
|
|
2359
|
+
// returns and the return-tail copies (pipeline.ts `structureReturnTailReading`), the reading
|
|
2360
|
+
// it did not publish: the only trees the `reading` pass enumerates, as `/ret-copy` where the
|
|
2361
|
+
// join was published and `/ret-join` where the copies were.
|
|
2362
|
+
const unpublishedDefault = new Map<string, { readonly reading: 'copies' | 'join'; readonly sfn: SFn }>();
|
|
2363
|
+
// DEFAULT-pass settings whose copies were dropped (they declined or failed a boundary
|
|
2364
|
+
// contract) or barred: the strip closure over them, as `dropped` is over whole settings, so
|
|
2365
|
+
// `X/merge-names` never publishes or offers the copies where `X`'s copies failed.
|
|
2366
|
+
const copiesDropped = new Set<string>();
|
|
2274
2367
|
// What `assertDefaultAccepts` learned about this fn, shared by every setting structured from it.
|
|
2275
2368
|
let acceptsMemo = new DefaultAcceptsMemo(fn);
|
|
2276
|
-
for (const pass of ['default', 'follow', 'sink'] as const) {
|
|
2369
|
+
for (const pass of ['default', 'follow', 'reading', 'sink'] as const) {
|
|
2277
2370
|
if (pass === 'follow' && !hasDivergentSharedRet(fn)) {
|
|
2278
2371
|
continue;
|
|
2279
2372
|
}
|
|
2373
|
+
if (pass === 'reading' && unpublishedDefault.size === 0) {
|
|
2374
|
+
continue;
|
|
2375
|
+
}
|
|
2280
2376
|
if (pass === 'sink') {
|
|
2281
2377
|
let sunk: boolean;
|
|
2282
2378
|
try {
|
|
@@ -2310,6 +2406,8 @@ export function enumerateCandidates(
|
|
|
2310
2406
|
const settingsForLift = usableStructureSettings.filter((s) =>
|
|
2311
2407
|
offForThisLift.every((variation) => !s[variation.flag]),
|
|
2312
2408
|
);
|
|
2409
|
+
// Per setting key structured in THIS pass, the `offWitness` hooks its structuring fired.
|
|
2410
|
+
const witnessed = new Map<string, Set<NonNullable<StructureVariation['offWitness']>>>();
|
|
2313
2411
|
// `/merge-names` combinations whose un-merged sibling was DROPPED. `structure()` already
|
|
2314
2412
|
// refuses to let the variation unlock a function the default declines, but it can only see its own
|
|
2315
2413
|
// refusals — a boundary contract fails out here, in `structureChecked`. Without this a
|
|
@@ -2326,14 +2424,50 @@ export function enumerateCandidates(
|
|
|
2326
2424
|
if (alternative && droppedDefault.has(key)) {
|
|
2327
2425
|
continue;
|
|
2328
2426
|
}
|
|
2329
|
-
|
|
2427
|
+
// THE OTHER RETURN-TAIL READING: the tree the default pass structured and did not publish,
|
|
2428
|
+
// enumerated as it is. It is the same setting's, so the witness and the strip closure
|
|
2429
|
+
// below already answered for it there.
|
|
2430
|
+
const otherReading = pass === 'reading' ? unpublishedDefault.get(key) : undefined;
|
|
2431
|
+
if (pass === 'reading' && otherReading === undefined) {
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
const settingLiftVariations: readonly Variation[] =
|
|
2435
|
+
otherReading === undefined
|
|
2436
|
+
? liftVariations
|
|
2437
|
+
: [...liftSetting.variations, otherReading.reading === 'copies' ? 'ret-copy' : 'ret-join'];
|
|
2438
|
+
// THE OFF-SIBLING WITNESS (`StructureVariation.offWitness`): a setting whose variation's
|
|
2439
|
+
// OFF sibling structured without firing the witness would rebuild that sibling's tree, so
|
|
2440
|
+
// it is skipped and reported as the tree dedup it is. A sibling not structured in this
|
|
2441
|
+
// pass (dropped, or skipped above) witnessed nothing, and the setting is structured.
|
|
2442
|
+
//
|
|
2443
|
+
// A setting skipped HERE builds its sibling's structuring, so it fires the hooks the
|
|
2444
|
+
// sibling fired, and it is recorded with the sibling's set. Without that the gate does
|
|
2445
|
+
// not compose: a setting carrying two witnessed variations (`tail-copies/for-continue`)
|
|
2446
|
+
// finds each sibling unrecorded and is structured however inert both are. A setting
|
|
2447
|
+
// skipped here in the default pass has no reading of its own in the `reading` pass:
|
|
2448
|
+
// the sibling's fired set is the union of both its readings' (`structureReturnTailReading`),
|
|
2449
|
+
// so both of this setting's readings are the sibling's trees, which the sibling enumerates.
|
|
2450
|
+
const sameAs = STRUCTURE_VARIATIONS.flatMap((variation) => {
|
|
2451
|
+
if (otherReading !== undefined || variation.offWitness === undefined || !s[variation.flag]) {
|
|
2452
|
+
return [];
|
|
2453
|
+
}
|
|
2454
|
+
const sibling = s.variations.filter((v) => v !== variation.name).join('/');
|
|
2455
|
+
const seen = witnessed.get(sibling);
|
|
2456
|
+
return seen !== undefined && !seen.has(variation.offWitness) ? [seen] : [];
|
|
2457
|
+
});
|
|
2458
|
+
if (sameAs.length > 0) {
|
|
2459
|
+
witnessed.set(key, sameAs[0]);
|
|
2460
|
+
opts.onTreeDeduped?.();
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
const stripped = (set: ReadonlySet<string>): boolean =>
|
|
2330
2464
|
STRUCTURE_VARIATIONS.some(
|
|
2331
2465
|
(variation) =>
|
|
2332
2466
|
variation.strip &&
|
|
2333
2467
|
s[variation.flag] &&
|
|
2334
|
-
|
|
2335
|
-
)
|
|
2336
|
-
) {
|
|
2468
|
+
set.has(s.variations.filter((v) => v !== variation.name).join('/')),
|
|
2469
|
+
);
|
|
2470
|
+
if (otherReading === undefined && stripped(dropped)) {
|
|
2337
2471
|
// A SKIPPED setting is recorded exactly like a dropped one, or the closure would not be
|
|
2338
2472
|
// transitive: with plain X dropped and X/inplace skipped-but-unrecorded,
|
|
2339
2473
|
// X/inplace/merge-names would find neither stripped key and run — shipping a
|
|
@@ -2345,36 +2479,97 @@ export function enumerateCandidates(
|
|
|
2345
2479
|
// which it re-derives from its map on every call, so both branch senses structure the same
|
|
2346
2480
|
// recovered function without re-lifting.
|
|
2347
2481
|
let sfn: SFn;
|
|
2482
|
+
const fired = new Set<NonNullable<StructureVariation['offWitness']>>();
|
|
2483
|
+
const witnessHooks = Object.fromEntries(
|
|
2484
|
+
STRUCTURE_VARIATIONS.flatMap((variation) =>
|
|
2485
|
+
variation.offWitness !== undefined && !s[variation.flag]
|
|
2486
|
+
? [[variation.offWitness, () => fired.add(variation.offWitness!)]]
|
|
2487
|
+
: [],
|
|
2488
|
+
),
|
|
2489
|
+
);
|
|
2490
|
+
let counterSignRefused = false;
|
|
2491
|
+
const structOpts = {
|
|
2492
|
+
...symbolSettingOpts,
|
|
2493
|
+
...(inferredSymbols.size ? { inferredSymbols } : {}),
|
|
2494
|
+
...(orderLicensed.size ? { orderLicensedGlobals: orderLicensed } : {}),
|
|
2495
|
+
preserveDivergentBranchSense: s.sense,
|
|
2496
|
+
negateJoinedBranchSense: s.join ? !defSense : defSense,
|
|
2497
|
+
...(s.flipSites ? { branchSenseFlipSites: s.flipSites } : {}),
|
|
2498
|
+
anchorConstCopies: s.anchor,
|
|
2499
|
+
anchorLoopEntryConsts: s.entry,
|
|
2500
|
+
spellBitfieldMembers: s.bitfields,
|
|
2501
|
+
spellPtrMemberElements: s.ptrElems,
|
|
2502
|
+
spellDeclaredSubscripts: s.declRank,
|
|
2503
|
+
...STRUCTURE_VARIATIONS.reduce(
|
|
2504
|
+
(acc, variation) => ({ ...acc, ...variation.options(s[variation.flag]) }),
|
|
2505
|
+
{},
|
|
2506
|
+
),
|
|
2507
|
+
...(alternative ? { followEarlyReturns: true, copyReturnTails: true } : {}),
|
|
2508
|
+
};
|
|
2509
|
+
const hooks = {
|
|
2510
|
+
...witnessHooks,
|
|
2511
|
+
onCounterSignRefused: () => {
|
|
2512
|
+
counterSignRefused = true;
|
|
2513
|
+
},
|
|
2514
|
+
};
|
|
2348
2515
|
try {
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2516
|
+
if (otherReading !== undefined) {
|
|
2517
|
+
sfn = otherReading.sfn;
|
|
2518
|
+
} else if (pass === 'default') {
|
|
2519
|
+
// THE RETURN-TAIL READING, bounded by size and read off the layout (pipeline.ts
|
|
2520
|
+
// `structureReturnTailReading`): on a target that declares the join past early
|
|
2521
|
+
// returns its default, the join, or the copies where they spell this setting's
|
|
2522
|
+
// function shorter or the asm layout or an unfolded return marks them; on every other
|
|
2523
|
+
// target the copies, structured first, and the join only where they refuse, with
|
|
2524
|
+
// their refusal reported under `ret-copy` (the reading the fan then cannot hold). A
|
|
2525
|
+
// setting fails only where both refuse, and that failure is handled below as any
|
|
2526
|
+
// structuring failure. The losing tree is kept for the `reading` pass, so neither
|
|
2527
|
+
// reading is structured twice.
|
|
2528
|
+
const copiesBarred = stripped(copiesDropped);
|
|
2529
|
+
const read = structureReturnTailReading(
|
|
2530
|
+
fn,
|
|
2531
|
+
{ ...structOpts, joinPastReturns: copiesBarred },
|
|
2532
|
+
(t) => backend.emit(t),
|
|
2533
|
+
hooks,
|
|
2534
|
+
acceptsMemo,
|
|
2535
|
+
);
|
|
2536
|
+
sfn = read.sfn;
|
|
2537
|
+
if (read.unpublished === undefined) {
|
|
2538
|
+
if (copiesBarred) {
|
|
2539
|
+
copiesDropped.add(key);
|
|
2540
|
+
}
|
|
2541
|
+
} else if ('error' in read.unpublished) {
|
|
2542
|
+
copiesDropped.add(key);
|
|
2543
|
+
reportThrow(
|
|
2544
|
+
[...liftVariations, 'ret-copy', ...s.variations, ...symbolSetting.variations],
|
|
2545
|
+
read.unpublished.error,
|
|
2546
|
+
);
|
|
2547
|
+
} else {
|
|
2548
|
+
unpublishedDefault.set(key, read.unpublished);
|
|
2549
|
+
}
|
|
2550
|
+
witnessed.set(key, fired);
|
|
2551
|
+
} else {
|
|
2552
|
+
sfn = structureChecked(fn, structOpts, hooks, acceptsMemo);
|
|
2553
|
+
witnessed.set(key, fired);
|
|
2554
|
+
}
|
|
2372
2555
|
} catch (e) {
|
|
2373
2556
|
if (e instanceof DefaultAcceptsMemoMisuse) {
|
|
2374
2557
|
throw e; // a memo wired to the wrong fn is a defect here, never a dropped variation
|
|
2375
2558
|
}
|
|
2376
2559
|
if (abortsRow([...liftVariations, ...symbolSetting.variations], s)) {
|
|
2377
|
-
|
|
2560
|
+
if (!(
|
|
2561
|
+
counterSignRefused &&
|
|
2562
|
+
!cand.signed &&
|
|
2563
|
+
pinnable &&
|
|
2564
|
+
e instanceof StructureError &&
|
|
2565
|
+
declinesOnCounterSign(fn, structOpts)
|
|
2566
|
+
)) {
|
|
2567
|
+
throw e; // the default lift's default setting keeps its behavior: a failure aborts the row
|
|
2568
|
+
}
|
|
2569
|
+
// THE UNSIGNED PIN'S DECLINE, HELD FOR THE SIGNED ONE — and the rest of the unsigned
|
|
2570
|
+
// pin is not built: every other setting of it would run behind a declined default.
|
|
2571
|
+
heldPinDecline ??= e;
|
|
2572
|
+
continue pins;
|
|
2378
2573
|
}
|
|
2379
2574
|
// Recorded for EVERY dropped setting: a candidate with more variations on looks its siblings
|
|
2380
2575
|
// up by stripping one variation at a time, and the stripped key can itself carry the other.
|
|
@@ -2425,7 +2620,13 @@ export function enumerateCandidates(
|
|
|
2425
2620
|
// The PRE-RESPELL variations (PRE_RESPELL_VARIATIONS, the last composition the POLICY note
|
|
2426
2621
|
// names): rewrite the TREE, then run the whole respell set over the result, so every
|
|
2427
2622
|
// respell variation derives from the rewrite instead of composing onto it. The gate is the pass's
|
|
2428
|
-
// own decline; the contracts are `respell`'s
|
|
2623
|
+
// own decline; the contracts are `respell`'s, for `respell`'s reasons, plus the effect count
|
|
2624
|
+
// `respell` cannot run (its note above): a pre-respell rewrite moves control, not just
|
|
2625
|
+
// spelling — `/unmerge` copies a join into its arms, `/case-end-goto` jumps over the code
|
|
2626
|
+
// between a loop and its arm's end — and the per-path count is what refuses a rewrite that
|
|
2627
|
+
// runs a call twice on one path or drops one. It is also the only production reader of
|
|
2628
|
+
// that count's case-end-goto paths (contracts.ts `PathCounts.end`): structuring never
|
|
2629
|
+
// writes the goto, so without it those paths are read by unit tests alone.
|
|
2429
2630
|
for (const pf of PRE_RESPELL_VARIATIONS) {
|
|
2430
2631
|
try {
|
|
2431
2632
|
const made = pf.apply(sfn);
|
|
@@ -2450,6 +2651,7 @@ export function enumerateCandidates(
|
|
|
2450
2651
|
assertNoOrphanedLocals(sfn, made);
|
|
2451
2652
|
assertLocalStorageStaysInteger(sfn, made);
|
|
2452
2653
|
assertFrameUnionsViewed(made);
|
|
2654
|
+
assertEffectsPreserved(fn, made);
|
|
2453
2655
|
// A backend refusal on this REWRITTEN tree is not a refusal of the row's own
|
|
2454
2656
|
// tree, so it never becomes the row's stated cause: `TreeSources.emit` is dropped
|
|
2455
2657
|
// here and only the call over the row's own tree above records one.
|
|
@@ -2525,7 +2727,7 @@ export function enumerateCandidates(
|
|
|
2525
2727
|
}
|
|
2526
2728
|
const variations: readonly Variation[] = [
|
|
2527
2729
|
cand.variation,
|
|
2528
|
-
...
|
|
2730
|
+
...settingLiftVariations,
|
|
2529
2731
|
...s.variations,
|
|
2530
2732
|
...sp.variations,
|
|
2531
2733
|
...symbolSetting.variations,
|
|
@@ -2559,6 +2761,9 @@ export function enumerateCandidates(
|
|
|
2559
2761
|
// Every tree the fan produced was refused by the backend. Each refusal on its own is a dropped
|
|
2560
2762
|
// candidate; all of them together is the row, and it stays LOUD — the alternative is a caller
|
|
2561
2763
|
// ranking an empty list and reporting no match for a function nothing ever tried to spell.
|
|
2764
|
+
if (out.length === 0 && heldPinDecline !== undefined) {
|
|
2765
|
+
throw heldPinDecline;
|
|
2766
|
+
}
|
|
2562
2767
|
if (out.length === 0) {
|
|
2563
2768
|
throw new NoSpellableCandidateError(
|
|
2564
2769
|
`no spellable candidate for '${name}': ${firstLine(lastEmitError ?? 'no candidate produced')}`,
|
|
@@ -2748,18 +2953,28 @@ export function compareScored<S extends { score: number }>(
|
|
|
2748
2953
|
a.preference - b.preference ||
|
|
2749
2954
|
(b.deviceVolatile ?? 0) - (a.deviceVolatile ?? 0) ||
|
|
2750
2955
|
(a.farStoreCast ? 1 : 0) - (b.farStoreCast ? 1 : 0) ||
|
|
2956
|
+
gotoCount(a.source) - gotoCount(b.source) ||
|
|
2751
2957
|
castCount(a.source) - castCount(b.source) ||
|
|
2752
2958
|
lineCount(a.source) - lineCount(b.source) ||
|
|
2753
2959
|
a.order - b.order
|
|
2754
2960
|
);
|
|
2755
2961
|
}
|
|
2756
2962
|
|
|
2757
|
-
/**
|
|
2758
|
-
*
|
|
2759
|
-
|
|
2760
|
-
|
|
2963
|
+
/** `goto` statements in a candidate's rendered source — the readability tie-break above, asked
|
|
2964
|
+
* before the casts. Each goto asmlift writes (l3/ast.ts: a jump to a case label of the switch it
|
|
2965
|
+
* sits in, or to the `break;` that ends its arm) is a ranked spelling against a goto-free one —
|
|
2966
|
+
* copies (`/case-goto`) or a flag (`/case-end-goto`) — and where the two score alike the goto-free
|
|
2967
|
+
* one is published: a goto is the narrow exception the differ has to earn, never a tie's winner.
|
|
2968
|
+
* Deterministic, and total on any string. */
|
|
2969
|
+
export function gotoCount(source: string): number {
|
|
2970
|
+
return source.match(/\bgoto [A-Za-z_]\w*;/g)?.length ?? 0;
|
|
2761
2971
|
}
|
|
2762
2972
|
|
|
2973
|
+
/** Non-blank lines in a candidate's rendered source — the compactness tie-break above, counted
|
|
2974
|
+
* exactly as `quality.ts` counts `lines`, and the one count the default's return-tail reading is
|
|
2975
|
+
* bounded by (pipeline.ts `sourceLineCount`). Deterministic, and total on any string. */
|
|
2976
|
+
const lineCount = sourceLineCount;
|
|
2977
|
+
|
|
2763
2978
|
/** Scalar casts in a candidate's rendered source — the readability tie-break above.
|
|
2764
2979
|
*
|
|
2765
2980
|
* A WITHIN-PREFERENCE tie-break over two spellings of ONE function, and deliberately NARROWER than
|