parseman 0.59.0 → 0.59.1

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 (61) hide show
  1. package/dist/analysis/diagnostics.js +19 -45
  2. package/dist/analysis/diagnostics.js.map +1 -1
  3. package/dist/cli/index.js +19 -45
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/combinators/choice.d.ts +0 -5
  6. package/dist/combinators/choice.d.ts.map +1 -1
  7. package/dist/combinators/scanTo.d.ts +1 -16
  8. package/dist/combinators/scanTo.d.ts.map +1 -1
  9. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  10. package/dist/compiler/linker.d.ts +0 -3
  11. package/dist/compiler/linker.d.ts.map +1 -1
  12. package/dist/compiler/token-alphabet.d.ts +5 -25
  13. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  14. package/dist/compiler/token-capability.js.map +1 -1
  15. package/dist/compiler/token-dispatch.d.ts +8 -79
  16. package/dist/compiler/token-dispatch.d.ts.map +1 -1
  17. package/dist/cst/root-trivia-scope.d.ts +0 -3
  18. package/dist/cst/root-trivia-scope.d.ts.map +1 -1
  19. package/dist/cst/trivia-entries.d.ts +0 -2
  20. package/dist/cst/trivia-entries.d.ts.map +1 -1
  21. package/dist/cst/trivia-kinds.d.ts +0 -5
  22. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  23. package/dist/index.js +20 -46
  24. package/dist/index.js.map +1 -1
  25. package/dist/language-service/index.js.map +1 -1
  26. package/dist/plugin/index.js +20 -46
  27. package/dist/plugin/index.js.map +1 -1
  28. package/dist/run/index.js.map +1 -1
  29. package/dist/spec/index.js.map +1 -1
  30. package/dist/spec/model.d.ts +0 -8
  31. package/dist/spec/model.d.ts.map +1 -1
  32. package/dist/table/encode.d.ts.map +1 -1
  33. package/dist/table/index.js +19 -45
  34. package/dist/table/index.js.map +1 -1
  35. package/dist/table/program.d.ts +0 -1
  36. package/dist/table/program.d.ts.map +1 -1
  37. package/dist/table/scalar-terminal.d.ts +0 -7
  38. package/dist/table/scalar-terminal.d.ts.map +1 -1
  39. package/dist/table/scan-shapes.d.ts +0 -8
  40. package/dist/table/scan-shapes.d.ts.map +1 -1
  41. package/dist/table/site-labels.d.ts +0 -2
  42. package/dist/table/site-labels.d.ts.map +1 -1
  43. package/dist/version.d.ts +1 -1
  44. package/package.json +1 -1
  45. package/src/combinators/choice.ts +1 -1
  46. package/src/combinators/scanTo.ts +1 -1
  47. package/src/compiler/ir-serialize.ts +2 -1
  48. package/src/compiler/linker.ts +1 -1
  49. package/src/compiler/token-alphabet.ts +14 -10
  50. package/src/compiler/token-dispatch.ts +10 -313
  51. package/src/compiler/token-scanner.ts +2 -2
  52. package/src/cst/root-trivia-scope.ts +1 -1
  53. package/src/cst/trivia-entries.ts +1 -1
  54. package/src/cst/trivia-kinds.ts +1 -1
  55. package/src/spec/model.ts +1 -1
  56. package/src/table/encode.ts +16 -39
  57. package/src/table/program.ts +1 -1
  58. package/src/table/scalar-terminal.ts +1 -1
  59. package/src/table/scan-shapes.ts +15 -46
  60. package/src/table/site-labels.ts +1 -1
  61. package/src/version.ts +1 -1
@@ -35,7 +35,7 @@
35
35
  import type { Alphabet } from './token-alphabet.ts'
36
36
  import { TOK_EOF, TOK_UNKNOWN, TOK_WS } from './token-alphabet.ts'
37
37
  import { firstSetFromRegex } from '../regex/first-set.ts'
38
- import { packInts, sharedHelperDecl } from './token-dispatch.ts'
38
+ import { packInts, unpackHelperDecl } from './token-dispatch.ts'
39
39
  import { foldCode, foldExpr } from './token-dispatch.ts'
40
40
 
41
41
  /**
@@ -193,7 +193,7 @@ export function emitScanner(alphabet: Alphabet, ns: string): ScannerEmission {
193
193
  // two halves of one encoding still lived in two files. They agree today — the
194
194
  // point is that widening the encoding (which the `RangeError` tells you to do)
195
195
  // must not be able to change one half and miss the other.
196
- decls.push(sharedHelperDecl('unpack', () => p('_tkUnpack')))
196
+ decls.push(unpackHelperDecl(p('_tkUnpack')))
197
197
  decls.push(`const ${p('_tkNext_')} = ${p('_tkUnpack')}(${packInts(NEXT)})`)
198
198
  decls.push(`const ${p('_tkStart')} = ${p('_tkUnpack')}(${packInts(START)})`)
199
199
  decls.push(`const ${p('_tkAccept')} = ${p('_tkUnpack')}(${packInts(ACCEPT)})`)
@@ -14,7 +14,7 @@
14
14
 
15
15
  /** The message `parser()` raises for an unclassified local scope. Exported so the
16
16
  * three engines cannot drift a word apart. */
17
- export const ROOT_TRIVIA_SCOPE_REFUSAL
17
+ const ROOT_TRIVIA_SCOPE_REFUSAL
18
18
  = 'parser(): selected root trivia requires classifiedTrivia() for every local trivia scope, or rootCapture: \'opaque\'.'
19
19
 
20
20
  /**
@@ -61,7 +61,7 @@ export type RootTriviaIndex = {
61
61
  const EMPTY_INDICES: readonly number[] = Object.freeze([])
62
62
  const EMPTY_GAPS: readonly RootTriviaGap[] = Object.freeze([])
63
63
  /** Number of packed fields in one selected-root trivia row. */
64
- export const SELECTED_ROOT_STRIDE = 5
64
+ const SELECTED_ROOT_STRIDE = 5
65
65
 
66
66
  type RootTriviaMaps = {
67
67
  before: Map<number, number[]>
@@ -60,7 +60,7 @@ function unwrapTrivia(p: Combinator<unknown>): Combinator<unknown> {
60
60
  }
61
61
 
62
62
  /** Strip `label()` wrapper; returns inner parser for matching. */
63
- export function peelLabel(p: Combinator<unknown>): { label: string; parser: Combinator<unknown> } | null {
63
+ function peelLabel(p: Combinator<unknown>): { label: string; parser: Combinator<unknown> } | null {
64
64
  if (p._def.tag === 'label') return { label: p._def.label, parser: p._def.parser }
65
65
  return null
66
66
  }
package/src/spec/model.ts CHANGED
@@ -247,7 +247,7 @@ function fixedString(alt: string): string | undefined {
247
247
  * boundary (`word()` / `keywords()` and every hand-written copy of them), and a
248
248
  * `\b…\b`-anchored word.
249
249
  */
250
- export function fixedStringsOfRegex(source: string): string[] | undefined {
250
+ function fixedStringsOfRegex(source: string): string[] | undefined {
251
251
  let src = source
252
252
  // `\b` anchors assert a boundary, exactly like the `(?![…])` guard.
253
253
  if (src.startsWith('\\b')) src = src.slice(2)
@@ -517,18 +517,10 @@ class Encoder {
517
517
  /** TOKEN bodies are an all-program decision. A complete occurrence inside an
518
518
  * otherwise incomplete final graph stays on CHARACTER with every sibling. */
519
519
  readonly selectLexicalBodies: boolean
520
- /**
521
- * Decides the extra operands laid down below. ALWAYS TRUE — see
522
- * `TableSettings.recovery`. Kept as a field rather than folded away because it
523
- * is what every emit site reads, and the emit sites are where the shape of the
524
- * row is stated.
525
- */
526
- readonly rec: boolean
527
520
  constructor(settings: TableSettings, selectLexicalBodies: boolean) {
528
521
  this.settings = settings
529
522
  this.track = settings.trackLines === true
530
523
  this.selectLexicalBodies = selectLexicalBodies
531
- this.rec = true
532
524
  this.plan = settings.coverage
533
525
  this.cov = settings.coverage === undefined
534
526
  ? undefined
@@ -1147,16 +1139,12 @@ class Encoder {
1147
1139
  }
1148
1140
  case 'sequence': {
1149
1141
  const kids = d.parsers.map(c => this.node(c).ip)
1150
- // A RECOVERY table appends one follow-set class per term AFTER the child
1151
- // slots. Appending keeps every existing read (`ip + 2 + i`) and the
1152
- // `collapseIndirection` slot map exactly as they were, so a strict table
1153
- // is word-for-word the table it always was.
1154
- const head = this.emitHead(d.valueUnused ? OP_SEQV : OP_SEQ, 1 + kids.length + (this.rec ? kids.length : 0))
1142
+ // One follow-set class per term (recovery's resync sentinel) follows the
1143
+ // child slots, so every child read stays `ip + 2 + i`.
1144
+ const head = this.emitHead(d.valueUnused ? OP_SEQV : OP_SEQ, 1 + 2 * kids.length)
1155
1145
  this.code[head + 1] = kids.length
1156
1146
  for (let i = 0; i < kids.length; i++) this.code[head + 2 + i] = kids[i]!
1157
- if (this.rec) {
1158
- for (let i = 0; i < kids.length; i++) this.code[head + 2 + kids.length + i] = this.followClass(d.parsers, i)
1159
- }
1147
+ for (let i = 0; i < kids.length; i++) this.code[head + 2 + kids.length + i] = this.followClass(d.parsers, i)
1160
1148
  return head
1161
1149
  }
1162
1150
  case 'choice': {
@@ -1369,14 +1357,10 @@ class Encoder {
1369
1357
  : this.charClass(firstSetOf(d.parser, new Set(), rr, this.activeTrivia))
1370
1358
  const op = d.valueUnused ? OP_REPV : OP_REP
1371
1359
  const min = d.tag === 'many' ? 0 : d.min
1372
- // ip + 6 is the ITEM's expected set and ip + 7 the separator's sentinel
1373
- // class (−1: there is no separator). Both are only laid down for a
1374
- // recovery table, where the resync error's payload has to be the ITEM's
1375
- // set — `deriveExpected(combinator)` in repeat.ts:170, and
1376
- // `deriveExpectedArr([def.parser])` in codegen's emitMany.
1377
- return this.rec
1378
- ? this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), itemClass)
1379
- : this.emit(op, child, min, d.max ?? -1, -1, 0)
1360
+ // ip + 6 is the ITEM's expected set and ip + 7 the item's sentinel class:
1361
+ // the resync error's payload is the ITEM's set (`deriveExpected` in
1362
+ // `combinators/repeat.ts`).
1363
+ return this.emit(op, child, min, d.max ?? -1, -1, 0, this.expected(deriveExpected(d.parser)), itemClass)
1380
1364
  }
1381
1365
  case 'sepBy': {
1382
1366
  const child = this.node(d.parser).ip
@@ -1393,20 +1377,15 @@ class Encoder {
1393
1377
  // it means the FIRST item failed, and that failure already set the
1394
1378
  // item's own set. So no committed grammar grows by a word.
1395
1379
  const flags = (d.trailing === 'allow' ? 1 : 0) | (d.keepSeparators === true ? 2 : 0) | (d.min >= 2 ? 4 : 0)
1396
- // A RECOVERY table always carries ip + 6 (the ITEM's expected set, which
1380
+ // Every list row carries ip + 6 (the ITEM's expected set, which
1397
1381
  // the resync error reports) and ip + 7 (the SEPARATOR's sentinel class, so
1398
1382
  // a tolerant list resyncs to its own separator OR the enclosing
1399
1383
  // delimiter — `orSentinel(separator, term)` in repeat.ts:454 and
1400
1384
  // `_rec.or(sepSent, mySync)` in codegen's emitSepBy).
1401
- if (this.rec) {
1402
- return this.emit(
1403
- OP_REP, child, d.min, d.max ?? -1, sep, flags,
1404
- this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver(), this.activeTrivia)),
1405
- )
1406
- }
1407
- return d.min >= 2
1408
- ? this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags, this.expected(deriveExpected(d.parser)))
1409
- : this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
1385
+ return this.emit(
1386
+ OP_REP, child, d.min, d.max ?? -1, sep, flags,
1387
+ this.expected(deriveExpected(d.parser)), this.charClass(firstSetOf(d.separator, new Set(), this.refResolver(), this.activeTrivia)),
1388
+ )
1410
1389
  }
1411
1390
  case 'optional': {
1412
1391
  const head = this.emit(OP_OPT, this.lookaheadGate(d.parser, this.node(d.parser).ip))
@@ -1424,7 +1403,7 @@ class Encoder {
1424
1403
  const inner = d.parser._def as ParserDef
1425
1404
  if (inner.tag === 'sequence' && !this.memo.has(d.parser) && !this.pending.has(d.parser)) {
1426
1405
  const kids = inner.parsers.map(c => this.node(c).ip)
1427
- const head = this.emitHead(OP_SEQX, 2 + kids.length + (this.rec ? kids.length : 0))
1406
+ const head = this.emitHead(OP_SEQX, 2 + 2 * kids.length)
1428
1407
  const projection = directArrayProjection(d.fn, d.fnSrc, kids.length)
1429
1408
  // A negative operand describes an already-parsed child (`~index`),
1430
1409
  // while every function-pool index remains non-negative. The row is
@@ -1435,9 +1414,7 @@ class Encoder {
1435
1414
  : ~projection
1436
1415
  this.code[head + 2] = kids.length
1437
1416
  for (let i = 0; i < kids.length; i++) this.code[head + 3 + i] = kids[i]!
1438
- if (this.rec) {
1439
- for (let i = 0; i < kids.length; i++) this.code[head + 3 + kids.length + i] = this.followClass(inner.parsers, i)
1440
- }
1417
+ for (let i = 0; i < kids.length; i++) this.code[head + 3 + kids.length + i] = this.followClass(inner.parsers, i)
1441
1418
  return head
1442
1419
  }
1443
1420
  const child = this.node(d.parser).ip
@@ -1938,7 +1915,7 @@ class Encoder {
1938
1915
  ...(this.settings.hostMode === undefined ? {} : { hostMode: this.settings.hostMode }),
1939
1916
  ...(this.runtimeOnly.size === 0 ? {} : { runtimeOnly: [...this.runtimeOnly].sort() }),
1940
1917
  ...(this.triviaSpecs.length === 0 ? {} : { triviaSpecs: this.triviaSpecs }),
1941
- ...(this.rec ? { rec: 1 as const } : {}),
1918
+ rec: 1,
1942
1919
  ...(this.cov === undefined ? {} : { cov: this.cov }),
1943
1920
  lines: this.track ? 1 : 0,
1944
1921
  }, undefined, {
@@ -213,7 +213,7 @@ export function covKindCode(kind: 'rule' | 'choice-arm' | 'dispatch-arm' | 'labe
213
213
  }
214
214
  }
215
215
 
216
- export function covKindName(code: 0 | 1 | 2 | 3): 'rule' | 'choice-arm' | 'dispatch-arm' | 'label' {
216
+ function covKindName(code: 0 | 1 | 2 | 3): 'rule' | 'choice-arm' | 'dispatch-arm' | 'label' {
217
217
  switch (code) {
218
218
  case 0: return 'rule'
219
219
  case 1: return 'choice-arm'
@@ -209,7 +209,7 @@ export function leadingScalarSequence(
209
209
  * choices, but only when EVERY arm yields literal authority. Longer literals
210
210
  * covered by a shorter prefix are removed (`@{-}` is already covered by `@{`).
211
211
  */
212
- export function leadingLiteralFamily(
212
+ function leadingLiteralFamily(
213
213
  code: ArrayLike<number>, constants: readonly unknown[], ip: number,
214
214
  minDepth = 2, limit = 4,
215
215
  ): readonly number[] | undefined {
@@ -94,7 +94,7 @@ export type ScanShape =
94
94
  | { kind: 'alt'; arms: ScanShape[]; disjoint: boolean; firsts: Array<CharSet | null> }
95
95
 
96
96
  /** Backslash (`\`) code point — the escape lead char in string shapes. */
97
- export const BACKSLASH = 92
97
+ const BACKSLASH = 92
98
98
 
99
99
  const META = new Set('()[]{}*+?|^$.'.split(''))
100
100
 
@@ -1137,15 +1137,6 @@ function caseFoldLiteralOrAlt(source: string): ScanShape | null {
1137
1137
  return { kind: 'alt', arms: litArms, disjoint: false, firsts: litArms.map(() => null) }
1138
1138
  }
1139
1139
 
1140
- /**
1141
- * `PARSEMAN_SCANTO=indexof` swaps the single-stop-char `until` scan loop for
1142
- * `String.prototype.indexOf`. Defaults OFF so a baseline build is provably
1143
- * byte-identical to the unmodified compiler, which is what makes the A/B a
1144
- * toggle rather than two different compilers (docs/design/derived-tokenization.md
1145
- * §16.3: the gate is a byte-identical parse tree against a toggled baseline).
1146
- */
1147
- const scanToIndexOfEnabled = (): boolean => process.env.PARSEMAN_SCANTO === 'indexof'
1148
-
1149
1140
  export const classCond = (cVar: string, ranges: Array<[number, number]>): string =>
1150
1141
  ranges
1151
1142
  .map(([lo, hi]) => (lo === hi ? `${cVar} === ${lo}` : `(${cVar} >= ${lo} && ${cVar} <= ${hi})`))
@@ -1162,10 +1153,10 @@ export const litCond = (base: string, cps: number[], firstVar?: string): string
1162
1153
  .join(' && ')
1163
1154
 
1164
1155
  /** Line-terminator code points `.` does not match (`\n \r \u2028 \u2029`). */
1165
- export const LINE_TERMINATORS = [10, 13, 8232, 8233] as const
1156
+ const LINE_TERMINATORS = [10, 13, 8232, 8233] as const
1166
1157
 
1167
1158
  /** The body-stop chars that abort a string match (excluded set minus quote/backslash). */
1168
- export function stringHardStop(
1159
+ function stringHardStop(
1169
1160
  shape: Extract<ScanShape, { kind: 'string' }>,
1170
1161
  ): Array<[number, number]> {
1171
1162
  return shape.excluded.filter(
@@ -1467,24 +1458,12 @@ export function emitShapeMatch(
1467
1458
  const openChk = openLen === 1
1468
1459
  ? `${codeAt(start, firstChar)} === ${shape.open[0]}`
1469
1460
  : litCond(start, shape.open, firstChar)
1470
- // A SINGLE stop code point makes this scan a `memchr`, and V8 implements
1471
- // `String.prototype.indexOf` with a SIMD one. Measured 4.3x over the char
1472
- // loop on a 0.97 MiB corpus, 61 of 61 interleaved rounds
1473
- // (scratchpad/native-primitive-bakeoff/micro-scanto.mjs, regime (a)).
1474
- // Semantics are identical: the loop stops at the first stop char or at
1475
- // `input.length`, which is exactly `indexOf`'s hit / not-found pair.
1476
- // Multi-member stop sets stay on the loop — 3x indexOf + min measured at
1477
- // parity (0.992), because the min-reduce rescans the tail and discards it.
1478
- const stopCp =
1479
- shape.stop.length === 1 && shape.stop[0]![0] === shape.stop[0]![1] ? shape.stop[0]![0] : null
1461
+ // A char loop, not `String.prototype.indexOf`. An opt-in `indexOf` variant
1462
+ // (`PARSEMAN_SCANTO=indexof`) won 4.3x in a microbenchmark but was never
1463
+ // claimed as a win on real grammars (CHANGELOG), and nothing enabled it; it
1464
+ // last existed at `4a5d0341`.
1480
1465
  const scanLine =
1481
- stopCp !== null && stopCp <= 0xffff && scanToIndexOfEnabled()
1482
- ? // No temp is minted: a temp would renumber every later variable in the
1483
- // function and show up as an artifact-size delta that is counter
1484
- // renumbering rather than this change (the §9.1.4 / §9.2 effect).
1485
- `${ind} ${j} = input.indexOf(${JSON.stringify(String.fromCharCode(stopCp))}, ${j})` +
1486
- `; if (${j} < 0) ${j} = input.length`
1487
- : `${ind} while (${j} < input.length && !(${classCond(`input.charCodeAt(${j})`, shape.stop)})) ${j}++`
1466
+ `${ind} while (${j} < input.length && !(${classCond(`input.charCodeAt(${j})`, shape.stop)})) ${j}++`
1488
1467
  return {
1489
1468
  setup: [
1490
1469
  `${ind}let ${j} = ${start}`,
@@ -1549,23 +1528,13 @@ export function emitShapeMatch(
1549
1528
  ? `${codeAt(start, firstChar)} === ${shape.open[0]}`
1550
1529
  : litCond(start, shape.open, firstChar)
1551
1530
  const closeChk = litCond(j, shape.close)
1552
- // Scanning to a fixed close literal IS `indexOf`, and V8's is SIMD. The
1553
- // rewrite is exact: the loop stops at the first close literal at or after `j`,
1554
- // or runs out of room and leaves the token unterminated (no match). `indexOf`
1555
- // returns that same first index, or -1 for "never occurs" — mapped to
1556
- // `input.length`, which makes the `j + closeLen <= input.length` guard fail
1557
- // exactly where the loop's room check did. When it IS found the close literal
1558
- // matches by construction, so the guard's re-comparison is dropped.
1559
- const closeStr = String.fromCharCode(...shape.close)
1560
- const scanLines = scanToIndexOfEnabled()
1561
- ? [
1562
- `${ind} ${j} = input.indexOf(${JSON.stringify(closeStr)}, ${j}); if (${j} < 0) ${j} = input.length`,
1563
- `${ind} if (${j} + ${closeLen} <= input.length) ${endV} = ${j} + ${closeLen}`,
1564
- ]
1565
- : [
1566
- `${ind} while (${j} + ${closeLen - 1} < input.length && !(${closeChk})) ${j}++`,
1567
- `${ind} if (${j} + ${closeLen} <= input.length && (${closeChk})) ${endV} = ${j} + ${closeLen}`,
1568
- ]
1531
+ // The loop stops at the first close literal at or after `j`, or runs out of
1532
+ // room and leaves the token unterminated (no match). The same `indexOf`
1533
+ // variant as `until` above was removed with it.
1534
+ const scanLines = [
1535
+ `${ind} while (${j} + ${closeLen - 1} < input.length && !(${closeChk})) ${j}++`,
1536
+ `${ind} if (${j} + ${closeLen} <= input.length && (${closeChk})) ${endV} = ${j} + ${closeLen}`,
1537
+ ]
1569
1538
  return {
1570
1539
  setup: [
1571
1540
  `${ind}let ${j} = ${start}`,
@@ -81,7 +81,7 @@ export const CAP_OFF = 1
81
81
  export const CAP_ON = 2
82
82
 
83
83
  /** Buffer raw-entry mode is not provable at this site. */
84
- export const RAW_UNKNOWN = 0
84
+ const RAW_UNKNOWN = 0
85
85
  /** Buffer retains raw entry values. */
86
86
  export const RAW_CAPTURE = 1
87
87
  /** Buffer retains only a raw source-order count. */
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.59.0'
16
+ export const PARSEMAN_VERSION = '0.59.1'