@asmlift/core 0.9.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/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
package/src/rank-variations.ts
CHANGED
|
@@ -24,10 +24,11 @@ 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';
|
|
30
|
-
import { narrowDeclarations, narrowReadDeclarations } from './l3/narrowdecl';
|
|
31
|
+
import { narrowDeclarations, narrowLoadDeclarations, narrowReadDeclarations } from './l3/narrowdecl';
|
|
31
32
|
import { pollGuards, pollReads } from './l3/pollguard';
|
|
32
33
|
import { unmergeJoins } from './l3/unmerge';
|
|
33
34
|
// TYPE-ONLY, and deliberately: the structure-variation table types its `options` as
|
|
@@ -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
|
|
@@ -71,14 +76,18 @@ export interface StructureVariation {
|
|
|
71
76
|
| 'derivedHome'
|
|
72
77
|
| 'mergeHome'
|
|
73
78
|
| 'escapeHome'
|
|
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[] = [
|
|
@@ -225,17 +234,22 @@ export const STRUCTURE_VARIATIONS: readonly StructureVariation[] = [
|
|
|
225
234
|
perLiftGate: hasEscapingExtension,
|
|
226
235
|
strip: true,
|
|
227
236
|
},
|
|
228
|
-
// `/
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
237
|
+
// `/signed-cmp` — spell an unsigned compare SIGNED (structure.ts unsignedCompareSpelling OFF): no
|
|
238
|
+
// `(u32)` cast at an icmp_u* and no u32 reconciliation of a mixed-claimant declaration. The
|
|
239
|
+
// default is the one spelling value-faithful without a proof; this one is a real source where the
|
|
240
|
+
// compiler PROVED both sides non-negative (only then does it emit the unsigned branch from a
|
|
241
|
+
// signed compare), which emission cannot always repeat. Where the proof does not hold it is a
|
|
242
|
+
// different program, so rank.ts judges every emitted tree (`signedOverUnsignedCompare`) and makes
|
|
243
|
+
// a signed rendering `Candidate.matchOnly`, whichever setting or respell spelled it. A tie or a
|
|
244
|
+
// better nonmatch score is no evidence for it (`synthetic:selloop:gcc2.7.2kmc`).
|
|
245
|
+
// KNOWN GAP, in the safe direction: the judgment reads the tree's declarations, and globals are
|
|
246
|
+
// not among them, so a compare that renders unsigned only through a `void *` global the backend
|
|
247
|
+
// declares (ptrcell.test.ts KR1, `g >= gLimit`) is still withheld on a nonmatch.
|
|
248
|
+
// Gated on the function having an unsigned compare at all.
|
|
235
249
|
{
|
|
236
|
-
flag: '
|
|
237
|
-
name: '
|
|
238
|
-
options: (on) => ({ unsignedCompareSpelling: on }),
|
|
250
|
+
flag: 'signedCmp',
|
|
251
|
+
name: 'signed-cmp',
|
|
252
|
+
options: (on) => ({ unsignedCompareSpelling: !on }),
|
|
239
253
|
sharedGate: (sharedLift) => sharedLift.blocks.some((b) => b.ops.some((op) => op.opcode.startsWith('icmp_u'))),
|
|
240
254
|
strip: true,
|
|
241
255
|
},
|
|
@@ -371,26 +385,107 @@ export const STRUCTURE_VARIATIONS: readonly StructureVariation[] = [
|
|
|
371
385
|
),
|
|
372
386
|
strip: false,
|
|
373
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
|
+
},
|
|
374
462
|
];
|
|
375
463
|
|
|
376
|
-
/** The
|
|
377
|
-
|
|
378
|
-
* variation outside this table, derived onto every source. Two members need not commute
|
|
379
|
-
* (`/narrow-decl` and `/narrow-read` compete for one cast), and table order then decides which one
|
|
380
|
-
* takes it in the all-together candidate. Each fires alone, plus
|
|
381
|
-
* all of them together in table order — not the full subset lattice; the pairs question is
|
|
382
|
-
* settled by applyStacked' skip-on-decline below, and a row demanding a true EXCLUSION pair —
|
|
383
|
-
* every member fires, the match needs a strict subset of them — is what would earn the lattice.
|
|
384
|
-
* So a member appended here also changes the all-together candidate wherever it fires beside two
|
|
385
|
-
* or more of the others. */
|
|
386
|
-
export const STACKED_VARIATIONS: { name: VariationName; apply: (sfn: SFn) => SFn | null }[] = [
|
|
464
|
+
/** The statement-order/shape members. */
|
|
465
|
+
const STACKED_SHAPES: { name: VariationName; apply: (sfn: SFn) => SFn | null }[] = [
|
|
387
466
|
{ name: 'initfirst', apply: initFirstGuards },
|
|
388
467
|
{ name: 'pollguard', apply: pollGuards },
|
|
389
468
|
{ name: 'pollread', apply: pollReads },
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
/** The declaration-width members: one dimension, the width each kind of local is declared at
|
|
472
|
+
* (l3/narrowdecl.ts). A function holding a cast-written local,
|
|
473
|
+
* load-written locals and call-result locals may need any combination of the three kinds narrow
|
|
474
|
+
* while all three fire (the matching suite compiles all eight), so every subset is offered. */
|
|
475
|
+
const STACKED_WIDTHS: { name: VariationName; apply: (sfn: SFn) => SFn | null }[] = [
|
|
390
476
|
{ name: 'narrow-decl', apply: narrowDeclarations },
|
|
477
|
+
{ name: 'narrow-load', apply: narrowLoadDeclarations },
|
|
391
478
|
{ name: 'narrow-read', apply: narrowReadDeclarations },
|
|
392
479
|
];
|
|
393
480
|
|
|
481
|
+
/** The STACKED variations (sanctioned in the POLICY note at rank.ts's respell site): each entry is
|
|
482
|
+
* a statement-order/shape or declaration-width respell variation orthogonal to every respell
|
|
483
|
+
* variation outside this table, derived onto every source. Two members need not commute
|
|
484
|
+
* (`/narrow-decl` and `/narrow-read` compete for one cast, `/narrow-load` and `/narrow-read` for
|
|
485
|
+
* one local), and table order then decides which one takes it in a candidate holding both. Which
|
|
486
|
+
* of them a candidate applies is STACKED_SUBSETS. */
|
|
487
|
+
export const STACKED_VARIATIONS = [...STACKED_SHAPES, ...STACKED_WIDTHS];
|
|
488
|
+
|
|
394
489
|
/** The PRE-RESPELL variations (sanctioned in the POLICY note at rank.ts's respell site): a tree
|
|
395
490
|
* rewrite applied BEFORE the respell set, so the whole set derives from its output instead of
|
|
396
491
|
* composing onto it. Same record type as STACKED_VARIATIONS above, and deliberately so — the only difference is
|
|
@@ -455,20 +550,60 @@ export const STACKED_VARIATIONS: { name: VariationName; apply: (sfn: SFn) => SFn
|
|
|
455
550
|
* `synthetic:ladidx1`, `synthetic:ladidx2`, `synthetic:revlad5s`. `bench regression` is an
|
|
456
551
|
* OUTCOME gate over all tiers, so every one of them turns it red — but `benchmark.yml` is
|
|
457
552
|
* `workflow_dispatch`, manual, with no cron, so nothing runs that gate on a PR. The scores and the
|
|
458
|
-
* method are in `apps/benchmark/dataset/synthetic.ts`'s `/unmerge` block.
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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 },
|
|
464
564
|
];
|
|
465
565
|
|
|
566
|
+
/** Every subset of `xs`, the empty one first, each in `xs`' order. */
|
|
567
|
+
const subsets = <X>(xs: readonly X[]): X[][] =>
|
|
568
|
+
xs.reduce<X[][]>((acc, x) => [...acc, ...acc.map((s) => [...s, x])], [[]]);
|
|
569
|
+
|
|
570
|
+
/** The stacked members a candidate applies: the table's two dimensions crossed, a shape choice
|
|
571
|
+
* (none, each shape alone, or every shape) by a width choice (any subset of the widths, none
|
|
572
|
+
* included), the shapes first. A product, because a function may need a shape AND a strict subset
|
|
573
|
+
* of the widths that fire: `initfirst` beside a narrow cast-written local and wide load-written
|
|
574
|
+
* ones (the matching suite compiles that pair both ways). The shapes are not a lattice of their own:
|
|
575
|
+
* applyStacked's skip-on-decline makes "every shape" each pair of them wherever the third declines,
|
|
576
|
+
* and a row demanding a strict subset of the shapes while all fire is what would earn one. Fewest
|
|
577
|
+
* members first, so where everything else ties the smaller set is enumerated first and wins. */
|
|
578
|
+
export const STACKED_SUBSETS: (typeof STACKED_VARIATIONS)[number][][] = subsets(STACKED_WIDTHS)
|
|
579
|
+
.flatMap((widths) =>
|
|
580
|
+
[[], ...STACKED_SHAPES.map((x) => [x]), ...(STACKED_SHAPES.length > 1 ? [STACKED_SHAPES] : [])].map((shapes) => [
|
|
581
|
+
...shapes,
|
|
582
|
+
...widths,
|
|
583
|
+
]),
|
|
584
|
+
)
|
|
585
|
+
.filter((subset) => subset.length > 0)
|
|
586
|
+
.sort((a, b) => a.length - b.length);
|
|
587
|
+
|
|
588
|
+
/** A stacked member that threw, named by the members that fired before it and then itself: the
|
|
589
|
+
* step that threw, which every subset reaching it shares. Reported under that name rather than
|
|
590
|
+
* the whole subset's, so a consumer deduping on the step prints one broken member once and not
|
|
591
|
+
* once per subset holding it. */
|
|
592
|
+
export class StackedMemberThrew extends Error {
|
|
593
|
+
constructor(
|
|
594
|
+
readonly variations: readonly VariationName[],
|
|
595
|
+
cause: unknown,
|
|
596
|
+
) {
|
|
597
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
466
601
|
/** The subset applied in table order, SKIP-ON-DECLINE: a member that declines contributes
|
|
467
602
|
* nothing rather than killing the combination — the all-shapes candidate is "everything that
|
|
468
603
|
* fires", so a pair is reachable whenever the third declines. The variations are the members that
|
|
469
|
-
* actually FIRED, so they never name a variation that declined; a fired
|
|
470
|
-
* duplicates a smaller subset
|
|
471
|
-
* nothing fired. */
|
|
604
|
+
* actually FIRED, so they never name a variation that declined; a fired set that
|
|
605
|
+
* duplicates a smaller subset's derives the same tree, which the caller emits once
|
|
606
|
+
* (`firstFired`). Null when nothing fired. A member that throws throws StackedMemberThrew. */
|
|
472
607
|
export const applyStacked = (
|
|
473
608
|
subset: readonly (typeof STACKED_VARIATIONS)[number][],
|
|
474
609
|
from: SFn,
|
|
@@ -476,7 +611,12 @@ export const applyStacked = (
|
|
|
476
611
|
let cur = from;
|
|
477
612
|
const fired: VariationName[] = [];
|
|
478
613
|
for (const sp of subset) {
|
|
479
|
-
|
|
614
|
+
let r: SFn | null;
|
|
615
|
+
try {
|
|
616
|
+
r = sp.apply(cur);
|
|
617
|
+
} catch (e) {
|
|
618
|
+
throw new StackedMemberThrew([...fired, sp.name], e);
|
|
619
|
+
}
|
|
480
620
|
if (r) {
|
|
481
621
|
cur = r;
|
|
482
622
|
fired.push(sp.name);
|
|
@@ -485,6 +625,17 @@ export const applyStacked = (
|
|
|
485
625
|
return fired.length > 0 ? { out: cur, variations: fired } : null;
|
|
486
626
|
};
|
|
487
627
|
|
|
628
|
+
/** Whether `fired` is a fired set not yet in `seen`, recording it. Two subsets that fire the same
|
|
629
|
+
* members derive the same tree, so a caller emits only the first. */
|
|
630
|
+
export const firstFired = (seen: Set<string>, fired: readonly VariationName[]): boolean => {
|
|
631
|
+
const key = fired.join('/');
|
|
632
|
+
if (seen.has(key)) {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
seen.add(key);
|
|
636
|
+
return true;
|
|
637
|
+
};
|
|
638
|
+
|
|
488
639
|
/** The locals a variation added — a NAME diff rather than a positional slice, so a pass that ever
|
|
489
640
|
* reorders locals cannot silently empty the set. It is what scopes `/volatile` to the pointers
|
|
490
641
|
* the variation itself created (volatilePtrLocals' `only`), leaving the tree's own locals alone. */
|