parseman 0.52.0 → 0.53.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 (62) hide show
  1. package/dist/analysis/diagnostics.js +358 -8
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +5929 -6172
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/parser.d.ts +34 -0
  6. package/dist/combinators/parser.d.ts.map +1 -1
  7. package/dist/combinators/ref.d.ts +0 -13
  8. package/dist/combinators/ref.d.ts.map +1 -1
  9. package/dist/compiler/borrowed-rules.d.ts +30 -0
  10. package/dist/compiler/borrowed-rules.d.ts.map +1 -0
  11. package/dist/compiler/compile-linkable-table.d.ts +1 -1
  12. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  13. package/dist/compiler/ir-serialize.d.ts +10 -4
  14. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  15. package/dist/compiler/linker.d.ts +48 -91
  16. package/dist/compiler/linker.d.ts.map +1 -1
  17. package/dist/coverage.d.ts +1 -1
  18. package/dist/coverage.d.ts.map +1 -1
  19. package/dist/cst/reflection.d.ts.map +1 -1
  20. package/dist/index.js +416 -704
  21. package/dist/index.js.map +3 -3
  22. package/dist/language-service/index.js.map +1 -1
  23. package/dist/plugin/evaluator.d.ts +38 -2
  24. package/dist/plugin/evaluator.d.ts.map +1 -1
  25. package/dist/plugin/index.d.ts.map +1 -1
  26. package/dist/plugin/index.js +1073 -523
  27. package/dist/plugin/index.js.map +3 -3
  28. package/dist/plugin/ir-eval.d.ts +21 -0
  29. package/dist/plugin/ir-eval.d.ts.map +1 -0
  30. package/dist/plugin/reducer-resolver.d.ts +6 -0
  31. package/dist/plugin/reducer-resolver.d.ts.map +1 -1
  32. package/dist/spec/index.js +555 -6109
  33. package/dist/spec/index.js.map +3 -3
  34. package/dist/spec/railroad-lib.d.ts +7 -2
  35. package/dist/spec/railroad-lib.d.ts.map +1 -1
  36. package/dist/spec/railroad.d.ts.map +1 -1
  37. package/dist/table/compile-rule-map.d.ts +11 -4
  38. package/dist/table/compile-rule-map.d.ts.map +1 -1
  39. package/dist/table/compile.d.ts +16 -0
  40. package/dist/table/compile.d.ts.map +1 -1
  41. package/dist/table/index.js +235 -87
  42. package/dist/table/index.js.map +2 -2
  43. package/dist/version.d.ts +1 -1
  44. package/package.json +1 -1
  45. package/src/combinators/parser.ts +133 -67
  46. package/src/combinators/ref.ts +10 -1
  47. package/src/compiler/borrowed-rules.ts +88 -0
  48. package/src/compiler/compile-linkable-table.ts +1 -1
  49. package/src/compiler/grammar-coverage-ids.ts +35 -6
  50. package/src/compiler/ir-serialize.ts +12 -118
  51. package/src/compiler/linker.ts +190 -666
  52. package/src/coverage.ts +6 -4
  53. package/src/cst/reflection.ts +6 -1
  54. package/src/plugin/evaluator.ts +113 -13
  55. package/src/plugin/index.ts +588 -110
  56. package/src/plugin/ir-eval.ts +165 -0
  57. package/src/plugin/reducer-resolver.ts +20 -3
  58. package/src/spec/railroad-lib.ts +18 -8
  59. package/src/spec/railroad.ts +3 -4
  60. package/src/table/compile-rule-map.ts +18 -8
  61. package/src/table/compile.ts +61 -0
  62. package/src/version.ts +1 -1
package/src/coverage.ts CHANGED
@@ -18,7 +18,7 @@ import { scanTo } from './combinators/scanTo.ts'
18
18
  import { sequence } from './combinators/sequence.ts'
19
19
  import { token, leaf, sourceLeaf } from './combinators/token.ts'
20
20
  import { withCtx } from './combinators/withCtx.ts'
21
- import { composedCoverageRules } from './compiler/linker.ts'
21
+ import { composedCoverageRules, linkedWinnersOf } from './compiler/linker.ts'
22
22
  import { buildGrammarPlan, type GrammarCoverageDefinition, type GrammarCoveragePlan } from './compiler/grammar-coverage-ids.ts'
23
23
 
24
24
  export type { GrammarCoverageDefinition } from './compiler/grammar-coverage-ids.ts'
@@ -156,11 +156,11 @@ export function grammarCoverageDefinitions(entry: Combinator<unknown>, winners?:
156
156
  }
157
157
 
158
158
  /** Definitions for a runtime `compose()` result, normalized through its final
159
- * IR winner map. Opaque precompiled pieces intentionally fail rather than
159
+ * winner map. Anything else — a compiled table, a plain map — fails rather than
160
160
  * reporting source-piece identities as though they were final grammar IDs. */
161
161
  export function composedGrammarCoverageDefinitions(grammar: Record<string, unknown>, startRule: string): readonly GrammarCoverageDefinition[] {
162
162
  const winners = composedCoverageRules(grammar)
163
- if (!winners) throw new TypeError('semantic coverage needs re-lowerable composed IR; this composition contains an opaque artifact')
163
+ if (!winners) throw new TypeError('semantic coverage needs a runtime compose() result (a linked interpreter grammar); this is not one')
164
164
  const entry = winners[startRule]
165
165
  if (!entry) throw new TypeError(`semantic coverage start rule ${JSON.stringify(startRule)} is not a final winner`)
166
166
  return grammarCoverageDefinitions(entry, winners)
@@ -380,7 +380,9 @@ function coverageEntry(entry: Combinator<unknown>, collector: GrammarCoverageCol
380
380
 
381
381
  export function runWithGrammarCoverage(entry: Runnable, input: string, options: RunOptions & { collector?: GrammarCoverageCollector; trace?: GrammarTraceSink } = {}): { result: RunResult; coverage: GrammarCoverageSnapshot } {
382
382
  if (typeof entry === 'function') throw new TypeError('runWithGrammarCoverage currently requires an interpreter combinator entry')
383
- const plan = buildGrammarPlan(entry)
383
+ // A runtime composition's rule is planned through its winner map, so its IDs are
384
+ // the ones `composedGrammarCoverageDefinitions` reports.
385
+ const plan = buildGrammarPlan(entry, linkedWinnersOf(entry))
384
386
  const definitions = plan.definitions
385
387
  const collector = options.collector ?? createGrammarCoverageCollector(definitions)
386
388
  const { collector: _collector, trace, ...runOptions } = options
@@ -115,6 +115,11 @@ export function collectGrammarReflection(
115
115
  ): GrammarReflection {
116
116
  const byType = new Map<string, string[]>()
117
117
  const followLazy = opts?.followLazy !== false
118
+ // ONE walk over the whole map. A node already visited from an earlier rule had
119
+ // its whole subgraph visited then, and `addNode` is idempotent, so a fresh set
120
+ // per rule only re-walked shared structure — O(rules × graph), and every unbound
121
+ // cross-piece hole threw (a stack-capturing Error) once per rule reaching it.
122
+ const seen = new Set<Combinator<unknown>>()
118
123
  for (const [, rule] of ruleMap) {
119
124
  if (!followLazy && rule._def.tag === 'lazy') {
120
125
  try {
@@ -123,7 +128,7 @@ export function collectGrammarReflection(
123
128
  } catch {}
124
129
  continue
125
130
  }
126
- collectFromCombinator(rule, byType, new Set(), followLazy)
131
+ collectFromCombinator(rule, byType, seen, followLazy)
127
132
  }
128
133
  return { nodes: [...byType].map(([type, tags]) => ({ type, tags })) }
129
134
  }
@@ -154,6 +154,69 @@ export type ScopeEntry = {
154
154
  }
155
155
  export type Scope = Map<string, ScopeEntry>
156
156
 
157
+ /** How a {@link ModuleScope} answers a name it does not hold: a value, the reason a
158
+ * binding the module DOES have cannot be known at build time, or `null` — not a
159
+ * binding this scope can speak for (an ordinary miss). */
160
+ export type FreeNameResolution = { value: unknown } | { unresolved: string } | null
161
+
162
+ /**
163
+ * A module's scope whose IMPORTED (and, for a foreign module, top-level) bindings
164
+ * resolve on first read instead of being absent.
165
+ *
166
+ * Absent was the defect. `anyValue` answered an imported name with `null`, so
167
+ * `rules({ scanSkip: [importedUnit] })` evaluated to nothing, the option quietly
168
+ * became `undefined`, and the build succeeded with no ambient skip — and a terminal
169
+ * imported into a rule body could not be lowered at all. Lazy, because a grammar
170
+ * module imports far more than combinators (AST classes, helpers) and resolving each
171
+ * one eagerly would read every one of those modules for nothing.
172
+ *
173
+ * A binding that exists but cannot be resolved is RECORDED in `unresolved` (shared by
174
+ * every scope of one transform) as "`name` <reason>", so the plugin can fail the build
175
+ * naming it rather than warn about an anonymous "isn't statically evaluable". Only a
176
+ * VALUE read records: `has()` is how the evaluator probes a name it may legitimately
177
+ * leave to runtime (a named reducer), so a probe that misses is not a failure.
178
+ */
179
+ export class ModuleScope extends Map<string, unknown> {
180
+ private readonly tried = new Map<string, string | undefined>()
181
+ private readonly resolveFree: (name: string) => FreeNameResolution
182
+ readonly unresolved: Set<string>
183
+ constructor(resolveFree: (name: string) => FreeNameResolution, unresolved: Set<string>) {
184
+ super()
185
+ this.resolveFree = resolveFree
186
+ this.unresolved = unresolved
187
+ }
188
+
189
+ override has(name: string): boolean {
190
+ return super.has(name) || this.resolve(name)
191
+ }
192
+
193
+ override get(name: string): unknown {
194
+ if (super.has(name) || this.resolve(name)) return super.get(name)
195
+ const why = this.tried.get(name)
196
+ if (why !== undefined) this.unresolved.add(`\`${name}\` ${why}`)
197
+ return undefined
198
+ }
199
+
200
+ /** A child scope (a factory body) that keeps resolving this module's free names. */
201
+ fork(): ModuleScope {
202
+ const child = new ModuleScope(this.resolveFree, this.unresolved)
203
+ for (const [k, v] of super.entries()) child.set(k, v)
204
+ return child
205
+ }
206
+
207
+ private resolve(name: string): boolean {
208
+ if (this.tried.has(name)) return false
209
+ const r = this.resolveFree(name)
210
+ if (r !== null && 'value' in r) {
211
+ super.set(name, r.value)
212
+ return true
213
+ }
214
+ // A name nothing binds YET is not remembered: a later declaration may bind it.
215
+ if (r !== null) this.tried.set(name, r.unresolved)
216
+ return false
217
+ }
218
+ }
219
+
157
220
  // Internal XScope also holds non-Combinator values (g proxy objects etc.)
158
221
  type XScopeVal = ScopeEntry | unknown
159
222
  type XScope = Map<string, XScopeVal>
@@ -556,6 +619,20 @@ function staticNodeOptions(expr: Expression, scope: XScope): StaticNodeOptions {
556
619
  return opts.unwrap || opts.collapse || opts.project !== undefined || opts.captureTrivia || opts.trailingTrivia || opts.tags !== undefined || opts.buildArity !== undefined ? opts : undefined
557
620
  }
558
621
 
622
+ /**
623
+ * `scanTo`/`balanced` options: `undefined` when absent, `UNKNOWN_OPTIONS` when present
624
+ * but not statically known. Passing the unknown value on as `null` is what built a
625
+ * region with NO skip units out of `balanced('(', ')', importedOpts)` — the options
626
+ * silently vanished.
627
+ */
628
+ const UNKNOWN_OPTIONS = Symbol('parseman.unknownOptions')
629
+ function scanOptions(optsArg: Expression | { type: 'SpreadElement' } | undefined, scope: XScope, code?: string): unknown {
630
+ if (optsArg === undefined) return undefined
631
+ if (optsArg.type === 'SpreadElement') return UNKNOWN_OPTIONS
632
+ const opts = anyValue(optsArg as Expression, scope, code, [])
633
+ return isHole(optsArg as Expression, opts, scope) ? UNKNOWN_OPTIONS : opts
634
+ }
635
+
559
636
  /**
560
637
  * Evaluate a call expression to a Combinator.
561
638
  * `mfs` accumulates mapFn source texts in depth-first order — must match
@@ -893,9 +970,8 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
893
970
  const open = anyValue(openArg as Expression, scope, code, [])
894
971
  const close = anyValue(closeArg as Expression, scope, code, [])
895
972
  if (typeof open !== 'string' || typeof close !== 'string') return null
896
- const opts = optsArg && optsArg.type !== 'SpreadElement'
897
- ? anyValue(optsArg as Expression, scope, code, [])
898
- : undefined
973
+ const opts = scanOptions(optsArg, scope, code)
974
+ if (opts === UNKNOWN_OPTIONS) return null
899
975
  try { return parseman.balanced(open, close, opts as parseman.ScanToOptions | undefined) } catch { return null }
900
976
  }
901
977
 
@@ -906,9 +982,8 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
906
982
  if (!sentinelArg || sentinelArg.type === 'SpreadElement') return null
907
983
  const sentinel = anyValue(sentinelArg as Expression, scope, code, [])
908
984
  if (!isCombinator(sentinel)) return null
909
- const opts = optsArg && optsArg.type !== 'SpreadElement'
910
- ? anyValue(optsArg as Expression, scope, code, [])
911
- : undefined
985
+ const opts = scanOptions(optsArg, scope, code)
986
+ if (opts === UNKNOWN_OPTIONS) return null
912
987
  try { return parseman.scanTo(sentinel, opts as parseman.ScanToOptions | undefined) } catch { return null }
913
988
  }
914
989
 
@@ -1039,6 +1114,22 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
1039
1114
  } catch { return null }
1040
1115
  }
1041
1116
 
1117
+ /**
1118
+ * An element that evaluated to `null` without BEING `null` — an unresolved name, a
1119
+ * call the evaluator could not build. Kept as a `null` slot, it made the container
1120
+ * look evaluated: `{ skip: [unknownUnit] }` became `{ skip: [null] }`, and an options
1121
+ * object went missing piece by piece with no warning. So a hole fails the whole
1122
+ * container. A function literal is exempt: it evaluates to `null` by design, its
1123
+ * source being captured by the caller that needs it.
1124
+ */
1125
+ function isHole(node: Expression, value: unknown, scope: XScope): boolean {
1126
+ if (value !== null) return false
1127
+ const n = unwrapStaticExpr(node) as { type: string; value?: unknown; name?: string }
1128
+ if (n.type === 'Identifier') return !scope.has(n.name!) // a bound nullish const is a value
1129
+ return !(n.type === 'Literal' && n.value === null)
1130
+ && n.type !== 'ArrowFunctionExpression' && n.type !== 'FunctionExpression'
1131
+ }
1132
+
1042
1133
  /** Evaluate any expression to its JS value (not necessarily a Combinator). */
1043
1134
  function anyValue(node: Expression, scope: XScope, code?: string, mfs?: string[]): unknown {
1044
1135
  if (node.type === 'TSAsExpression'
@@ -1064,7 +1155,9 @@ function anyValue(node: Expression, scope: XScope, code?: string, mfs?: string[]
1064
1155
  for (const el of arr.elements) {
1065
1156
  if (el === null) { out.push(null); continue }
1066
1157
  if ((el as { type: string }).type === 'SpreadElement') return null
1067
- out.push(anyValue(el as Expression, scope, code, mfs))
1158
+ const v = anyValue(el as Expression, scope, code, mfs)
1159
+ if (isHole(el as Expression, v, scope)) return null
1160
+ out.push(v)
1068
1161
  }
1069
1162
  return out
1070
1163
  }
@@ -1074,7 +1167,10 @@ function anyValue(node: Expression, scope: XScope, code?: string, mfs?: string[]
1074
1167
  for (const prop of node.properties) {
1075
1168
  const key = propName(prop as never)
1076
1169
  if (key === null) return null
1077
- obj[key] = anyValue((prop as unknown as ObjectProperty).value as Expression, scope, code, mfs)
1170
+ const valueNode = (prop as unknown as ObjectProperty).value as Expression
1171
+ const v = anyValue(valueNode, scope, code, mfs)
1172
+ if (isHole(valueNode, v, scope)) return null
1173
+ obj[key] = v
1078
1174
  }
1079
1175
  return obj
1080
1176
  }
@@ -1184,15 +1280,16 @@ export function evaluateExpr(
1184
1280
  * Evaluate a `const X = [combinator, …]` array literal into an array of
1185
1281
  * Combinators. Lets a shared option array (e.g. a `skip` set reused across
1186
1282
  * `scanTo`/`balanced` calls) be referenced by name — `{ skip: X }` — instead of
1187
- * inlining the array at every call site. Returns null when `node` isn't an array
1188
- * literal of statically-resolvable combinators.
1283
+ * inlining the array at every call site. Returns null when `node` doesn't evaluate
1284
+ * to an array of statically-resolvable combinators.
1189
1285
  */
1190
1286
  export function evaluateCombinatorArray(
1191
1287
  node: Expression,
1192
1288
  scope: Scope,
1193
1289
  code?: string,
1194
1290
  ): Combinator<unknown>[] | null {
1195
- if (node.type !== 'ArrayExpression') return null
1291
+ // Any expression, not just an array literal: `rules({ scanSkip: sharedSkip })`
1292
+ // names the array, and requiring a literal here is what dropped it.
1196
1293
  const val = anyValue(node, scope as XScope, code, [])
1197
1294
  if (!Array.isArray(val) || val.length === 0) return null
1198
1295
  if (!val.every(isCombinator)) return null
@@ -1453,7 +1550,7 @@ export function evaluateParserFactory(
1453
1550
  built = rules(options ?? {}, (g: Record<string, Combinator<unknown>>) => {
1454
1551
  // Outer ScopeEntry values carry their mfSrcs and are replayed by scopeGet()
1455
1552
  // when body statements or return expressions reference them.
1456
- const localScope: XScope = new Map(scope as XScope)
1553
+ const localScope: XScope = scope instanceof ModuleScope ? scope.fork() : new Map(scope as XScope)
1457
1554
  localScope.set(proxyName, g)
1458
1555
 
1459
1556
  // ── Phase 1: the factory's own body statements ────────────────────────
@@ -1564,7 +1661,10 @@ export function applyDefineStatement(
1564
1661
  /** Check if an AST node references any name from the given scope or names set. */
1565
1662
  export function referencesAny(node: Node, names: Set<string>, scope: Scope): boolean {
1566
1663
  if (node.type === 'Identifier') {
1567
- return names.has(node.name) || scope.has(node.name)
1664
+ // The names the module has BOUND so far, not every import it could resolve: this
1665
+ // decides whether a declaration is macro code at all, and an ordinary
1666
+ // `const x = importedThing` must not start reading modules to find out.
1667
+ return names.has(node.name) || Map.prototype.has.call(scope, node.name)
1568
1668
  }
1569
1669
  for (const key of Object.keys(node) as (keyof typeof node)[]) {
1570
1670
  const child = node[key]