parseman 0.60.2 → 0.60.4

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 (40) hide show
  1. package/dist/analysis/commitment.d.ts +1 -1
  2. package/dist/analysis/commitment.d.ts.map +1 -1
  3. package/dist/analysis/diagnostics.js +128 -43
  4. package/dist/analysis/diagnostics.js.map +2 -2
  5. package/dist/cli/index.js +128 -43
  6. package/dist/cli/index.js.map +2 -2
  7. package/dist/compiler/ir-serialize.d.ts +3 -1
  8. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  9. package/dist/compiler/linker.d.ts +10 -0
  10. package/dist/compiler/linker.d.ts.map +1 -1
  11. package/dist/index.js +155 -57
  12. package/dist/index.js.map +2 -2
  13. package/dist/plugin/evaluator.d.ts +8 -0
  14. package/dist/plugin/evaluator.d.ts.map +1 -1
  15. package/dist/plugin/index.d.ts.map +1 -1
  16. package/dist/plugin/index.js +218 -59
  17. package/dist/plugin/index.js.map +2 -2
  18. package/dist/table/assemble.d.ts +11 -0
  19. package/dist/table/assemble.d.ts.map +1 -1
  20. package/dist/table/emit-assembly.d.ts +1 -0
  21. package/dist/table/emit-assembly.d.ts.map +1 -1
  22. package/dist/table/encode.d.ts.map +1 -1
  23. package/dist/table/exec.d.ts.map +1 -1
  24. package/dist/table/index.js +128 -43
  25. package/dist/table/index.js.map +2 -2
  26. package/dist/table/program.d.ts +9 -0
  27. package/dist/table/program.d.ts.map +1 -1
  28. package/dist/version.d.ts +1 -1
  29. package/package.json +1 -1
  30. package/src/analysis/commitment.ts +29 -15
  31. package/src/compiler/ir-serialize.ts +14 -3
  32. package/src/compiler/linker.ts +28 -12
  33. package/src/plugin/evaluator.ts +11 -0
  34. package/src/plugin/index.ts +29 -11
  35. package/src/table/assemble.ts +35 -10
  36. package/src/table/emit-assembly.ts +41 -11
  37. package/src/table/encode.ts +57 -9
  38. package/src/table/exec.ts +5 -2
  39. package/src/table/program.ts +19 -0
  40. package/src/version.ts +1 -1
@@ -54,7 +54,7 @@ import {
54
54
  OP_LEX_BODY, OP_LEX_PROGRAM, OP_GAPGATE, OP_STRICT,
55
55
  } from './ops.ts'
56
56
  import {
57
- choiceCannotCommit, choiceRollbackMask, failureRollbackClean, validateDispatchSpec,
57
+ choiceCannotCommit, choiceLogRollbackMask, choiceRollbackMask, failureLogRollback, failureRollbackClean, validateDispatchSpec,
58
58
  type ResolvedClass, type ResolvedTable, type TableProgram,
59
59
  } from './program.ts'
60
60
  import { emitShapeMatch, scanShapeFromRegex } from './scan-shapes.ts'
@@ -406,6 +406,19 @@ function emitRollback(t: string, buf: boolean, rawMode: number, s: Sinks = NO_SI
406
406
  return `if(${t}n){rollbackTriviaAt(ctx,${t}raw,${t}tl,${t}lv,${s.fd ? `${t}fd` : 'undefined'},${s.er ? `${t}er` : 'undefined'},${t}lg,${t}rt)}`
407
407
  }
408
408
 
409
+ /**
410
+ * A mark of the two trivia logs alone, for a child the census counts as clean but
411
+ * whose failure can leave trivia rows a failed node recorded
412
+ * (`program.ts` `choiceLogRollbackMask`, `failureLogRollback`).
413
+ */
414
+ function emitLogMark(t: string): string {
415
+ return `const ${t}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0,${t}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0`
416
+ }
417
+ function emitLogRollback(t: string): string {
418
+ return `{const g=ctx._triviaLog;if(g!==undefined)while(g.length>${t}lg)g.pop()}
419
+ {const r=ctx._rootTriviaLog;if(r!==undefined)while(r.length>${t}rt)r.pop()}`
420
+ }
421
+
409
422
  /**
410
423
  * ONE SEQUENCE TERM — `assemble.ts`'s `nextTerm`, inlined at the site.
411
424
  *
@@ -597,6 +610,7 @@ export function emitAssemblySource(
597
610
  tolerant: boolean
598
611
  coverage: boolean
599
612
  probe: boolean
613
+ inheritedTrivia?: boolean
600
614
  },
601
615
  extraIps: readonly number[] = [],
602
616
  staticBuild = false,
@@ -618,6 +632,10 @@ export function emitAssemblySource(
618
632
  const { code, k, fx, disp, dsp, triviaLabelled } = t
619
633
  const swapLegal = !cfg.trackLines
620
634
  const hostCst = cfg.hostCst
635
+ // Trivia rows are recorded only by a program with trivia of its own, or under
636
+ // trivia a caller supplied (`RunCfg.inheritedTrivia`). Otherwise no site
637
+ // restores the trivia logs alone: nothing can have written them.
638
+ const LOGS = (prog.triviaSpecs?.length ?? 0) > 0 || cfg.inheritedTrivia === true
621
639
 
622
640
  /**
623
641
  * RECOVERY IS EMITTED, and the refusal this replaces was wrong in SCOPE.
@@ -1769,12 +1787,13 @@ return v
1769
1787
  case OP_ATTEMPT: {
1770
1788
  const child = link(code[ip + 1]!)
1771
1789
  const clean = !REC && failureRollbackClean(prog, ip)
1790
+ const logs = clean && LOGS && failureLogRollback(prog, ip)
1772
1791
  const p = tmp()
1773
1792
  return `${head}
1774
- ${clean ? '' : emitMark(p, L.buf, L.raw, sinks)}
1793
+ ${clean ? logs ? emitLogMark(p) : '' : emitMark(p, L.buf, L.raw, sinks)}
1775
1794
  const v=${child}(input,pos,ctx)
1776
1795
  if(v!==FAIL)return v
1777
- ${clean ? '' : emitRollback(p, L.buf, L.raw, sinks)}
1796
+ ${clean ? logs ? emitLogRollback(p) : '' : emitRollback(p, L.buf, L.raw, sinks)}
1778
1797
  ${code[ip + 2] === 1 ? 'ctx._fc=false' : 'if(ctx._fc===true)return FAIL'}
1779
1798
  ctx._fe=pos
1780
1799
  return FAIL
@@ -1801,6 +1820,7 @@ return FAIL
1801
1820
  ${lookaheadMiss(code[ip + 1]!, 'EC.e=pos;return null')}${emitMark(p, L.buf, L.raw, sinks)}
1802
1821
  ${cfg.probe ? 'const pb=ctx._probe?.best\n' : ''}const v=${child}(input,pos,ctx)
1803
1822
  ${emitRollback(p, L.buf, L.raw, sinks)}
1823
+ ctx._fc=false
1804
1824
  ${cfg.probe ? 'if(ctx._probe!==undefined)ctx._probe.best=pb??null\n' : ''}if(v===FAIL){EC.e=pos;return null}
1805
1825
  ctx._fe=pos
1806
1826
  ctx._fx=${xf}
@@ -1826,6 +1846,7 @@ return null
1826
1846
  ${lookaheadMiss(code[ip + 1]!, `ctx._fe=pos;ctx._fx=${xf};return FAIL`)}${emitMark(p, L.buf, L.raw, sinks)}
1827
1847
  ${cfg.probe ? 'const pb=ctx._probe?.best\n' : ''}const v=${child}(input,pos,ctx)
1828
1848
  ${emitRollback(p, L.buf, L.raw, sinks)}
1849
+ ctx._fc=false
1829
1850
  ${cfg.probe ? 'if(ctx._probe!==undefined)ctx._probe.best=pb??null\n' : ''}if(v===FAIL){ctx._fe=pos;ctx._fx=${xf};return FAIL}
1830
1851
  EC.e=pos
1831
1852
  return null
@@ -1835,14 +1856,15 @@ return null
1835
1856
  case OP_OPT: {
1836
1857
  const child = link(code[ip + 1]!)
1837
1858
  const clean = !REC && failureRollbackClean(prog, ip)
1859
+ const logs = clean && LOGS && failureLogRollback(prog, ip)
1838
1860
  const p = tmp()
1839
1861
  return `${head}
1840
- ${lookaheadMiss(code[ip + 1]!, 'EC.e=pos;return null')}${clean ? '' : emitMark(p, L.buf, L.raw, sinks)}
1862
+ ${lookaheadMiss(code[ip + 1]!, 'EC.e=pos;return null')}${clean ? logs ? emitLogMark(p) : '' : emitMark(p, L.buf, L.raw, sinks)}
1841
1863
  ctx._fc=false
1842
1864
  const v=${child}(input,pos,ctx)
1843
1865
  if(v===FAIL){
1844
1866
  if(ctx._fc===true)return FAIL
1845
- ${clean ? '' : emitRollback(p, L.buf, L.raw, sinks)}
1867
+ ${clean ? logs ? emitLogRollback(p) : '' : emitRollback(p, L.buf, L.raw, sinks)}
1846
1868
  EC.e=pos
1847
1869
  return null
1848
1870
  }
@@ -2052,6 +2074,13 @@ return FAIL
2052
2074
  const encodedRollbackMask = choiceRollbackMask(prog, ip) ?? -1
2053
2075
  const rollbackMask = REC ? -1 : encodedRollbackMask
2054
2076
  const hasRollback = rollbackMask !== 0
2077
+ // Arms that leave only trivia-log rows: restored from the full mark when
2078
+ // one is taken, else from a mark of the two logs taken as that arm is
2079
+ // entered. An earlier arm that failed was restored, so the lengths there
2080
+ // are the choice's own, and an arm the char gate skips takes no mark.
2081
+ const logMask = rollbackMask === -1 || !LOGS ? 0 : choiceLogRollbackMask(prog, ip)
2082
+ const choiceMark = hasRollback ? emitMark(p, L.buf, L.raw, sinks) : ''
2083
+ const armMark = (i: number): string => !hasRollback && (logMask & (1 << i)) !== 0 ? `${emitLogMark(p)}\n` : ''
2055
2084
  // An always-consuming leading scalar makes a failure at `pos`
2056
2085
  // statically exact: it is the arm's derived opener set, and the
2057
2086
  // choice's own `choiceFx` already concatenates every such set in
@@ -2082,7 +2111,8 @@ return FAIL
2082
2111
  const rollbackFor = (i: number): string =>
2083
2112
  rollbackMask === -1 || (rollbackMask & (1 << i)) !== 0
2084
2113
  ? emitRollback(p, L.buf, L.raw, sinks)
2085
- : ''
2114
+ : (logMask & (1 << i)) === 0 ? ''
2115
+ : hasRollback ? emitRollback(p, L.buf, L.raw, sinks) : emitLogRollback(p)
2086
2116
  if (choiceCannotCommit(prog, ip) && !startFailureExact) {
2087
2117
  // A successful ordered choice does not expose any losing arm's
2088
2118
  // diagnostic. Snapshot each entered arm's failure in scalar locals;
@@ -2121,7 +2151,7 @@ ctx._fc=false
2121
2151
  ${remember(i, 'at', pretest.sequence.expected.map((value, at) => `${decision}===${~at}?${value}:`).join('') + pretest.sequence.expected.at(-1))}}
2122
2152
  }`
2123
2153
  return `${decision === '' ? '' : `let ${decision}=${pretest?.sequence === undefined ? -1 : 0}\n`}if(${gate}){
2124
- ctx._fc=false
2154
+ ${armMark(i)}ctx._fc=false
2125
2155
  {const v=${arm}(input,pos,ctx)
2126
2156
  if(v!==FAIL)return v}
2127
2157
  {const at=ctx._fe??pos
@@ -2140,7 +2170,7 @@ ctx._fe=pos;ctx._fx=acc??${choiceFx}
2140
2170
  return FAIL`
2141
2171
  return `${head}
2142
2172
  const c=${directAsciiMask ? 'input.charCodeAt(pos)' : 'lead(input,pos)'}
2143
- ${hasRollback ? emitMark(p, L.buf, L.raw, sinks) : ''}
2173
+ ${choiceMark}
2144
2174
  ${deepAt.map((at, i) => `let ${at},${deepFx[i]}`).join('\n')}
2145
2175
  ${maskable ? `if(c<128){
2146
2176
  const bits=${maskName}[${directAsciiMask ? 'c' : 'c<0?128:c'}]
@@ -2191,7 +2221,7 @@ if(at>best){best=at;acc=undefined}
2191
2221
  if(at===best${startFailureExact ? '&&at>pos' : ''})acc=_accSet(${pretest.sequence.expected.map((value, at) => `${decision}===${~at}?${value}:`).join('')}${pretest.sequence.expected.at(-1)},acc)}
2192
2222
  }`
2193
2223
  return `${decision === '' ? '' : `let ${decision}=${pretest?.sequence === undefined ? -1 : 0}\n`}if((bits&${1 << i})!==0${condition}){
2194
- ctx._fc=false
2224
+ ${armMark(i)}ctx._fc=false
2195
2225
  {const v=${arm}(input,pos,ctx)
2196
2226
  if(v!==FAIL)return v}
2197
2227
  ${startFailureExact ? '' : `if(best===pos)acc=${catchName}(${i},prev,acc)
@@ -2231,7 +2261,7 @@ if(at>best){best=at;acc=undefined}
2231
2261
  if(at===best${startFailureExact ? '&&at>pos' : ''})acc=_accSet(${pretest.token.expected},acc)
2232
2262
  }${startFailureExact ? '' : `else if(best===pos)acc=_accSet(${expected[i]},acc)`}`
2233
2263
  return `${decision === '' ? '' : `let ${decision}=${pretest?.sequence === undefined ? -1 : 0}\n`}if((${gateRefs[i]}===null||classHas(${gateRefs[i]},c))${condition}){
2234
- ctx._fc=false
2264
+ ${armMark(i)}ctx._fc=false
2235
2265
  {const v=${arm}(input,pos,ctx)
2236
2266
  if(v!==FAIL)return v}
2237
2267
  {const at=ctx._fe??pos
@@ -2250,7 +2280,7 @@ ${rollbackFor(i)}
2250
2280
  // `choiceFx`, the arms' flatMap, set by set.
2251
2281
  return `${head}
2252
2282
  const c=${directAsciiMask ? 'input.charCodeAt(pos)' : 'lead(input,pos)'}
2253
- ${hasRollback ? emitMark(p, L.buf, L.raw, sinks) : ''}
2283
+ ${choiceMark}
2254
2284
  let acc
2255
2285
  let best=pos
2256
2286
  ${maskable ? `if(c<128){
@@ -190,6 +190,33 @@ class Encoder {
190
190
  private choiceRollbackMasks = new Map<number, number>()
191
191
  private nonCommittingChoiceSites = new Set<number>()
192
192
  private failureRollbackCleanSites = new Set<number>()
193
+ /**
194
+ * THE TRIVIA LOGS A FAILED NODE LEAVES. A node drops its own capture frame when
195
+ * it fails, but the trivia its body recorded into `_triviaLog`/`_rootTriviaLog`
196
+ * is not part of that frame. So a child the census above counts as clean can
197
+ * still leave trivia rows when it reaches a node whose body can fail after a
198
+ * trivia scan (`mayLeavePartialCapture`'s `throughNodes`). These are the arms
199
+ * and sites that need only the two logs restored. A program with no trivia of
200
+ * its own records rows only under trivia its caller supplies, so its emitted
201
+ * assembly takes these marks only for such a parse (`RunCfg.inheritedTrivia`).
202
+ */
203
+ private choiceLogRollbackMasks = new Map<number, number>()
204
+ private failureLogRollbackSites = new Set<number>()
205
+ /**
206
+ * HOW EACH CHOICE WAS LOWERED, for the census above. A choice on the ordered
207
+ * path whose arms cannot commit a failure restores the logs after every arm
208
+ * it enters (its full mark, or its own two-log mark), so it leaves no rows.
209
+ * An exclusive dispatch runs its one arm with no mark, and an ordered choice
210
+ * returns a committed arm's failure before restoring, so either leaves what an
211
+ * arm leaves. A choice in neither set, or in both, is taken to leave rows. Both
212
+ * sets are complete only once every rule is encoded, so the trivia-log census
213
+ * runs then (`logCensus`).
214
+ */
215
+ private restoringChoices = new Set<Combinator<unknown>>()
216
+ private leakingChoices = new Set<Combinator<unknown>>()
217
+ private logCensus: (() => void)[] = []
218
+ private readonly restoresArms = (choice: Combinator<unknown>): boolean =>
219
+ this.restoringChoices.has(choice) && !this.leakingChoices.has(choice)
193
220
  /**
194
221
  * `reachesSideWriter`'s answers, shared by every `failureNeedsRollback` call
195
222
  * for the lifetime of this `Encoder`. Every entry is exact — it depends only on
@@ -816,15 +843,25 @@ class Encoder {
816
843
  private failureNeedsRollback(
817
844
  p: Combinator<unknown>,
818
845
  autoNot: readonly AutoNotCheck[] | null | undefined,
846
+ throughNodes: false | ((choice: Combinator<unknown>) => boolean),
819
847
  ): boolean {
820
848
  const refName = (p as unknown as { _ruleName?: string })._ruleName
821
849
  const winner = refName === undefined ? undefined : this.winners?.[refName]
822
850
  const root = winner !== undefined && !winnerWrapsReference(winner, p) ? winner : p
823
- return mayLeavePartialCapture(root, new Set(), true)
851
+ return mayLeavePartialCapture(root, new Set(), true, throughNodes)
824
852
  || this.reachesSideWriter(root)
825
853
  || ((autoNot?.length ?? 0) > 0 && capturesLeaf(root))
826
854
  }
827
855
 
856
+ /** The rollback census of an `optional()` or `attempt()` row at `ip` over `child`. */
857
+ private censusTransaction(child: Combinator<unknown>, ip: number): void {
858
+ if (this.failureNeedsRollback(child, undefined, false)) return
859
+ this.failureRollbackCleanSites.add(ip)
860
+ this.logCensus.push(() => {
861
+ if (this.failureNeedsRollback(child, undefined, this.restoresArms)) this.failureLogRollbackSites.add(ip)
862
+ })
863
+ }
864
+
828
865
  /**
829
866
  * Whether a field, recovered error, interpreted scanner or unresolvable shape
830
867
  * is reachable from `root` — the writer census of `failureNeedsRollback`,
@@ -1671,6 +1708,7 @@ class Encoder {
1671
1708
  }
1672
1709
  const dispIdx = this.disp.length
1673
1710
  this.disp.push(classes)
1711
+ const exclusive = dispatchIsExclusive(classes, this.cc)
1674
1712
  // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
1675
1713
  // failure remains at the choice position, an arm declined by the driver's
1676
1714
  // char gate contributes the static opener set the interpreter would get by
@@ -1683,10 +1721,7 @@ class Encoder {
1683
1721
  let rollbackMask = kids.length > 31 ? -1 : 0
1684
1722
  if (rollbackMask === 0) {
1685
1723
  for (let i = 0; i < arms.length; i++) {
1686
- const src = order[i]!
1687
- if (this.failureNeedsRollback(arms[i]!, d.autoNot[src])) {
1688
- rollbackMask |= 1 << i
1689
- }
1724
+ if (this.failureNeedsRollback(arms[i]!, d.autoNot[order[i]!], false)) rollbackMask |= 1 << i
1690
1725
  }
1691
1726
  }
1692
1727
  const head = this.emitHead(OP_CHOICE, 3 + 2 * kids.length)
@@ -1701,9 +1736,19 @@ class Encoder {
1701
1736
  this.code[head + 4 + kids.length + i] = this.expected(deriveExpected(arms[i]!))
1702
1737
  }
1703
1738
  this.choiceRollbackMasks.set(head, rollbackMask)
1704
- if (arms.every(arm => !mayCommitFailure(arm, rr, this.commitFailureMemo))) {
1705
- this.nonCommittingChoiceSites.add(head)
1739
+ if (rollbackMask !== -1) {
1740
+ const full = rollbackMask
1741
+ this.logCensus.push(() => {
1742
+ let logMask = 0
1743
+ for (let i = 0; i < arms.length; i++) {
1744
+ if ((full & (1 << i)) === 0 && this.failureNeedsRollback(arms[i]!, d.autoNot[order[i]!], this.restoresArms)) logMask |= 1 << i
1745
+ }
1746
+ if (logMask !== 0) this.choiceLogRollbackMasks.set(head, logMask)
1747
+ })
1706
1748
  }
1749
+ const nonCommitting = arms.every(arm => !mayCommitFailure(arm, rr, this.commitFailureMemo))
1750
+ if (nonCommitting) this.nonCommittingChoiceSites.add(head)
1751
+ ;(!exclusive && nonCommitting ? this.restoringChoices : this.leakingChoices).add(p)
1707
1752
  return head
1708
1753
  }
1709
1754
  case 'many':
@@ -1765,7 +1810,7 @@ class Encoder {
1765
1810
  }
1766
1811
  case 'optional': {
1767
1812
  const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
1768
- if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(head)
1813
+ this.censusTransaction(d.parser, head)
1769
1814
  return head
1770
1815
  }
1771
1816
  case 'transform': {
@@ -2095,7 +2140,7 @@ class Encoder {
2095
2140
  // lowering was correct only for a choice arm.
2096
2141
  case 'attempt': {
2097
2142
  const inner = this.emit(OP_ATTEMPT, this.node(d.parser).ip, d.contain === true ? 1 : 0)
2098
- if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(inner)
2143
+ this.censusTransaction(d.parser, inner)
2099
2144
  // THE FIRST-SET FAIL-FAST GUARD, lowered as the `OP_GATE` row the `node()`
2100
2145
  // case already uses — it is the same guard, written twice in the
2101
2146
  // interpreter (`attempt.ts:22`, `node.ts:239`). It is NOT merely an
@@ -2295,6 +2340,7 @@ class Encoder {
2295
2340
  finish(): TableProgram {
2296
2341
  if (this.code.length === 0) this.emit(OP_EMPTY)
2297
2342
  this.collapseIndirection()
2343
+ for (const census of this.logCensus) census()
2298
2344
  return ownTableProgram({
2299
2345
  code: this.code, k: this.k, fns: this.fns, cc: this.cc,
2300
2346
  fx: this.fx, disp: this.disp, dsp: this.dsp, rules: this.rules,
@@ -2315,6 +2361,8 @@ class Encoder {
2315
2361
  }, undefined, {
2316
2362
  choiceRollbackMasks: this.choiceRollbackMasks,
2317
2363
  failureRollbackCleanSites: this.failureRollbackCleanSites,
2364
+ choiceLogRollbackMasks: this.choiceLogRollbackMasks,
2365
+ failureLogRollbackSites: this.failureLogRollbackSites,
2318
2366
  nonCommittingChoiceSites: this.nonCommittingChoiceSites,
2319
2367
  })
2320
2368
  }
package/src/table/exec.ts CHANGED
@@ -1558,8 +1558,10 @@ function makeDriver(
1558
1558
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1559
1559
  const v = exec(code[ip + 1]!, input, pos, ctx)
1560
1560
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1561
+ // A lookahead contains its child's commitment (`not.ts` returns a fresh
1562
+ // result): a generic child's committed failure is cleared here.
1563
+ ctx._fc = savedFc
1561
1564
  if (scalarChild) {
1562
- ctx._fc = savedFc
1563
1565
  ctx._fe = savedFe
1564
1566
  ctx._fx = savedFx
1565
1567
  }
@@ -1588,7 +1590,8 @@ function makeDriver(
1588
1590
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1589
1591
  const v = exec(code[ip + 1]!, input, pos, ctx)
1590
1592
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1591
- if (scalarChild) ctx._fc = savedFc
1593
+ // A lookahead contains its child's commitment, as under `not()`.
1594
+ ctx._fc = savedFc
1592
1595
  if (ctx._probe !== undefined) ctx._probe.best = savedProbe ?? null
1593
1596
  // `peek.ts:60` — the ASSERTION's own set, at the assertion's position.
1594
1597
  if (v === FAIL) { ctx._fe = pos; ctx._fx = fx[code[ip + 2]!] as string[]; return FAIL }
@@ -531,6 +531,8 @@ type RuntimeTableProgram = TableProgram & {
531
531
  readonly choiceRollbackMasks?: ReadonlyMap<number, number>
532
532
  readonly nonCommittingChoiceSites?: ReadonlySet<number>
533
533
  readonly failureRollbackCleanSites?: ReadonlySet<number>
534
+ readonly choiceLogRollbackMasks?: ReadonlyMap<number, number>
535
+ readonly failureLogRollbackSites?: ReadonlySet<number>
534
536
  }
535
537
  }
536
538
 
@@ -542,6 +544,8 @@ export function ownTableProgram(
542
544
  readonly choiceRollbackMasks?: ReadonlyMap<number, number>
543
545
  readonly failureRollbackCleanSites?: ReadonlySet<number>
544
546
  readonly nonCommittingChoiceSites?: ReadonlySet<number>
547
+ readonly choiceLogRollbackMasks?: ReadonlyMap<number, number>
548
+ readonly failureLogRollbackSites?: ReadonlySet<number>
545
549
  } = {},
546
550
  ): TableProgram {
547
551
  const runtime = (prog as RuntimeTableProgram)[TABLE_RUNTIME]
@@ -552,6 +556,8 @@ export function ownTableProgram(
552
556
  choiceRollbackMasks: caches.choiceRollbackMasks ?? runtime?.choiceRollbackMasks,
553
557
  nonCommittingChoiceSites: caches.nonCommittingChoiceSites ?? runtime?.nonCommittingChoiceSites,
554
558
  failureRollbackCleanSites: caches.failureRollbackCleanSites ?? runtime?.failureRollbackCleanSites,
559
+ choiceLogRollbackMasks: caches.choiceLogRollbackMasks ?? runtime?.choiceLogRollbackMasks,
560
+ failureLogRollbackSites: caches.failureLogRollbackSites ?? runtime?.failureLogRollbackSites,
555
561
  },
556
562
  } as RuntimeTableProgram
557
563
  }
@@ -575,6 +581,19 @@ export function failureRollbackClean(prog: TableProgram, ip: number): boolean {
575
581
  return (prog as RuntimeTableProgram)[TABLE_RUNTIME]?.failureRollbackCleanSites?.has(ip) === true
576
582
  }
577
583
 
584
+ /** The arms of the choice at `ip` the census counts as clean whose failure can
585
+ * still leave trivia-log rows: a node they reach fails after recording trivia
586
+ * (`encode.ts` `choiceLogRollbackMasks`). Zero without that authority. */
587
+ export function choiceLogRollbackMask(prog: TableProgram, ip: number): number {
588
+ return (prog as RuntimeTableProgram)[TABLE_RUNTIME]?.choiceLogRollbackMasks?.get(ip) ?? 0
589
+ }
590
+
591
+ /** An `optional()` or `attempt()` row `failureRollbackClean` admits whose failed
592
+ * child can still leave trivia-log rows, for the same reason. */
593
+ export function failureLogRollback(prog: TableProgram, ip: number): boolean {
594
+ return (prog as RuntimeTableProgram)[TABLE_RUNTIME]?.failureLogRollbackSites?.has(ip) === true
595
+ }
596
+
578
597
  /** A compiled entry, shaped exactly like a codegen rule function. */
579
598
  export type TableRule = (input: string, pos: number, ctx: ParseContext) => ParseResult<unknown>
580
599
 
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.60.2'
16
+ export const PARSEMAN_VERSION = '0.60.4'