parseman 0.50.0 → 0.50.3

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 (55) hide show
  1. package/README.md +14 -10
  2. package/dist/analysis/diagnostics.js +455 -226
  3. package/dist/analysis/diagnostics.js.map +3 -3
  4. package/dist/cli/index.js +445 -191
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/choice.d.ts.map +1 -1
  7. package/dist/combinators/grammar.d.ts.map +1 -1
  8. package/dist/combinators/keywords.d.ts.map +1 -1
  9. package/dist/combinators/literal.d.ts.map +1 -1
  10. package/dist/combinators/map.d.ts.map +1 -1
  11. package/dist/combinators/ref.d.ts.map +1 -1
  12. package/dist/combinators/regex.d.ts.map +1 -1
  13. package/dist/combinators/repeat.d.ts.map +1 -1
  14. package/dist/combinators/scalar.d.ts +6 -0
  15. package/dist/combinators/scalar.d.ts.map +1 -0
  16. package/dist/combinators/sequence.d.ts.map +1 -1
  17. package/dist/combinators/trivia-skip.d.ts +2 -0
  18. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  19. package/dist/compiler/token-capability.js +0 -3
  20. package/dist/compiler/token-capability.js.map +2 -2
  21. package/dist/index.js +446 -192
  22. package/dist/index.js.map +3 -3
  23. package/dist/language-service/index.js +68 -172
  24. package/dist/language-service/index.js.map +3 -3
  25. package/dist/parse-context.d.ts.map +1 -1
  26. package/dist/plugin/index.js +491 -306
  27. package/dist/plugin/index.js.map +3 -3
  28. package/dist/run/index.js +1 -0
  29. package/dist/run/index.js.map +1 -1
  30. package/dist/spec/index.js +399 -277
  31. package/dist/spec/index.js.map +3 -3
  32. package/dist/table/emit-assembly.d.ts.map +1 -1
  33. package/dist/table/encode.d.ts.map +1 -1
  34. package/dist/table/index.js +331 -188
  35. package/dist/table/index.js.map +3 -3
  36. package/dist/types.d.ts +8 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/version.d.ts +1 -1
  39. package/package.json +4 -2
  40. package/src/combinators/choice.ts +34 -1
  41. package/src/combinators/grammar.ts +39 -0
  42. package/src/combinators/keywords.ts +14 -3
  43. package/src/combinators/literal.ts +26 -0
  44. package/src/combinators/map.ts +60 -0
  45. package/src/combinators/ref.ts +26 -7
  46. package/src/combinators/regex.ts +23 -0
  47. package/src/combinators/repeat.ts +109 -16
  48. package/src/combinators/scalar.ts +30 -0
  49. package/src/combinators/sequence.ts +76 -161
  50. package/src/combinators/trivia-skip.ts +14 -1
  51. package/src/parse-context.ts +1 -0
  52. package/src/table/emit-assembly.ts +39 -16
  53. package/src/table/encode.ts +30 -10
  54. package/src/types.ts +8 -0
  55. package/src/version.ts +1 -1
@@ -259,6 +259,16 @@ function q(s: string): string {
259
259
  return JSON.stringify(s)
260
260
  }
261
261
 
262
+ /**
263
+ * A code-unit lookup is a complete first-code-point decision only for a class
264
+ * with no non-ASCII range. At EOF `charCodeAt` yields NaN; lone surrogates and
265
+ * the leading surrogate of an astral point are all outside the 128-slot array,
266
+ * so each reads `undefined` and declines exactly as `classHas(lead(...))` does.
267
+ */
268
+ function isAsciiOnlyClass(cls: ResolvedClass | null | undefined): cls is ResolvedClass {
269
+ return cls !== null && cls !== undefined && cls.hi.length === 0
270
+ }
271
+
262
272
  /**
263
273
  * WHICH SIDE SINKS A MARK IN THIS ASSEMBLY HAS TO COVER.
264
274
  *
@@ -1259,12 +1269,15 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1259
1269
 
1260
1270
  case OP_GATE: {
1261
1271
  const child = link(code[ip + 2]!)
1262
- const ci = classes.push([t.cc[code[ip + 1]!]!]) - 1
1272
+ const resolved = t.cc[code[ip + 1]!]!
1273
+ const ci = classes.push([resolved]) - 1
1263
1274
  classPlan.push([code[ip + 1]!])
1264
1275
  const cls = hoist('cl', `CLS[${ci}][0]`)
1265
1276
  const xf = fxRef(code[ip + 3]!)
1266
1277
  return `${head}
1267
- if(!classHas(${cls},lead(input,pos))){ctx._fe=pos;ctx._fx=${xf};return FAIL}
1278
+ if(${staticBuild && isAsciiOnlyClass(resolved)
1279
+ ? `${cls}.ascii[input.charCodeAt(pos)]!==1`
1280
+ : `!classHas(${cls},lead(input,pos))`}){ctx._fe=pos;ctx._fx=${xf};return FAIL}
1268
1281
  return ${child}(input,pos,ctx)
1269
1282
  }`
1270
1283
  }
@@ -1700,10 +1713,11 @@ return v
1700
1713
  // array index feeding ONE call site, which is the megamorphic site
1701
1714
  // this unit exists to remove.
1702
1715
  const armSwitch = arms.map((a, i) => `case ${i}:v=${a}(input,pos,ctx);break`).join('\n')
1716
+ const directAscii = staticBuild && table.hi.length === 0
1703
1717
  return `${head}
1704
- const c=lead(input,pos)
1718
+ const c=${directAscii ? 'input.charCodeAt(pos)' : 'lead(input,pos)'}
1705
1719
  let arm=-1
1706
- if(c>=0&&c<128){const a=${asc}[c];if(a!==0)arm=a-1}
1720
+ if(${directAscii ? 'c<128' : 'c>=0&&c<128'}){const a=${asc}[c];if(a!==0)arm=a-1}
1707
1721
  ${hiArr.length === 0 ? '' : `else if(c>=128){const h=${hoist('hi', `DISP[${di}].hi`)}
1708
1722
  for(let i=0;i<h.length;i+=3){if(c>=h[i]&&c<=h[i+1]){arm=h[i+2];break}}}`}
1709
1723
  if(arm>=0){
@@ -1729,7 +1743,11 @@ return FAIL
1729
1743
  const di = code[ip + 1]!
1730
1744
  const expected = Array.from({ length: n }, (_, i) => fxRef(code[base + n + i]!))
1731
1745
  const gates = Array.from({ length: n }, (_, i) => table.armCls[i] ?? null)
1732
- const gateRefs = Array.from({ length: n }, (_, i) => hoist('g', `DISP[${di}].armCls[${i}]`))
1746
+ const maskable = n <= 32
1747
+ const directAsciiMask = staticBuild && maskable && gates.every(isAsciiOnlyClass)
1748
+ const gateRefs = directAsciiMask
1749
+ ? []
1750
+ : Array.from({ length: n }, (_, i) => hoist('g', `DISP[${di}].armCls[${i}]`))
1733
1751
  type ChoicePretest = {
1734
1752
  readonly scalar?: readonly string[]
1735
1753
  readonly sequence?: ScalarSequenceRef
@@ -1754,7 +1772,6 @@ return FAIL
1754
1772
  const sequence = largeChoiceScalarSequences.get(code[base + i]!)
1755
1773
  return sequence === undefined ? undefined : { sequence: scalarSequenceRef(sequence) }
1756
1774
  })
1757
- const maskable = n <= 32
1758
1775
  const maskName = maskable
1759
1776
  ? hoist('mk', `MASK[${masks.push(maskForClassRow(gates)) - 1}]`)
1760
1777
  : ''
@@ -1856,16 +1873,16 @@ ${merge}
1856
1873
  ctx._fe=pos;ctx._fx=acc??${choiceFx}
1857
1874
  return FAIL`
1858
1875
  return `${head}
1859
- const c=lead(input,pos)
1876
+ const c=${directAsciiMask ? 'input.charCodeAt(pos)' : 'lead(input,pos)'}
1860
1877
  ${hasRollback ? emitMark(p, L.buf, L.raw, sinks) : ''}
1861
1878
  ${deepAt.map((at, i) => `let ${at},${deepFx[i]}`).join('\n')}
1862
1879
  ${maskable ? `if(c<128){
1863
- const bits=${maskName}[c<0?128:c]
1880
+ const bits=${maskName}[${directAsciiMask ? 'c' : 'c<0?128:c'}]
1864
1881
  if(bits===0){ctx._fe=pos;ctx._fx=${choiceFx};return FAIL}
1865
1882
  ${lazyArms(true)}
1866
- }else{
1883
+ }${directAsciiMask ? '' : `else{
1867
1884
  ${lazyArms(false)}
1868
- }
1885
+ }`}
1869
1886
  ` : lazyArms(false)}
1870
1887
  ${finish}
1871
1888
  }`
@@ -1920,7 +1937,7 @@ if(ctx._fc===true){${startFailureExact ? `if(best===pos){acc=${catchName}(${i},0
1920
1937
  ${rollbackFor(i)}
1921
1938
  }${routeMiss}${sequenceMiss}`
1922
1939
  }).join('\n') : ''
1923
- const generalArms = arms.map((arm, i) => {
1940
+ const generalArms = directAsciiMask ? '' : arms.map((arm, i) => {
1924
1941
  const pretest = pretests[i]
1925
1942
  const decision = pretest?.token === undefined && pretest?.sequence === undefined ? '' : tmp()
1926
1943
  const scalarCondition = pretest?.scalar === undefined ? ''
@@ -1963,12 +1980,12 @@ ${rollbackFor(i)}
1963
1980
  // arm can run, so the choice's encoded flat expected set is already
1964
1981
  // the exact result. Do not walk the arm ladder merely to rebuild it.
1965
1982
  return `${head}
1966
- const c=lead(input,pos)
1983
+ const c=${directAsciiMask ? 'input.charCodeAt(pos)' : 'lead(input,pos)'}
1967
1984
  ${hasRollback ? emitMark(p, L.buf, L.raw, sinks) : ''}
1968
1985
  let acc
1969
1986
  let best=pos
1970
1987
  ${maskable ? `if(c<128){
1971
- const bits=${maskName}[c<0?128:c]
1988
+ const bits=${maskName}[${directAsciiMask ? 'c' : 'c<0?128:c'}]
1972
1989
  if(bits===0){ctx._fe=pos;ctx._fx=${choiceFx};return FAIL}
1973
1990
  ${startFailureExact ? '' : 'let prev=0'}
1974
1991
  ${maskArms}
@@ -2076,14 +2093,20 @@ return [key,v]
2076
2093
  const itemFx = reportItem ? fxRef(code[ip + 6]!) : 'EMPTY_FX'
2077
2094
  const collect = op === OP_REP
2078
2095
  let itemCls: string | undefined
2096
+ let itemAscii = false
2079
2097
  if (!REC) {
2080
2098
  const itemClassIndex = sepIp < 0 ? code[ip + 7]! : -1
2081
2099
  if (itemClassIndex >= 0) {
2082
- const ci = classes.push([t.cc[itemClassIndex]!]) - 1
2100
+ const resolved = t.cc[itemClassIndex]!
2101
+ const ci = classes.push([resolved]) - 1
2083
2102
  classPlan.push([itemClassIndex])
2084
2103
  itemCls = hoist('ri', `CLS[${ci}][0]`)
2104
+ itemAscii = staticBuild && isAsciiOnlyClass(resolved)
2085
2105
  }
2086
2106
  }
2107
+ const itemMiss = (at: string): string => itemAscii
2108
+ ? `${itemCls}.ascii[input.charCodeAt(${at})]!==1`
2109
+ : `!classHas(${itemCls},lead(input,${at}))`
2087
2110
  const skipBeforeFirst = sepIp < 0 && min === 0
2088
2111
  const p = tmp()
2089
2112
  // `viaRepItem` is `sep === undefined && count >= min && (count > 0 ||
@@ -2199,7 +2222,7 @@ const out=${collect ? '[]' : 'undefined'}
2199
2222
  ${knownTrivia === undefined ? 'const hasTrivia=ctx.trivia!==undefined\n' : ''}${L.buf ? '' : 'const needMark=_rollbackNeeded(ctx)\n'}${REC ? `const ${my}=ctx._sync\n` : ''}${itemCls === undefined ? '' : `const ${p}gate=ctx._probe===undefined\n`}let cur=pos
2200
2223
  let count=0
2201
2224
  for(;;){
2202
- ${max >= 0 ? `if(count>=${max})break\n` : ''}${sep !== undefined ? `if(count>0&&count>=${min}&&cur>=input.length)break\n` : ''}${itemCls !== undefined && sep === undefined ? `if(count>=${min}&&${p}gate&&${hasTrivia === 'false' ? 'true' : hasTrivia === 'true' ? 'false' : '!hasTrivia'}&&!classHas(${itemCls},lead(input,cur)))break\n` : ''}let ${p}raw=0,${p}tl=0,${p}lv=0,${p}lg=0,${p}rt=0${sinks.fd ? `,${p}fd=0` : ''}${sinks.er ? `,${p}er=0` : ''}
2225
+ ${max >= 0 ? `if(count>=${max})break\n` : ''}${sep !== undefined ? `if(count>0&&count>=${min}&&cur>=input.length)break\n` : ''}${itemCls !== undefined && sep === undefined ? `if(count>=${min}&&${p}gate&&${hasTrivia === 'false' ? 'true' : hasTrivia === 'true' ? 'false' : '!hasTrivia'}&&${itemMiss('cur')})break\n` : ''}let ${p}raw=0,${p}tl=0,${p}lv=0,${p}lg=0,${p}rt=0${sinks.fd ? `,${p}fd=0` : ''}${sinks.er ? `,${p}er=0` : ''}
2203
2226
  ${markBody}
2204
2227
  let itemStart=cur
2205
2228
  let sepEnd=-1
@@ -2221,7 +2244,7 @@ itemStart=${hasTrivia === 'false' ? 'EC.e' : hasTrivia === 'true' ? `${skip}(inp
2221
2244
  ${rb}
2222
2245
  ${trailingAllowed ? 'if(sepEnd>=0)cur=sepEnd\n' : ''}break
2223
2246
  }
2224
- ${itemCls === undefined ? '' : `if(count>=${min}&&${p}gate&&!classHas(${itemCls},lead(input,itemStart))){
2247
+ ${itemCls === undefined ? '' : `if(count>=${min}&&${p}gate&&${itemMiss('itemStart')}){
2225
2248
  ${rb}
2226
2249
  ${trailingAllowed ? 'if(sepEnd>=0)cur=sepEnd\n' : ''}break
2227
2250
  }
@@ -145,6 +145,22 @@ class Encoder {
145
145
  private choiceRollbackMasks = new Map<number, number>()
146
146
  private nonCommittingChoiceSites = new Set<number>()
147
147
  private failureRollbackCleanSites = new Set<number>()
148
+ /**
149
+ * `hasSideWriter` memo for `failureNeedsRollback`, shared across every call
150
+ * for the lifetime of this `Encoder`. A cached entry is written only when
151
+ * `complete` is true, which means the answer is a path-independent fact
152
+ * about that combinator node — it does not depend on which root's traversal
153
+ * asked, only on the node's own reachable subgraph. Scoping the memo per call
154
+ * (as a local inside `failureNeedsRollback`) was correct but discarded that
155
+ * fact after every arm, so a grammar with many ordered-choice arms / optional
156
+ * / attempt sites re-walked the SAME shared rule graph from scratch for each
157
+ * site — O(sites × graph size) instead of O(graph size). Measured on the
158
+ * scss-parser grammar (composed CSS-base superset, the deepest/largest of the
159
+ * four grammars): 47-50s for one `compileRuleMap` with the per-call memo,
160
+ * versus the whole macro transform completing in under a second once this
161
+ * memo is hoisted here.
162
+ */
163
+ private sideWriterMemo = new Map<Combinator<unknown>, boolean>()
148
164
  labels: readonly string[] | undefined = undefined
149
165
  /** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
150
166
  activeTrivia: Combinator<unknown> | undefined = undefined
@@ -633,16 +649,19 @@ class Encoder {
633
649
  const refName = (p as unknown as { _ruleName?: string })._ruleName
634
650
  const winner = refName === undefined ? undefined : this.winners?.[refName]
635
651
  const root = winner !== undefined && !winnerWrapsReference(winner, p) ? winner : p
636
- const sideWriterMemo = new Map<Combinator<unknown>, boolean>()
637
- const hasSideWriter = (
638
- current: Combinator<unknown>,
639
- seen = new Set<Combinator<unknown>>(),
640
- ): { found: boolean; complete: boolean } => {
652
+ const sideWriterMemo = this.sideWriterMemo
653
+ // A single mutable `seen` set threaded through the whole call, rather than
654
+ // copied at every recursion level: this DFS visits children strictly
655
+ // sequentially, so marking `current` in progress before recursing and
656
+ // clearing it once its subtree is fully explored reproduces the exact same
657
+ // ancestor-path membership an immutable copy-per-level would have computed,
658
+ // without the O(depth) allocation on every node.
659
+ const seen = new Set<Combinator<unknown>>()
660
+ const hasSideWriter = (current: Combinator<unknown>): { found: boolean; complete: boolean } => {
641
661
  const cached = sideWriterMemo.get(current)
642
662
  if (cached !== undefined) return { found: cached, complete: true }
643
663
  if (seen.has(current)) return { found: false, complete: false }
644
- const next = new Set(seen)
645
- next.add(current)
664
+ seen.add(current)
646
665
  const def = current._def
647
666
  let result: { found: boolean; complete: boolean }
648
667
  switch (def.tag) {
@@ -657,10 +676,10 @@ class Encoder {
657
676
  const name = (current as unknown as { _ruleName?: string })._ruleName
658
677
  const resolvedWinner = name === undefined ? undefined : this.winners?.[name]
659
678
  if (resolvedWinner !== undefined && !winnerWrapsReference(resolvedWinner, current)) {
660
- result = hasSideWriter(resolvedWinner, next)
679
+ result = hasSideWriter(resolvedWinner)
661
680
  break
662
681
  }
663
- try { result = hasSideWriter(def.thunk(), next) }
682
+ try { result = hasSideWriter(def.thunk()) }
664
683
  catch { result = { found: true, complete: true } }
665
684
  break
666
685
  }
@@ -668,7 +687,7 @@ class Encoder {
668
687
  let complete = true
669
688
  result = { found: false, complete: true }
670
689
  for (const child of childrenOf(def)) {
671
- const childResult = hasSideWriter(child, next)
690
+ const childResult = hasSideWriter(child)
672
691
  if (childResult.found) {
673
692
  result = { found: true, complete: true }
674
693
  break
@@ -678,6 +697,7 @@ class Encoder {
678
697
  if (!result.found) result = { found: false, complete }
679
698
  }
680
699
  }
700
+ seen.delete(current)
681
701
  if (result.found || result.complete) sideWriterMemo.set(current, result.found)
682
702
  return result
683
703
  }
package/src/types.ts CHANGED
@@ -115,6 +115,10 @@ export type Combinator<T> = {
115
115
  readonly _tag: string
116
116
  readonly _meta: ParserMeta
117
117
  readonly _def: ParserDef
118
+ /** Experimental strict interpreter ABI: end >= 0 succeeds, ~failurePos fails. */
119
+ readonly _parseScalar?: ((input: string, pos: number, ctx: ParseContext) => number) | undefined
120
+ /** Construction-owned sticky matcher for exact terminal fusion. */
121
+ readonly _stickyRegex?: RegExp
118
122
  parse(input: string, pos: number, ctx: ParseContext): ParseResult<T>
119
123
  }
120
124
 
@@ -311,6 +315,8 @@ export type ParseContext = {
311
315
  _fx?: string[] | undefined
312
316
  /** Framework-internal compiled-output committed-failure flag. */
313
317
  _fc?: boolean | undefined
318
+ /** Experimental strict interpreter semantic-value register. */
319
+ _sv?: unknown
314
320
  /**
315
321
  * When set by completionsAt(), tracks the highest-position ParseFail seen
316
322
  * during parsing up to _probe.offset. Used to return completions at the cursor
@@ -427,6 +433,8 @@ export type ParserMeta = {
427
433
  isTrivia: boolean
428
434
  /** User-defined labels for labeled trivia arms (`label(name, parser)`). */
429
435
  triviaKindLabels?: readonly string[]
436
+ /** Preclassified lightweight trivia scanner for the ordinary skip path. */
437
+ triviaScanner?: ((input: string, cur: number) => number) | null
430
438
  /** Set only by `classifiedTrivia()`: each root-visible category is a separate
431
439
  * grammar arm rather than an arbitrary label on a broad recognizer. */
432
440
  rootTriviaClassified?: true
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.50.0'
16
+ export const PARSEMAN_VERSION = '0.50.3'