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,615 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a compiled rule map BACK to a compact combinator-construction source
|
|
3
|
+
* expression — the "IR" a composed grammar carries instead of ~1MB of fully
|
|
4
|
+
* lowered `_r_<Name>` function source. At fuse time the expression is evaluated
|
|
5
|
+
* (with the combinator constructors in scope) to reconstruct the rule map, then
|
|
6
|
+
* re-lowered via `compileLinkable` — identical behaviour, a fraction of the bytes.
|
|
7
|
+
*
|
|
8
|
+
* The output mirrors how a grammar is WRITTEN, not how it lowers:
|
|
9
|
+
* - a rule reference (`g.Foo`, a `lazy` carrying `_ruleName`) → `g["Foo"]`
|
|
10
|
+
* - a sub-combinator shared by identity (referenced ≥2×, e.g. a `balanced` const
|
|
11
|
+
* reused across rules) → a hoisted `const _s<N> = …`, referenced by name, so it
|
|
12
|
+
* stays shared through the round trip (and `compileLinkable` re-hoists it)
|
|
13
|
+
* - a self-referential combinator (`balanced`'s internal `self = ref()`) → a
|
|
14
|
+
* `ref()` + `.define()` closure, the same shape the library combinator emits
|
|
15
|
+
* - `transform`/`node` callbacks → their captured source (`fnSrc`/`buildSrc`)
|
|
16
|
+
*
|
|
17
|
+
* Returns null if the map can't be faithfully serialized (a callback without
|
|
18
|
+
* source, or an unsupported construct) — the caller then keeps the lowered source.
|
|
19
|
+
*/
|
|
20
|
+
import type { Combinator, DispatchMatcherCase, ParserDef } from '../types.ts'
|
|
21
|
+
import { rules } from '../combinators/parser.ts'
|
|
22
|
+
import { ref } from '../combinators/ref.ts'
|
|
23
|
+
import { regex } from '../combinators/regex.ts'
|
|
24
|
+
import { literal } from '../combinators/literal.ts'
|
|
25
|
+
import { keywords } from '../combinators/keywords.ts'
|
|
26
|
+
import { sequence } from '../combinators/sequence.ts'
|
|
27
|
+
import { choice } from '../combinators/choice.ts'
|
|
28
|
+
import { dispatch, endsWith, matches, otherwise, routed, startsWith, when } from '../combinators/dispatch.ts'
|
|
29
|
+
import { attempt } from '../combinators/attempt.ts'
|
|
30
|
+
import { many, oneOrMore, optional, sepBy, keepSeparator } from '../combinators/repeat.ts'
|
|
31
|
+
import { not } from '../combinators/not.ts'
|
|
32
|
+
import { peek } from '../combinators/peek.ts'
|
|
33
|
+
import { node } from '../combinators/node.ts'
|
|
34
|
+
import { parser } from '../combinators/grammar.ts'
|
|
35
|
+
import { scanTo, balanced, type BalancedAmbient } from '../combinators/scanTo.ts'
|
|
36
|
+
import { token, leaf } from '../combinators/token.ts'
|
|
37
|
+
import { classifiedTrivia, transform, trivia, label, field } from '../combinators/map.ts'
|
|
38
|
+
import { analyzeLabeledTrivia } from '../cst/trivia-kinds.ts'
|
|
39
|
+
import { expect as expectC } from '../combinators/expect.ts'
|
|
40
|
+
import { withCtx } from '../combinators/withCtx.ts'
|
|
41
|
+
import { adjacent, notAdjacent } from '../combinators/adjacency.ts'
|
|
42
|
+
|
|
43
|
+
type Comb = Combinator<unknown>
|
|
44
|
+
|
|
45
|
+
/** Child combinators of a def, in construction order (mirrors codegen's childrenOf). */
|
|
46
|
+
function childrenOf(def: ParserDef): Comb[] {
|
|
47
|
+
switch (def.tag) {
|
|
48
|
+
case 'sequence':
|
|
49
|
+
case 'choice': return def.parsers
|
|
50
|
+
case 'dispatch': return [def.selector, ...def.cases.map(entry => entry.parser), ...(def.matchers ? def.matchers.map(entry => entry.parser) : []), ...(def.otherwise ? [def.otherwise] : [])]
|
|
51
|
+
case 'many':
|
|
52
|
+
case 'oneOrMore':
|
|
53
|
+
case 'optional':
|
|
54
|
+
case 'attempt':
|
|
55
|
+
case 'transform':
|
|
56
|
+
case 'trivia':
|
|
57
|
+
case 'token':
|
|
58
|
+
case 'leaf':
|
|
59
|
+
case 'label':
|
|
60
|
+
case 'field':
|
|
61
|
+
case 'not':
|
|
62
|
+
case 'peek':
|
|
63
|
+
case 'node':
|
|
64
|
+
case 'withCtx':
|
|
65
|
+
case 'expect': return [def.parser]
|
|
66
|
+
case 'grammar': return def.triviaParser ? [def.parser, def.triviaParser] : [def.parser]
|
|
67
|
+
case 'sepBy': return [def.parser, def.separator]
|
|
68
|
+
case 'scanTo': return [def.sentinel, ...def.skip]
|
|
69
|
+
case 'routed': return def.fallback ? [def.fallback] : []
|
|
70
|
+
case 'lazy':
|
|
71
|
+
case 'literal':
|
|
72
|
+
case 'regex':
|
|
73
|
+
case 'keywords':
|
|
74
|
+
case 'guard':
|
|
75
|
+
case 'adjacency':
|
|
76
|
+
case 'recover':
|
|
77
|
+
case 'unknown': return []
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ruleNameOf = (c: Comb): string | undefined =>
|
|
82
|
+
(c as unknown as { _ruleName?: string })._ruleName
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A `balanced()` that must re-resolve ambient `scanSkip` into its interior.
|
|
86
|
+
*
|
|
87
|
+
* The obligation is recorded as an OWN PROPERTY (`_balancedAmbient`), deliberately
|
|
88
|
+
* outside `_def`, so every static analysis keeps seeing the eager interior. Structural
|
|
89
|
+
* serialization therefore loses it: the round trip emits the raw interior, the rebuilt
|
|
90
|
+
* object is an ordinary `transform`, and codegen's ambient-rebuild branch never fires.
|
|
91
|
+
* The composed parser then stops at a delimiter hidden inside a string or comment,
|
|
92
|
+
* while the interpreter and a direct compile of the same grammar do not.
|
|
93
|
+
*
|
|
94
|
+
* So a balanced round-trips as the CONSTRUCTOR CALL that built it, not as its lowered
|
|
95
|
+
* shape — the marker is re-created by `balanced()` itself. Its interior is derived, so
|
|
96
|
+
* the serializer treats it as an atom: it is not descended for sharing/recursion
|
|
97
|
+
* analysis (the interior's `self` ref is internal and must not surface as a cycle).
|
|
98
|
+
*/
|
|
99
|
+
const balancedOf = (c: Comb): { open: string; close: string; ownSkip: Comb[] } | undefined =>
|
|
100
|
+
(c as BalancedAmbient)._balancedAmbient
|
|
101
|
+
|
|
102
|
+
/** Resolve a lazy's target, or null if it isn't defined yet (external ref). */
|
|
103
|
+
function lazyTarget(c: Comb): Comb | null {
|
|
104
|
+
if (c._def.tag !== 'lazy') return null
|
|
105
|
+
try { return c._def.thunk() } catch { return null }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class Unserializable extends Error {}
|
|
109
|
+
|
|
110
|
+
/** `, { min, max, trailing }` — omitted entirely when everything is at its default. */
|
|
111
|
+
function repeatOpts(min: number, max: number | undefined, trailing?: string): string {
|
|
112
|
+
const parts: string[] = []
|
|
113
|
+
if (min !== 0) parts.push(`min: ${min}`)
|
|
114
|
+
if (max !== undefined) parts.push(`max: ${max}`)
|
|
115
|
+
if (trailing !== undefined) parts.push(`trailing: ${JSON.stringify(trailing)}`)
|
|
116
|
+
return parts.length === 0 ? '' : `, { ${parts.join(', ')} }`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function projectOpt(project: Extract<ParserDef, { tag: 'node' }>['project']): string | undefined {
|
|
120
|
+
if (project === undefined) return undefined
|
|
121
|
+
return `project: ${project}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function tagsOpt(tags: Extract<ParserDef, { tag: 'node' }>['tags']): string | undefined {
|
|
125
|
+
if (tags === undefined || tags.length === 0) return undefined
|
|
126
|
+
return `tags: ${JSON.stringify(tags)}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function matcherExpr(entry: DispatchMatcherCase): string {
|
|
130
|
+
switch (entry.kind) {
|
|
131
|
+
case 'startsWith':
|
|
132
|
+
return `startsWith(${JSON.stringify(entry.value)})`
|
|
133
|
+
case 'endsWith':
|
|
134
|
+
return `endsWith(${JSON.stringify(entry.value)})`
|
|
135
|
+
case 'matches':
|
|
136
|
+
return `matches(${new RegExp(entry.value, entry.flags ?? '').toString()})`
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function serializeRuleMap(
|
|
141
|
+
ruleMap: ReadonlyArray<readonly [string, Comb]>,
|
|
142
|
+
scanSkip?: readonly Comb[] | null,
|
|
143
|
+
): string | null {
|
|
144
|
+
try {
|
|
145
|
+
return new Serializer(ruleMap, scanSkip ?? []).run()
|
|
146
|
+
} catch (e) {
|
|
147
|
+
if (e instanceof Unserializable) {
|
|
148
|
+
if (process.env.PARSEMAN_IR_DEBUG) console.error(`[ir] fallback: ${(e as Error).message}`)
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
throw e
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Reconstruct a rule map from serialized IR (the inverse of `serializeRuleMap`) —
|
|
156
|
+
* evaluate the combinator-construction expression with every constructor in scope.
|
|
157
|
+
* Used at fuse time (runtime linker + build-time plugin) to re-lower carried IR. */
|
|
158
|
+
export function evalRuleMapIR(ir: string): Array<[string, Comb]> {
|
|
159
|
+
// `_tf`/`_nd` reconstruct a transform/node AND restore its captured callback
|
|
160
|
+
// source (`_def.fnSrc`/`buildSrc`) so re-lowering inlines it statically. The live
|
|
161
|
+
// fn is only needed for interpreted mode; a self-contained transform source is
|
|
162
|
+
// eval'd, a node build (which may reference imported AST classes) is left to its
|
|
163
|
+
// source only. Macro-only validation metadata is carried as plain data: this
|
|
164
|
+
// runtime module never parses callback source or imports a compiler frontend.
|
|
165
|
+
const _tf = (child: Comb, src: string, recognitionOnly = false): Comb => {
|
|
166
|
+
let fn: (...a: unknown[]) => unknown
|
|
167
|
+
// eslint-disable-next-line no-eval
|
|
168
|
+
try { fn = (0, eval)(`(${src})`) } catch { fn = () => { throw new Error('IR transform fn not materialized') } }
|
|
169
|
+
const t = transform(child as never, fn as never)
|
|
170
|
+
;(t._def as { fnSrc?: string }).fnSrc = src
|
|
171
|
+
if (recognitionOnly) (t._def as { recognitionOnly?: boolean }).recognitionOnly = true
|
|
172
|
+
return t as Comb
|
|
173
|
+
}
|
|
174
|
+
const _lf = (child: Comb, src: string): Comb => {
|
|
175
|
+
let fn: (...a: unknown[]) => unknown
|
|
176
|
+
// eslint-disable-next-line no-eval
|
|
177
|
+
try { fn = (0, eval)(`(${src})`) } catch { fn = () => { throw new Error('IR leaf fn not materialized') } }
|
|
178
|
+
const l = leaf(child as never, fn as never)
|
|
179
|
+
;(l._def as { fnSrc?: string }).fnSrc = src
|
|
180
|
+
return l as Comb
|
|
181
|
+
}
|
|
182
|
+
const _nd = (type: string, child: Comb, src: string, opts?: unknown, staticError?: readonly string[], sigSrc?: string): Comb => {
|
|
183
|
+
if (staticError !== undefined && staticError.length > 0) {
|
|
184
|
+
throw new Error(`IR direct node builder for ${type} must be macro-static and self-contained; unsupported binding(s): ${staticError.join(', ')}`)
|
|
185
|
+
}
|
|
186
|
+
// A serialized direct builder needs an inert sentinel as well as buildSrc.
|
|
187
|
+
// `node(..., undefined)` is structural, so re-lowering a composed artifact
|
|
188
|
+
// silently routes it through ctx.build/default CST even though the IR still
|
|
189
|
+
// carries the callback source. The compiler is the only consumer that may
|
|
190
|
+
// materialize `buildSrc`; raw IR interpretation deliberately rejects direct
|
|
191
|
+
// builders rather than evaluating arbitrary captured source at runtime.
|
|
192
|
+
const n = node(type, child as never, (() => { throw new Error('IR node build requires static re-lowering') }) as never, opts as never)
|
|
193
|
+
;(n._def as { buildSrc?: string; buildStaticError?: readonly string[] }).buildSrc = src
|
|
194
|
+
if (staticError !== undefined) (n._def as { buildStaticError?: readonly string[] }).buildStaticError = staticError
|
|
195
|
+
// Analysis-only resolved reducer signature (see `buildAnalysisSrc`). Without it a
|
|
196
|
+
// re-lowered composed artifact silently re-acquires the fail-open capture cost the
|
|
197
|
+
// authoring module had already resolved away.
|
|
198
|
+
if (sigSrc !== undefined) (n._def as { buildSigSrc?: string }).buildSigSrc = sigSrc
|
|
199
|
+
return n as Comb
|
|
200
|
+
}
|
|
201
|
+
// `_gch` rebuilds a GATED choice AND restores its `_def.gateSrcs` (parallel to the
|
|
202
|
+
// arms) — the gate mirror of `_tf`/`_nd`. Each item is either a plain arm or a
|
|
203
|
+
// `[gateSource, arm]` tuple: the source is eval'd to the live predicate (interpreted
|
|
204
|
+
// mode) and recorded as the arm's gateSrcs entry so re-lowering INLINES the gate
|
|
205
|
+
// statically (keeping the artifact fusible via `emitFusedSource`). A missing/failed
|
|
206
|
+
// source falls back to a throwing predicate — the same contract as `_tf`.
|
|
207
|
+
const _gch = (items: Array<Comb | [string, Comb]>): Comb => {
|
|
208
|
+
const gateSrcs: (string | null)[] = []
|
|
209
|
+
const arms = items.map(it => {
|
|
210
|
+
if (Array.isArray(it)) {
|
|
211
|
+
const [src, comb] = it
|
|
212
|
+
gateSrcs.push(src)
|
|
213
|
+
let gate: (s: unknown) => boolean
|
|
214
|
+
// eslint-disable-next-line no-eval
|
|
215
|
+
try { gate = (0, eval)(`(${src})`) } catch { gate = () => { throw new Error('IR gate fn not materialized') } }
|
|
216
|
+
return { gate, combinator: comb }
|
|
217
|
+
}
|
|
218
|
+
gateSrcs.push(null)
|
|
219
|
+
return it
|
|
220
|
+
})
|
|
221
|
+
const c = (choice as (...a: unknown[]) => Comb)(...arms)
|
|
222
|
+
// `choice` is the authority on gate alignment; `gateSrcs` must line up 1:1 with
|
|
223
|
+
// the constructed `_def.gates`. They always do on the normal path (both from the
|
|
224
|
+
// same `items.map`), but assert it so a future change in `choice` that coalesces
|
|
225
|
+
// or drops arms can't silently ship a mis-aligned gate-source array.
|
|
226
|
+
const gates = (c._def as { gates?: unknown[] }).gates
|
|
227
|
+
if (gates && gates.length !== gateSrcs.length) {
|
|
228
|
+
throw new Error(`_gch: gateSrcs length ${gateSrcs.length} != choice gates length ${gates.length}`)
|
|
229
|
+
}
|
|
230
|
+
;(c._def as { gateSrcs?: (string | null)[] }).gateSrcs = gateSrcs
|
|
231
|
+
return c
|
|
232
|
+
}
|
|
233
|
+
// `_wc` rebuilds a `withCtx` AND restores its `_def.extraSrc` (the source of the
|
|
234
|
+
// `extra`/state value) — the withCtx mirror of `_tf`/`_nd`/`_gch`. The source is
|
|
235
|
+
// eval'd to the live `extra` value (interpreted mode) and recorded on the def so
|
|
236
|
+
// re-lowering INLINES the state getter statically (`() => (extraSrc)`), keeping the
|
|
237
|
+
// artifact fusible via `emitFusedSource`. A plain `withCtx(value, inner)` would
|
|
238
|
+
// leave `extraSrc` unset → codegen emits a source-less runtime closure (a non-static
|
|
239
|
+
// callback) → `emitFusedSource` fails and a downstream `compose()` silently falls
|
|
240
|
+
// back to a runtime fuse. A missing/failed source falls back to `undefined` state —
|
|
241
|
+
// the same best-effort contract as `_tf`.
|
|
242
|
+
const _wc = (src: string, inner: Comb): Comb => {
|
|
243
|
+
let extra: unknown
|
|
244
|
+
// eslint-disable-next-line no-eval
|
|
245
|
+
try { extra = (0, eval)(`(${src})`) } catch { extra = undefined }
|
|
246
|
+
const w = (withCtx as (e: unknown, c: Comb) => Comb)(extra, inner)
|
|
247
|
+
;(w._def as { extraSrc?: string }).extraSrc = src
|
|
248
|
+
return w
|
|
249
|
+
}
|
|
250
|
+
// eslint-disable-next-line no-new-func
|
|
251
|
+
const fn = new Function(
|
|
252
|
+
'rules', 'ref', 'regex', 'literal', 'keywords', 'sequence', 'choice', 'dispatch', 'when', 'startsWith', 'endsWith', 'matches', 'otherwise', 'routed', 'attempt',
|
|
253
|
+
'many', 'oneOrMore', 'optional', 'sepBy', 'keepSeparator', 'not', 'peek', 'node', 'parser',
|
|
254
|
+
'scanTo', 'balanced', 'token', 'leaf', 'transform', 'trivia', 'classifiedTrivia', 'label', 'field', 'expect', 'adjacent', 'notAdjacent', '_tf', '_lf', '_nd', '_gch', '_wc',
|
|
255
|
+
`return (${ir})`,
|
|
256
|
+
)
|
|
257
|
+
const map = fn(
|
|
258
|
+
rules, ref, regex, literal, keywords, sequence, choice, dispatch, when, startsWith, endsWith, matches, otherwise, routed, attempt,
|
|
259
|
+
many, oneOrMore, optional, sepBy, keepSeparator, not, peek, node, parser,
|
|
260
|
+
scanTo, balanced, token, leaf, transform, trivia, classifiedTrivia, label, field, expectC, adjacent, notAdjacent, _tf, _lf, _nd, _gch, _wc,
|
|
261
|
+
) as Record<string, Comb>
|
|
262
|
+
return Object.entries(map)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** A recursive combinator currently being emitted, and the local `ref()` var that
|
|
266
|
+
* stands in for its back-edge. Frames NEST: an inline recursive combinator inside
|
|
267
|
+
* another one must still be able to name the outer one's ref var. */
|
|
268
|
+
type SelfFrame = { comb: Comb; var: string }
|
|
269
|
+
|
|
270
|
+
class Serializer {
|
|
271
|
+
private counts = new Map<Comb, number>()
|
|
272
|
+
private selfRef = new Set<Comb>() // combinators reached by an unnamed lazy pointing back into their own subtree
|
|
273
|
+
private cycleInterior = new Set<Comb>() // combinators lying ON a self-reference cycle (strictly below its target)
|
|
274
|
+
private constName = new Map<Comb, string>()
|
|
275
|
+
private emitted = new Set<string>()
|
|
276
|
+
private decls: string[] = []
|
|
277
|
+
private ruleValues = new Set<Comb>()
|
|
278
|
+
private localRuleNames = new Map<Comb, string>()
|
|
279
|
+
private ruleMap: ReadonlyArray<readonly [string, Comb]>
|
|
280
|
+
private scanSkip: readonly Comb[]
|
|
281
|
+
|
|
282
|
+
constructor(ruleMap: ReadonlyArray<readonly [string, Comb]>, scanSkip: readonly Comb[] = []) {
|
|
283
|
+
this.ruleMap = ruleMap
|
|
284
|
+
this.scanSkip = scanSkip
|
|
285
|
+
for (const [name, c] of ruleMap) {
|
|
286
|
+
this.localRuleNames.set(c, name)
|
|
287
|
+
this.ruleValues.add(this.body(c))
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** A grammar-level `scanSkip` unit is an opaque TERMINAL (a string, a balanced
|
|
292
|
+
* bracket) — it must not reach a grammar-rule ref (`g[name]`), because the
|
|
293
|
+
* serialized `scanSkip` array sits in the `rules({ scanSkip })` OPTIONS position,
|
|
294
|
+
* outside the `(g) => …` factory where `g` is bound. A unit that does reference a
|
|
295
|
+
* rule can't be carried in options; bail to the full-pieces fallback. */
|
|
296
|
+
private assertNoRuleRef(c: Comb, seen: Set<Comb>): void {
|
|
297
|
+
if (seen.has(c)) return
|
|
298
|
+
seen.add(c)
|
|
299
|
+
if (c._def.tag === 'lazy') {
|
|
300
|
+
if (ruleNameOf(c) !== undefined) throw new Unserializable('scanSkip unit references a grammar rule')
|
|
301
|
+
const target = lazyTarget(c)
|
|
302
|
+
if (target) this.assertNoRuleRef(target, seen)
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
for (const child of childrenOf(c._def)) this.assertNoRuleRef(child, seen)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** A `rules()` map wraps each rule value in a named `lazy` proxy; the real body is
|
|
309
|
+
* its target (compileLinkable unwraps the same way). A ref INSIDE a body stays a
|
|
310
|
+
* `g[name]` ref — only the top-level entry is unwrapped. */
|
|
311
|
+
private body(c: Comb): Comb {
|
|
312
|
+
return c._def.tag === 'lazy' && ruleNameOf(c) !== undefined ? (lazyTarget(c) ?? c) : c
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
run(): string {
|
|
316
|
+
for (const [, c] of this.ruleMap) this.analyze(this.body(c), new Set())
|
|
317
|
+
for (const c of this.scanSkip) { this.assertNoRuleRef(c, new Set()); this.analyze(c, new Set()) }
|
|
318
|
+
// A shared sub-combinator (count ≥ 2) or a self-ref target gets a const, EXCEPT
|
|
319
|
+
// a top-level rule value (it already lives under its rule name) and EXCEPT a
|
|
320
|
+
// combinator lying on a self-reference cycle.
|
|
321
|
+
//
|
|
322
|
+
// A cycle's single lazy edge is the local `ref()` var bound by the enclosing
|
|
323
|
+
// `ref()/define()` closure, so it is only expressible INSIDE that closure. A
|
|
324
|
+
// hoisted const is emitted outside it, at decl scope, where the ref var does not
|
|
325
|
+
// exist — the back-edge then re-resolves through the cycle target's OWN const and
|
|
326
|
+
// the two decls reference each other eagerly. That is a genuine `const` cycle, and
|
|
327
|
+
// since `emitDecl` pushes the inner decl first it throws at compose time:
|
|
328
|
+
// const _s1 = many(choice(_s0, …)); const _s0 = (() => { … _s1 … })()
|
|
329
|
+
// ReferenceError: Cannot access '_s0' before initialization
|
|
330
|
+
// No declaration order fixes it, so cycle-interior nodes are inlined instead. They
|
|
331
|
+
// are re-hoisted by identity on re-lowering; the only cost is IR text, once per
|
|
332
|
+
// reference. `balanced()`'s interior is referenced once, so nothing existing moves.
|
|
333
|
+
let n = 0
|
|
334
|
+
for (const [c, count] of this.counts) {
|
|
335
|
+
if (this.ruleValues.has(c)) continue
|
|
336
|
+
if (this.cycleInterior.has(c)) continue
|
|
337
|
+
if (count >= 2 || this.selfRef.has(c)) this.constName.set(c, `_s${n++}`)
|
|
338
|
+
}
|
|
339
|
+
// Two decl scopes. `scanSkip` is emitted FIRST into `outerDecls` (an IIFE that
|
|
340
|
+
// wraps the whole rules() call) because the `{ scanSkip }` options object
|
|
341
|
+
// precedes the `(g) => …` factory and so cannot see consts declared inside it.
|
|
342
|
+
// A scanSkip unit is grammar-rule free (asserted above), so every const it
|
|
343
|
+
// reaches is g-free and safe in the outer scope; the `emitted` guard then keeps
|
|
344
|
+
// each remaining (factory-only, possibly `g[name]`-referencing) const inside the
|
|
345
|
+
// factory. With no scanSkip, `outerDecls` stays empty and the output is
|
|
346
|
+
// byte-identical to the pre-scanSkip form.
|
|
347
|
+
const outerDecls: string[] = []
|
|
348
|
+
const innerDecls: string[] = []
|
|
349
|
+
let optionsArg = ''
|
|
350
|
+
if (this.scanSkip.length > 0) {
|
|
351
|
+
this.decls = outerDecls
|
|
352
|
+
const units = this.scanSkip.map(c => this.ref(c, []))
|
|
353
|
+
optionsArg = `{ scanSkip: [${units.join(', ')}] }, `
|
|
354
|
+
}
|
|
355
|
+
this.decls = innerDecls
|
|
356
|
+
const body = this.ruleMap
|
|
357
|
+
.map(([name, c]) => ` ${JSON.stringify(name)}: ${this.wrap(this.body(c), [])}`)
|
|
358
|
+
.join(',\n')
|
|
359
|
+
// Shared consts go INSIDE the factory: they can reference `g[name]` rule refs,
|
|
360
|
+
// and `g` only exists in the factory scope.
|
|
361
|
+
const factory = innerDecls.length === 0
|
|
362
|
+
? `(g) => ({\n${body}\n})`
|
|
363
|
+
: `(g) => {\n${innerDecls.map(d => ' ' + d).join('\n')}\n return ({\n${body}\n})\n}`
|
|
364
|
+
const call = `rules(${optionsArg}${factory})`
|
|
365
|
+
return outerDecls.length === 0
|
|
366
|
+
? call
|
|
367
|
+
: `(() => {\n${outerDecls.map(d => ' ' + d).join('\n')}\n return ${call}\n})()`
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Count identity references and flag self-referential subtrees. `active` is the
|
|
371
|
+
* set of combinators on the current DFS path (to detect a lazy pointing back). */
|
|
372
|
+
private analyze(c: Comb, active: Set<Comb>): void {
|
|
373
|
+
const def = c._def
|
|
374
|
+
if (def.tag === 'lazy') {
|
|
375
|
+
if (ruleNameOf(c) !== undefined) return // named rule ref — resolved by g[name]
|
|
376
|
+
const target = lazyTarget(c)
|
|
377
|
+
if (!target) throw new Unserializable('unresolved unnamed ref')
|
|
378
|
+
if (active.has(target)) { // recursion into an ancestor
|
|
379
|
+
this.selfRef.add(target)
|
|
380
|
+
// Everything on the DFS path strictly BELOW `target` is on the cycle. `active`
|
|
381
|
+
// is a Set built in path order, so the suffix after `target` is exactly that
|
|
382
|
+
// path. These must not be hoisted out of the cycle's ref()/define() closure.
|
|
383
|
+
let past = false
|
|
384
|
+
for (const a of active) {
|
|
385
|
+
if (past) this.cycleInterior.add(a)
|
|
386
|
+
else if (a === target) past = true
|
|
387
|
+
}
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
this.analyze(target, active) // inline (unnamed, non-recursive) — rare
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
if (def.tag === 'guard' || def.tag === 'recover' || def.tag === 'unknown') {
|
|
394
|
+
throw new Unserializable(`unsupported tag ${def.tag}`)
|
|
395
|
+
}
|
|
396
|
+
this.counts.set(c, (this.counts.get(c) ?? 0) + 1)
|
|
397
|
+
if ((this.counts.get(c) ?? 0) > 1) return // already descended
|
|
398
|
+
// A balanced() is an atom: its interior is derived by the constructor, so descending
|
|
399
|
+
// would count derived nodes and mistake its internal `self` ref for a user cycle.
|
|
400
|
+
// Its own skip units are authored, so they are still analyzed.
|
|
401
|
+
const bal = balancedOf(c)
|
|
402
|
+
if (bal) {
|
|
403
|
+
active.add(c)
|
|
404
|
+
for (const unit of bal.ownSkip) this.analyze(unit, active)
|
|
405
|
+
active.delete(c)
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
active.add(c)
|
|
409
|
+
for (const child of childrenOf(def)) this.analyze(child, active)
|
|
410
|
+
active.delete(c)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Reference `c`: a const var if it has one (emitting its decl first), else inline.
|
|
414
|
+
* `frames` are the recursive combinators currently open around this position — an
|
|
415
|
+
* unnamed lazy back into any of them emits that frame's ref var instead of recursing.
|
|
416
|
+
* A const is never cycle-interior (see `run`), so its decl is emitted with NO frames:
|
|
417
|
+
* decl scope cannot see any ref var, and it never needs to. */
|
|
418
|
+
private ref(c: Comb, frames: readonly SelfFrame[]): string {
|
|
419
|
+
const cn = this.constName.get(c)
|
|
420
|
+
if (cn) { this.emitDecl(c); return cn }
|
|
421
|
+
// An un-hoisted recursive target still needs its own ref()/define() closure, and
|
|
422
|
+
// the enclosing frames stay lexically visible inside it.
|
|
423
|
+
if (this.selfRef.has(c)) return this.wrap(c, frames)
|
|
424
|
+
return this.expr(c, frames)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private emitDecl(c: Comb): void {
|
|
428
|
+
const name = this.constName.get(c)!
|
|
429
|
+
if (this.emitted.has(name)) return
|
|
430
|
+
this.emitted.add(name) // mark first (cycle-safe)
|
|
431
|
+
this.decls.push(`const ${name} = ${this.wrap(c, [])}`)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private selfN = 0
|
|
435
|
+
|
|
436
|
+
/** Emit `c`'s own constructor call, wrapping it in a `ref()`+`.define()` closure
|
|
437
|
+
* if it is self-referential (an unnamed lazy in its subtree points back to it) —
|
|
438
|
+
* the shape `balanced()` and other recursive library combinators build. */
|
|
439
|
+
private wrap(c: Comb, frames: readonly SelfFrame[]): string {
|
|
440
|
+
if (!this.selfRef.has(c)) return this.expr(c, frames)
|
|
441
|
+
const rv = `_rr${this.selfN++}`
|
|
442
|
+
const bodyExpr = this.expr(c, [...frames, { comb: c, var: rv }])
|
|
443
|
+
return `(() => { const ${rv} = ref(); const _b = ${bodyExpr}; ${rv}.define(_b); return _b })()`
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** Emit the constructor-call source for `c` (never a const shortcut for `c` itself). */
|
|
447
|
+
private expr(c: Comb, frames: readonly SelfFrame[]): string {
|
|
448
|
+
const def = c._def
|
|
449
|
+
const kid = (x: Comb) => this.ref(x, frames)
|
|
450
|
+
// Re-emit a balanced as its constructor call so `balanced()` re-creates the ambient
|
|
451
|
+
// marker on the far side. `raw: true` balanceds carry no marker and stay structural,
|
|
452
|
+
// which is correct — they opt out of ambient resolution by definition.
|
|
453
|
+
const bal = balancedOf(c)
|
|
454
|
+
if (bal) {
|
|
455
|
+
const skipArg = bal.ownSkip.length === 0 ? '' : `, { skip: [${bal.ownSkip.map(kid).join(', ')}] }`
|
|
456
|
+
return `balanced(${JSON.stringify(bal.open)}, ${JSON.stringify(bal.close)}${skipArg})`
|
|
457
|
+
}
|
|
458
|
+
switch (def.tag) {
|
|
459
|
+
case 'lazy': {
|
|
460
|
+
const name = this.localRuleNames.get(c) ?? ruleNameOf(c)
|
|
461
|
+
if (name !== undefined) return `g[${JSON.stringify(name)}]`
|
|
462
|
+
const target = lazyTarget(c)!
|
|
463
|
+
// Innermost matching frame wins.
|
|
464
|
+
for (let i = frames.length - 1; i >= 0; i--) {
|
|
465
|
+
const f = frames[i]!
|
|
466
|
+
if (f.comb === target) return f.var
|
|
467
|
+
}
|
|
468
|
+
return this.ref(target, frames)
|
|
469
|
+
}
|
|
470
|
+
case 'literal':
|
|
471
|
+
return `literal(${JSON.stringify(def.value)}${def.caseInsensitive ? ', { caseInsensitive: true }' : ''})`
|
|
472
|
+
case 'regex':
|
|
473
|
+
return `regex(${JSON.stringify(def.source)}, ${JSON.stringify(def.flags)})`
|
|
474
|
+
case 'keywords':
|
|
475
|
+
return `keywords(${JSON.stringify(def.words)}, { caseInsensitive: ${def.caseInsensitive}${def.boundary !== undefined ? `, boundary: ${JSON.stringify(def.boundary)}` : ''} })`
|
|
476
|
+
case 'sequence':
|
|
477
|
+
return `sequence(${def.parsers.map(kid).join(', ')})`
|
|
478
|
+
case 'choice': {
|
|
479
|
+
// Ungated choices stay `choice(a, b, …)` byte-for-byte as before.
|
|
480
|
+
const gates = def.gates
|
|
481
|
+
if (gates.every(g => g === null)) return `choice(${def.parsers.map(kid).join(', ')})`
|
|
482
|
+
// A gated choice round-trips through `_gch`, which rebuilds the choice AND
|
|
483
|
+
// re-attaches `_def.gateSrcs` from each arm's captured gate SOURCE. Preserving
|
|
484
|
+
// gateSrcs is load-bearing: on re-lowering, codegen inlines the gate from its
|
|
485
|
+
// source (a static callback), so the re-lowered artifact stays STATICALLY
|
|
486
|
+
// FUSIBLE. A plain `choice({ gate: fn, … })` would rebuild the predicate as a
|
|
487
|
+
// source-less runtime closure → a non-static callback → `emitFusedSource`
|
|
488
|
+
// (macro static fusion) fails and `compose()` silently falls back to a runtime
|
|
489
|
+
// fuse. Without a captured source we cannot re-emit the gate → keep full pieces.
|
|
490
|
+
const gateSrcs = def.gateSrcs
|
|
491
|
+
const items = def.parsers.map((p, i) => {
|
|
492
|
+
if (gates[i] === null) return kid(p)
|
|
493
|
+
const src = gateSrcs?.[i]
|
|
494
|
+
if (src == null) throw new Unserializable('choice gate() has no captured source')
|
|
495
|
+
// `[gateSource, arm]` — `_gch` evals the source to the live predicate and
|
|
496
|
+
// records it as the arm's `gateSrcs` entry.
|
|
497
|
+
return `[${JSON.stringify(src)}, ${kid(p)}]`
|
|
498
|
+
})
|
|
499
|
+
return `_gch([${items.join(', ')}])`
|
|
500
|
+
}
|
|
501
|
+
case 'dispatch': {
|
|
502
|
+
const arms = def.cases.map(entry =>
|
|
503
|
+
`when(${entry.keys.length === 1 ? JSON.stringify(entry.keys[0]) : JSON.stringify(entry.keys)}, ${kid(entry.parser)}${entry.caseInsensitive ? ', { caseInsensitive: true }' : ''})`
|
|
504
|
+
)
|
|
505
|
+
if (def.matchers) {
|
|
506
|
+
for (const entry of def.matchers) {
|
|
507
|
+
arms.push(`when(${matcherExpr(entry)}, ${kid(entry.parser)}${entry.caseInsensitive ? ', { caseInsensitive: true }' : ''})`)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (def.otherwise) arms.push(`otherwise(${kid(def.otherwise)})`)
|
|
511
|
+
return `dispatch(${kid(def.selector)}${arms.length === 0 ? '' : `, ${arms.join(', ')}`})`
|
|
512
|
+
}
|
|
513
|
+
case 'many': return `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`
|
|
514
|
+
// A `oneOrMore` def with min > 1 came from `many(x, { min: n })`; round-trip
|
|
515
|
+
// it through `many` so the bounds survive (oneOrMore defaults min to 1).
|
|
516
|
+
case 'oneOrMore': return def.min === 1 && def.max === undefined
|
|
517
|
+
? `oneOrMore(${kid(def.parser)})`
|
|
518
|
+
: `many(${kid(def.parser)}${repeatOpts(def.min, def.max)})`
|
|
519
|
+
case 'optional': return `optional(${kid(def.parser)})`
|
|
520
|
+
case 'attempt': return `attempt(${kid(def.parser)})`
|
|
521
|
+
// `keepSeparators` is an opt-in expressed at the SEPARATOR, not in the options
|
|
522
|
+
// bag, so it must round-trip as `keepSeparator(sep)` — serializing it as an
|
|
523
|
+
// option would reconstruct a grammar whose call site no longer states its own
|
|
524
|
+
// children arity, which is the exact defect this API shape exists to prevent.
|
|
525
|
+
case 'sepBy': return `sepBy(${kid(def.parser)}, ${def.keepSeparators ? `keepSeparator(${kid(def.separator)})` : kid(def.separator)}${repeatOpts(def.min, def.max, def.trailing)})`
|
|
526
|
+
case 'not': return `not(${kid(def.parser)})`
|
|
527
|
+
case 'peek': return `peek(${kid(def.parser)})`
|
|
528
|
+
case 'routed': return def.fallback === undefined ? 'routed()' : `routed(${kid(def.fallback)})`
|
|
529
|
+
case 'trivia': return `trivia(${kid(def.parser)})`
|
|
530
|
+
case 'token': return `token(${kid(def.parser)})`
|
|
531
|
+
case 'leaf': {
|
|
532
|
+
if (def.fnSrc === undefined) throw new Unserializable('leaf without fnSrc')
|
|
533
|
+
return `_lf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)})`
|
|
534
|
+
}
|
|
535
|
+
case 'label': return `label(${JSON.stringify(def.label)}, ${kid(def.parser)})`
|
|
536
|
+
case 'field': return `field(${JSON.stringify(def.name)}, ${kid(def.parser)})`
|
|
537
|
+
case 'expect': return `expect(${kid(def.parser)}${def.label !== undefined ? `, ${JSON.stringify(def.label)}` : ''})`
|
|
538
|
+
case 'scanTo':
|
|
539
|
+
return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(', ')}]${def.raw ? ', raw: true' : ''}, orEOF: ${def.orEOF} })`
|
|
540
|
+
case 'transform': {
|
|
541
|
+
if (def.fnSrc === undefined) throw new Unserializable('transform without fnSrc')
|
|
542
|
+
// `_tf` sets `_def.fnSrc` so re-lowering INLINES the callback (a plain
|
|
543
|
+
// `transform(child, fn)` would leave fnSrc unset → a non-static runtime
|
|
544
|
+
// callback that emitFusedSource can't inline).
|
|
545
|
+
return `_tf(${kid(def.parser)}, ${JSON.stringify(def.fnSrc)}${def.recognitionOnly ? ', true' : ''})`
|
|
546
|
+
}
|
|
547
|
+
case 'node': {
|
|
548
|
+
if (def.build !== undefined && def.buildSrc === undefined) throw new Unserializable('node build without buildSrc')
|
|
549
|
+
const projectEntry = projectOpt(def.project)
|
|
550
|
+
const tagEntry = tagsOpt(def.tags)
|
|
551
|
+
const optEntries = [
|
|
552
|
+
...(def.unwrap ? ['unwrap: true'] : []),
|
|
553
|
+
...(def.collapse ? ['collapse: true'] : []),
|
|
554
|
+
...(projectEntry === undefined ? [] : [projectEntry]),
|
|
555
|
+
...(def.captureTrivia ? ['captureTrivia: true'] : []),
|
|
556
|
+
...(def.trailingTrivia ? ['trailingTrivia: true'] : []),
|
|
557
|
+
// Declared or resolved arity. Carried so a re-lowered composed artifact does
|
|
558
|
+
// not silently re-acquire the fail-open capture cost this module resolved away.
|
|
559
|
+
...(def.buildArity === undefined ? [] : [`buildArity: ${def.buildArity}`]),
|
|
560
|
+
...(tagEntry === undefined ? [] : [tagEntry]),
|
|
561
|
+
]
|
|
562
|
+
const opts = optEntries.length > 0 ? `, { ${optEntries.join(', ')} }` : ''
|
|
563
|
+
// `_nd` sets `_def.buildSrc` (same reason as `_tf`). No build → plain node.
|
|
564
|
+
if (def.type === undefined) {
|
|
565
|
+
if (def.buildSrc !== undefined) throw new Unserializable('inferred node build without inferred type')
|
|
566
|
+
return opts ? `node(${kid(def.parser)}, undefined${opts})` : `node(${kid(def.parser)})`
|
|
567
|
+
}
|
|
568
|
+
if (def.buildSrc !== undefined) {
|
|
569
|
+
// Trailing optionals, emitted only when present so an artifact that carries
|
|
570
|
+
// neither is byte-identical to what previous versions produced.
|
|
571
|
+
const tail: string[] = []
|
|
572
|
+
if (def.buildStaticError !== undefined || def.buildSigSrc !== undefined) {
|
|
573
|
+
if (!opts) tail.push('undefined')
|
|
574
|
+
tail.push(def.buildStaticError === undefined ? 'undefined' : JSON.stringify(def.buildStaticError))
|
|
575
|
+
}
|
|
576
|
+
if (def.buildSigSrc !== undefined) tail.push(JSON.stringify(def.buildSigSrc))
|
|
577
|
+
const trailing = tail.length > 0 ? `, ${tail.join(', ')}` : ''
|
|
578
|
+
return `_nd(${JSON.stringify(def.type)}, ${kid(def.parser)}, ${JSON.stringify(def.buildSrc)}${opts}${trailing})`
|
|
579
|
+
}
|
|
580
|
+
return opts ? `node(${JSON.stringify(def.type)}, ${kid(def.parser)}, undefined${opts})` : `node(${JSON.stringify(def.type)}, ${kid(def.parser)})`
|
|
581
|
+
}
|
|
582
|
+
case 'grammar': {
|
|
583
|
+
const classified = def.triviaParser?._meta.rootTriviaClassified === true
|
|
584
|
+
const spec = classified && def.triviaParser ? analyzeLabeledTrivia(def.triviaParser) : null
|
|
585
|
+
if (classified && !spec) throw new Unserializable('classified trivia without labeled arms')
|
|
586
|
+
const trivia = def.clearTrivia
|
|
587
|
+
? 'null'
|
|
588
|
+
: spec
|
|
589
|
+
? `classifiedTrivia({ ${spec.arms.map(arm => `${JSON.stringify(arm.label)}: ${kid(arm.parser)}`).join(', ')} })`
|
|
590
|
+
: def.triviaParser ? kid(def.triviaParser) : 'undefined'
|
|
591
|
+
return `parser({ trivia: ${trivia}${def.captureTrivia ? ', captureTrivia: true' : ''}${def.rootCapture ? `, rootCapture: ${JSON.stringify(def.rootCapture)}` : ''}${def.trackLines ? ', trackLines: true' : ''} }, ${kid(def.parser)})`
|
|
592
|
+
}
|
|
593
|
+
case 'withCtx': {
|
|
594
|
+
// A `withCtx` round-trips through `_wc`, which rebuilds it AND re-attaches
|
|
595
|
+
// `_def.extraSrc` from the captured `extra` SOURCE. Preserving extraSrc is
|
|
596
|
+
// load-bearing for the same reason as `_tf`/`_gch`: on re-lowering, codegen
|
|
597
|
+
// inlines the state getter from source (a static callback), so the re-lowered
|
|
598
|
+
// artifact stays STATICALLY FUSIBLE. Without a captured source we cannot
|
|
599
|
+
// re-emit the state statically → keep full pieces (interpreter fallback).
|
|
600
|
+
if (def.extraSrc === undefined) throw new Unserializable('withCtx without extraSrc')
|
|
601
|
+
return `_wc(${JSON.stringify(def.extraSrc)}, ${kid(def.parser)})`
|
|
602
|
+
}
|
|
603
|
+
case 'adjacency':
|
|
604
|
+
return def.polarity === 'adjacent'
|
|
605
|
+
? 'adjacent()'
|
|
606
|
+
: def.kinds === undefined
|
|
607
|
+
? 'notAdjacent()'
|
|
608
|
+
: `notAdjacent({ kinds: ${JSON.stringify(def.kinds)} })`
|
|
609
|
+
case 'guard':
|
|
610
|
+
case 'recover':
|
|
611
|
+
case 'unknown':
|
|
612
|
+
throw new Unserializable(`unsupported tag ${def.tag}`)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|