parseman 0.60.4 → 0.60.6

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 (54) hide show
  1. package/dist/analysis/commitment.d.ts.map +1 -1
  2. package/dist/analysis/diagnostics.js +31 -5
  3. package/dist/analysis/diagnostics.js.map +1 -1
  4. package/dist/cli/index.js +639 -613
  5. package/dist/cli/index.js.map +3 -3
  6. package/dist/combinators/parser.d.ts +34 -3
  7. package/dist/combinators/parser.d.ts.map +1 -1
  8. package/dist/combinators/trivia-boundary.d.ts +5 -3
  9. package/dist/combinators/trivia-boundary.d.ts.map +1 -1
  10. package/dist/compiler/compile-linkable-table.d.ts.map +1 -1
  11. package/dist/compiler/linker.d.ts +11 -3
  12. package/dist/compiler/linker.d.ts.map +1 -1
  13. package/dist/compiler/token-capability.js +6 -0
  14. package/dist/compiler/token-capability.js.map +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +93 -25
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugin/evaluator.d.ts +3 -1
  20. package/dist/plugin/evaluator.d.ts.map +1 -1
  21. package/dist/plugin/index.d.ts.map +1 -1
  22. package/dist/plugin/index.js +116 -75
  23. package/dist/plugin/index.js.map +1 -1
  24. package/dist/spec/index.js +3 -1
  25. package/dist/spec/index.js.map +1 -1
  26. package/dist/spec/model.d.ts.map +1 -1
  27. package/dist/table/assemble.d.ts.map +1 -1
  28. package/dist/table/emit-assembly.d.ts.map +1 -1
  29. package/dist/table/encode.d.ts.map +1 -1
  30. package/dist/table/exec.d.ts.map +1 -1
  31. package/dist/table/index.js +31 -5
  32. package/dist/table/index.js.map +1 -1
  33. package/dist/table/ops.d.ts +6 -0
  34. package/dist/table/ops.d.ts.map +1 -1
  35. package/dist/version.d.ts +1 -1
  36. package/package.json +1 -1
  37. package/src/analysis/commitment.ts +3 -1
  38. package/src/combinators/grammar.ts +1 -1
  39. package/src/combinators/parser.ts +82 -9
  40. package/src/combinators/trivia-boundary.ts +11 -3
  41. package/src/compiler/compile-linkable-table.ts +4 -1
  42. package/src/compiler/ir-serialize.ts +10 -4
  43. package/src/compiler/linker.ts +22 -12
  44. package/src/index.ts +2 -1
  45. package/src/plugin/evaluator.ts +16 -5
  46. package/src/plugin/index.ts +27 -104
  47. package/src/spec/model.ts +3 -1
  48. package/src/table/assemble.ts +11 -1
  49. package/src/table/emit-assembly.ts +10 -1
  50. package/src/table/encode.ts +16 -9
  51. package/src/table/exec.ts +11 -9
  52. package/src/table/ops.ts +6 -0
  53. package/src/table/site-labels.ts +3 -3
  54. package/src/version.ts +1 -1
@@ -210,6 +210,12 @@ export declare const OP_TOKEN = 29;
210
210
  * is why this restores the saved value rather than writing `false`.
211
211
  */
212
212
  export declare const OP_SCOPE_CAP = 30;
213
+ /**
214
+ * `k` of a `SCOPE_CAP` row whose `parser({ captureTrivia: true })` names no trivia.
215
+ * It raises capture and leaves the trivia the scope was entered with in force,
216
+ * where `k = -1` clears it (`trivia: null`).
217
+ */
218
+ export declare const SCOPE_INHERIT = -2;
213
219
  /**
214
220
  * `WITHCTX k c` — `withCtx(extra, c)`. `k >= 0` is `extra` in the const pool
215
221
  * (the replace form); `k < 0` is the derive form, `~k` indexing the author's
@@ -1 +1 @@
1
- {"version":3,"file":"ops.d.ts","sourceRoot":"","sources":["../../src/table/ops.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ops.d.ts","sourceRoot":"","sources":["../../src/table/ops.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -13,5 +13,5 @@
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 declare const PARSEMAN_VERSION = "0.60.4";
16
+ export declare const PARSEMAN_VERSION = "0.60.6";
17
17
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parseman",
3
- "version": "0.60.4",
3
+ "version": "0.60.6",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "engines": {
6
6
  "node": "^20.19.0 || >=22.13.0"
@@ -676,7 +676,9 @@ export function classifyRuleMap(
676
676
  let resolved: Combinator<unknown> | undefined
677
677
  try { resolved = def.thunk() } catch { resolved = undefined }
678
678
  if (resolved === undefined) {
679
- if ((p as unknown as { _ruleName?: string })._ruleName) externalRefs.add(p)
679
+ // A `prior.X` placeholder is a hole too: the composition fills it by name.
680
+ const hole = p as unknown as { _ruleName?: string; _priorOf?: string }
681
+ if (hole._ruleName !== undefined || hole._priorOf !== undefined) externalRefs.add(p)
680
682
  return
681
683
  }
682
684
  scanExternal(resolved)
@@ -215,7 +215,7 @@ export function parser<T>(opts: ParserOptions, root: Combinator<T>): ParsemanPar
215
215
  ctx.triviaKindLabels = undefined
216
216
  } else if (scopeTrivia !== undefined) {
217
217
  ctx.trivia = scopeTrivia
218
- if (scopeLabels) ctx.triviaKindLabels = scopeLabels
218
+ ctx.triviaKindLabels = scopeLabels
219
219
  }
220
220
  if (forceCaptureTrivia || _ctx?.captureTrivia) ctx.captureTrivia = true
221
221
  // Kind-filter for per-node capture. Resolve against this scope's trivia
@@ -1,5 +1,7 @@
1
1
  import type { Combinator } from '../types.ts'
2
2
  import type { HostMode } from '../cst/host-mode.ts'
3
+ import type { Runnable } from '../functional/run.ts'
4
+ import { any } from './first-set.ts'
3
5
  import { ref } from './ref.ts'
4
6
  import { markUnusedValues } from '../compiler/value-usage.ts'
5
7
  import { parser as grammarParser } from './grammar.ts'
@@ -28,12 +30,20 @@ function isDefinableRef(v: unknown): v is DefinableRef {
28
30
  && typeof (v as { define?: unknown }).define === 'function'
29
31
  }
30
32
 
33
+ /** The rule name a `prior.X` placeholder stands for (see `linkRules`), or `undefined`. */
34
+ export function priorNameOf(r: Combinator<unknown>): string | undefined {
35
+ return (r as unknown as { _priorOf?: string })._priorOf
36
+ }
37
+
31
38
  function ruleNameOf(r: Combinator<unknown>): string | undefined {
32
39
  return (r as unknown as { _ruleName?: string })._ruleName
33
40
  }
34
41
 
35
42
  function isNamedRuleRefForAnotherRule(r: Combinator<unknown>, key: string): boolean {
36
- const name = r._def.tag === 'lazy' ? ruleNameOf(r) : undefined
43
+ if (r._def.tag !== 'lazy') return false
44
+ // A `prior.X` hole stands for an earlier piece's rule, never for this key.
45
+ if (priorNameOf(r) !== undefined) return true
46
+ const name = ruleNameOf(r)
37
47
  return name !== undefined && name !== key
38
48
  }
39
49
 
@@ -157,13 +167,58 @@ type RuleMapNodeTag<T extends Record<string, Combinator<unknown>>> = {
157
167
  type RulesResult<T extends Record<string, Combinator<unknown>>> =
158
168
  T & GrammarWithReflection<RuleMapNodeType<T>, RuleMapNodeTag<T>>
159
169
 
160
- export function rules<T extends Record<string, Combinator<unknown>>>(factory: (self: any) => T): RulesResult<T>
161
- export function rules<T extends Record<string, Combinator<unknown>>>(options: RulesOptions, factory: (self: any) => T): RulesResult<T>
162
- export function rules<T extends Record<string, Combinator<unknown>>>(
163
- a: ((self: any) => T) | RulesOptions,
164
- b?: (self: any) => T,
170
+ /**
171
+ * The second parameter of a `rules((g, prior) => …)` factory. `prior.X` is the
172
+ * definition of `X` that the grammars composed BEFORE this one gave it, with a
173
+ * top-level `node()` unwrapped to its body, so a composing grammar can re-reduce,
174
+ * wrap or extend an inherited rule without re-spelling it:
175
+ *
176
+ * rules((g, prior: PriorRules<typeof css>) => ({
177
+ * Value: node('Value', prior.Value, reduceValue),
178
+ * }))
179
+ *
180
+ * Naming the base grammar types `prior.X`, and a misspelled rule fails to compile.
181
+ *
182
+ * Its `g.X` references stay bound to the composition (open recursion). A rule's value
183
+ * is `unknown` here, because unwrapping a `node()` changes it. Unnamed, the parameter
184
+ * answers any string, which `noUncheckedIndexedAccess` reads as possibly undefined.
185
+ * See `docs/guide/extending.md`.
186
+ */
187
+ export type PriorRules<Base = Record<string, Combinator<unknown>>> =
188
+ [KnownRuleNames<Base>] extends [never]
189
+ ? { readonly [name: string]: Combinator<unknown> }
190
+ : { readonly [K in KnownRuleNames<Base>]: Combinator<unknown> }
191
+
192
+ /** The rule names a grammar's type spells out: not its index signature, if it has one. */
193
+ export type KnownRuleNames<Base> = keyof {
194
+ [K in keyof Base as string extends K ? never : number extends K ? never : Base[K] extends Combinator<unknown> | Runnable ? K : never]: unknown
195
+ } & string
196
+
197
+ const priorUnfilled = (key: string): Error =>
198
+ new Error(`compose: prior.${key} is read, but no grammar composed before it defines "${key}"`)
199
+
200
+ /** The placeholder for a `prior.X` no earlier piece has defined yet. Composition
201
+ * replaces it; running it, or analysing what it leads to, reports the name. */
202
+ function priorHole(key: string): Combinator<unknown> {
203
+ const unfilled = priorUnfilled(key)
204
+ const fail = (): never => { throw unfilled }
205
+ return {
206
+ _tag: 'lazy',
207
+ _meta: { firstSet: any(), canMatchNewline: true, isTrivia: false },
208
+ _def: { tag: 'lazy', thunk: fail },
209
+ parse: fail,
210
+ _priorOf: key,
211
+ } as Combinator<unknown>
212
+ }
213
+
214
+ export function rules<T extends Record<string, Combinator<unknown>>, P extends object = PriorRules>(factory: (self: any, prior: P) => T): RulesResult<T>
215
+ export function rules<T extends Record<string, Combinator<unknown>>, P extends object = PriorRules>(options: RulesOptions, factory: (self: any, prior: P) => T): RulesResult<T>
216
+ export function rules<T extends Record<string, Combinator<unknown>>, P extends object = PriorRules>(
217
+ a: ((self: any, prior: P) => T) | RulesOptions,
218
+ b?: (self: any, prior: P) => T,
165
219
  ): RulesResult<T> {
166
- const factory = (typeof a === 'function' ? a : b) as (self: any) => T
220
+ // The proxy answers every name, so it satisfies whatever rules `P` names.
221
+ const factory = (typeof a === 'function' ? a : b) as unknown as RulesRecipe['factory']
167
222
  const options = (typeof a === 'function' ? b : a) as RulesOptions | undefined
168
223
  const recipe: RulesRecipe = { factory, options }
169
224
  const map = linkRules([recipe])
@@ -180,7 +235,7 @@ export function rules<T extends Record<string, Combinator<unknown>>>(
180
235
  */
181
236
  export const RULES_RECIPE = Symbol.for('parseman.rulesRecipe')
182
237
  export type RulesRecipe = {
183
- readonly factory: (self: Record<string, Combinator<unknown>>) => Record<string, Combinator<unknown>>
238
+ readonly factory: (self: Record<string, Combinator<unknown>>, prior: PriorRules) => Record<string, Combinator<unknown>>
184
239
  readonly options: RulesOptions | undefined
185
240
  }
186
241
 
@@ -234,8 +289,26 @@ export function linkRules(
234
289
  // before the rule's own parser is built — enabling forward references. A later
235
290
  // recipe's definition of a name WINS; the name keeps its first position.
236
291
  const winners = new Map<string, { parser: Combinator<unknown>; options: RulesOptions | undefined }>()
292
+ // `prior.X`: the winner for X BEFORE the running recipe, read while that recipe's
293
+ // factory runs (its own definitions join `winners` only after it returns). It is
294
+ // the earlier definition itself, inlined into the new rule's graph, so it costs no
295
+ // hop at parse time. A grammar not yet composed has no earlier piece: `prior.X` is
296
+ // then a placeholder the serializer writes back as `prior.X`, and a composition
297
+ // that still has no earlier X refuses it.
298
+ const priorHoles = new Map<string, Combinator<unknown>>()
299
+ const prior = new Proxy<PriorRules>({}, {
300
+ get(_target, key) {
301
+ if (typeof key !== 'string') return undefined
302
+ const earlier = winners.get(key)?.parser
303
+ if (earlier !== undefined) return earlier._def.tag === 'node' ? earlier._def.parser : earlier
304
+ if (link !== undefined) throw priorUnfilled(key)
305
+ let hole = priorHoles.get(key)
306
+ if (hole === undefined) priorHoles.set(key, hole = priorHole(key))
307
+ return hole
308
+ },
309
+ })
237
310
  for (const recipe of recipes) {
238
- const definitions = recipe.factory(proxy)
311
+ const definitions = recipe.factory(proxy, prior)
239
312
  for (const key of Object.keys(definitions)) {
240
313
  winners.set(key, { parser: definitions[key]!, options: recipe.options })
241
314
  }
@@ -20,9 +20,11 @@ const MAX_DEPTH = 12
20
20
  *
21
21
  * `lazy` is deliberately false and is never followed: a reference is the
22
22
  * delegation this question is about, so following it would both loop and answer
23
- * for a different rule. `grammar` is false because it declares its own scope, so
24
- * an outer install never reaches its body. `dispatch` is false because it is a
25
- * ROUTE: its arms re-enter from the routed token and spell their own padding.
23
+ * for a different rule. A `grammar` that names or clears trivia declares its own
24
+ * scope, so an outer install never reaches its body and the answer is false; one
25
+ * that does not (`parser({ trackLines: true }, …)`, which `rules({ trackLines })`
26
+ * wraps every rule in) inherits, so it is transparent. `dispatch` is false because
27
+ * it is a ROUTE: its arms re-enter from the routed token and spell their own padding.
26
28
  *
27
29
  * Structural and pure — callers resolve it ONCE, not per parse.
28
30
  */
@@ -35,6 +37,12 @@ export function hasOwnTriviaBoundary(p: Combinator<unknown>, depth: number = MAX
35
37
  // Multi-term forms: the gaps BETWEEN terms are the ambient-trivia positions.
36
38
  case 'sequence': return d.parsers.length > 1
37
39
  case 'many': case 'oneOrMore': case 'sepBy': return true
40
+ // A scope that names or clears trivia keeps an outer install out of its body.
41
+ // Any other inherits, so it falls through at no cost in depth: line tracking
42
+ // wraps every rule in one, and must not change what this walk sees.
43
+ case 'grammar':
44
+ if (d.triviaParser || d.clearTrivia) return false
45
+ depth++
38
46
  // Transparent wrappers — they add no position, so ask what they wrap.
39
47
  case 'node': case 'transform': case 'leaf': case 'label': case 'field':
40
48
  case 'token': case 'attempt': case 'optional': case 'expect': case 'withCtx':
@@ -4,6 +4,7 @@ import { childrenOf } from '../analysis/gating.ts'
4
4
  import { collectGrammarReflection, type GrammarReflection } from '../cst/reflection.ts'
5
5
  import { classifyRuleMap } from '../analysis/commitment.ts'
6
6
  import { serializeRuleMap } from './ir-serialize.ts'
7
+ import { priorNameOf } from '../combinators/parser.ts'
7
8
  import { compileRuleMap, compileRuleMapRunnable, type TableRuleMapOptions } from '../table/compile-rule-map.ts'
8
9
  import type { TableProgram, TableRule } from '../table/program.ts'
9
10
  import { PARSEMAN_VERSION } from '../version.ts'
@@ -132,7 +133,9 @@ function externalNames(
132
133
  let resolved: Combinator<unknown> | undefined
133
134
  try { resolved = d.thunk() } catch { resolved = undefined }
134
135
  if (resolved === undefined) {
135
- const name = (p as unknown as { _ruleName?: string })._ruleName
136
+ // A `prior.X` the composition fills from an earlier piece is a hole too.
137
+ const prior = priorNameOf(p)
138
+ const name = prior === undefined ? (p as unknown as { _ruleName?: string })._ruleName : `prior.${prior}`
136
139
  if (name !== undefined && !out.includes(name)) out.push(name)
137
140
  return
138
141
  }
@@ -18,7 +18,7 @@
18
18
  * source, or an unsupported construct) — the caller then keeps the lowered source.
19
19
  */
20
20
  import type { Combinator, DispatchMatcherCase, ParserDef } from '../types.ts'
21
- import { rules } from '../combinators/parser.ts'
21
+ import { priorNameOf, rules } from '../combinators/parser.ts'
22
22
  import { ref } from '../combinators/ref.ts'
23
23
  import { regex } from '../combinators/regex.ts'
24
24
  import { literal } from '../combinators/literal.ts'
@@ -191,6 +191,8 @@ class Serializer {
191
191
  private localRuleNames = new Map<Comb, string>()
192
192
  private ruleMap: ReadonlyArray<readonly [string, Comb]>
193
193
  private scanSkip: readonly Comb[]
194
+ /** Whether any rule reads a `prior.X` hole, so the factory takes `(g, prior)`. */
195
+ private usesPrior = false
194
196
 
195
197
  private accessed: readonly string[]
196
198
  private referenced = new Set<string>()
@@ -214,7 +216,7 @@ class Serializer {
214
216
  if (seen.has(c)) return
215
217
  seen.add(c)
216
218
  if (c._def.tag === 'lazy') {
217
- if (ruleNameOf(c) !== undefined) throw new Unserializable('scanSkip unit references a grammar rule')
219
+ if (ruleNameOf(c) !== undefined || priorNameOf(c) !== undefined) throw new Unserializable('scanSkip unit references a grammar rule')
218
220
  const target = lazyTarget(c)
219
221
  if (target) this.assertNoRuleRef(target, seen)
220
222
  return
@@ -307,9 +309,10 @@ class Serializer {
307
309
  }
308
310
  // Shared consts go INSIDE the factory: they can reference `g[name]` rule refs,
309
311
  // and `g` only exists in the factory scope.
312
+ const params = this.usesPrior ? '(g, prior)' : '(g)'
310
313
  const factory = innerDecls.length === 0
311
- ? `(g) => ({\n${body}\n})`
312
- : `(g) => {\n${innerDecls.map(d => ' ' + d).join('\n')}\n return ({\n${body}\n})\n}`
314
+ ? `${params} => ({\n${body}\n})`
315
+ : `${params} => {\n${innerDecls.map(d => ' ' + d).join('\n')}\n return ({\n${body}\n})\n}`
313
316
  const call = `rules(${optionsArg}${factory})`
314
317
  return outerDecls.length === 0
315
318
  ? call
@@ -321,6 +324,7 @@ class Serializer {
321
324
  private analyze(c: Comb, active: Set<Comb>): void {
322
325
  const def = c._def
323
326
  if (def.tag === 'lazy') {
327
+ if (priorNameOf(c) !== undefined) { this.usesPrior = true; return } // resolved by prior[name]
324
328
  if (ruleNameOf(c) !== undefined) return // named rule ref — resolved by g[name]
325
329
  const target = lazyTarget(c)
326
330
  if (!target) throw new Unserializable('unresolved unnamed ref')
@@ -422,6 +426,8 @@ class Serializer {
422
426
  }
423
427
  switch (def.tag) {
424
428
  case 'lazy': {
429
+ const prior = priorNameOf(c)
430
+ if (prior !== undefined) return `prior[${JSON.stringify(prior)}]`
425
431
  const name = this.localRuleNames.get(c) ?? ruleNameOf(c)
426
432
  if (name !== undefined) { this.referenced.add(name); return `g[${JSON.stringify(name)}]` }
427
433
  const target = lazyTarget(c)!
@@ -16,7 +16,7 @@ import { borrowedRuleMessage, borrowedRuleReference } from './borrowed-rules.ts'
16
16
  import { FUSED_HOST_MODE, FUSED_HOST_ELIDED, type HostMode } from '../cst/host-mode.ts'
17
17
  import { compileLinkableTable, type LinkableTable } from './compile-linkable-table.ts'
18
18
  import { GRAMMAR_REFLECTION } from '../cst/reflection.ts'
19
- import { linkRules, RULE_ORDER, RULES_RECIPE, type LinkOptions, type RulesRecipe } from '../combinators/parser.ts'
19
+ import { linkRules, RULE_ORDER, RULES_RECIPE, type KnownRuleNames, type LinkOptions, type RulesRecipe } from '../combinators/parser.ts'
20
20
  import type { BuildHost, Combinator, CstCollapsePredicate, ParseContext, ParseResult } from '../types.ts'
21
21
  import type { Runnable } from '../functional/run.ts'
22
22
  import { PlainNode, PlainTaggedNode } from '../cst/plain.js'
@@ -342,7 +342,7 @@ function recipesOf(item: LinkableTable | Record<string, unknown>): readonly Rule
342
342
  * `rules({ trivia }, …)` map's own, or a runtime composition's composing trivia.
343
343
  * Outermost wins over every linked rule, inherited ones included; `parser` /
344
344
  * `noTrivia` still override locally. */
345
- function composingTriviaOf(items: Array<LinkableTable | Record<string, unknown>>): Combinator<unknown> | undefined {
345
+ function composingTriviaOf(items: ReadonlyArray<LinkableTable | Record<string, unknown>>): Combinator<unknown> | undefined {
346
346
  for (let i = items.length - 1; i >= 0; i--) {
347
347
  const item = items[i] as Record<symbol, unknown>
348
348
  if (item[LINKED] !== undefined) {
@@ -359,7 +359,7 @@ function composingTriviaOf(items: Array<LinkableTable | Record<string, unknown>>
359
359
 
360
360
  /** The rule names a linked composition defines, in first-declaration order —
361
361
  * known WITHOUT linking, from each item's own declaration order. */
362
- function declaredNamesOf(items: Array<LinkableTable | Record<string, unknown>>): string[] {
362
+ function declaredNamesOf(items: ReadonlyArray<LinkableTable | Record<string, unknown>>): string[] {
363
363
  const names = new Set<string>()
364
364
  for (const item of items) {
365
365
  const linked = (item as Record<symbol, unknown>)[LINKED] !== undefined
@@ -419,6 +419,16 @@ function linkComposition(
419
419
  return map
420
420
  }
421
421
 
422
+ /** The rules each item spells out. An item that spells out none, such as a plain
423
+ * `Record<string, …>`, could hold anything, so the whole result stays open. */
424
+ type ItemRuleNames<Item> = Item extends unknown
425
+ ? [KnownRuleNames<Item>] extends [never] ? string : KnownRuleNames<Item>
426
+ : never
427
+
428
+ /** A composition's rules: every name its items spell out, plus lookup by any string. */
429
+ export type ComposedRules<Items extends readonly unknown[]> =
430
+ Record<string, Runnable> & { [K in ItemRuleNames<Items[number]>]: Runnable }
431
+
422
432
  /**
423
433
  * Compose grammars into ONE parser map — the only public composition entry point.
424
434
  * `compose([base, ext, …])`: a later entry's rule WINS by name, and because every
@@ -435,23 +445,23 @@ function linkComposition(
435
445
  * costs a walk of the items' declared names. For speed, `compile()` a rule of it;
436
446
  * `compile()` specialises when the environment allows and falls back under CSP.
437
447
  */
438
- export function compose(
439
- items: Array<LinkableTable | Record<string, unknown>>,
448
+ export function compose<const Items extends ReadonlyArray<LinkableTable | Record<string, unknown>>>(
449
+ items: Items,
440
450
  /**
441
451
  * Host mode for the composed grammar, same meaning as `compile(g, { hostMode })`.
442
452
  * The interpreter routes by host at parse time; this records `'cst'` on every rule
443
453
  * so `run()` can refuse a mismatched host instead of building the wrong tree.
444
454
  */
445
455
  opts?: { hostMode?: HostMode },
446
- ): Record<string, Runnable> {
456
+ ): ComposedRules<Items> {
447
457
  if (items.some(item => (item as Record<symbol, unknown>)[LEAF_COMPOSED] === true)) {
448
458
  throw new Error('compose: a composeLeaf() result is terminal and cannot be composed again')
449
459
  }
450
- return linkedMap(items, opts, false)
460
+ return linkedMap(items, opts, false) as ComposedRules<Items>
451
461
  }
452
462
 
453
463
  function linkedMap(
454
- items: Array<LinkableTable | Record<string, unknown>>,
464
+ items: ReadonlyArray<LinkableTable | Record<string, unknown>>,
455
465
  opts: { hostMode?: HostMode } | undefined,
456
466
  leaf: boolean,
457
467
  ): Record<string, Runnable> {
@@ -485,10 +495,10 @@ function linkedMap(
485
495
  * At runtime it is the same lazy link as `compose()`, marked terminal. The bench and
486
496
  * differential harnesses import un-macro'd grammar modules and run this path.
487
497
  */
488
- export function composeLeaf(
489
- items: Array<LinkableTable | Record<string, unknown>>,
490
- ): Record<string, Runnable> {
491
- return linkedMap(items, undefined, true)
498
+ export function composeLeaf<const Items extends ReadonlyArray<LinkableTable | Record<string, unknown>>>(
499
+ items: Items,
500
+ ): ComposedRules<Items> {
501
+ return linkedMap(items, undefined, true) as ComposedRules<Items>
492
502
  }
493
503
 
494
504
  /** Whether `map` is a runtime composition (a combinator map) rather than a
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export type { AttemptOptions } from './combinators/attempt.ts'
23
23
  export { many, oneOrMore, optional, sepBy, oneOrMoreSep, keepSeparator } from './combinators/repeat.ts'
24
24
  export type { RepeatOptions, SepByOptions, TrailingSeparator, KeptSeparator } from './combinators/repeat.ts'
25
25
  export { rules } from './combinators/parser.ts'
26
+ export type { PriorRules } from './combinators/parser.ts'
26
27
  export { ref } from './combinators/ref.ts'
27
28
  export { not } from './combinators/not.ts'
28
29
  export { peek } from './combinators/peek.ts'
@@ -140,7 +141,7 @@ export type {
140
141
  // back to `FusedRule`, `isInterpretedFuse` would have to be re-exported in the same
141
142
  // change — a dual shape with no way to detect it is the worse of the two.
142
143
  export { compose, composeLeaf, cstBuildHost } from './compiler/linker.ts'
143
- export type { CstBuildHostOptions, FusedRule } from './compiler/linker.ts'
144
+ export type { ComposedRules, CstBuildHostOptions, FusedRule } from './compiler/linker.ts'
144
145
 
145
146
  export { buildLineIndex, createLineIndex, recordLineRange, normalizeLineIndex, offsetToLineCol, annotateSpan, annotateTreeSpans } from './line-index.ts'
146
147
  export type { LineIndex } from './line-index.ts'
@@ -16,7 +16,7 @@ import type {
16
16
  import type { Combinator } from '../types.ts'
17
17
  import type { DispatchArm } from '../combinators/dispatch.ts'
18
18
  import { ref } from '../combinators/ref.ts'
19
- import { rules, type RulesOptions } from '../combinators/parser.ts'
19
+ import { rules, RULES_RECIPE, type PriorRules, type RulesOptions, type RulesRecipe } from '../combinators/parser.ts'
20
20
  import * as parseman from '../index.ts'
21
21
  import { confirmedBuildParamUnused } from '../compiler/build-arity.ts'
22
22
  import { directBuilderBindings } from './direct-builder-static.ts'
@@ -1478,11 +1478,17 @@ export function evaluateParserFactory(
1478
1478
 
1479
1479
  const factory = factoryNode as unknown as ArrowFunctionExpression | OxcFunction
1480
1480
  const params = factory.params
1481
- if (params.length !== 1) return null
1482
- const param = params[0] as unknown as { type: string; name?: string }
1481
+ if (params.length !== 1 && params.length !== 2) return null
1483
1482
  // FormalParameter is { decorators? } & BindingPattern — BindingIdentifier has type "Identifier"
1484
- const proxyName = param.type === 'Identifier' ? param.name ?? null : null
1483
+ const paramName = (param: unknown): string | null => {
1484
+ const p = param as { type: string; name?: string }
1485
+ return p.type === 'Identifier' ? p.name ?? null : null
1486
+ }
1487
+ const proxyName = paramName(params[0])
1485
1488
  if (!proxyName) return null
1489
+ // `(g, prior)`: the second parameter is the `prior` handle (`PriorRules`).
1490
+ const priorName = params.length === 2 ? paramName(params[1]) : undefined
1491
+ if (priorName === null) return null
1486
1492
 
1487
1493
  const body = factory.body
1488
1494
  if (!body) return null
@@ -1566,11 +1572,12 @@ export function evaluateParserFactory(
1566
1572
  const ABORT = Symbol('parseman: factory not statically evaluable')
1567
1573
  let built: Record<string, Combinator<unknown>>
1568
1574
  try {
1569
- built = rules(options ?? {}, (g: Record<string, Combinator<unknown>>) => {
1575
+ built = rules(options ?? {}, (g: Record<string, Combinator<unknown>>, prior: PriorRules) => {
1570
1576
  // Outer ScopeEntry values carry their mfSrcs and are replayed by scopeGet()
1571
1577
  // when body statements or return expressions reference them.
1572
1578
  const localScope: XScope = scope instanceof ModuleScope ? scope.fork() : new Map(scope as XScope)
1573
1579
  localScope.set(proxyName, g)
1580
+ if (priorName !== undefined) localScope.set(priorName, prior)
1574
1581
 
1575
1582
  // ── Phase 1: the factory's own body statements ────────────────────────
1576
1583
  if (!evalBodyStatements(statements, localScope, code, out)) throw ABORT
@@ -1615,9 +1622,13 @@ export function evaluateParserFactory(
1615
1622
  for (const key of keys) map.set(key, built[key]!)
1616
1623
  const declared = new Set(keys)
1617
1624
  externalAccesses.set(map, Object.keys(built).filter(name => !declared.has(name)))
1625
+ ruleMapRecipes.set(map, (built as unknown as Record<symbol, readonly RulesRecipe[]>)[RULES_RECIPE]!)
1618
1626
  return map
1619
1627
  }
1620
1628
 
1629
+ /** The recipe an evaluated rule map came from, for a composition that links it. */
1630
+ export const ruleMapRecipes = new WeakMap<ReadonlyMap<string, Combinator<unknown>>, readonly RulesRecipe[]>()
1631
+
1621
1632
  /**
1622
1633
  * Every EXTERNAL name an evaluated factory read through `g`, keyed by the map
1623
1634
  * `evaluateParserFactory` returned. The map holds declared rules only, so a `g.X`