parseman 0.56.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/analysis/diagnostics.js +379 -72
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +379 -72
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/node.d.ts +1 -1
  6. package/dist/compiler/build-arity.d.ts +1 -1
  7. package/dist/compiler/build-arity.d.ts.map +1 -1
  8. package/dist/cst/trivia-kinds.d.ts +9 -1
  9. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  10. package/dist/index.js +446 -75
  11. package/dist/index.js.map +3 -3
  12. package/dist/language-service/index.js +1 -0
  13. package/dist/language-service/index.js.map +1 -1
  14. package/dist/parse-context.d.ts.map +1 -1
  15. package/dist/plugin/entry-supercompiler.d.ts +1 -1
  16. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  17. package/dist/plugin/index.d.ts +27 -1
  18. package/dist/plugin/index.d.ts.map +1 -1
  19. package/dist/plugin/index.js +930 -157
  20. package/dist/plugin/index.js.map +3 -3
  21. package/dist/plugin/ir-eval.d.ts +2 -0
  22. package/dist/plugin/ir-eval.d.ts.map +1 -1
  23. package/dist/regex/classes.d.ts.map +1 -1
  24. package/dist/run/index.js +1 -0
  25. package/dist/run/index.js.map +1 -1
  26. package/dist/table/assemble.d.ts +9 -0
  27. package/dist/table/assemble.d.ts.map +1 -1
  28. package/dist/table/child-slots.d.ts +4 -6
  29. package/dist/table/child-slots.d.ts.map +1 -1
  30. package/dist/table/compile-rule-map.d.ts +14 -2
  31. package/dist/table/compile-rule-map.d.ts.map +1 -1
  32. package/dist/table/emit-assembly.d.ts +17 -2
  33. package/dist/table/emit-assembly.d.ts.map +1 -1
  34. package/dist/table/emit.d.ts +5 -1
  35. package/dist/table/emit.d.ts.map +1 -1
  36. package/dist/table/encode.d.ts.map +1 -1
  37. package/dist/table/exec-baseline.d.ts.map +1 -1
  38. package/dist/table/exec.d.ts.map +1 -1
  39. package/dist/table/index.js +442 -70
  40. package/dist/table/index.js.map +3 -3
  41. package/dist/table/ops.d.ts +9 -6
  42. package/dist/table/ops.d.ts.map +1 -1
  43. package/dist/table/program.d.ts +13 -0
  44. package/dist/table/program.d.ts.map +1 -1
  45. package/dist/table/select-hot.d.ts +6 -0
  46. package/dist/table/select-hot.d.ts.map +1 -0
  47. package/dist/table/trivia-gap.d.ts +31 -0
  48. package/dist/table/trivia-gap.d.ts.map +1 -0
  49. package/dist/types.d.ts +17 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/version.d.ts +1 -1
  52. package/package.json +4 -1
  53. package/src/combinators/node.ts +6 -13
  54. package/src/combinators/trivia-skip.ts +44 -1
  55. package/src/compiler/build-arity.ts +3 -3
  56. package/src/cst/trivia-kinds.ts +12 -1
  57. package/src/parse-context.ts +1 -0
  58. package/src/plugin/entry-supercompiler.ts +8 -9
  59. package/src/plugin/index.ts +138 -13
  60. package/src/plugin/ir-eval.ts +7 -1
  61. package/src/regex/classes.ts +3 -0
  62. package/src/table/assemble.ts +122 -24
  63. package/src/table/child-slots.ts +4 -6
  64. package/src/table/compile-rule-map.ts +55 -15
  65. package/src/table/emit-assembly.ts +139 -26
  66. package/src/table/emit.ts +8 -7
  67. package/src/table/encode.ts +69 -21
  68. package/src/table/exec-baseline.ts +1 -3
  69. package/src/table/exec.ts +7 -6
  70. package/src/table/ops.ts +9 -6
  71. package/src/table/program.ts +24 -0
  72. package/src/table/select-hot.ts +70 -0
  73. package/src/table/trivia-gap.ts +109 -0
  74. package/src/types.ts +18 -0
  75. package/src/version.ts +1 -1
@@ -32,7 +32,7 @@ import {
32
32
  LEX_NODE_SEQUENCE2, LEX_NODE_SEQUENCE3, LEX_NODE_SEQUENCE4, LEX_NODE_SEQUENCE5,
33
33
  LEX_NODE_TERMINAL,
34
34
  } from './lex-program.ts'
35
- import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
35
+ import { covKindCode, dispatchIsExclusive, encodeClassSpec, ownTableProgram } from './program.ts'
36
36
  import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
37
37
  import { directArrayProjection } from '../compiler/direct-projection.ts'
38
38
  import {
@@ -699,6 +699,38 @@ class Encoder {
699
699
  }
700
700
  }
701
701
 
702
+ /**
703
+ * THE FIRST-CHARACTER GATE OF AN `optional`, `not` OR `peek` CHILD.
704
+ *
705
+ * A non-nullable child cannot match at a character outside its first set, so
706
+ * the wrapper's outcome there is already decided: `optional` yields `null`,
707
+ * `not` succeeds, `peek` fails with its own set. The interpreter's `optional`
708
+ * makes that test itself (`repeat.ts` `startsFirstSet`); the table entered the
709
+ * child — a rollback mark, the call, the child's own failure — to learn it.
710
+ *
711
+ * The gate is an `OP_GATE` row around the child, so every engine runs it
712
+ * unchanged, a probe or tolerant parse forwards through it as for a node's
713
+ * gate, and the wrapper's row keeps its layout. The engines test the class
714
+ * before taking the wrapper's mark. The first set is resolved through
715
+ * `winners`, the analysis a choice arm's gate uses, so a composed rule's set
716
+ * is the merged one. A scalar `not` child stays bare: `scalarTerminalNotChild`
717
+ * already reduces it to one recognizer call.
718
+ */
719
+ private lookaheadGate(p: Combinator<unknown>, child: number): number {
720
+ // By opcode FAMILY: a line-tracked variant spells the same terminal
721
+ // `OP_LIT_TRACK`/`OP_RX_TRACK`, and every variant of one grammar must lay
722
+ // down the same words (`foldPrograms`).
723
+ const op = this.code[child]
724
+ if (op === OP_GATE || op === OP_LIT || op === OP_LIT_CI || op === OP_LIT_TRACK || op === OP_LIT_CI_TRACK
725
+ || op === OP_RX || op === OP_RX_TRACK) return child
726
+ const rr = this.refResolver()
727
+ if (matchesEmpty(p, new Set(), rr)) return child
728
+ const cls = this.charClass(firstSetOf(p, new Set(), rr, this.activeTrivia))
729
+ if (cls < 0) return child
730
+ const e = deriveExpected(p)
731
+ return this.emit(OP_GATE, cls, child, this.expected(e.length > 0 ? e : [p._tag]))
732
+ }
733
+
702
734
  /** A first set becomes a char class string, or −1 for `any`. */
703
735
  private charClass(fs: FirstSet): number {
704
736
  if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
@@ -715,14 +747,21 @@ class Encoder {
715
747
  return i
716
748
  }
717
749
 
718
- /** Bind a guard only when its class is already part of the program. Repeat
719
- * gating must not grow the compact artifact merely to avoid a failed call. */
720
- private existingCharClass(fs: FirstSet): number {
721
- if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
722
- const hit = this.ccIndex.get(encodeClassSpec(fs.ranges))
723
- // This reuses the recovery row's existing `-1` slot. Keep its decimal width
724
- // at two bytes or less so a speed hint cannot grow a compact artifact.
725
- return hit !== undefined && hit < 100 ? hit : -1
750
+ /**
751
+ * The first set a node's or an attempt's fail-fast gate tests. A bare
752
+ * cross-rule ref resolves through `winners` (a compose OVERRIDE may widen
753
+ * it). Any other body keeps its construction-time `_meta.firstSet`, except
754
+ * where that is `any`: under `compose()` a body that reaches a rule of
755
+ * another piece is built before that rule exists, so its cached set is
756
+ * `any` and the node got NO gate (jess's `CompoundSelector` was entered at
757
+ * `{`, `Call` at `}`). The resolved set is the one a choice arm's gate uses,
758
+ * over the final merged map.
759
+ */
760
+ private gateFirstSet(p: Combinator<unknown>): FirstSet {
761
+ const cached = p._meta.firstSet
762
+ return p._def.tag === 'lazy' || cached.kind === 'any'
763
+ ? firstSetOf(p, new Set(), this.refResolver(), this.activeTrivia)
764
+ : cached
726
765
  }
727
766
 
728
767
  /**
@@ -1202,6 +1241,19 @@ class Encoder {
1202
1241
  if (transactionalFirst[i] !== undefined) classes[i] = -1
1203
1242
  }
1204
1243
  }
1244
+ // A STATE-GATED ARM ON THE ORDERED PATH IS ALWAYS ENTERED. The interpreter
1245
+ // skips a gated-off arm and contributes nothing for it (choice.ts
1246
+ // firstMatch), and gates it on only by running it. A char-gated skip would
1247
+ // instead add the arm's static opener set whatever the gate says, so an
1248
+ // ordered site leaves the decision to `OP_ARMGATE`. An exclusive site keeps
1249
+ // its classes: there the dispatched arm's gate failing IS the choice
1250
+ // failing, with that arm's set, as in the interpreter's disjoint path.
1251
+ if (d.gates.some(g => g !== null && g !== undefined) && !dispatchIsExclusive(classes, this.cc)) {
1252
+ for (let k = 0; k < classes.length; k++) {
1253
+ const gate = d.gates[order[k]!]
1254
+ if (gate !== null && gate !== undefined) classes[k] = -1
1255
+ }
1256
+ }
1205
1257
  const dispIdx = this.disp.length
1206
1258
  this.disp.push(classes)
1207
1259
  // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
@@ -1245,7 +1297,7 @@ class Encoder {
1245
1297
  const rr = this.refResolver()
1246
1298
  const itemClass = matchesEmpty(d.parser, new Set(), rr)
1247
1299
  ? -1
1248
- : this.existingCharClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
1300
+ : this.charClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
1249
1301
  const op = d.valueUnused ? OP_REPV : OP_REP
1250
1302
  const min = d.tag === 'many' ? 0 : d.min
1251
1303
  // ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
@@ -1288,7 +1340,7 @@ class Encoder {
1288
1340
  : this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
1289
1341
  }
1290
1342
  case 'optional': {
1291
- const head = this.emit(OP_OPT, this.node(d.parser).ip)
1343
+ const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
1292
1344
  if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(head)
1293
1345
  return head
1294
1346
  }
@@ -1409,16 +1461,14 @@ class Encoder {
1409
1461
  // `deriveExpected(body)`, which for a body with nullable leading terms
1410
1462
  // names tokens the run would have skipped — `['"@"', '"(", '"x"']` where
1411
1463
  // running the body reports `['"x"']` alone.
1412
- if (matchesEmpty(d.parser)) return body
1464
+ if (matchesEmpty(d.parser, new Set(), this.refResolver())) return body
1413
1465
  // A node whose BODY is a bare cross-rule ref (`node('X', g.Y, …)`) caches its
1414
1466
  // first set from the ref's base binding. Under a compose OVERRIDE the winner
1415
1467
  // for `Y` may be WIDER, so the cached set gates out input the merged rule
1416
1468
  // accepts (the reducer reroutes via `winners`, but this recognizer gate did
1417
1469
  // not). Resolve the ref's gate through `winners`, exactly as the `lazy` case
1418
1470
  // resolves the call. Non-ref bodies keep the cached set (byte-identical).
1419
- const fs = d.parser._def.tag === 'lazy'
1420
- ? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
1421
- : d.parser._meta.firstSet
1471
+ const fs = this.gateFirstSet(d.parser)
1422
1472
  if (fs.kind === 'any') return body
1423
1473
  const cls = this.charClass(fs)
1424
1474
  if (cls < 0) return body
@@ -1558,7 +1608,7 @@ class Encoder {
1558
1608
  // other engine reported `['not(literal)']`. It hid because `expected` is a
1559
1609
  // TOP-LEVEL field on `RunResult` and is NOT part of the identity digest.
1560
1610
  case 'not':
1561
- return this.emit(OP_NOT, this.node(d.parser).ip, this.expected(assertionFailureExpected(false, d.parser._tag)))
1611
+ return this.emit(OP_NOT, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(false, d.parser._tag)))
1562
1612
  case 'guard':
1563
1613
  // `'gate'`, the public name — see gate.ts. The def tag stays `'guard'`.
1564
1614
  return this.emit(OP_GUARD, this.fn(d.predicate, d.predSrc ?? null), this.expected(['gate']))
@@ -1603,7 +1653,7 @@ class Encoder {
1603
1653
  // lookahead's failure is "the guard did not hold", and naming the body's
1604
1654
  // internals offers a token the parse never asked the author for. The
1605
1655
  // table propagated the body's set instead.
1606
- return this.emit(OP_PEEK, this.node(d.parser).ip, this.expected(assertionFailureExpected(true, d.parser._tag)))
1656
+ return this.emit(OP_PEEK, this.lookaheadGate(d.parser, this.node(d.parser).ip), this.expected(assertionFailureExpected(true, d.parser._tag)))
1607
1657
  // A TRANSACTION IS A ROW. See `OP_ATTEMPT` for why the transparent
1608
1658
  // lowering was correct only for a choice arm.
1609
1659
  case 'attempt': {
@@ -1619,10 +1669,8 @@ class Encoder {
1619
1669
  // not a slower equivalent.
1620
1670
  // Same override-aware resolution as the node gate: a bare-ref inner resolves
1621
1671
  // its gate through `winners` so a widened winner is not gated out.
1622
- const fs = d.parser._def.tag === 'lazy'
1623
- ? firstSetOf(d.parser, new Set(), this.refResolver(), this.activeTrivia)
1624
- : d.parser._meta.firstSet
1625
- if (fs.kind === 'any' || matchesEmpty(d.parser)) return inner
1672
+ const fs = this.gateFirstSet(d.parser)
1673
+ if (fs.kind === 'any' || matchesEmpty(d.parser, new Set(), this.refResolver())) return inner
1626
1674
  const cls = this.charClass(fs)
1627
1675
  if (cls < 0) return inner
1628
1676
  const e = deriveExpected(d.parser)
@@ -487,9 +487,7 @@ function makeDriver(
487
487
  children: readonly unknown[], fields: undefined, span: { start: number; end: number },
488
488
  rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
489
489
  ) => unknown
490
- const st = (flags & 8) !== 0 && ctx.state !== undefined
491
- ? Object.assign({}, ctx.state as Record<string, unknown>)
492
- : undefined
490
+ const st = (flags & 8) !== 0 ? ctx.state : undefined
493
491
  const nd = build(
494
492
  cap.children, undefined,
495
493
  code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end },
package/src/table/exec.ts CHANGED
@@ -1123,7 +1123,10 @@ function makeDriver(
1123
1123
  // choice.ts:150 is a `continue`, not a failure — the arm is treated as
1124
1124
  // never entered, so a cut it might have raised must not cut the choice.
1125
1125
  ctx._fc = false
1126
- ctx._fe = pos
1126
+ // No failure POSITION: an ordered choice ranks arms by `_fe`, and a skipped
1127
+ // arm must not rank. A dispatched choice reads `_fx` alone, so this arm's
1128
+ // set is still the answer when it is the one selected.
1129
+ ctx._fe = -1
1127
1130
  ctx._fx = fx[code[ip + 3]!] as string[]
1128
1131
  return FAIL
1129
1132
  }
@@ -1479,9 +1482,7 @@ function makeDriver(
1479
1482
  if (v === FAIL) return FAIL
1480
1483
  const end = EC.e
1481
1484
  const span = code[ip] === OP_NODE_TRACK ? spanLines(ctx, pos, end) : { start: pos, end }
1482
- const st = (flags & 8) !== 0 && ctx.state !== undefined
1483
- ? Object.assign({}, ctx.state as Record<string, unknown>)
1484
- : undefined
1485
+ const st = (flags & 8) !== 0 ? ctx.state : undefined
1485
1486
  const tagIdx = code[ip + 6]!
1486
1487
  const tags = tagIdx < 0 ? undefined : k[tagIdx] as readonly string[]
1487
1488
 
@@ -1518,13 +1519,13 @@ function makeDriver(
1518
1519
  nd = kids[0]
1519
1520
  } else if (proj >= 0) {
1520
1521
  nd = hostCst && host !== undefined
1521
- ? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, (flags & 8) !== 0 ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
1522
+ ? host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
1522
1523
  : projectChild(kids, proj, type)
1523
1524
  } else if (buildIdx >= 0) {
1524
1525
  if (hostCst && host !== undefined) {
1525
1526
  // A direct builder is bypassed under a CST host: the host must never
1526
1527
  // receive an arbitrary AST object as a child of a CST node.
1527
- nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, (flags & 8) !== 0 ? st : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined, tags)
1528
+ nd = host(type, kids, fieldMap, span, cap.rawChildren, cap.triviaLog, ctx.state, tags)
1528
1529
  } else {
1529
1530
  const build = fns[buildIdx] as (
1530
1531
  children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
package/src/table/ops.ts CHANGED
@@ -339,13 +339,16 @@ export const OP_REJECT = 35
339
339
  * arm INSTEAD of sitting inside it, so the encoder still reads the arm's own
340
340
  * first set for `disp` and the site keeps its dispatch slot.
341
341
  *
342
- * A blocked arm is SKIPPED, not failed — `choice.ts:150` is `continue`. The row
343
- * therefore clears `_fc` on the gate-false path exactly as `OP_REJECT` does:
342
+ * A blocked arm is SKIPPED, not failed — `choice.ts` firstMatch `continue`s. The
343
+ * row therefore clears `_fc` on the gate-false path exactly as `OP_REJECT` does:
344
344
  * "pretend this arm was never entered", so no cut it might have raised survives
345
- * to cut the choice. It still reports `e`, because the OTHER path a blocked arm
346
- * can be reached by is first-char dispatch, where skip-and-retry and
347
- * fail-the-choice are the same thing (choice.ts:23-34, :100-105) and the
348
- * interpreter answers with `deriveExpected(arm)` — this set.
345
+ * to cut the choice. It sets `_fe` to −1, so an ORDERED choice, which ranks arms
346
+ * by failure position, takes nothing from it, as the interpreter does. It still
347
+ * leaves `e` in `_fx`, because the other path a blocked arm can be reached by is
348
+ * exclusive first-char dispatch, where skip-and-retry and fail-the-choice are the
349
+ * same thing and the interpreter answers with `deriveExpected(arm)` — this set.
350
+ * On an ordered site the encoder gives a gated arm no char class, so it is always
351
+ * entered and this row, not a char gate, decides what it contributes.
349
352
  *
350
353
  * The predicate is a live function, like `OP_GUARD`'s, so a grammar using one is
351
354
  * runtime-only for `emitTableModule` unless `fnSources` are supplied.
@@ -273,6 +273,19 @@ export function scanRefs(s: ScanSpec): SubtreeRef[] {
273
273
  return refs
274
274
  }
275
275
 
276
+ /**
277
+ * The sites an assembly links from OUTSIDE its rule entries — the scan pool's
278
+ * subtrees, then the scan-skip sets — in the one order every assembler names
279
+ * them. The emitted source is keyed by this order, so a second copy of the list
280
+ * that disagrees prints different source for the same program.
281
+ */
282
+ export function assemblyExtraIps(prog: TableProgram): number[] {
283
+ const ips: number[] = []
284
+ for (const s of prog.scans ?? []) for (const r of scanRefs(s)) ips.push(r[0])
285
+ for (const set of prog.scanSkip ?? []) for (const r of set) ips.push(r[0])
286
+ return ips
287
+ }
288
+
276
289
  /** A trivia combinator as data. `arms` empty means a plain `trivia(regex)`. */
277
290
  export type TriviaSpec = {
278
291
  /** `[label, regexSource, regexFlags]` per classified arm. */
@@ -644,6 +657,17 @@ export function classHas(cls: ResolvedClass, code: number): boolean {
644
657
  return false
645
658
  }
646
659
 
660
+ /**
661
+ * Whether a choice with these arm classes (indices into the class-spec pool `cc`)
662
+ * takes the O(1) exclusive dispatch — the same decision `resolveDispatch` makes,
663
+ * for an encoder that has only the specs.
664
+ */
665
+ export function dispatchIsExclusive(arms: readonly number[], cc: readonly string[]): boolean {
666
+ const resolved: ResolvedClass[] = []
667
+ for (const ci of arms) if (ci >= 0) resolved[ci] = resolveClass(cc[ci]!)
668
+ return resolveDispatch(arms, resolved).exclusive
669
+ }
670
+
647
671
  function resolveDispatch(arms: readonly number[], cc: readonly ResolvedClass[]): ResolvedDispatch {
648
672
  const ascii = new Uint8Array(128)
649
673
  const hi: number[] = []
@@ -0,0 +1,70 @@
1
+ /**
2
+ * WHICH SITES A SELECTIVE BUILD-TIME ASSEMBLY EMITS.
3
+ *
4
+ * The emitted engine beats the closure engine per site, but its bytes are paid by
5
+ * every process that loads the module. Emitting every site made jess's Less AST
6
+ * artifact 20.8x its grammar source. So a build emits only the sites with the
7
+ * highest ESTIMATED hotness per byte, up to a byte budget, and the closure engine
8
+ * runs the rest (`assemble.ts`, the `HYBRID` path).
9
+ *
10
+ * Hotness is estimated statically, since a macro build has no workload to
11
+ * profile: every rule entry starts at 1, a repetition multiplies its body by
12
+ * `LOOP`, and a choice or dispatch splits its weight evenly across its arms. The
13
+ * weights are propagated `ROUNDS` times, so a recursive cycle saturates at `CAP`
14
+ * instead of diverging. Sites are then taken greedily by weight per emitted byte.
15
+ * A build given a corpus (`assemblyProfile`) ranks by measured calls instead.
16
+ */
17
+ import { childSlots } from './child-slots.ts'
18
+ import { OP_CHOICE, OP_DISPATCH, OP_REP, OP_REPV } from './ops.ts'
19
+
20
+ const LOOP = 8
21
+ const ROUNDS = 60
22
+ const CAP = 1e12
23
+
24
+ /** The sites to emit: highest weight per byte first, while they fit in `budget` bytes. */
25
+ export function selectHotSites(
26
+ code: readonly number[],
27
+ entries: readonly number[],
28
+ siteBytes: ReadonlyMap<number, number>,
29
+ budget: number,
30
+ /** Measured calls per site (`countSiteVisits` over a corpus). Replaces the
31
+ * static estimate when given. */
32
+ profile?: ArrayLike<number>,
33
+ ): Set<number> {
34
+ const selected = new Set<number>()
35
+ if (budget <= 0) return selected
36
+ const weight = profile === undefined ? staticWeights(code, entries) : undefined
37
+ const w = (ip: number): number => (weight !== undefined ? weight.get(ip) : profile![ip]) ?? 0
38
+ // Ties (saturated cycles) break by site offset, so a build is deterministic.
39
+ const ranked = [...siteBytes.keys()].sort((a, b) =>
40
+ w(b) / siteBytes.get(b)! - w(a) / siteBytes.get(a)! || a - b)
41
+ let spent = 0
42
+ for (const ip of ranked) {
43
+ if (w(ip) === 0) break
44
+ const bytes = siteBytes.get(ip)!
45
+ if (spent + bytes > budget) continue
46
+ spent += bytes
47
+ selected.add(ip)
48
+ }
49
+ return selected
50
+ }
51
+
52
+ function staticWeights(code: readonly number[], entries: readonly number[]): Map<number, number> {
53
+ let weight = new Map<number, number>()
54
+ const kids: number[] = []
55
+ for (let round = 0; round < ROUNDS; round++) {
56
+ const next = new Map<number, number>()
57
+ for (const ip of entries) next.set(ip, 1)
58
+ for (const [ip, w] of weight) {
59
+ kids.length = 0
60
+ childSlots(code, ip, kids)
61
+ const op = code[ip]
62
+ const scale = op === OP_REP || op === OP_REPV ? LOOP
63
+ : op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
64
+ : 1
65
+ for (const kid of kids) next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
66
+ }
67
+ weight = next
68
+ }
69
+ return weight
70
+ }
@@ -0,0 +1,109 @@
1
+ import type { CharRange, Combinator, ParseContext } from '../types.ts'
2
+ import type { LabeledTriviaSpec } from '../cst/trivia-kinds.ts'
3
+ import { commitTriviaScan, fastTriviaScanner, scanTriviaCompact } from '../combinators/trivia-skip.ts'
4
+
5
+ /**
6
+ * THE TABLE ENGINES' DEFERRED-COMMIT TRIVIA SKIP: `commitTriviaScan(scanTriviaCompact(…))`,
7
+ * the path every labelled trivia scan takes once a log is live, answered without
8
+ * the scan where the answer is already known. Both engines call it: the closure
9
+ * engine's `skipTrivia` and the emitted assembly's skips.
10
+ *
11
+ * ONLY THE ROOT LOG IS LIVE, so only a root-selected chunk can be recorded,
12
+ * and a run in which no character can start a selected arm holds none: its end
13
+ * is the whole answer, and the unlabelled scanner gives it without the
14
+ * chunk-by-chunk visit. Every chunk starts inside the run, so a run free of
15
+ * selected leaders is free of selected chunks. jess parses with
16
+ * `rootTrivia: { select: [comment labels] }`, which is this case for every run
17
+ * of plain whitespace.
18
+ *
19
+ * A SEQUENCE TERM THAT MATCHES NOTHING hands back the cursor from BEFORE its
20
+ * trivia, so the next term asks for the same gap again: on `benchmark.less`,
21
+ * 21,741 of the closure engine's 53,607 trivia scans per parse started where
22
+ * the previous one did. A scan that recorded nothing records nothing again from
23
+ * the same position, in the same parse, under the same trivia, root selection,
24
+ * root-capture policy, trivia log and state, and with no CST capture, and it
25
+ * ends at the same place, so the last such scan answers the repeat (`ctx._gap`).
26
+ * The state is part of the key because a trivia arm can read it (`gate`), and
27
+ * the log because a `leaf` sets it aside while its child runs. The memo lives
28
+ * on the context so it is released with the parse.
29
+ *
30
+ * Neither applies with `trackLines` (a scan records line ranges) or under CST
31
+ * capture (a scan records into the open buffer).
32
+ */
33
+ export function skipDeferredTrivia(input: string, cur: number, ctx: ParseContext): number {
34
+ const triviaP = ctx.trivia
35
+ const plain = ctx.trackLines !== true && ctx.captureTrivia !== true
36
+ const gap = ctx._gap
37
+ if (plain && gap !== undefined && cur === gap.at && input === gap.input && triviaP === gap.of
38
+ && ctx._rootTriviaCapture === gap.root && ctx.state === gap.state
39
+ && ctx._rootTriviaKindIndex === gap.rootKinds && ctx._triviaLog === gap.log) return gap.end
40
+ const end = clearRunEnd(input, cur, ctx, triviaP, plain)
41
+ if (end >= 0) {
42
+ remember(cur, end, ctx, input, triviaP)
43
+ return end
44
+ }
45
+ const scan = scanTriviaCompact(input, cur, ctx)
46
+ if (typeof scan !== 'number') return commitTriviaScan(scan)
47
+ if (plain) remember(cur, scan, ctx, input, triviaP)
48
+ return scan
49
+ }
50
+
51
+ /** The end of the run at `cur` when nothing root-selected lies in it, else −1. */
52
+ function clearRunEnd(input: string, cur: number, ctx: ParseContext, triviaP: Combinator<unknown> | undefined, plain: boolean): number {
53
+ const rootKinds = ctx._rootTriviaKindIndex
54
+ const spec = triviaP?._meta.labeledTriviaSpec
55
+ if (!plain || triviaP === undefined || spec == null || rootKinds === undefined || ctx._triviaLog !== undefined) return -1
56
+ if (spec.rootKinds !== rootKinds) prime(spec, triviaP, rootKinds)
57
+ const fast = spec.rootScan
58
+ if (fast === null) return -1
59
+ const end = fast(input, cur)
60
+ return ctx._rootTriviaCapture === false || clearOf(spec.rootLeaders, input, cur, end) ? end : -1
61
+ }
62
+
63
+ function remember(cur: number, end: number, ctx: ParseContext, input: string, triviaP: Combinator<unknown> | undefined): void {
64
+ const gap = ctx._gap
65
+ if (gap === undefined) {
66
+ ctx._gap = { at: cur, end, input, of: triviaP, root: ctx._rootTriviaCapture, rootKinds: ctx._rootTriviaKindIndex, log: ctx._triviaLog, state: ctx.state }
67
+ return
68
+ }
69
+ gap.at = cur
70
+ gap.end = end
71
+ gap.input = input
72
+ gap.of = triviaP
73
+ gap.root = ctx._rootTriviaCapture
74
+ gap.rootKinds = ctx._rootTriviaKindIndex
75
+ gap.log = ctx._triviaLog
76
+ gap.state = ctx.state
77
+ }
78
+
79
+ /**
80
+ * Prime `spec` for the selection `rootKinds`: the first-character ranges of the
81
+ * arms it selects, and the scanner, which is `null` where one of those arms is
82
+ * unbounded or the trivia has no unlabelled scanner. `run()` builds one
83
+ * selection per parse, so this runs once per parse per trivia.
84
+ */
85
+ function prime(spec: LabeledTriviaSpec, triviaP: Combinator<unknown>, rootKinds: Readonly<Record<string, number>>): void {
86
+ const ranges: CharRange[] = []
87
+ let bounded = true
88
+ for (const arm of spec.arms) {
89
+ if (rootKinds[arm.label] === undefined) continue
90
+ const fs = arm.parser._meta.firstSet
91
+ if (fs.kind === 'any') bounded = false
92
+ else if (fs.kind === 'ranges') ranges.push(...fs.ranges)
93
+ }
94
+ const classified = triviaP._meta.triviaScanner
95
+ spec.rootKinds = rootKinds
96
+ spec.rootLeaders = ranges
97
+ spec.rootScan = !bounded ? null : classified === undefined ? fastTriviaScanner(triviaP) : classified
98
+ }
99
+
100
+ /** No character in `[from, to)` falls in `ranges`. A surrogate is never clear:
101
+ * the ranges are code points, and a code unit cannot be checked against them. */
102
+ function clearOf(ranges: readonly CharRange[], input: string, from: number, to: number): boolean {
103
+ for (let i = from; i < to; i++) {
104
+ const c = input.charCodeAt(i)
105
+ if (c >= 0xD800 && c <= 0xDFFF) return false
106
+ for (const r of ranges) if (c >= r.lo && c <= r.hi) return false
107
+ }
108
+ return true
109
+ }
package/src/types.ts CHANGED
@@ -398,6 +398,12 @@ export type ParseContext = {
398
398
  * selected markers into the document-root capture.
399
399
  */
400
400
  _rootTriviaCapture?: boolean | undefined
401
+ /**
402
+ * Framework-internal: the last deferred-commit trivia scan in this parse that
403
+ * recorded nothing (`table/trivia-gap.ts`). Held on the context so it is freed
404
+ * with the parse.
405
+ */
406
+ _gap?: TriviaGapMemo | undefined
401
407
  /**
402
408
  * Framework-internal: flat per-node trivia log for CST capture mode.
403
409
  * When set alongside _cstRawChildren, each trivia entry is recorded as three
@@ -606,3 +612,15 @@ export type CompiledParser<T> = {
606
612
  /** Present only when compiled with `{ coverage: true }`. */
607
613
  coverageDefinitions?: readonly import('./compiler/grammar-coverage-ids.ts').GrammarCoverageDefinition[]
608
614
  }
615
+
616
+ /** A trivia gap scanned from `at` to `end` that recorded nothing, and the key it holds for. */
617
+ export interface TriviaGapMemo {
618
+ at: number
619
+ end: number
620
+ input: string
621
+ of: Combinator<unknown> | undefined
622
+ root: boolean | undefined
623
+ rootKinds: Readonly<Record<string, number>> | undefined
624
+ log: number[] | undefined
625
+ state: unknown
626
+ }
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.56.0'
16
+ export const PARSEMAN_VERSION = '0.57.0'