parseman 0.61.4 → 0.61.6
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/dist/analysis/diagnostics.js +844 -522
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +846 -522
- package/dist/cli/index.js.map +3 -3
- package/dist/cst/capture-buffer.d.ts +3 -2
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/index.js +890 -523
- package/dist/index.js.map +3 -3
- package/dist/plugin/entry-supercompiler.d.ts +15 -0
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/index.js +959 -503
- package/dist/plugin/index.js.map +3 -3
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/child-slots.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +1 -1
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/fail-clean.d.ts.map +1 -1
- package/dist/table/hold-gap.d.ts.map +1 -1
- package/dist/table/index.js +421 -54
- package/dist/table/index.js.map +3 -3
- package/dist/table/late-frame.d.ts +18 -0
- package/dist/table/late-frame.d.ts.map +1 -0
- package/dist/table/ops.d.ts +17 -0
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/select-hot.d.ts +9 -0
- package/dist/table/select-hot.d.ts.map +1 -1
- package/dist/table/site-labels.d.ts +5 -1
- package/dist/table/site-labels.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/cst/capture-buffer.ts +20 -11
- package/src/plugin/entry-supercompiler.ts +110 -1
- package/src/table/assemble.ts +124 -4
- package/src/table/child-slots.ts +9 -1
- package/src/table/emit-assembly.ts +101 -14
- package/src/table/encode.ts +69 -6
- package/src/table/exec.ts +7 -1
- package/src/table/fail-clean.ts +8 -1
- package/src/table/hold-gap.ts +5 -1
- package/src/table/late-frame.ts +54 -0
- package/src/table/ops.ts +19 -0
- package/src/table/select-hot.ts +96 -18
- package/src/table/site-labels.ts +38 -14
- package/src/version.ts +1 -1
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
OP_LABEL, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NAMES,
|
|
52
52
|
OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX,
|
|
53
53
|
OP_RX_TRACK, OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, SCOPE_INHERIT, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_WITHCTX, OP_XFORM,
|
|
54
|
-
OP_LEX_BODY, OP_LEX_PROGRAM, OP_GAPGATE, OP_STRICT,
|
|
54
|
+
OP_LEX_BODY, OP_LEX_PROGRAM, OP_GAPGATE, OP_STRICT, OP_CONT, OP_SEED,
|
|
55
55
|
} from './ops.ts'
|
|
56
56
|
import {
|
|
57
57
|
choiceCannotCommit, choiceLogRollbackMask, choiceRollbackMask, failureLogRollback, failureRollbackClean, validateDispatchSpec,
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
scalarTerminalNodeChild, scalarTerminalNotChild, scalarTerminalPeekChild, type LeadingScalarSequence,
|
|
68
68
|
} from './scalar-terminal.ts'
|
|
69
69
|
import { childSlots } from './child-slots.ts'
|
|
70
|
+
import { canDecide, continuationGate } from './late-frame.ts'
|
|
70
71
|
import { holdsGap, rowNullable } from './hold-gap.ts'
|
|
71
72
|
import { gapRows } from './gap-gate.ts'
|
|
72
73
|
import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
|
|
@@ -139,7 +140,7 @@ export const EMITTED_PARAMS = [
|
|
|
139
140
|
// The deferred-commit trivia skip (`trivia-gap.ts`).
|
|
140
141
|
'skipDeferredTrivia',
|
|
141
142
|
// The pass-through collapse (`run-support.ts`).
|
|
142
|
-
'collapseEmpty',
|
|
143
|
+
'collapseEmpty', 'popCstChild',
|
|
143
144
|
// Constructor-built tree objects (`cst/plain.d.ts`): a `new` call has no
|
|
144
145
|
// allocation site, so V8 cannot pretenure them.
|
|
145
146
|
'_SP', '_LF', '_ND', '_FC', '_BUF',
|
|
@@ -674,7 +675,8 @@ export function emitAssemblySource(
|
|
|
674
675
|
// less than reading the slot costs; only a gap matched by its RegExp takes the
|
|
675
676
|
// slot (`gapRegexLowers`).
|
|
676
677
|
const GAP_ROWS = gapNeutral ? new Set<number>() : new Set([...gapRows(code, roots)].filter(row => !gapRegexLowers(k[code[row + 1]!])))
|
|
677
|
-
const labels = computeSiteLabels(code, roots, hostCst
|
|
678
|
+
const labels = computeSiteLabels(code, roots, hostCst,
|
|
679
|
+
cfg.tolerant || cfg.probe ? undefined : (ip, tri) => canDecide(code, ip, tri, t.triviaScan))
|
|
678
680
|
// Eligibility is pooled by the terminal's existing constant operand, matching
|
|
679
681
|
// the closure recognizer pool: distinct terminal rows sharing one spec share
|
|
680
682
|
// both the recognizer and the ordinary-terminal lowering.
|
|
@@ -1037,6 +1039,17 @@ return 1
|
|
|
1037
1039
|
return c => `${c}<55296&&!(${leads.map(r => r.lo === r.hi ? `${c}===${r.lo}` : `(${c}>=${r.lo}&&${c}<=${r.hi})`).join('||')})`
|
|
1038
1040
|
}
|
|
1039
1041
|
|
|
1042
|
+
/** `ctx.captureTrivia === true && (a sink is open)`, as far as the label resolves it. */
|
|
1043
|
+
function skipCapturing(l: SiteLabel): string {
|
|
1044
|
+
if (l.tri < 0 || !swapLegal || triviaLabelled[l.tri]! || t.triviaScan[l.tri] == null) return 'true'
|
|
1045
|
+
return capturesHere(l)
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function capturesHere(l: SiteLabel): string {
|
|
1049
|
+
const sink = l.buf ? 'true' : '(ctx._cstBuf!==undefined||ctx._cstTriviaLog!==undefined)'
|
|
1050
|
+
return l.cap === CAP_OFF ? 'false' : l.cap === CAP_ON ? sink : `(ctx.captureTrivia===true&&${sink})`
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1040
1053
|
function skipFor(l: SiteLabel): string {
|
|
1041
1054
|
if (l.tri < 0) return '_skipTrivia'
|
|
1042
1055
|
const ki = l.tri
|
|
@@ -1205,6 +1218,8 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
|
|
|
1205
1218
|
if (op === OP_GATE && (cfg.tolerant || cfg.probe)) return code[ip + 2]!
|
|
1206
1219
|
if (op === OP_RULE || op === OP_GAPGATE) return code[ip + 1]!
|
|
1207
1220
|
if (op === OP_STRICT) return code[cfg.tolerant || cfg.probe ? ip + 2 : ip + 1]!
|
|
1221
|
+
if (op === OP_CONT && !labels.late.has(ip)) return code[ip + 2]!
|
|
1222
|
+
if (op === OP_SEED) return code[ip + 1]!
|
|
1208
1223
|
// A SCOPE THAT INSTALLS WHAT IS ALREADY INSTALLED. `encode.ts:520` wraps
|
|
1209
1224
|
// EVERY rule of a `rules({ trivia }, …)` map in its own `OP_SCOPE_PLAIN`, so a
|
|
1210
1225
|
// grammar with one ambient trivia re-installs the same slot at every rule
|
|
@@ -1220,6 +1235,9 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
|
|
|
1220
1235
|
return undefined
|
|
1221
1236
|
}
|
|
1222
1237
|
|
|
1238
|
+
/** Set by `OP_CONT` for the `OP_NODE` it lowers in its own function. */
|
|
1239
|
+
let LATE: { prefix: string; body: string } | undefined
|
|
1240
|
+
|
|
1223
1241
|
function lower(ip: number, fname: string): string {
|
|
1224
1242
|
const op = code[ip]
|
|
1225
1243
|
const head = `function ${fname}(input,pos,ctx){`
|
|
@@ -1428,6 +1446,70 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
|
|
|
1428
1446
|
// hand-built-program idiom this file's driver tests use.
|
|
1429
1447
|
return `${head}EC.e=pos;return ''}`
|
|
1430
1448
|
|
|
1449
|
+
case OP_CONT: {
|
|
1450
|
+
const tri = L.tri
|
|
1451
|
+
const g = continuationGate(code, code[ip + 3]!)!
|
|
1452
|
+
const missAt = (idx: number, at: string): string => {
|
|
1453
|
+
const resolved = t.cc[idx]!
|
|
1454
|
+
const ci = classes.push([resolved]) - 1
|
|
1455
|
+
classPlan.push([idx])
|
|
1456
|
+
const ref = hoist('cg', `CLS[${ci}][0]`)
|
|
1457
|
+
return isAsciiOnlyClass(resolved) ? `${ref}.ascii[input.charCodeAt(${at})]!==1` : `!classHas(${ref},lead(input,${at}))`
|
|
1458
|
+
}
|
|
1459
|
+
// The gap the tail would start past, read without recording anything: no scan
|
|
1460
|
+
// at a character no trivia can start.
|
|
1461
|
+
let scanned = ''
|
|
1462
|
+
if (tri >= 0) {
|
|
1463
|
+
const scan = hoist('cs', `TRIVIASCAN[${tri}]`)
|
|
1464
|
+
const miss = triviaMiss(tri)
|
|
1465
|
+
scanned = miss === undefined
|
|
1466
|
+
? `const q0=${scan}(input,c1)\n`
|
|
1467
|
+
: `const cc=input.charCodeAt(c1)\nconst q0=${miss('cc')}?c1:${scan}(input,c1)\n`
|
|
1468
|
+
}
|
|
1469
|
+
const at = tri >= 0 ? 'q0' : 'c1'
|
|
1470
|
+
let test: string
|
|
1471
|
+
if (g.gapRow < 0) test = `if(c0!==undefined&&${missAt(g.cls, at)})return c0\n`
|
|
1472
|
+
else {
|
|
1473
|
+
const re = hoist('cr', `K[${code[g.gapRow + 1]!}]`)
|
|
1474
|
+
const other = g.other >= 0 ? missAt(g.other, at) : 'true'
|
|
1475
|
+
test = `${re}.lastIndex=${at}\nconst gq=${re}.test(input)?${re}.lastIndex:-1\nif(c0!==undefined&&(gq<0||${missAt(g.follow, 'gq')})&&${other})return c0\n`
|
|
1476
|
+
}
|
|
1477
|
+
const seq = code[code[ip + 2]! + 2]!
|
|
1478
|
+
const seqLabel = labels.at(seq)
|
|
1479
|
+
const tail = link(code[ip + 3]!)
|
|
1480
|
+
const recorded = tri === TRI_NONE ? '' : `const tl0=ctx._cstBuf!==undefined?(ctx._cstBuf.tl!==undefined?ctx._cstBuf.tl.length:0):(ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0)
|
|
1481
|
+
const lg0=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
|
|
1482
|
+
const rt0=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0
|
|
1483
|
+
const s0=${skipFor(seqLabel)}(input,c1,ctx)
|
|
1484
|
+
const tl1=ctx._cstBuf!==undefined?(ctx._cstBuf.tl!==undefined?ctx._cstBuf.tl.length:0):(ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0)
|
|
1485
|
+
const lg1=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
|
|
1486
|
+
const rt1=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0
|
|
1487
|
+
v=${tail}(input,s0,ctx)
|
|
1488
|
+
if(v!==FAIL&&EC.e<=s0){rollbackScannedTriviaAt(ctx,tl0,tl1,lg0,lg1,rt0,rt1);EC.e=c1}`
|
|
1489
|
+
const quiet = `v=${tail}(input,${at},ctx)
|
|
1490
|
+
if(v!==FAIL&&EC.e<=${at})EC.e=c1`
|
|
1491
|
+
const capture = capturesHere(seqLabel)
|
|
1492
|
+
// No gap: nothing to record, keep or take back. Nothing recording: not scanned again.
|
|
1493
|
+
const body = `let v
|
|
1494
|
+
${tri === TRI_NONE ? `v=${tail}(input,c1,ctx)`
|
|
1495
|
+
: `if(q0===c1)v=${tail}(input,c1,ctx)
|
|
1496
|
+
else ${capture === 'true' ? `{\n${recorded}\n}`
|
|
1497
|
+
: `if(ctx._triviaLog===undefined&&ctx._rootTriviaLog===undefined${capture === 'false' ? '' : `&&!${capture}`}){
|
|
1498
|
+
${quiet}
|
|
1499
|
+
}else{
|
|
1500
|
+
${recorded}
|
|
1501
|
+
}`}`}`
|
|
1502
|
+
const wrapperIp = code[ip + 2]!
|
|
1503
|
+
LATE = {
|
|
1504
|
+
prefix: `const c0=${link(code[ip + 1]!)}(input,pos,ctx)
|
|
1505
|
+
if(c0===FAIL)return FAIL
|
|
1506
|
+
const c1=EC.e
|
|
1507
|
+
${scanned}${test}`,
|
|
1508
|
+
body,
|
|
1509
|
+
}
|
|
1510
|
+
return lower(wrapperIp, fname)
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1431
1513
|
case OP_GATE: {
|
|
1432
1514
|
const child = link(code[ip + 2]!)
|
|
1433
1515
|
const xf = fxRef(code[ip + 3]!)
|
|
@@ -2661,7 +2743,9 @@ return out
|
|
|
2661
2743
|
case OP_NODE:
|
|
2662
2744
|
case OP_NODE_TRACK: {
|
|
2663
2745
|
const flags = code[ip + 3]!
|
|
2664
|
-
const
|
|
2746
|
+
const late = LATE
|
|
2747
|
+
LATE = undefined
|
|
2748
|
+
const scalarChild = late === undefined ? scalarTerminalNodeChild(code, ip) : -1
|
|
2665
2749
|
if (scalarChild >= 0 && scalarSpecs.has(code[scalarChild + 1]!)) {
|
|
2666
2750
|
const recognize = recognizerRef(code[scalarChild + 1]!)
|
|
2667
2751
|
const spec = code[scalarChild] === OP_RX ? null : k[code[scalarChild + 1]!] as string
|
|
@@ -2687,7 +2771,9 @@ EC.e=end
|
|
|
2687
2771
|
return nd
|
|
2688
2772
|
}`
|
|
2689
2773
|
}
|
|
2690
|
-
const child = link(code[ip + 2]!)
|
|
2774
|
+
const child = late === undefined ? link(code[ip + 2]!) : ''
|
|
2775
|
+
const runBody = late === undefined ? `const v=${child}(input,pos,ctx)` : late.body
|
|
2776
|
+
const takeBack = late === undefined ? '' : 'let c0Raw\nif(sBuf===undefined&&sRaw===undefined&&sCh!==undefined)sCh.pop()\nelse if(sBuf!==undefined||sCh!==undefined)c0Raw=popCstChild(ctx)\n'
|
|
2691
2777
|
const proj = code[ip + 4]!
|
|
2692
2778
|
const buildIdx = code[ip + 1]!
|
|
2693
2779
|
const type = k[code[ip + 5]!] as string
|
|
@@ -2714,7 +2800,7 @@ return nd
|
|
|
2714
2800
|
// THE PASS-THROUGH COLLAPSE (`run-support.ts` `collapseEmpty`): a
|
|
2715
2801
|
// collapse over one node is that node. `passCollapse` is the closure
|
|
2716
2802
|
// engine's test too.
|
|
2717
|
-
if (build !== undefined && passCollapse(code, ip, hostCst)) {
|
|
2803
|
+
if (late === undefined && build !== undefined && passCollapse(code, ip, hostCst)) {
|
|
2718
2804
|
return `${head}
|
|
2719
2805
|
const v=${child}(input,pos,ctx)
|
|
2720
2806
|
if(v!==undefined)return v
|
|
@@ -2725,7 +2811,7 @@ return collapseEmpty(ctx,${build},input,pos,EC.e,${flags === 32})
|
|
|
2725
2811
|
// character at `pos` opens none of the body's optional terms and no
|
|
2726
2812
|
// trivia, the body is its node term, run as a pass-through collapse.
|
|
2727
2813
|
// Not under recovery or a probe: there a gated-out term still runs.
|
|
2728
|
-
const gated = build !== undefined && !cfg.tolerant && !cfg.probe ? gatedPassCollapse(code, ip, hostCst) : undefined
|
|
2814
|
+
const gated = late === undefined && build !== undefined && !cfg.tolerant && !cfg.probe ? gatedPassCollapse(code, ip, hostCst) : undefined
|
|
2729
2815
|
const triviaLead = gated === undefined ? undefined : triviaLeadRanges(labels.at(code[ip + 2]!).tri, t.trivia)
|
|
2730
2816
|
const gp = gated !== undefined && triviaLead !== undefined && triviaLead.length <= 16
|
|
2731
2817
|
? `const gc=lead(input,pos)
|
|
@@ -2777,9 +2863,9 @@ sCh.push(nd)
|
|
|
2777
2863
|
if(sRaw!==undefined)sRaw.push(rawEntry(nd,input,pos,end))
|
|
2778
2864
|
}`
|
|
2779
2865
|
return `${head}
|
|
2780
|
-
${gp}const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
|
|
2866
|
+
${late?.prefix ?? ''}${gp}const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
|
|
2781
2867
|
const sCap=ctx.captureTrivia,sBuf=ctx._cstBuf,sFields=ctx._fields
|
|
2782
|
-
const flat
|
|
2868
|
+
${takeBack}const flat=${late === undefined ? '[]' : '[c0]'}
|
|
2783
2869
|
ctx._cstBuf=undefined
|
|
2784
2870
|
ctx._cstChildren=flat
|
|
2785
2871
|
ctx._cstLeaves=flat
|
|
@@ -2787,7 +2873,7 @@ ctx._cstRawChildren=undefined
|
|
|
2787
2873
|
ctx._cstTriviaLog=undefined
|
|
2788
2874
|
ctx.captureTrivia=false
|
|
2789
2875
|
ctx._fields=${fields ? '[]' : 'undefined'}
|
|
2790
|
-
|
|
2876
|
+
${runBody}
|
|
2791
2877
|
${trailingTrivia && L.tri !== TRI_NONE
|
|
2792
2878
|
? `if(v!==FAIL${L.tri === TRI_UNKNOWN ? '&&ctx.trivia!==undefined' : ''})EC.e=consumeTrivia(input,EC.e,ctx)\n`
|
|
2793
2879
|
: ''}const captured=_capturedFlatChildren(flat)
|
|
@@ -2871,16 +2957,17 @@ const tlog=buf.tl??EMPTY_TLOG`
|
|
|
2871
2957
|
const hostKids=kids
|
|
2872
2958
|
const rawKids=buf.raw??(buf.rawSingle!==undefined?[buf.rawSingle]:EMPTY_CH)
|
|
2873
2959
|
const tlog=buf.tl??EMPTY_TLOG`
|
|
2960
|
+
const seedCapture = late === undefined ? '' : omitsRaw ? 'buf.single=c0\nbuf.rawLen=1\n' : 'buf.single=c0\nbuf.rawSingle=c0Raw!==undefined?c0Raw:rawEntry(c0,input,pos,c1)\n'
|
|
2874
2961
|
return `${head}
|
|
2875
|
-
${!hostCst && !tracked ? gp : ''}const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
|
|
2962
|
+
${late?.prefix ?? ''}${!hostCst && !tracked ? gp : ''}const sCh=ctx._cstChildren,sLv=ctx._cstLeaves,sRaw=ctx._cstRawChildren,sTl=ctx._cstTriviaLog
|
|
2876
2963
|
const sCap=ctx.captureTrivia,sBuf=ctx._cstBuf
|
|
2877
|
-
${openCapture}
|
|
2878
|
-
ctx.captureTrivia=${captureWide}
|
|
2964
|
+
${takeBack}${openCapture}
|
|
2965
|
+
${seedCapture}ctx.captureTrivia=${captureWide}
|
|
2879
2966
|
const savedFields=ctx._fields
|
|
2880
2967
|
ctx._fields=${wantFields ? '[]' : 'undefined'}
|
|
2881
2968
|
${structural ? `const savedMask=ctx._triviaCaptureMask
|
|
2882
2969
|
if(_pfHost!==undefined&&_pfHost._parsemanTriviaKinds!==undefined)ctx._triviaCaptureMask=_pfHost._parsemanTriviaKinds(${ty})
|
|
2883
|
-
` : ''}
|
|
2970
|
+
` : ''}${runBody}
|
|
2884
2971
|
${trailingTrivia && L.tri !== TRI_NONE
|
|
2885
2972
|
? `if(v!==FAIL${L.tri === TRI_UNKNOWN ? '&&ctx.trivia!==undefined' : ''})EC.e=consumeTrivia(input,EC.e,ctx)\n`
|
|
2886
2973
|
: ''}const fieldMap=${wantFields ? 'buildFieldMap(ctx._fields)' : 'undefined'}
|
package/src/table/encode.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, SCOPE_INHERIT, OP_EXPECT, OP_SEQX, OP_SCAN,
|
|
19
19
|
OP_LIVE, OP_ATTEMPT, OP_LABEL,
|
|
20
20
|
OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD,
|
|
21
|
-
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_COV, OP_GAPGATE, OP_RGATE2, OP_STRICT,
|
|
21
|
+
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_COV, OP_GAPGATE, OP_RGATE2, OP_STRICT, OP_CONT, OP_SEED,
|
|
22
22
|
OP_LEX_BODY, OP_LEX_PROGRAM,
|
|
23
23
|
} from './ops.ts'
|
|
24
24
|
import { adjacencyExpected } from '../combinators/adjacency.ts'
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
collectLexicalCapabilities, winnerWrapsReference,
|
|
42
42
|
} from '../compiler/token-capability.ts'
|
|
43
43
|
import { directExecutableTokenBody, type ExecutableLexNode } from '../compiler/token-alphabet.ts'
|
|
44
|
+
import { continuationGate } from './late-frame.ts'
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* Can `emitConst` print this? Mirrors the guard in `emit.ts` — scalars, arrays
|
|
@@ -159,6 +160,30 @@ function isGap(p: Combinator<unknown>): boolean {
|
|
|
159
160
|
})
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Does this combinator publish exactly one node child and return that same value, on
|
|
165
|
+
* every path? Only then can a late frame (`OP_CONT`) take the corner's publication back
|
|
166
|
+
* and be the child it would have captured. A `transform` returns what its function
|
|
167
|
+
* makes of the child; a `dispatch` returns `[key, arm]` and its selector can publish too;
|
|
168
|
+
* an `expect` publishes nothing when its child fails. A `ref()` not defined yet proves nothing.
|
|
169
|
+
*/
|
|
170
|
+
function publishesOneNode(p: Combinator<unknown>, rr: RefResolver | undefined, seen = new Set<Combinator<unknown>>()): boolean {
|
|
171
|
+
if (seen.has(p)) return true
|
|
172
|
+
seen.add(p)
|
|
173
|
+
const d = p._def
|
|
174
|
+
switch (d.tag) {
|
|
175
|
+
case 'node': return true
|
|
176
|
+
case 'lazy': {
|
|
177
|
+
let target = winnerForRef(p, rr)
|
|
178
|
+
if (target === undefined) { try { target = d.thunk() } catch { return false } }
|
|
179
|
+
return publishesOneNode(target, rr, seen)
|
|
180
|
+
}
|
|
181
|
+
case 'choice': return d.parsers.every(a => publishesOneNode(a, rr, seen))
|
|
182
|
+
case 'grammar': case 'withCtx': case 'label': return publishesOneNode(d.parser, rr, seen)
|
|
183
|
+
default: return false
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
162
187
|
class Encoder {
|
|
163
188
|
code: number[] = []
|
|
164
189
|
k: unknown[] = []
|
|
@@ -1859,7 +1884,8 @@ class Encoder {
|
|
|
1859
1884
|
// without, on match and on failure.
|
|
1860
1885
|
// An AUTHORING error, not a table gap — raise what the other two engines do.
|
|
1861
1886
|
if (d.type === undefined) missingInferredType()
|
|
1862
|
-
const
|
|
1887
|
+
const late = this.opensLate(d)
|
|
1888
|
+
const child = late ? this.lateBody(d.parser._def as Extract<ParserDef, { tag: 'sequence' }>) : this.node(d.parser).ip
|
|
1863
1889
|
// Capture flags, resolved HERE from the reducer's declared arity using the
|
|
1864
1890
|
// same analysis codegen runs (`src/compiler/build-arity.ts`). `hostMode:
|
|
1865
1891
|
// 'cst'` forces them on, exactly as the emitted `cstOut` path does. The
|
|
@@ -1928,7 +1954,15 @@ class Encoder {
|
|
|
1928
1954
|
// `deriveExpected(body)`, which for a body with nullable leading terms
|
|
1929
1955
|
// names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
|
|
1930
1956
|
// running the body reports `['"x"']` alone.
|
|
1931
|
-
|
|
1957
|
+
// `child` is `SEQ 2 seed tail …`, and `seed` is `SEED corner`. A tail with no first
|
|
1958
|
+
// test has nothing to decide on: the node runs as written, over its corner.
|
|
1959
|
+
let run = body
|
|
1960
|
+
if (late) {
|
|
1961
|
+
const corner = this.code[this.code[child + 2]! + 1]!
|
|
1962
|
+
if (continuationGate(this.code, this.code[child + 3]!) === undefined) this.code[child + 2] = corner
|
|
1963
|
+
else run = this.emit(OP_CONT, corner, body, this.code[child + 3]!)
|
|
1964
|
+
}
|
|
1965
|
+
if (matchesEmpty(d.parser, new Set(), this.refResolver())) return run
|
|
1932
1966
|
// A node whose BODY is a bare cross-rule ref (`node('X', g.Y, …)`) caches its
|
|
1933
1967
|
// first set from the ref's base binding. Under a compose OVERRIDE the winner
|
|
1934
1968
|
// for `Y` may be WIDER, so the cached set gates out input the merged rule
|
|
@@ -1940,11 +1974,11 @@ class Encoder {
|
|
|
1940
1974
|
// that set match what entering the body reports (a `keywords()` term, a
|
|
1941
1975
|
// list's item set, an optional lead are only known by running it).
|
|
1942
1976
|
const fs = this.gateFirstSet(d.parser, d.guarded === true, false)
|
|
1943
|
-
if (fs.kind === 'any') return
|
|
1977
|
+
if (fs.kind === 'any') return run
|
|
1944
1978
|
const cls = this.charClass(fs)
|
|
1945
|
-
if (cls < 0) return
|
|
1979
|
+
if (cls < 0) return run
|
|
1946
1980
|
const e = deriveExpected(d.parser)
|
|
1947
|
-
return this.emit(OP_GATE, cls,
|
|
1981
|
+
return this.emit(OP_GATE, cls, run, this.expected(e.length > 0 ? e : [d.parser._tag]))
|
|
1948
1982
|
}
|
|
1949
1983
|
case 'token': {
|
|
1950
1984
|
// A token runs its body with trivia cleared (`token.ts`), so its body is
|
|
@@ -2271,6 +2305,33 @@ class Encoder {
|
|
|
2271
2305
|
}
|
|
2272
2306
|
}
|
|
2273
2307
|
|
|
2308
|
+
/**
|
|
2309
|
+
* `node(sequence(corner, tail), build, { collapse: true })` with a tail that can take
|
|
2310
|
+
* nothing and a corner that is one node: the frame can open after the corner, only if
|
|
2311
|
+
* the tail can start. Any other node keeps its frame from the start, and so does one
|
|
2312
|
+
* with no reducer, whose host can scope the corner's trivia to its type.
|
|
2313
|
+
*/
|
|
2314
|
+
private opensLate(d: Extract<ParserDef, { tag: 'node' }>): boolean {
|
|
2315
|
+
if (this.plan !== undefined || d.collapse !== true || d.build === undefined || d.trailingTrivia === true) return false
|
|
2316
|
+
const seq = d.parser._def
|
|
2317
|
+
return seq.tag === 'sequence' && seq.parsers.length === 2
|
|
2318
|
+
&& matchesEmpty(seq.parsers[1]!, new Set(), this.refResolver())
|
|
2319
|
+
&& publishesOneNode(seq.parsers[0]!, this.refResolver())
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
/** The body of a late frame: the sequence as written, its corner behind `OP_SEED`. */
|
|
2323
|
+
private lateBody(seq: Extract<ParserDef, { tag: 'sequence' }>): number {
|
|
2324
|
+
const seed = this.emit(OP_SEED, this.node(seq.parsers[0]!).ip)
|
|
2325
|
+
const tailIp = this.node(seq.parsers[1]!).ip
|
|
2326
|
+
const head = this.emitHead(seq.valueUnused ? OP_SEQV : OP_SEQ, 5)
|
|
2327
|
+
this.code[head + 1] = 2
|
|
2328
|
+
this.code[head + 2] = seed
|
|
2329
|
+
this.code[head + 3] = tailIp
|
|
2330
|
+
this.code[head + 4] = this.followClass(seq.parsers, 0)
|
|
2331
|
+
this.code[head + 5] = this.followClass(seq.parsers, 1)
|
|
2332
|
+
return head
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2274
2335
|
/**
|
|
2275
2336
|
* Peephole: an `OP_RULE` row is pure indirection — a switch dispatch and a
|
|
2276
2337
|
* call frame to do nothing but jump. Rewrite every child slot that points at
|
|
@@ -2306,6 +2367,8 @@ class Encoder {
|
|
|
2306
2367
|
case OP_REJECT: return [ip + 1]
|
|
2307
2368
|
case OP_REP: case OP_REPV: return this.code[ip + 4]! >= 0 ? [ip + 1, ip + 4] : [ip + 1]
|
|
2308
2369
|
case OP_STRICT: return [ip + 1, ip + 2]
|
|
2370
|
+
case OP_CONT: return [ip + 1, ip + 2, ip + 3]
|
|
2371
|
+
case OP_SEED: return [ip + 1]
|
|
2309
2372
|
default: return []
|
|
2310
2373
|
}
|
|
2311
2374
|
}
|
package/src/table/exec.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, SCOPE_INHERIT, OP_EXPECT, OP_SEQX, OP_SCAN,
|
|
22
22
|
OP_LIVE,
|
|
23
23
|
OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
|
|
24
|
-
OP_COV, OP_GAPGATE, OP_STRICT,
|
|
24
|
+
OP_COV, OP_GAPGATE, OP_STRICT, OP_CONT, OP_SEED,
|
|
25
25
|
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_RGATE2,
|
|
26
26
|
OP_LEX_BODY, OP_LEX_PROGRAM,
|
|
27
27
|
} from './ops.ts'
|
|
@@ -477,6 +477,12 @@ function makeDriver(
|
|
|
477
477
|
case OP_STRICT:
|
|
478
478
|
ip = code[ctx._tolerant || ctx._probe !== undefined ? ip + 2 : ip + 1]!; continue rows
|
|
479
479
|
|
|
480
|
+
// The reference runs a late frame's node as written, and is the leg it is compared with.
|
|
481
|
+
case OP_CONT:
|
|
482
|
+
ip = code[ip + 2]!; continue rows
|
|
483
|
+
case OP_SEED:
|
|
484
|
+
ip = code[ip + 1]!; continue rows
|
|
485
|
+
|
|
480
486
|
case OP_LIVE: {
|
|
481
487
|
// A HAND-WRITTEN COMBINATOR, run through its own `.parse` — mirrors
|
|
482
488
|
// codegen's `emitRuntimeFallback`. Its result IS the interpreter's, so
|
package/src/table/fail-clean.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
OP_ATTEMPT, OP_CHOICE, OP_COV, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE, OP_GUARD, OP_LABEL,
|
|
25
25
|
OP_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK,
|
|
26
26
|
OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX, OP_RX_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ,
|
|
27
|
-
OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM, OP_GAPGATE,
|
|
27
|
+
OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM, OP_GAPGATE, OP_CONT, OP_SEED,
|
|
28
28
|
} from './ops.ts'
|
|
29
29
|
|
|
30
30
|
export type FailClean = (ip: number) => boolean
|
|
@@ -113,6 +113,13 @@ export function failCleanAnalysis(code: ArrayLike<number>): FailClean {
|
|
|
113
113
|
case OP_ROUTED:
|
|
114
114
|
out = code[ip + 1]! < 0 || clean(code[ip + 1]!)
|
|
115
115
|
break
|
|
116
|
+
case OP_SEED:
|
|
117
|
+
out = clean(code[ip + 1]!)
|
|
118
|
+
break
|
|
119
|
+
// The corner may fail; the wrapper fails only through a tail that can.
|
|
120
|
+
case OP_CONT:
|
|
121
|
+
out = clean(code[ip + 1]!) && neverFails(code[ip + 3]!)
|
|
122
|
+
break
|
|
116
123
|
// An arm's failure and the fallback's are committed; only the selector's is
|
|
117
124
|
// not, and without a fallback an unknown key fails after the selector wrote.
|
|
118
125
|
case OP_DISPATCH:
|
package/src/table/hold-gap.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import {
|
|
18
18
|
OP_ADJ, OP_ATTEMPT, OP_CHOICE, OP_COV, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI,
|
|
19
19
|
OP_NODE, OP_NOT, OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ,
|
|
20
|
-
OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM, OP_GAPGATE,
|
|
20
|
+
OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM, OP_GAPGATE, OP_CONT, OP_SEED,
|
|
21
21
|
} from './ops.ts'
|
|
22
22
|
import { childSlots } from './child-slots.ts'
|
|
23
23
|
import { leadingTriviaFieldAt } from './field-spec.ts'
|
|
@@ -48,6 +48,10 @@ export function rowNullable(code: ArrayLike<number>, k: readonly unknown[], ip:
|
|
|
48
48
|
out = kids.length === 0 || kids.some(c => rowNullable(code, k, c, memo))
|
|
49
49
|
} else if (op === OP_REP || op === OP_REPV) {
|
|
50
50
|
out = code[ip + 2] === 0 || rowNullable(code, k, code[ip + 1]!, memo)
|
|
51
|
+
} else if (op === OP_SEED) {
|
|
52
|
+
out = rowNullable(code, k, code[ip + 1]!, memo)
|
|
53
|
+
} else if (op === OP_CONT) {
|
|
54
|
+
out = rowNullable(code, k, code[ip + 1]!, memo) && rowNullable(code, k, code[ip + 3]!, memo)
|
|
51
55
|
} else if (WRAPPERS.has(op)) {
|
|
52
56
|
if (childSlots(code, ip, kids) && kids.length === 1) out = rowNullable(code, k, kids[0]!, memo)
|
|
53
57
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT THE TAIL OF A LATE FRAME CAN START WITH, read off the row that runs it.
|
|
3
|
+
*
|
|
4
|
+
* `OP_CONT` asks, after the corner, whether the tail could do anything at the next
|
|
5
|
+
* token. The answer is the repeat's or the optional's own first test (`OP_REP` ip+7
|
|
6
|
+
* and its `OP_GAPGATE`, an `OP_OPT` over `OP_GATE`), so it holds exactly where the row
|
|
7
|
+
* would have ended without entering its item. Any other tail has no answer, and with
|
|
8
|
+
* no answer the frame always opens: that site runs as written.
|
|
9
|
+
*/
|
|
10
|
+
import { OP_GAPGATE, OP_GATE, OP_OPT, OP_REP, OP_REPV, OP_RULE, OP_WITHCTX } from './ops.ts'
|
|
11
|
+
import { TRI_NONE } from './site-labels.ts'
|
|
12
|
+
|
|
13
|
+
export type ContinuationGate = {
|
|
14
|
+
/** Class of the first character, past any trivia. */
|
|
15
|
+
readonly cls: number
|
|
16
|
+
/** The regex row of a leading gap, or −1. */
|
|
17
|
+
readonly gapRow: number
|
|
18
|
+
/** Class of the character after the gap. */
|
|
19
|
+
readonly follow: number
|
|
20
|
+
/** Class of an arm that opens without the gap, or −1. */
|
|
21
|
+
readonly other: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function continuationGate(code: ArrayLike<number>, ip: number): ContinuationGate | undefined {
|
|
25
|
+
let at = ip
|
|
26
|
+
for (let n = 0; n < 8; n++) {
|
|
27
|
+
const op = code[at]
|
|
28
|
+
if (op === OP_RULE) at = code[at + 1]!
|
|
29
|
+
else if (op === OP_WITHCTX) at = code[at + 2]!
|
|
30
|
+
else break
|
|
31
|
+
}
|
|
32
|
+
const op = code[at]
|
|
33
|
+
if (op === OP_OPT) {
|
|
34
|
+
const c = code[at + 1]!
|
|
35
|
+
return code[c] === OP_GATE ? { cls: code[c + 1]!, gapRow: -1, follow: -1, other: -1 } : undefined
|
|
36
|
+
}
|
|
37
|
+
if (op !== OP_REP && op !== OP_REPV) return undefined
|
|
38
|
+
// A separated list keeps ip+7 for its recovery sentinel; a mandatory item reports.
|
|
39
|
+
if (code[at + 2] !== 0 || code[at + 4]! >= 0 || code[at + 7]! < 0) return undefined
|
|
40
|
+
const item = code[at + 1]!
|
|
41
|
+
return code[item] === OP_GAPGATE
|
|
42
|
+
? { cls: code[at + 7]!, gapRow: code[item + 2]!, follow: code[item + 3]!, other: code[item + 4]! }
|
|
43
|
+
: { cls: code[at + 7]!, gapRow: -1, follow: -1, other: -1 }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Can this assembly decide at this `OP_CONT`? It needs the tail's test, and the
|
|
48
|
+
* trivia the label names: a scope it cannot name, or one with no scanner, would cost a
|
|
49
|
+
* hit more calls than the frame it saves on a miss.
|
|
50
|
+
*/
|
|
51
|
+
export function canDecide(code: ArrayLike<number>, ip: number, tri: number, triviaScan: readonly unknown[]): boolean {
|
|
52
|
+
if (tri !== TRI_NONE && (tri < 0 || triviaScan[tri] == null)) return false
|
|
53
|
+
return continuationGate(code, code[ip + 3]!) !== undefined
|
|
54
|
+
}
|
package/src/table/ops.ts
CHANGED
|
@@ -509,6 +509,23 @@ export const OP_RGATE2 = 47
|
|
|
509
509
|
* assembly resolves it to one child; it has no body of its own.
|
|
510
510
|
*/
|
|
511
511
|
export const OP_STRICT = 48
|
|
512
|
+
/**
|
|
513
|
+
* `CONT corner wrapper tail` — a `collapse` node that is `sequence(corner, tail)`, with
|
|
514
|
+
* a tail that can take nothing and a corner that is one node. A strict assembly runs
|
|
515
|
+
* `corner` in the PARENT's collector and looks at the next token. Where `tail` cannot
|
|
516
|
+
* start there, the corner is the result: no frame, no children, no builder. Where it
|
|
517
|
+
* can, the corner's publication is taken back and `wrapper`'s frame opens around it and
|
|
518
|
+
* `tail`, in this one function. `tail` is read for the decision (`late-frame.ts`). Where
|
|
519
|
+
* no decision can be made, and under recovery, a probe or the reference driver, this
|
|
520
|
+
* is `wrapper`, the node as written.
|
|
521
|
+
*/
|
|
522
|
+
export const OP_CONT = 49
|
|
523
|
+
/**
|
|
524
|
+
* `SEED corner` — marks the first term of `wrapper`'s body. Run, it is `corner`. It
|
|
525
|
+
* exists so the labels can leave out the edge from a late frame to its corner, which
|
|
526
|
+
* never runs there.
|
|
527
|
+
*/
|
|
528
|
+
export const OP_SEED = 50
|
|
512
529
|
/**
|
|
513
530
|
* `DISPATCH sel d other otherRouted n a1 … an` — `dispatch()`.
|
|
514
531
|
*
|
|
@@ -548,4 +565,6 @@ export const OP_NAMES: Record<number, string> = {
|
|
|
548
565
|
[OP_GAPGATE]: 'GAPGATE',
|
|
549
566
|
[OP_RGATE2]: 'RGATE2',
|
|
550
567
|
[OP_STRICT]: 'STRICT',
|
|
568
|
+
[OP_CONT]: 'CONT',
|
|
569
|
+
[OP_SEED]: 'SEED',
|
|
551
570
|
}
|
package/src/table/select-hot.ts
CHANGED
|
@@ -12,14 +12,28 @@
|
|
|
12
12
|
* `LOOP`, and a choice or dispatch splits its weight evenly across its arms. The
|
|
13
13
|
* weights are propagated `ROUNDS` times, so a recursive cycle saturates at `CAP`
|
|
14
14
|
* instead of diverging. Sites are then taken greedily by weight per emitted byte.
|
|
15
|
-
*
|
|
15
|
+
*
|
|
16
|
+
* A build given a corpus (`assemblyProfile`) ranks by measured calls, and takes
|
|
17
|
+
* sites connected: every call from an emitted site to a closure site, or back,
|
|
18
|
+
* crosses the engine boundary through a wrapper, so a hot site beside the emitted
|
|
19
|
+
* ones is worth more than the same site alone. Ranking by calls per byte alone
|
|
20
|
+
* leaves islands: on Jess's Less grammar at budget 0.5, it made 6.7% to 9.0% more calls
|
|
21
|
+
* per parse than the static ranking.
|
|
16
22
|
*/
|
|
17
23
|
import { childSlots } from './child-slots.ts'
|
|
18
|
-
import { OP_CHOICE, OP_DISPATCH, OP_GAPGATE, OP_REP, OP_REPV } from './ops.ts'
|
|
24
|
+
import { OP_CHOICE, OP_CONT, OP_DISPATCH, OP_GAPGATE, OP_REP, OP_REPV, OP_SEED } from './ops.ts'
|
|
19
25
|
|
|
20
26
|
const LOOP = 8
|
|
21
27
|
const ROUNDS = 60
|
|
22
28
|
const CAP = 1e12
|
|
29
|
+
/**
|
|
30
|
+
* What a crossing a site removes counts for, against one call of the site itself.
|
|
31
|
+
* Better than no weight, on every file and budget measured with real builds of
|
|
32
|
+
* Jess's grammars, but not tuned: 1 and 16 were only tried on a prototype, 2 and 8
|
|
33
|
+
* were never tried, and only 4 has been built and measured.
|
|
34
|
+
*/
|
|
35
|
+
const CROSSING_WEIGHT = 4
|
|
36
|
+
const TAKEN = -1
|
|
23
37
|
|
|
24
38
|
/**
|
|
25
39
|
* The sites to emit: highest weight per byte first, stopping at the first that
|
|
@@ -38,14 +52,8 @@ export function selectHotSites(
|
|
|
38
52
|
): Set<number> {
|
|
39
53
|
const selected = new Set<number>()
|
|
40
54
|
if (budget <= 0) return selected
|
|
41
|
-
const weight = profile === undefined ? staticWeights(code, entries) : undefined
|
|
42
|
-
const w = (ip: number): number => (weight !== undefined ? weight.get(ip) : profile![ip]) ?? 0
|
|
43
|
-
// Ties (saturated cycles) break by site offset, so a build is deterministic.
|
|
44
|
-
const ranked = [...siteBytes.keys()].sort((a, b) =>
|
|
45
|
-
w(b) / siteBytes.get(b)! - w(a) / siteBytes.get(a)! || a - b)
|
|
46
55
|
let spent = 0
|
|
47
|
-
for (const ip of
|
|
48
|
-
if (w(ip) === 0) break
|
|
56
|
+
for (const ip of profile === undefined ? staticRanking(code, entries, siteBytes) : connectedRanking(code, siteBytes, profile)) {
|
|
49
57
|
spent += siteBytes.get(ip)!
|
|
50
58
|
if (spent > budget) break
|
|
51
59
|
selected.add(ip)
|
|
@@ -53,6 +61,82 @@ export function selectHotSites(
|
|
|
53
61
|
return selected
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The sites the function for `ip` calls, as `emit-assembly.ts` lowers it. An `OP_CONT`
|
|
66
|
+
* with a body is a late frame, lowered with its wrapper in one function, calling its
|
|
67
|
+
* corner and its tail. With no body, the emitter can't decide there and it's an alias
|
|
68
|
+
* to its wrapper, as `OP_SEED` is to its corner. A gap gate has no body either, so its
|
|
69
|
+
* item counts as the repeat's own child, and adding the row moves no other site's
|
|
70
|
+
* estimate (`gap-gate.ts`).
|
|
71
|
+
*/
|
|
72
|
+
export function edgesAsLowered(code: readonly number[], siteBytes: ReadonlyMap<number, number>, ip: number, out: number[]): void {
|
|
73
|
+
out.length = 0
|
|
74
|
+
if (code[ip] !== OP_CONT) childSlots(code, ip, out)
|
|
75
|
+
else if (siteBytes.has(ip)) out.push(code[ip + 1]!, code[ip + 3]!)
|
|
76
|
+
else out.push(code[ip + 2]!)
|
|
77
|
+
for (let i = 0; i < out.length; i++) {
|
|
78
|
+
let row = out[i]!
|
|
79
|
+
for (;;) {
|
|
80
|
+
const op = code[row]
|
|
81
|
+
if (op === OP_GAPGATE || op === OP_SEED) row = code[row + 1]!
|
|
82
|
+
else if (op === OP_CONT && !siteBytes.has(row)) row = code[row + 2]!
|
|
83
|
+
else break
|
|
84
|
+
}
|
|
85
|
+
out[i] = row
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The sites by estimated weight per byte. Ties (saturated cycles) break by site
|
|
90
|
+
* offset, so a build is deterministic. */
|
|
91
|
+
function staticRanking(code: readonly number[], entries: readonly number[], siteBytes: ReadonlyMap<number, number>): number[] {
|
|
92
|
+
const weight = staticWeights(code, entries, siteBytes)
|
|
93
|
+
const w = (ip: number): number => weight.get(ip) ?? 0
|
|
94
|
+
return [...siteBytes.keys()].filter(ip => w(ip) > 0).sort((a, b) => w(b) / siteBytes.get(b)! - w(a) / siteBytes.get(a)! || a - b)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The sites the corpus ran, each next one being the best by calls plus the crossings
|
|
99
|
+
* it removes, per byte. A crossing with a neighbour is estimated by the calls of
|
|
100
|
+
* whichever is the callee, since the profile counts sites, not edges. Nothing bounds
|
|
101
|
+
* the sum: a callee with N emitted callers gains N crossings' worth of its own calls,
|
|
102
|
+
* and `sequence(g.A, g.A)` counts twice. The order doesn't depend on the budget, so a
|
|
103
|
+
* larger budget's selection is still a superset.
|
|
104
|
+
*
|
|
105
|
+
* ponytail: rescans every site per pick, O(n^2) in sites the corpus ran. 3 ms of a
|
|
106
|
+
* 50 s build on Jess's Less grammar. A heap if a grammar's ever takes seconds.
|
|
107
|
+
*/
|
|
108
|
+
function* connectedRanking(code: readonly number[], siteBytes: ReadonlyMap<number, number>, calls: ArrayLike<number>): Generator<number> {
|
|
109
|
+
// Ascending, so the first best a scan meets is the lowest: a tie breaks by site.
|
|
110
|
+
const ran = [...siteBytes.keys()].filter(ip => calls[ip]! > 0).sort((a, b) => a - b)
|
|
111
|
+
const slot = new Map(ran.map((ip, i) => [ip, i]))
|
|
112
|
+
const bytes = ran.map(ip => siteBytes.get(ip)!)
|
|
113
|
+
const worth = ran.map(ip => calls[ip]!)
|
|
114
|
+
const score = worth.map((value, i) => value / bytes[i]!)
|
|
115
|
+
const crossings: [neighbour: number, gain: number][][] = ran.map(() => [])
|
|
116
|
+
const kids: number[] = []
|
|
117
|
+
ran.forEach((ip, from) => {
|
|
118
|
+
edgesAsLowered(code, siteBytes, ip, kids)
|
|
119
|
+
for (const callee of kids) {
|
|
120
|
+
const to = slot.get(callee)
|
|
121
|
+
if (to === undefined) continue
|
|
122
|
+
const gain = CROSSING_WEIGHT * calls[callee]!
|
|
123
|
+
crossings[from]!.push([to, gain])
|
|
124
|
+
crossings[to]!.push([from, gain])
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
for (let left = ran.length; left > 0; left--) {
|
|
128
|
+
let best = 0
|
|
129
|
+
for (let i = 1; i < score.length; i++) if (score[i]! > score[best]!) best = i
|
|
130
|
+
score[best] = TAKEN
|
|
131
|
+
yield ran[best]!
|
|
132
|
+
for (const [neighbour, gain] of crossings[best]!) {
|
|
133
|
+
if (score[neighbour] === TAKEN) continue
|
|
134
|
+
worth[neighbour] = worth[neighbour]! + gain
|
|
135
|
+
score[neighbour] = worth[neighbour]! / bytes[neighbour]!
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
56
140
|
/**
|
|
57
141
|
* The largest prefix of the ranking whose real source fits `ceiling`. `siteBytes`
|
|
58
142
|
* leaves out what sites add around their bodies, so the estimate can overshoot:
|
|
@@ -80,25 +164,19 @@ export function fitHotSites(
|
|
|
80
164
|
return undefined
|
|
81
165
|
}
|
|
82
166
|
|
|
83
|
-
function staticWeights(code: readonly number[], entries: readonly number[]): Map<number, number> {
|
|
167
|
+
function staticWeights(code: readonly number[], entries: readonly number[], siteBytes: ReadonlyMap<number, number>): Map<number, number> {
|
|
84
168
|
let weight = new Map<number, number>()
|
|
85
169
|
const kids: number[] = []
|
|
86
170
|
for (let round = 0; round < ROUNDS; round++) {
|
|
87
171
|
const next = new Map<number, number>()
|
|
88
172
|
for (const ip of entries) next.set(ip, 1)
|
|
89
173
|
for (const [ip, w] of weight) {
|
|
90
|
-
|
|
91
|
-
childSlots(code, ip, kids)
|
|
174
|
+
edgesAsLowered(code, siteBytes, ip, kids)
|
|
92
175
|
const op = code[ip]
|
|
93
176
|
const scale = op === OP_REP || op === OP_REPV ? LOOP
|
|
94
177
|
: op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
|
|
95
178
|
: 1
|
|
96
|
-
for (const
|
|
97
|
-
// A gap gate has no body: weigh its item as the repeat's own child, so
|
|
98
|
-
// adding the row moves no other site's estimate (`gap-gate.ts`).
|
|
99
|
-
const kid = code[k] === OP_GAPGATE ? code[k + 1]! : k
|
|
100
|
-
next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
|
|
101
|
-
}
|
|
179
|
+
for (const kid of kids) next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
|
|
102
180
|
}
|
|
103
181
|
weight = next
|
|
104
182
|
}
|