parseman 0.61.3 → 0.61.5

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.
Files changed (45) hide show
  1. package/dist/analysis/diagnostics.js +844 -522
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +846 -522
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/cst/capture-buffer.d.ts +3 -2
  6. package/dist/cst/capture-buffer.d.ts.map +1 -1
  7. package/dist/index.js +846 -520
  8. package/dist/index.js.map +3 -3
  9. package/dist/plugin/index.d.ts +22 -3
  10. package/dist/plugin/index.d.ts.map +1 -1
  11. package/dist/plugin/index.js +872 -520
  12. package/dist/plugin/index.js.map +3 -3
  13. package/dist/table/assemble.d.ts.map +1 -1
  14. package/dist/table/child-slots.d.ts.map +1 -1
  15. package/dist/table/emit-assembly.d.ts +1 -1
  16. package/dist/table/emit-assembly.d.ts.map +1 -1
  17. package/dist/table/encode.d.ts.map +1 -1
  18. package/dist/table/exec.d.ts.map +1 -1
  19. package/dist/table/fail-clean.d.ts.map +1 -1
  20. package/dist/table/hold-gap.d.ts.map +1 -1
  21. package/dist/table/index.js +368 -42
  22. package/dist/table/index.js.map +3 -3
  23. package/dist/table/late-frame.d.ts +18 -0
  24. package/dist/table/late-frame.d.ts.map +1 -0
  25. package/dist/table/ops.d.ts +17 -0
  26. package/dist/table/ops.d.ts.map +1 -1
  27. package/dist/table/select-hot.d.ts.map +1 -1
  28. package/dist/table/site-labels.d.ts +5 -1
  29. package/dist/table/site-labels.d.ts.map +1 -1
  30. package/dist/version.d.ts +1 -1
  31. package/package.json +1 -1
  32. package/src/cst/capture-buffer.ts +20 -11
  33. package/src/plugin/index.ts +76 -23
  34. package/src/table/assemble.ts +124 -4
  35. package/src/table/child-slots.ts +9 -1
  36. package/src/table/emit-assembly.ts +101 -14
  37. package/src/table/encode.ts +69 -6
  38. package/src/table/exec.ts +7 -1
  39. package/src/table/fail-clean.ts +8 -1
  40. package/src/table/hold-gap.ts +5 -1
  41. package/src/table/late-frame.ts +54 -0
  42. package/src/table/ops.ts +19 -0
  43. package/src/table/select-hot.ts +7 -1
  44. package/src/table/site-labels.ts +38 -14
  45. 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 scalarChild = scalarTerminalNodeChild(code, ip)
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
- const v=${child}(input,pos,ctx)
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
- ` : ''}const v=${child}(input,pos,ctx)
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'}
@@ -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 child = this.node(d.parser).ip
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
- if (matchesEmpty(d.parser, new Set(), this.refResolver())) return body
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 body
1977
+ if (fs.kind === 'any') return run
1944
1978
  const cls = this.charClass(fs)
1945
- if (cls < 0) return body
1979
+ if (cls < 0) return run
1946
1980
  const e = deriveExpected(d.parser)
1947
- return this.emit(OP_GATE, cls, body, this.expected(e.length > 0 ? e : [d.parser._tag]))
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
@@ -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:
@@ -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
  }
@@ -15,7 +15,8 @@
15
15
  * A build given a corpus (`assemblyProfile`) ranks by measured calls instead.
16
16
  */
17
17
  import { childSlots } from './child-slots.ts'
18
- import { OP_CHOICE, OP_DISPATCH, OP_GAPGATE, OP_REP, OP_REPV } from './ops.ts'
18
+ import { continuationGate } from './late-frame.ts'
19
+ import { OP_CHOICE, OP_CONT, OP_DISPATCH, OP_GAPGATE, OP_REP, OP_REPV } from './ops.ts'
19
20
 
20
21
  const LOOP = 8
21
22
  const ROUNDS = 60
@@ -90,6 +91,11 @@ function staticWeights(code: readonly number[], entries: readonly number[]): Map
90
91
  kids.length = 0
91
92
  childSlots(code, ip, kids)
92
93
  const op = code[ip]
94
+ // A late frame is lowered with its wrapper in one function, calling these two.
95
+ if (op === OP_CONT && continuationGate(code, code[ip + 3]!) !== undefined) {
96
+ kids.length = 0
97
+ kids.push(code[ip + 1]!, code[ip + 3]!)
98
+ }
93
99
  const scale = op === OP_REP || op === OP_REPV ? LOOP
94
100
  : op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
95
101
  : 1
@@ -60,7 +60,7 @@
60
60
  * label licenses at a leaf is dropping the TEST — never the capture.
61
61
  */
62
62
  import {
63
- OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_SOURCE_LEAF,
63
+ OP_CONT, OP_SEED, OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_SOURCE_LEAF,
64
64
  OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE,
65
65
  OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, SCOPE_INHERIT, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
66
66
  } from './ops.ts'
@@ -161,7 +161,7 @@ export function passCollapse(code: Int32Array, ip: number, hostCst: boolean): bo
161
161
  else if (op === OP_RULE) at = code[at + 1]!
162
162
  else break
163
163
  }
164
- return code[at] === OP_NODE
164
+ return code[at] === OP_NODE || code[at] === OP_CONT
165
165
  }
166
166
 
167
167
  /**
@@ -266,7 +266,11 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
266
266
  }
267
267
 
268
268
  /** A site's label, or `TOP` for anything the walk did not reach. */
269
- export type SiteLabels = { at: (ip: number) => SiteLabel }
269
+ export type SiteLabels = {
270
+ at: (ip: number) => SiteLabel
271
+ /** The `OP_CONT` sites whose corner runs in the parent's collector in every engine. */
272
+ late: ReadonlySet<number>
273
+ }
270
274
 
271
275
  /**
272
276
  * Compute a label for every site reachable from `roots`.
@@ -284,10 +288,11 @@ export function computeSiteLabels(
284
288
  code: Int32Array,
285
289
  roots: Iterable<number>,
286
290
  hostCst: boolean,
291
+ /** Does this assembly run this `OP_CONT` late, in EVERY engine it holds? Absent: none. */
292
+ late?: (ip: number, tri: number) => boolean,
287
293
  ): SiteLabels {
288
294
  const labels = new Map<number, SiteLabel>()
289
295
  const work: number[] = []
290
-
291
296
  const push = (ip: number, l: SiteLabel): void => {
292
297
  const cur = labels.get(ip)
293
298
  if (cur === undefined) {
@@ -300,19 +305,38 @@ export function computeSiteLabels(
300
305
  labels.set(ip, next)
301
306
  work.push(ip)
302
307
  }
303
-
304
- for (const r of roots) push(r, TOP)
305
-
308
+ // A corner a late frame never runs takes no label from that frame. So every frame is
309
+ // late until a site proves undecidable, when its seed's edge joins the same fixpoint: a
310
+ // frame does not change the trivia in force, so no other site's answer moves.
311
+ const opened = new Set<number>()
306
312
  const kids: number[] = []
307
- while (work.length > 0) {
308
- const ip = work.pop()!
309
- const down = transfer(code, ip, labels.get(ip)!, hostCst)
310
- kids.length = 0
311
- childSlots(code, ip, kids)
312
- for (const c of kids) push(c, down)
313
+ const lateIps = new Set<number>()
314
+ for (const r of roots) push(r, TOP)
315
+ for (let pass = 0; pass < 2; pass++) {
316
+ while (work.length > 0) {
317
+ const ip = work.pop()!
318
+ const down = transfer(code, ip, labels.get(ip)!, hostCst)
319
+ kids.length = 0
320
+ if (late === undefined || code[ip] !== OP_SEED || opened.has(ip)) childSlots(code, ip, kids)
321
+ for (const c of kids) push(c, down)
322
+ }
323
+ if (late === undefined || pass === 1) break
324
+ for (const [ip, l] of [...labels]) {
325
+ if (code[ip] !== OP_CONT) continue
326
+ if (late(ip, l.tri)) lateIps.add(ip)
327
+ else {
328
+ const seed = seedOf(code, ip)
329
+ opened.add(seed)
330
+ work.push(seed)
331
+ }
332
+ }
313
333
  }
334
+ return { at: (ip: number): SiteLabel => labels.get(ip) ?? TOP, late: lateIps }
335
+ }
314
336
 
315
- return { at: (ip: number): SiteLabel => labels.get(ip) ?? TOP }
337
+ /** `CONT corner wrapper tail`, `NODE … body`, `SEQ n seed tail`. */
338
+ function seedOf(code: ArrayLike<number>, cont: number): number {
339
+ return code[code[code[cont + 2]! + 2]! + 2]!
316
340
  }
317
341
 
318
342
  /**
package/src/version.ts CHANGED
@@ -13,4 +13,4 @@
13
13
  * Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
14
14
  * the build if this constant drifts from the published version.
15
15
  */
16
- export const PARSEMAN_VERSION = '0.61.3'
16
+ export const PARSEMAN_VERSION = '0.61.5'