parseman 0.46.0 → 0.47.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.
- package/README.md +8 -3
- package/dist/analysis/choice-cost-render.d.ts +12 -4
- package/dist/analysis/choice-cost-render.d.ts.map +1 -1
- package/dist/analysis/choice-cost.d.ts +2 -2
- package/dist/analysis/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/diagnostics.cjs +10339 -7787
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +10339 -7787
- package/dist/analysis/diagnostics.js.map +3 -4
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix.d.ts +3 -3
- package/dist/analysis/fix.d.ts.map +1 -1
- package/dist/analysis/format-number.d.ts +23 -0
- package/dist/analysis/format-number.d.ts.map +1 -0
- package/dist/analysis/gating.d.ts +39 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/analysis/terminal.d.ts +5 -2
- package/dist/analysis/terminal.d.ts.map +1 -1
- package/dist/cli/index.cjs +10924 -8506
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10924 -8506
- package/dist/cli/index.js.map +3 -4
- package/dist/combinators/adjacency.d.ts +100 -0
- package/dist/combinators/adjacency.d.ts.map +1 -0
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/completions.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts +8 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/gate.d.ts.map +1 -1
- package/dist/combinators/grammar.d.ts +22 -0
- package/dist/combinators/grammar.d.ts.map +1 -1
- package/dist/combinators/literal.d.ts +8 -0
- package/dist/combinators/literal.d.ts.map +1 -1
- package/dist/combinators/map.d.ts +0 -1
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +13 -3
- package/dist/combinators/node.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +1 -1
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts +1 -16
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts +30 -2
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/scanTo.d.ts +71 -1
- package/dist/combinators/scanTo.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/token.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +26 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -0
- package/dist/combinators/trivia-skip.d.ts +45 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/combinators/withCtx.d.ts.map +1 -1
- package/dist/compiler/compile-linkable-table.d.ts +104 -0
- package/dist/compiler/compile-linkable-table.d.ts.map +1 -0
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +13 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts +5 -5
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +47 -53
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +81 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -0
- package/dist/compiler/token-dispatch.d.ts +95 -0
- package/dist/compiler/token-dispatch.d.ts.map +1 -0
- package/dist/compiler/token-scanner.d.ts +53 -0
- package/dist/compiler/token-scanner.d.ts.map +1 -0
- package/dist/compiler/value-usage.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/capture-buffer.d.ts +35 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/cst/reflection-attach.d.ts +8 -0
- package/dist/cst/reflection-attach.d.ts.map +1 -0
- package/dist/cst/reflection-symbols.d.ts +10 -0
- package/dist/cst/reflection-symbols.d.ts.map +1 -0
- package/dist/cst/reflection.d.ts +2 -4
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/root-trivia-scope.d.ts +26 -0
- package/dist/cst/root-trivia-scope.d.ts.map +1 -0
- package/dist/cst/trivia-charscan.d.ts +18 -0
- package/dist/cst/trivia-charscan.d.ts.map +1 -0
- package/dist/cst/trivia-kinds.d.ts +19 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/functional/doc.d.ts.map +1 -1
- package/dist/functional/run-tabled.d.ts +12 -0
- package/dist/functional/run-tabled.d.ts.map +1 -0
- package/dist/functional/run.d.ts +25 -42
- package/dist/functional/run.d.ts.map +1 -1
- package/dist/grammar-metadata.d.ts +3 -0
- package/dist/grammar-metadata.d.ts.map +1 -0
- package/dist/index.cjs +9290 -7181
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +18 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9300 -7191
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +313 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +313 -121
- package/dist/language-service/index.js.map +3 -4
- package/dist/oracle/index.cjs.map +0 -1
- package/dist/oracle/index.js.map +0 -1
- package/dist/parse-context.d.ts +49 -0
- package/dist/parse-context.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts +40 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.cjs +11175 -9534
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11175 -9534
- package/dist/plugin/index.js.map +3 -4
- package/dist/regex/classes.d.ts +28 -0
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/regex/first-set.d.ts +14 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/run/index.cjs +136 -51
- package/dist/run/index.cjs.map +2 -3
- package/dist/run/index.js +136 -51
- package/dist/run/index.js.map +2 -3
- package/dist/spec/index.cjs +2934 -1611
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2934 -1611
- package/dist/spec/index.js.map +3 -4
- package/dist/spec/model.d.ts +8 -0
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +307 -0
- package/dist/table/assemble.d.ts.map +1 -0
- package/dist/table/cell.d.ts +42 -0
- package/dist/table/cell.d.ts.map +1 -0
- package/dist/table/child-slots.d.ts +30 -0
- package/dist/table/child-slots.d.ts.map +1 -0
- package/dist/table/compile-rule-map.d.ts +136 -0
- package/dist/table/compile-rule-map.d.ts.map +1 -0
- package/dist/table/compile.d.ts +56 -0
- package/dist/table/compile.d.ts.map +1 -0
- package/dist/table/duplication-hook.d.ts +32 -0
- package/dist/table/duplication-hook.d.ts.map +1 -0
- package/dist/table/emit-assembly.d.ts +139 -0
- package/dist/table/emit-assembly.d.ts.map +1 -0
- package/dist/table/emit.d.ts +113 -0
- package/dist/table/emit.d.ts.map +1 -0
- package/dist/table/encode-baseline.d.ts +16 -0
- package/dist/table/encode-baseline.d.ts.map +1 -0
- package/dist/table/encode.d.ts +71 -0
- package/dist/table/encode.d.ts.map +1 -0
- package/dist/table/exec-baseline.d.ts +9 -0
- package/dist/table/exec-baseline.d.ts.map +1 -0
- package/dist/table/exec.d.ts +72 -0
- package/dist/table/exec.d.ts.map +1 -0
- package/dist/table/fold.d.ts +25 -0
- package/dist/table/fold.d.ts.map +1 -0
- package/dist/table/index.cjs +11463 -0
- package/dist/table/index.cjs.map +6 -0
- package/dist/table/index.d.ts +100 -0
- package/dist/table/index.d.ts.map +1 -0
- package/dist/table/index.js +11440 -0
- package/dist/table/index.js.map +6 -0
- package/dist/table/inspect.d.ts +17 -0
- package/dist/table/inspect.d.ts.map +1 -0
- package/dist/table/ops.d.ts +453 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +496 -0
- package/dist/table/program.d.ts.map +1 -0
- package/dist/table/run-support.d.ts +38 -0
- package/dist/table/run-support.d.ts.map +1 -0
- package/dist/{compiler/scannable-run.d.ts → table/scan-shapes.d.ts} +14 -13
- package/dist/table/scan-shapes.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +58 -0
- package/dist/table/site-labels.d.ts.map +1 -0
- package/dist/table/stamp.d.ts +70 -0
- package/dist/table/stamp.d.ts.map +1 -0
- package/dist/types.d.ts +80 -24
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +26 -2
- package/src/analysis/choice-cost-gate.ts +359 -0
- package/src/analysis/choice-cost-render.ts +293 -0
- package/src/analysis/choice-cost.ts +1008 -0
- package/src/analysis/commitment.ts +557 -0
- package/src/analysis/corpus.ts +130 -0
- package/src/analysis/diagnose-render.ts +625 -0
- package/src/analysis/diagnose.ts +317 -0
- package/src/analysis/diagnostics.ts +33 -0
- package/src/analysis/duplication.ts +1955 -0
- package/src/analysis/fix-render.ts +217 -0
- package/src/analysis/fix.ts +669 -0
- package/src/analysis/format-number.ts +31 -0
- package/src/analysis/gating.ts +792 -0
- package/src/analysis/grammar.ts +72 -0
- package/src/analysis/rebuild.ts +287 -0
- package/src/analysis/terminal.ts +336 -0
- package/src/cli/index.ts +413 -0
- package/src/combinators/adjacency.ts +171 -0
- package/src/combinators/attempt.ts +41 -0
- package/src/combinators/case-fold.ts +90 -0
- package/src/combinators/choice.ts +466 -0
- package/src/combinators/completions.ts +64 -0
- package/src/combinators/dispatch.ts +358 -0
- package/src/combinators/expect.ts +159 -0
- package/src/combinators/first-set.ts +348 -0
- package/src/combinators/gate.ts +50 -0
- package/src/combinators/grammar.ts +272 -0
- package/src/combinators/keywords.ts +145 -0
- package/src/combinators/literal.ts +102 -0
- package/src/combinators/map.ts +106 -0
- package/src/combinators/node.ts +345 -0
- package/src/combinators/not.ts +55 -0
- package/src/combinators/parser.ts +266 -0
- package/src/combinators/peek.ts +63 -0
- package/src/combinators/probe.ts +25 -0
- package/src/combinators/ref.ts +109 -0
- package/src/combinators/regex.ts +170 -0
- package/src/combinators/repeat.ts +561 -0
- package/src/combinators/scanTo.ts +393 -0
- package/src/combinators/sequence.ts +237 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +852 -0
- package/src/combinators/withCtx.ts +45 -0
- package/src/compiler/build-arity.ts +330 -0
- package/src/compiler/compile-linkable-table.ts +209 -0
- package/src/compiler/degradation.ts +266 -0
- package/src/compiler/fields.ts +191 -0
- package/src/compiler/grammar-coverage-ids.ts +129 -0
- package/src/compiler/ir-serialize.ts +615 -0
- package/src/compiler/linker.ts +954 -0
- package/src/compiler/token-alphabet.ts +209 -0
- package/src/compiler/token-dispatch.ts +353 -0
- package/src/compiler/token-scanner.ts +318 -0
- package/src/compiler/value-usage.ts +147 -0
- package/src/coverage.ts +383 -0
- package/src/cst/capture-buffer.ts +297 -0
- package/src/cst/host-mode.ts +69 -0
- package/src/cst/reflection-attach.ts +11 -0
- package/src/cst/reflection-symbols.ts +9 -0
- package/src/cst/reflection.ts +149 -0
- package/src/cst/relative-spans.ts +207 -0
- package/src/cst/root-trivia-scope.ts +30 -0
- package/src/cst/trivia-charscan.ts +312 -0
- package/src/cst/trivia-entries.ts +285 -0
- package/src/cst/trivia-index.ts +131 -0
- package/src/cst/trivia-kinds.ts +358 -0
- package/src/cst/types.ts +76 -0
- package/src/cst/walk.ts +101 -0
- package/src/functional/doc.ts +959 -0
- package/src/functional/run-tabled.ts +153 -0
- package/src/functional/run.ts +558 -0
- package/src/grammar-metadata.ts +2 -0
- package/src/index.ts +189 -0
- package/src/language-service/index.ts +221 -0
- package/src/line-index.ts +131 -0
- package/src/oracle/digest.ts +620 -0
- package/src/oracle/index.ts +55 -0
- package/src/parse-context.ts +91 -0
- package/src/plugin/direct-builder-static.ts +102 -0
- package/src/plugin/evaluator.ts +1505 -0
- package/src/plugin/free-identifiers.ts +338 -0
- package/src/plugin/index.ts +2281 -0
- package/src/plugin/reducer-resolver.ts +606 -0
- package/src/recovery/scan.ts +155 -0
- package/src/regex/classes.ts +183 -0
- package/src/regex/first-set.ts +316 -0
- package/src/run/index.ts +31 -0
- package/src/spec/ebnf.ts +128 -0
- package/src/spec/index.ts +54 -0
- package/src/spec/model.ts +636 -0
- package/src/spec/railroad-lib.ts +559 -0
- package/src/spec/railroad.ts +256 -0
- package/src/table/assemble.ts +3006 -0
- package/src/table/cell.ts +42 -0
- package/src/table/child-slots.ts +113 -0
- package/src/table/compile-rule-map.ts +336 -0
- package/src/table/compile.ts +244 -0
- package/src/table/duplication-hook.ts +89 -0
- package/src/table/emit-assembly.ts +1777 -0
- package/src/table/emit.ts +429 -0
- package/src/table/encode-baseline.ts +340 -0
- package/src/table/encode.ts +1569 -0
- package/src/table/exec-baseline.ts +537 -0
- package/src/table/exec.ts +1680 -0
- package/src/table/fold.ts +38 -0
- package/src/table/index.ts +108 -0
- package/src/table/inspect.ts +52 -0
- package/src/table/ops.ts +467 -0
- package/src/table/program.ts +876 -0
- package/src/table/run-support.ts +66 -0
- package/src/table/scan-shapes.ts +1578 -0
- package/src/table/site-labels.ts +228 -0
- package/src/table/stamp.ts +229 -0
- package/src/types.ts +563 -0
- package/src/version.ts +16 -0
- package/dist/compiler/codegen.d.ts +0 -334
- package/dist/compiler/codegen.d.ts.map +0 -1
- package/dist/compiler/inline-build.d.ts +0 -11
- package/dist/compiler/inline-build.d.ts.map +0 -1
- package/dist/compiler/inline-callback.d.ts +0 -21
- package/dist/compiler/inline-callback.d.ts.map +0 -1
- package/dist/compiler/line-index.d.ts +0 -2
- package/dist/compiler/line-index.d.ts.map +0 -1
- package/dist/compiler/module-hoist.d.ts +0 -106
- package/dist/compiler/module-hoist.d.ts.map +0 -1
- package/dist/compiler/scannable-run.d.ts.map +0 -1
- package/dist/compiler/scannable-terminal.d.ts +0 -23
- package/dist/compiler/scannable-terminal.d.ts.map +0 -1
- package/dist/compiler/trivia-fast-path.d.ts +0 -38
- package/dist/compiler/trivia-fast-path.d.ts.map +0 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type { Combinator, FieldMap, ParseContext, ParseResult, ParserMeta, ParserDef } from '../types.ts'
|
|
2
|
+
import { beginCstNodeCapture, endCstNodeCapture, pushCstChild } from '../cst/capture-buffer.ts'
|
|
3
|
+
import { buildReadsTrivia, buildReadsState, cstOutputHost } from '../compiler/build-arity.ts'
|
|
4
|
+
import { buildFieldMap, buildReadsFields, parserHasOwnFields } from '../compiler/fields.ts'
|
|
5
|
+
import { consumeTrivia } from './trivia-skip.ts'
|
|
6
|
+
import { matchesEmpty, startsFirstSet } from './first-set.ts'
|
|
7
|
+
import { deriveExpected } from './expect.ts'
|
|
8
|
+
import { annotateSpanFromLineContext } from '../line-index.ts'
|
|
9
|
+
import { NODE_TAG, NODE_TYPE } from '../cst/reflection-symbols.ts'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A CST/AST node rule. Runs `combinator` while collecting its terminals into
|
|
13
|
+
* `children` / `rawChildren` arrays and trivia spans into `triviaLog`, then
|
|
14
|
+
* calls `build(children, fields, span, rawChildren, triviaLog, state)` to produce the node.
|
|
15
|
+
*
|
|
16
|
+
* - `children` — structural items in source order: spanned CSTLeaf terminals
|
|
17
|
+
* and sub-nodes (whatever `build` returned for inner nodes).
|
|
18
|
+
* - `rawChildren` — structural children only (same items as `children`).
|
|
19
|
+
* - `triviaLog` — flat log of trivia entries: each entry is `[start, end, insertIdx]`
|
|
20
|
+
* consumed between terms. `insertIdx` is the rawChildren index
|
|
21
|
+
* before which the trivia was consumed. Use `buildTriviaIndex`
|
|
22
|
+
* to turn this into a before/after lookup table.
|
|
23
|
+
*
|
|
24
|
+
* If `build` returns a non-node value (e.g. a bare string for an unwrapped rule),
|
|
25
|
+
* the parent records it as a spanned leaf so its source span is still recoverable.
|
|
26
|
+
*/
|
|
27
|
+
export type BuildNode<N> = (
|
|
28
|
+
children: ReadonlyArray<unknown>,
|
|
29
|
+
fields: FieldMap | undefined,
|
|
30
|
+
span: { start: number; end: number },
|
|
31
|
+
rawChildren: ReadonlyArray<unknown>,
|
|
32
|
+
triviaLog: readonly number[],
|
|
33
|
+
state: unknown,
|
|
34
|
+
) => N
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for `node()`.
|
|
38
|
+
* - `unwrap` — an AST/value wrapper rule that IS its single child when it
|
|
39
|
+
* captured exactly one. A leaf unwraps to its string value; a sub-node is
|
|
40
|
+
* returned as-is.
|
|
41
|
+
* - `collapse` — a structural wrapper rule that returns its single child exactly
|
|
42
|
+
* as captured. A leaf stays a CSTLeaf; a node stays a node.
|
|
43
|
+
* - `project` — return one captured semantic child by index while retaining the
|
|
44
|
+
* complete node frame for CST hosts. Leaves unwrap to their string value.
|
|
45
|
+
* - `captureTrivia` — capture trivia consumed inside this node even when its
|
|
46
|
+
* enclosing `parser()` did not opt into document-wide trivia capture. This is
|
|
47
|
+
* scoped to the node; sibling and parent nodes retain their own setting.
|
|
48
|
+
* - `trailingTrivia` — after a successful node body, consume the active grammar
|
|
49
|
+
* trivia once into THIS node's log. Intended for a document root whose body is
|
|
50
|
+
* a repetition; do not set it on a block that already has a closing delimiter.
|
|
51
|
+
* - `tags` — grammar-level CST categories for visitor dispatch. Stored once in
|
|
52
|
+
* grammar reflection; not copied onto every CST node.
|
|
53
|
+
* Both skip `build` only for a one-child match; zero or two-plus children go
|
|
54
|
+
* through `build` normally.
|
|
55
|
+
*/
|
|
56
|
+
type ParserValue<P> = P extends Combinator<infer T> ? T : never
|
|
57
|
+
type ProjectValue<P extends Combinator<unknown>, I extends number> =
|
|
58
|
+
ParserValue<P> extends readonly unknown[]
|
|
59
|
+
? I extends keyof ParserValue<P> ? ParserValue<P>[I] : unknown
|
|
60
|
+
: I extends 0 ? ParserValue<P> : unknown
|
|
61
|
+
|
|
62
|
+
export type NodeCombinator<T, NodeType extends string = never, NodeTag extends string = never> =
|
|
63
|
+
Combinator<T> & {
|
|
64
|
+
/** Type-only CST node identity carried for grammar-aware visitors. */
|
|
65
|
+
readonly [NODE_TYPE]?: NodeType
|
|
66
|
+
/** Type-only CST tags carried for grammar-aware visitors. */
|
|
67
|
+
readonly [NODE_TAG]?: NodeTag
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type NodeOptions<Tags extends readonly string[] = readonly never[]> = {
|
|
71
|
+
unwrap?: boolean
|
|
72
|
+
collapse?: boolean
|
|
73
|
+
project?: number
|
|
74
|
+
captureTrivia?: boolean
|
|
75
|
+
trailingTrivia?: boolean
|
|
76
|
+
tags?: Tags
|
|
77
|
+
/**
|
|
78
|
+
* Declared positional arity of `build` — the ESCAPE HATCH for a reducer parseman
|
|
79
|
+
* cannot read.
|
|
80
|
+
*
|
|
81
|
+
* Arity gates five capture tiers (`>= 1` children, `>= 2` fields, `>= 4` rawChildren,
|
|
82
|
+
* `>= 5` triviaLog, `>= 6` a clone of `_ctx.state`), and parseman normally derives it
|
|
83
|
+
* from the reducer's declaration — including across module boundaries. A few shapes
|
|
84
|
+
* are genuinely undecidable: a rest parameter (`...args`), a body that reads
|
|
85
|
+
* `arguments`, a reassigned or dynamically constructed reducer. Those fail OPEN, which
|
|
86
|
+
* is safe but pays for every tier on every match, forever.
|
|
87
|
+
*
|
|
88
|
+
* Declaring the arity here turns "undecidable" into "declared". You are asserting the
|
|
89
|
+
* highest positional argument the reducer reads; parseman then elides everything above
|
|
90
|
+
* it exactly as if it had read the parameter list.
|
|
91
|
+
*
|
|
92
|
+
* DECLARING TOO LOW UNDER-CAPTURES: the reducer will receive an empty `rawChildren` /
|
|
93
|
+
* `triviaLog` or an absent `state` rather than a wrong value. Count the parameters.
|
|
94
|
+
*/
|
|
95
|
+
buildArity?: number
|
|
96
|
+
}
|
|
97
|
+
export type NodeProjectOptions<I extends number = number> =
|
|
98
|
+
Omit<NodeOptions, 'project' | 'unwrap' | 'collapse'> & { project: I; unwrap?: never; collapse?: never }
|
|
99
|
+
|
|
100
|
+
/** A captured child's value form: a leaf unwraps to its string value, else as-is. */
|
|
101
|
+
export function unwrapChild(child: unknown): unknown {
|
|
102
|
+
return child !== null && typeof child === 'object' && (child as { _tag?: string })._tag === 'leaf'
|
|
103
|
+
? (child as { value: unknown }).value
|
|
104
|
+
: child
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isCstChild(value: unknown): boolean {
|
|
108
|
+
return typeof value === 'object'
|
|
109
|
+
&& value !== null
|
|
110
|
+
&& ((value as { _tag?: string })._tag === 'node'
|
|
111
|
+
|| (value as { _tag?: string })._tag === 'leaf'
|
|
112
|
+
|| (value as { _tag?: string })._tag === 'parseError')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The authoring error for `node()` with no rules() key, raised IDENTICALLY by
|
|
117
|
+
* every engine. Exported because the table encoder used to answer it with
|
|
118
|
+
* `UnsupportedConstruct: no opcode for 'node(inferred type)'` — a message that
|
|
119
|
+
* reads as a table gap and names neither the mistake nor the fix.
|
|
120
|
+
*/
|
|
121
|
+
export function missingInferredType(): never {
|
|
122
|
+
throw new Error('node(): inferred node type requires a rules() key; pass node("Type", parser) outside rules()')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeBuildArity(arity: number | undefined): number | undefined {
|
|
126
|
+
if (arity === undefined) return undefined
|
|
127
|
+
if (!Number.isInteger(arity) || arity < 0 || arity > 6) {
|
|
128
|
+
throw new Error('node() buildArity must be an integer 0..6 (children, fields, span, rawChildren, triviaLog, state)')
|
|
129
|
+
}
|
|
130
|
+
return arity
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeProject(project: number | undefined): number | undefined {
|
|
134
|
+
if (project === undefined) return undefined
|
|
135
|
+
if (!Number.isInteger(project) || project < 0) {
|
|
136
|
+
throw new Error('node() project child must be a non-negative integer')
|
|
137
|
+
}
|
|
138
|
+
return project
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function projectChild(children: ReadonlyArray<unknown>, project: number, type: string): unknown {
|
|
142
|
+
if (!(project in children)) {
|
|
143
|
+
throw new Error(`node(${JSON.stringify(type)}) project child ${project} was not captured`)
|
|
144
|
+
}
|
|
145
|
+
return unwrapChild(children[project])
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function node<P extends Combinator<unknown>, const I extends number, const Tags extends readonly string[] = readonly never[]>(combinator: P, opts: NodeProjectOptions<I> & { tags?: Tags }): NodeCombinator<ProjectValue<P, I>, never, Tags[number]>
|
|
149
|
+
export function node<N, const Tags extends readonly string[] = readonly never[]>(combinator: Combinator<unknown>, build?: BuildNode<N>, opts?: NodeOptions<Tags>): NodeCombinator<N, never, Tags[number]>
|
|
150
|
+
export function node<N, const Tags extends readonly string[] = readonly never[]>(combinator: Combinator<unknown>, opts?: NodeOptions<Tags>): NodeCombinator<N, never, Tags[number]>
|
|
151
|
+
export function node<const Type extends string, P extends Combinator<unknown>, const I extends number, const Tags extends readonly string[] = readonly never[]>(type: Type, combinator: P, opts: NodeProjectOptions<I> & { tags?: Tags }): NodeCombinator<ProjectValue<P, I>, Type, Tags[number]>
|
|
152
|
+
// `Type` carries a default so that `node<N>('Type', …)` — one explicit type
|
|
153
|
+
// argument, the node value type — still MATCHES these signatures. Without it
|
|
154
|
+
// the arity check rejects them, resolution falls back to the combinator-first
|
|
155
|
+
// overloads above, and the caller gets `TS2345 string is not assignable to
|
|
156
|
+
// Combinator` plus a TS7006 implicit-any reducer from the rejected contextual
|
|
157
|
+
// type. See test/unit/node-type-argument.test.ts.
|
|
158
|
+
//
|
|
159
|
+
// RESIDUAL COST: `= string` is the only shape available. TypeScript fills a
|
|
160
|
+
// missing type argument from its DEFAULT and never infers it (there is no
|
|
161
|
+
// partial type-argument inference), so `node<N>('Type', …)` resolves `Type` to
|
|
162
|
+
// `string` and loses the string literal. Write `node('Type', …)` (no explicit
|
|
163
|
+
// type arguments) or `node<N, 'Type'>('Type', …)` where the literal identity
|
|
164
|
+
// matters to a grammar-aware visitor. The only literal-preserving alternative
|
|
165
|
+
// is a curried call form, which would change the public call surface.
|
|
166
|
+
export function node<N, const Type extends string = string, const Tags extends readonly string[] = readonly never[]>(type: Type, combinator: Combinator<unknown>, build?: BuildNode<N>, opts?: NodeOptions<Tags>): NodeCombinator<N, Type, Tags[number]>
|
|
167
|
+
export function node<N, const Type extends string = string, const Tags extends readonly string[] = readonly never[]>(type: Type, combinator: Combinator<unknown>, opts?: NodeOptions<Tags>): NodeCombinator<N, Type, Tags[number]>
|
|
168
|
+
export function node<N>(
|
|
169
|
+
typeOrCombinator: string | Combinator<unknown>,
|
|
170
|
+
combinatorOrBuild?: Combinator<unknown> | BuildNode<N> | NodeOptions,
|
|
171
|
+
buildOrOpts?: BuildNode<N> | NodeOptions,
|
|
172
|
+
maybeOpts?: NodeOptions,
|
|
173
|
+
): Combinator<N> {
|
|
174
|
+
const hasExplicitType = typeof typeOrCombinator === 'string'
|
|
175
|
+
const type = hasExplicitType ? typeOrCombinator : undefined
|
|
176
|
+
const combinator = (hasExplicitType ? combinatorOrBuild : typeOrCombinator) as Combinator<unknown>
|
|
177
|
+
const buildArg = hasExplicitType ? buildOrOpts : combinatorOrBuild
|
|
178
|
+
const build = typeof buildArg === 'function' ? buildArg as BuildNode<N> : undefined
|
|
179
|
+
const opts = (typeof buildArg === 'function' ? maybeOpts : buildArg ?? maybeOpts) as NodeOptions | undefined
|
|
180
|
+
const project = normalizeProject(opts?.project)
|
|
181
|
+
if (project !== undefined && build !== undefined) {
|
|
182
|
+
throw new Error('node() options cannot combine project with a build callback')
|
|
183
|
+
}
|
|
184
|
+
const baseDef = { tag: 'node' as const, parser: combinator, ...(type === undefined ? {} : { type }), ...(build === undefined ? {} : { build }) }
|
|
185
|
+
const meta: ParserMeta = {
|
|
186
|
+
firstSet: combinator._meta.firstSet,
|
|
187
|
+
canMatchNewline: combinator._meta.canMatchNewline,
|
|
188
|
+
isTrivia: false,
|
|
189
|
+
}
|
|
190
|
+
const unwrap = opts?.unwrap === true
|
|
191
|
+
const collapse = opts?.collapse === true
|
|
192
|
+
const captureTrivia = opts?.captureTrivia === true
|
|
193
|
+
const trailingTrivia = opts?.trailingTrivia === true
|
|
194
|
+
const tags = opts?.tags
|
|
195
|
+
const buildArity = normalizeBuildArity(opts?.buildArity)
|
|
196
|
+
if (unwrap && collapse) {
|
|
197
|
+
throw new Error('node() options cannot set both unwrap and collapse')
|
|
198
|
+
}
|
|
199
|
+
if (project !== undefined && (unwrap || collapse)) {
|
|
200
|
+
throw new Error('node() options cannot combine project with unwrap or collapse')
|
|
201
|
+
}
|
|
202
|
+
const def: Extract<ParserDef, { tag: 'node' }> = unwrap || collapse || project !== undefined || captureTrivia || trailingTrivia || buildArity !== undefined || (tags !== undefined && tags.length > 0)
|
|
203
|
+
? { ...baseDef, ...(unwrap ? { unwrap: true } : {}), ...(collapse ? { collapse: true } : {}), ...(project !== undefined ? { project } : {}), ...(captureTrivia ? { captureTrivia: true } : {}), ...(trailingTrivia ? { trailingTrivia: true } : {}), ...(buildArity !== undefined ? { buildArity } : {}), ...(tags !== undefined && tags.length > 0 ? { tags } : {}) }
|
|
204
|
+
: baseDef
|
|
205
|
+
// Arity-gated elision — decided once, identically to the compiler (build-arity.ts).
|
|
206
|
+
// When the build never reads the trivia (4th) arg, disable per-node CST-trivia
|
|
207
|
+
// capture for the inner scope; when it never reads state (5th), skip the state clone.
|
|
208
|
+
// A STRUCTURAL node (no own build) defers to `ctx.build` / a default CST, which
|
|
209
|
+
// may read either, so capture both.
|
|
210
|
+
// These are the DIRECT builder's own needs, and they are not the whole story. A node
|
|
211
|
+
// with its own build is re-routed through `ctx.build` when that host marks itself
|
|
212
|
+
// `_parsemanCstOutput` (see the dispatch below) — and then the HOST, not the builder,
|
|
213
|
+
// is the consumer. Nearly every AST builder is `children => …`, arity 1, so under a CST
|
|
214
|
+
// host these nodes handed the host an EMPTY triviaLog and absent fields and state, and
|
|
215
|
+
// an empty trivia log is indistinguishable from a node that genuinely had none.
|
|
216
|
+
//
|
|
217
|
+
// The COMPILED engine settles this at build time (`compile(g, { hostMode: 'cst' })`,
|
|
218
|
+
// added in 0.37.0), where it costs nothing. The interpreter has no compile step and
|
|
219
|
+
// stays dynamic, so it re-decides per parse — which is the same answer, reached the
|
|
220
|
+
// only way this engine can reach it.
|
|
221
|
+
const capturesTrivia = captureTrivia || trailingTrivia || (build ? buildReadsTrivia(def) : project === undefined)
|
|
222
|
+
const clonesState = build ? buildReadsState(def) : project === undefined
|
|
223
|
+
const hasOwnFields = parserHasOwnFields(combinator)
|
|
224
|
+
const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : project === undefined)
|
|
225
|
+
// First-set fail-fast (mirrors emitNode's codegen guard): a node whose body can't
|
|
226
|
+
// match empty and whose first set can't start here can only fail, so reject BEFORE
|
|
227
|
+
// allocating the CST capture frame. Sound and output-neutral — the failing body
|
|
228
|
+
// would capture nothing and beginCstNodeCapture saves/restores every buffer, so
|
|
229
|
+
// skipping returns the same failure the body's start-fail produces. Skipped under a
|
|
230
|
+
// completions probe / tolerant recovery, where the swallowed failure still feeds the
|
|
231
|
+
// probe (matching the codegen guard's `!ctx.recovery` gate).
|
|
232
|
+
const guardable = combinator._meta.firstSet.kind !== 'any' && !matchesEmpty(combinator)
|
|
233
|
+
let failExpected: string[] | undefined
|
|
234
|
+
return {
|
|
235
|
+
_tag: 'node',
|
|
236
|
+
_meta: meta,
|
|
237
|
+
_def: def,
|
|
238
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<N> {
|
|
239
|
+
if (guardable && ctx._probe === undefined && !ctx._tolerant && !startsFirstSet(combinator, input, pos)) {
|
|
240
|
+
if (failExpected === undefined) {
|
|
241
|
+
const e = deriveExpected(combinator)
|
|
242
|
+
failExpected = e.length > 0 ? e : [combinator._tag]
|
|
243
|
+
}
|
|
244
|
+
return { ok: false, expected: failExpected, span: { start: pos, end: pos } }
|
|
245
|
+
}
|
|
246
|
+
// Under a positioned-CST host a direct builder is bypassed, so capture must follow
|
|
247
|
+
// what the HOST reads. Mirrors `hostMode: 'cst'` in the compiled engine, which
|
|
248
|
+
// captures unconditionally there because it knows the mode at compile time.
|
|
249
|
+
const hasDirectValue = build !== undefined || project !== undefined
|
|
250
|
+
const hostCst = hasDirectValue && cstOutputHost(ctx.build)
|
|
251
|
+
const structural = !hasDirectValue
|
|
252
|
+
const hostCapturesThisType = structural
|
|
253
|
+
&& def.type !== undefined
|
|
254
|
+
&& ctx.build?._parsemanCaptureTrivia !== undefined
|
|
255
|
+
? ctx.build._parsemanCaptureTrivia(def.type)
|
|
256
|
+
: undefined
|
|
257
|
+
// Structural nodes capture for their host by default, but the host may
|
|
258
|
+
// narrow that default by node type. Grammar-owned capture is authoritative:
|
|
259
|
+
// an explicit `captureTrivia`/`trailingTrivia` request cannot be disabled by
|
|
260
|
+
// the host. Direct builders (including a CST host that bypasses one) retain
|
|
261
|
+
// their existing arity/CST-host decision and never consult this predicate.
|
|
262
|
+
const effTrivia = structural
|
|
263
|
+
? captureTrivia || trailingTrivia || hostCapturesThisType !== false
|
|
264
|
+
: capturesTrivia || hostCst
|
|
265
|
+
const effFields = capturesFields || (hostCst && hasOwnFields)
|
|
266
|
+
const saved = beginCstNodeCapture(ctx)
|
|
267
|
+
const savedFields = ctx._fields
|
|
268
|
+
ctx._fields = effFields ? [] : undefined
|
|
269
|
+
// Per-node-type trivia-kind mask: a structural (host-built) node may want
|
|
270
|
+
// only certain kinds captured (comments for Ruleset, whitespace for
|
|
271
|
+
// CompoundSelector). Scoped here, restored below — matches the compiled path.
|
|
272
|
+
const savedMask = ctx._triviaCaptureMask
|
|
273
|
+
if (build === undefined && project === undefined && ctx.build?._parsemanTriviaKinds !== undefined && def.type !== undefined) {
|
|
274
|
+
ctx._triviaCaptureMask = ctx.build._parsemanTriviaKinds(def.type)
|
|
275
|
+
}
|
|
276
|
+
// Short-circuit the per-node trivia push (scanTrivia gates on captureTrivia)
|
|
277
|
+
// without touching the global _triviaLog, which is committed independently.
|
|
278
|
+
if (!effTrivia) ctx.captureTrivia = false
|
|
279
|
+
let r = combinator.parse(input, pos, ctx)
|
|
280
|
+
if (r.ok && trailingTrivia && ctx.trivia) {
|
|
281
|
+
const end = consumeTrivia(input, r.span.end, ctx)
|
|
282
|
+
r = { ...r, span: { start: r.span.start, end } }
|
|
283
|
+
}
|
|
284
|
+
const fields = effFields ? buildFieldMap(ctx._fields) : undefined
|
|
285
|
+
ctx._fields = savedFields
|
|
286
|
+
ctx._triviaCaptureMask = savedMask
|
|
287
|
+
const { children, rawChildren, triviaLog } = endCstNodeCapture(ctx, saved)
|
|
288
|
+
|
|
289
|
+
if (!r.ok) return r
|
|
290
|
+
|
|
291
|
+
// unwrap/collapse: a single captured child IS the value — skip build.
|
|
292
|
+
const st = clonesState && ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined
|
|
293
|
+
const nodeType = def.type ?? missingInferredType()
|
|
294
|
+
const cstOutput = cstOutputHost(ctx.build)
|
|
295
|
+
const span = ctx.trackLines ? annotateSpanFromLineContext(r.span, ctx) : r.span
|
|
296
|
+
// A direct builder that never declared `state` still owes the host its snapshot.
|
|
297
|
+
// The clone happens AFTER the body, so unlike trivia it needs no gate up front —
|
|
298
|
+
// build it here, on a branch the eval-AST path never takes. Matches what
|
|
299
|
+
// `hostMode: 'cst'` does in the compiled engine.
|
|
300
|
+
const hostState = clonesState
|
|
301
|
+
? st
|
|
302
|
+
: ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined
|
|
303
|
+
const built: unknown = unwrap && children.length === 1
|
|
304
|
+
? unwrapChild(children[0])
|
|
305
|
+
: collapse && children.length === 1
|
|
306
|
+
? children[0]
|
|
307
|
+
// Collapse applies wherever the node's VALUE comes from the host: a structural
|
|
308
|
+
// node, or ANY node under a positioned-CST host, where the direct builder is
|
|
309
|
+
// bypassed and the produced thing is a host-owned CST node either way. Gating on
|
|
310
|
+
// `!build` alone made `cstBuildHost({ collapse })` a documented no-op for every
|
|
311
|
+
// grammar whose rules carry reducers — silently, in both engines.
|
|
312
|
+
: (cstOutput || (!build && project === undefined))
|
|
313
|
+
&& ctx.build?._parsemanCstCollapse
|
|
314
|
+
&& children.length === 1
|
|
315
|
+
&& rawChildren.length === 1
|
|
316
|
+
&& ctx.build._parsemanCstCollapse(nodeType, children[0], children, rawChildren)
|
|
317
|
+
? children[0]
|
|
318
|
+
: project !== undefined
|
|
319
|
+
? cstOutput && ctx.build
|
|
320
|
+
? ctx.build(nodeType, children, fields, span, rawChildren, triviaLog, hostState, tags)
|
|
321
|
+
: projectChild(children, project, nodeType)
|
|
322
|
+
: build
|
|
323
|
+
// A direct builder normally owns its result. The positioned-CST host is
|
|
324
|
+
// the one exception: it must never receive an arbitrary AST object as a
|
|
325
|
+
// child of a CST node, so build this grammar node through that host.
|
|
326
|
+
? cstOutput && ctx.build
|
|
327
|
+
? ctx.build(nodeType, children, fields, span, rawChildren, triviaLog, hostState, tags)
|
|
328
|
+
: build(children, fields, span, rawChildren, triviaLog, st)
|
|
329
|
+
// Structural node: a `ctx.build` host if present, else a default CST.
|
|
330
|
+
: ctx.build
|
|
331
|
+
? ctx.build(nodeType, children, fields, span, rawChildren, triviaLog, st, tags)
|
|
332
|
+
: { _tag: 'node', type: nodeType, span: ctx.trackLines ? { ...span } : { start: r.span.start, end: r.span.end }, state: st ?? null, children }
|
|
333
|
+
const rawEntry = isCstChild(built)
|
|
334
|
+
? built
|
|
335
|
+
// A direct semantic object is opaque to the raw CST, but its source is
|
|
336
|
+
// not. Preserve the exact matched span so legacy/structural parents can
|
|
337
|
+
// retain text and trivia without fabricating an empty token.
|
|
338
|
+
: { _tag: 'leaf', value: typeof built === 'string' ? built : typeof built === 'object' && built !== null ? input.slice(r.span.start, r.span.end) : '', span }
|
|
339
|
+
if (saved.buf !== undefined || saved.ch !== undefined) {
|
|
340
|
+
pushCstChild(ctx, built, rawEntry)
|
|
341
|
+
}
|
|
342
|
+
return { ok: true, value: built as N, span }
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
|
|
2
|
+
import { any } from './first-set.ts'
|
|
3
|
+
import { saveLookaheadMark, rollbackLookahead } from './trivia-skip.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Negative lookahead. Succeeds (consuming nothing) when `combinator` fails;
|
|
7
|
+
* fails when `combinator` succeeds.
|
|
8
|
+
*
|
|
9
|
+
* The standard way to match a keyword without also matching the prefix
|
|
10
|
+
* of a longer identifier:
|
|
11
|
+
*
|
|
12
|
+
* const kwTrue = sequence(literal('true'), not(regex(/\w/)))
|
|
13
|
+
* // matches "true" in "true && x" but NOT in "trueish" or "trueness"
|
|
14
|
+
*/
|
|
15
|
+
export function not(combinator: Combinator<unknown>): Combinator<null> {
|
|
16
|
+
const meta: ParserMeta = {
|
|
17
|
+
firstSet: any(), // we don't know what NOT matches
|
|
18
|
+
canMatchNewline: false,
|
|
19
|
+
isTrivia: false,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
_tag: 'not',
|
|
24
|
+
_meta: meta,
|
|
25
|
+
_def: { tag: 'not', parser: combinator },
|
|
26
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<null> {
|
|
27
|
+
// Negative lookahead is a pure predicate: whatever the inner attempt
|
|
28
|
+
// captured (CST leaves/rawChildren/per-node trivia/fields), recovered (a
|
|
29
|
+
// ParseError pushed to ctx._errors + embedded via captureError), or committed
|
|
30
|
+
// to the GLOBAL `_triviaLog` must be rolled back on BOTH outcomes — not()
|
|
31
|
+
// consumes nothing and so may leave no side effect.
|
|
32
|
+
//
|
|
33
|
+
// `rollbackLookahead`, not `rollbackCstCapture`: the latter's mark omits two
|
|
34
|
+
// further sinks a probe can write.
|
|
35
|
+
// - `_triviaLog`, which `scanTrivia().commit()` writes when the probed body
|
|
36
|
+
// skips ambient trivia between terms. Missing it duplicates every probed
|
|
37
|
+
// span once the region is parsed for real — `_triviaLog` has no dedup
|
|
38
|
+
// anywhere (`triviaEntries()` is a positional view over the flat array),
|
|
39
|
+
// so the duplicate reaches output.
|
|
40
|
+
// - `_probe.best`, the completions tracker, for the same reason `peek()`
|
|
41
|
+
// restores it: expectations raised INSIDE a zero-width probe are not
|
|
42
|
+
// reachable from the enclosing grammar at the cursor, so leaving them
|
|
43
|
+
// offers unreachable tokens as completions.
|
|
44
|
+
// Using the shared lookahead helper means neither can silently regress the
|
|
45
|
+
// next time a sink is added.
|
|
46
|
+
const mark = saveLookaheadMark(ctx)
|
|
47
|
+
const result = combinator.parse(input, pos, ctx)
|
|
48
|
+
rollbackLookahead(ctx, mark)
|
|
49
|
+
if (result.ok) {
|
|
50
|
+
return { ok: false, expected: [`not(${combinator._tag})`], span: { start: pos, end: pos } }
|
|
51
|
+
}
|
|
52
|
+
return { ok: true, value: null, span: { start: pos, end: pos } }
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import type { Combinator } from '../types.ts'
|
|
2
|
+
import type { HostMode } from '../cst/host-mode.ts'
|
|
3
|
+
import { ref } from './ref.ts'
|
|
4
|
+
import { markUnusedValues } from '../compiler/value-usage.ts'
|
|
5
|
+
import { parser as grammarParser } from './grammar.ts'
|
|
6
|
+
import { collectGrammarReflection, NODE_TAG, NODE_TYPE, type GrammarWithReflection } from '../cst/reflection.ts'
|
|
7
|
+
import { attachGrammarReflection } from '../cst/reflection-attach.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Non-enumerable key on a `rules()` result holding the factory's declaration
|
|
11
|
+
* order (the returned object's key order), which differs from the result's own
|
|
12
|
+
* reference-creation key order. Read by `parseman/spec` for source-order output.
|
|
13
|
+
*/
|
|
14
|
+
export const RULE_ORDER = '__parsemanRuleOrder'
|
|
15
|
+
|
|
16
|
+
function tagRule(r: Combinator<unknown>, key: string): void {
|
|
17
|
+
;(r as unknown as { _ruleName?: string })._ruleName = key
|
|
18
|
+
if (r._def.tag === 'node' && r._def.type === undefined) r._def.type = key
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type DefinableRef = Combinator<unknown> & { define(p: Combinator<unknown>): void }
|
|
22
|
+
|
|
23
|
+
function isDefinableRef(v: unknown): v is DefinableRef {
|
|
24
|
+
return !!v
|
|
25
|
+
&& typeof v === 'object'
|
|
26
|
+
&& '_def' in v
|
|
27
|
+
&& (v as { _def: { tag?: string } })._def.tag === 'lazy'
|
|
28
|
+
&& typeof (v as { define?: unknown }).define === 'function'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ruleNameOf(r: Combinator<unknown>): string | undefined {
|
|
32
|
+
return (r as unknown as { _ruleName?: string })._ruleName
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isNamedRuleRefForAnotherRule(r: Combinator<unknown>, key: string): boolean {
|
|
36
|
+
const name = r._def.tag === 'lazy' ? ruleNameOf(r) : undefined
|
|
37
|
+
return name !== undefined && name !== key
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Define named grammar rules without forward declarations.
|
|
42
|
+
*
|
|
43
|
+
* Pass a factory that receives all rule names as references (via a Proxy)
|
|
44
|
+
* and returns a record of combinators. rules() handles creating ref()
|
|
45
|
+
* placeholders and wiring them up — the user never sees ref() at all.
|
|
46
|
+
*
|
|
47
|
+
* const { value } = rules(g => ({
|
|
48
|
+
* value: choice(g.object, g.array, str, num, bool, nil),
|
|
49
|
+
* object: transform(sequence('{', sepBy(g.pair, ','), '}'), Object.fromEntries),
|
|
50
|
+
* array: transform(sequence('[', sepBy(g.value, ','), ']'), ([, items]) => items),
|
|
51
|
+
* pair: transform(sequence(g.key, literal(':'), g.value), ([k,, v]) => [k, v]),
|
|
52
|
+
* }))
|
|
53
|
+
*
|
|
54
|
+
* Not every name in the factory must appear in the returned object — local helpers
|
|
55
|
+
* (like `comma`, `key`) can be plain const inside the factory and composed normally.
|
|
56
|
+
* Only names that OTHER rules reference via `g.xxx` need to be in the returned record.
|
|
57
|
+
*
|
|
58
|
+
* TypeScript: use an explicit type parameter for full type safety on `g`:
|
|
59
|
+
* rules<{ value: Combinator<JSONValue>; array: Combinator<JSONValue[]> }>(g => ({ ... }))
|
|
60
|
+
* Without it, `g.*` accesses are typed as `any` but the return is still inferred.
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* Grammar-level options for `rules()`. Parity with `parser({...})`, but declared
|
|
64
|
+
* ONCE for the whole grammar instead of wrapped around a scope. `trivia` becomes
|
|
65
|
+
* the ambient trivia for every rule (installed at the parse entry, inherited
|
|
66
|
+
* everywhere, incremental parse included); `parser({trivia})` / `noTrivia` still
|
|
67
|
+
* override it locally for a sub-region.
|
|
68
|
+
*/
|
|
69
|
+
export type RulesOptions = {
|
|
70
|
+
/** Ambient trivia for the whole grammar. See `parser({ trivia })` for the shape
|
|
71
|
+
* (`null` clears it — equivalent to omitting it at the grammar level). */
|
|
72
|
+
trivia?: Combinator<unknown> | null
|
|
73
|
+
/**
|
|
74
|
+
* Ambient scan-skip for the whole grammar: opaque non-trivia units (strings,
|
|
75
|
+
* balanced brackets, …) that a `scanTo`/`balanced` with no explicit `skip`
|
|
76
|
+
* consults so a sentinel hidden inside one is never matched. Declared ONCE here
|
|
77
|
+
* and inherited everywhere, mirroring `trivia`. `null`/absent = none.
|
|
78
|
+
*/
|
|
79
|
+
scanSkip?: Combinator<unknown>[] | null
|
|
80
|
+
/**
|
|
81
|
+
* Compile-time host mode for this grammar, same meaning as `compile(g, { hostMode })`
|
|
82
|
+
* and `compose(items, { hostMode })`: `'ast'` (default) emits each direct builder's
|
|
83
|
+
* own result and NO positioned-CST branch; `'cst'` builds every node through the
|
|
84
|
+
* `ctx.build` host and captures unconditionally.
|
|
85
|
+
*
|
|
86
|
+
* Declaring it HERE is what lets ONE grammar source serve both consumers under the
|
|
87
|
+
* macro, which cannot take a compile option any other way:
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* const factory = (g: any) => ({ … })
|
|
91
|
+
* export const grammar = rules({ trivia: rw }, factory)
|
|
92
|
+
* export const cstGrammar = rules({ trivia: rw, hostMode: 'cst' }, factory)
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* Two call sites over one factory, so the macro emits two independent top-level
|
|
96
|
+
* artifacts and each bundle tree-shakes away the one it does not import. Neither
|
|
97
|
+
* pays the other's cost — which is the whole reason host mode is a compile-time
|
|
98
|
+
* decision rather than a per-node runtime read.
|
|
99
|
+
*
|
|
100
|
+
* The INTERPRETER routes dynamically off the host and does not need this; it is
|
|
101
|
+
* still recorded, so `run()` can refuse a mismatched host once per parse instead of
|
|
102
|
+
* quietly producing the wrong tree shape.
|
|
103
|
+
*/
|
|
104
|
+
hostMode?: HostMode
|
|
105
|
+
/**
|
|
106
|
+
* Compile-time line tracking for this grammar. Under the macro this emits a
|
|
107
|
+
* separate line-aware artifact from the same factory; the default artifact
|
|
108
|
+
* remains free of line-tracking helpers and branches.
|
|
109
|
+
*/
|
|
110
|
+
trackLines?: boolean
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Options-first, mirroring `parser({ opts }, combinator)` — set once on the grammar
|
|
114
|
+
// vs scope it locally, same options in the same position. The bare `rules(factory)`
|
|
115
|
+
// form is unchanged. The impl also tolerates the legacy `rules(factory, opts)` order.
|
|
116
|
+
type RuleNodeType<K extends string, C> =
|
|
117
|
+
C extends { readonly [NODE_TYPE]?: infer T extends string }
|
|
118
|
+
? [T] extends [never] ? K : T
|
|
119
|
+
: K
|
|
120
|
+
|
|
121
|
+
type RuleNodeTag<C> =
|
|
122
|
+
C extends { readonly [NODE_TAG]?: infer Tag extends string } ? Tag : never
|
|
123
|
+
|
|
124
|
+
type RuleMapNodeType<T extends Record<string, Combinator<unknown>>> = {
|
|
125
|
+
[K in keyof T & string]: RuleNodeType<K, T[K]>
|
|
126
|
+
}[keyof T & string]
|
|
127
|
+
|
|
128
|
+
type RuleMapNodeTag<T extends Record<string, Combinator<unknown>>> = {
|
|
129
|
+
[K in keyof T & string]: RuleNodeTag<T[K]>
|
|
130
|
+
}[keyof T & string]
|
|
131
|
+
|
|
132
|
+
type RulesResult<T extends Record<string, Combinator<unknown>>> =
|
|
133
|
+
T & GrammarWithReflection<RuleMapNodeType<T>, RuleMapNodeTag<T>>
|
|
134
|
+
|
|
135
|
+
export function rules<T extends Record<string, Combinator<unknown>>>(factory: (self: any) => T): RulesResult<T>
|
|
136
|
+
export function rules<T extends Record<string, Combinator<unknown>>>(options: RulesOptions, factory: (self: any) => T): RulesResult<T>
|
|
137
|
+
export function rules<T extends Record<string, Combinator<unknown>>>(
|
|
138
|
+
a: ((self: any) => T) | RulesOptions,
|
|
139
|
+
b?: (self: any) => T,
|
|
140
|
+
): RulesResult<T> {
|
|
141
|
+
const factory = (typeof a === 'function' ? a : b) as (self: any) => T
|
|
142
|
+
const options = (typeof a === 'function' ? b : a) as RulesOptions | undefined
|
|
143
|
+
const cache: Partial<T> = {}
|
|
144
|
+
|
|
145
|
+
// Proxy: accessing any property creates a ref() placeholder on first touch.
|
|
146
|
+
const proxy = new Proxy(cache, {
|
|
147
|
+
get(target, key) {
|
|
148
|
+
if (typeof key !== 'string') return undefined
|
|
149
|
+
const record = target as Record<string, Combinator<unknown>>
|
|
150
|
+
if (!(key in record)) {
|
|
151
|
+
const r = ref()
|
|
152
|
+
// Tag the placeholder with its rule name so the linkable compiler can
|
|
153
|
+
// emit a by-name `_r_<key>` call for a reference to a rule defined in
|
|
154
|
+
// ANOTHER artifact (resolved at fuse time) — see compileLinkable.
|
|
155
|
+
tagRule(r, key)
|
|
156
|
+
record[key] = r
|
|
157
|
+
}
|
|
158
|
+
return record[key]
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Evaluate all rule definitions. JavaScript evaluates object-literal values
|
|
163
|
+
// left-to-right, so any `g.ruleName` access inside triggers placeholder creation
|
|
164
|
+
// before the rule's own parser is built — enabling forward references.
|
|
165
|
+
const definitions = factory(proxy)
|
|
166
|
+
|
|
167
|
+
// Fill each ref with its actual definition, or store directly if never accessed via proxy.
|
|
168
|
+
for (const key of Object.keys(definitions)) {
|
|
169
|
+
const placeholder = (cache as Record<string, Combinator<unknown>>)[key]
|
|
170
|
+
const parser = (definitions as Record<string, Combinator<unknown>>)[key]!
|
|
171
|
+
if (placeholder === parser) {
|
|
172
|
+
throw new Error(`rules(): rule "${key}" cannot be a direct alias to itself`)
|
|
173
|
+
}
|
|
174
|
+
if (isDefinableRef(placeholder)) {
|
|
175
|
+
if (!isNamedRuleRefForAnotherRule(parser, key)) tagRule(parser, key)
|
|
176
|
+
placeholder.define(parser)
|
|
177
|
+
// Propagate actual first-set so later choices wrapping this ref get correct dispatch.
|
|
178
|
+
placeholder._meta.firstSet = parser._meta.firstSet
|
|
179
|
+
placeholder._meta.canMatchNewline = parser._meta.canMatchNewline
|
|
180
|
+
} else if (isNamedRuleRefForAnotherRule(parser, key)) {
|
|
181
|
+
const alias = ref()
|
|
182
|
+
tagRule(alias, key)
|
|
183
|
+
alias.define(parser)
|
|
184
|
+
alias._meta.firstSet = parser._meta.firstSet
|
|
185
|
+
alias._meta.canMatchNewline = parser._meta.canMatchNewline
|
|
186
|
+
;(cache as Record<string, Combinator<unknown>>)[key] = alias
|
|
187
|
+
} else {
|
|
188
|
+
tagRule(parser, key)
|
|
189
|
+
;(cache as Record<string, Combinator<unknown>>)[key] = parser
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Declare the grammar-level ambient trivia on every rule, so parsing ANY rule
|
|
194
|
+
// as an entry installs it (run()/parse() read this), and the macro can seed the
|
|
195
|
+
// compiled map. `parser({trivia})` / `noTrivia` still override it locally.
|
|
196
|
+
// `!= null`: a `trivia: null` grammar clears trivia — same as omitting it at the
|
|
197
|
+
// grammar level — so store nothing (and never write null, which has no `_meta`).
|
|
198
|
+
if (options?.trivia != null) {
|
|
199
|
+
for (const key of Object.keys(definitions)) {
|
|
200
|
+
const rule = (cache as Record<string, Combinator<unknown>>)[key]
|
|
201
|
+
// Skip trivia rules (e.g. the grammar's `rw`, returned so the driver can
|
|
202
|
+
// reach it as `g.rw`): a trivia rule must never carry ambient trivia, or it
|
|
203
|
+
// would recursively skip trivia within itself. Mirrors the codegen guard.
|
|
204
|
+
if (rule && !rule._meta.isTrivia) (rule._meta as { grammarTrivia?: Combinator<unknown> }).grammarTrivia = options.trivia
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Grammar-level ambient scan-skip, mirroring the trivia stamp above: every
|
|
209
|
+
// non-trivia rule carries it so any parse entry installs `ctx.scanSkip` and the
|
|
210
|
+
// compiled map can seed it. `!= null` so `scanSkip: null` clears (stores nothing).
|
|
211
|
+
if (options?.scanSkip != null) {
|
|
212
|
+
for (const key of Object.keys(definitions)) {
|
|
213
|
+
const rule = (cache as Record<string, Combinator<unknown>>)[key]
|
|
214
|
+
if (rule && !rule._meta.isTrivia) (rule._meta as { grammarScanSkip?: Combinator<unknown>[] }).grammarScanSkip = options.scanSkip
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Grammar-level host mode, mirroring the two stamps above. Only `'cst'` is recorded:
|
|
219
|
+
// `'ast'` is the default everywhere, so stamping it would put a field on every rule of
|
|
220
|
+
// every grammar to say "unchanged". Trivia rules are skipped for the same reason as
|
|
221
|
+
// above — they build no nodes, so the mode is meaningless on them.
|
|
222
|
+
if (options?.hostMode === 'cst') {
|
|
223
|
+
for (const key of Object.keys(definitions)) {
|
|
224
|
+
const rule = (cache as Record<string, Combinator<unknown>>)[key]
|
|
225
|
+
if (rule && !rule._meta.isTrivia) (rule._meta as { grammarHostMode?: HostMode }).grammarHostMode = 'cst'
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (options?.trackLines === true) {
|
|
230
|
+
for (const key of Object.keys(definitions)) {
|
|
231
|
+
const rule = (cache as Record<string, Combinator<unknown>>)[key]
|
|
232
|
+
if (rule && !rule._meta.isTrivia) (rule._meta as { grammarTrackLines?: true }).grammarTrackLines = true
|
|
233
|
+
}
|
|
234
|
+
for (const key of Object.keys(definitions)) {
|
|
235
|
+
const rule = (cache as Record<string, Combinator<unknown>>)[key]
|
|
236
|
+
if (rule && !rule._meta.isTrivia && rule._def.tag !== 'grammar') {
|
|
237
|
+
const wrapped = grammarParser({ trackLines: true }, rule)
|
|
238
|
+
tagRule(wrapped, key)
|
|
239
|
+
;(wrapped._meta as { grammarTrackLines?: true }).grammarTrackLines = true
|
|
240
|
+
;(cache as Record<string, Combinator<unknown>>)[key] = wrapped
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Dead-value analysis: mark container aggregates that only feed a node()'s
|
|
246
|
+
// capture so the interpreter (and, via the same flag, the compiled output) skips
|
|
247
|
+
// building them. Each rule is its own root — refs are boundaries (see value-usage).
|
|
248
|
+
for (const key of Object.keys(definitions)) {
|
|
249
|
+
markUnusedValues((cache as Record<string, Combinator<unknown>>)[key]!)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Record the factory's DECLARATION order (the returned object's key order).
|
|
253
|
+
// `cache`'s own key order is reference-creation order — a Proxy artifact — so
|
|
254
|
+
// it can lead with an internal rule instead of the entry rule. Consumers that
|
|
255
|
+
// want the order the author actually wrote (e.g. `parseman/spec`) read this.
|
|
256
|
+
// Non-enumerable, so Object.keys / spread / for-in over the grammar are
|
|
257
|
+
// unaffected and every existing consumer sees exactly the rules it did before.
|
|
258
|
+
Object.defineProperty(cache, RULE_ORDER, {
|
|
259
|
+
value: Object.keys(definitions),
|
|
260
|
+
enumerable: false,
|
|
261
|
+
configurable: true,
|
|
262
|
+
})
|
|
263
|
+
attachGrammarReflection(cache, collectGrammarReflection(Object.keys(definitions).map(key => [key, (cache as Record<string, Combinator<unknown>>)[key]!])))
|
|
264
|
+
|
|
265
|
+
return cache as RulesResult<T>
|
|
266
|
+
}
|