parseman 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +9929 -7465
- package/dist/analysis/diagnostics.cjs.map +3 -4
- package/dist/analysis/diagnostics.d.ts +2 -2
- package/dist/analysis/diagnostics.js +9929 -7465
- 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 +10865 -8535
- package/dist/cli/index.cjs.map +3 -4
- package/dist/cli/index.js +10865 -8535
- 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 +40 -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 +9224 -7203
- 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 +9219 -7198
- package/dist/index.js.map +3 -4
- package/dist/language-service/index.cjs +311 -121
- package/dist/language-service/index.cjs.map +3 -4
- package/dist/language-service/index.js +311 -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 +11146 -9593
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11146 -9593
- 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 +2898 -1623
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1623
- 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 +11375 -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 +11352 -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 +446 -0
- package/dist/table/ops.d.ts.map +1 -0
- package/dist/table/program.d.ts +493 -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 +25 -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 +322 -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 +221 -0
- package/src/combinators/token.ts +120 -0
- package/src/combinators/trivia-boundary.ts +48 -0
- package/src/combinators/trivia-skip.ts +821 -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 +2994 -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 +1766 -0
- package/src/table/emit.ts +418 -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 +1672 -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 +459 -0
- package/src/table/program.ts +855 -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,954 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The linker: compose independently-carried grammar pieces into ONE runnable rule map.
|
|
3
|
+
*
|
|
4
|
+
* Composition is a RULE-MAP MERGE plus a single encode. Each piece carries its
|
|
5
|
+
* combinator graph as IR; `compose()` evaluates every piece's IR back to a rule map,
|
|
6
|
+
* lets a later piece's name override an earlier one, and encodes the merged map ONCE.
|
|
7
|
+
* `enc.winners` binds every by-name reference against that merged map, so overriding a
|
|
8
|
+
* rule reroutes EVERY call to it — including calls inside a base piece's own rules
|
|
9
|
+
* (open recursion) — with no shared scope and no relocation. Later piece wins per
|
|
10
|
+
* rule name.
|
|
11
|
+
*
|
|
12
|
+
* This replaced a textual splice. The source lowering compiled each piece to namespaced
|
|
13
|
+
* `_r_<Name>` function sources and concatenated them into one `new Function` scope,
|
|
14
|
+
* patching `@FS:` first-set placeholders per winner. That is why the linker needed
|
|
15
|
+
* `'unsafe-eval'`; the merge is now data, so it does not.
|
|
16
|
+
*/
|
|
17
|
+
import { ruleDependencies, childrenOf } from '../analysis/gating.ts'
|
|
18
|
+
import { FUSED_HOST_MODE, FUSED_HOST_ELIDED, type HostMode } from '../cst/host-mode.ts'
|
|
19
|
+
import { evalRuleMapIR, serializeRuleMap } from './ir-serialize.ts'
|
|
20
|
+
import { compileLinkableTable, type LinkableTable } from './compile-linkable-table.ts'
|
|
21
|
+
import { compileRuleMapRunnable } from '../table/compile-rule-map.ts'
|
|
22
|
+
import { tableRules } from '../table/assemble.ts'
|
|
23
|
+
import { GRAMMAR_REFLECTION } from '../cst/reflection.ts'
|
|
24
|
+
import { PARSEMAN_VERSION } from '../version.ts'
|
|
25
|
+
import type { BuildHost, Combinator, CstCollapsePredicate, ParseContext, ParseResult } from '../types.ts'
|
|
26
|
+
import type { Runnable } from '../functional/run.ts'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compile a `rules()` map to a **linkable artifact** — the composable, shippable
|
|
30
|
+
* form (RULE_ABI_PLAN §4). A package exports `linkable(rules(g => …))`; consumers
|
|
31
|
+
* import that artifact and `fuse([...])` it — **no source of the base grammar is
|
|
32
|
+
* ever read**. Under the macro this is precompiled to static pieces; in the
|
|
33
|
+
* interpreter it compiles here at load (like `compile()`).
|
|
34
|
+
*
|
|
35
|
+
* `ns` is a per-artifact namespace; omit it to auto-assign a process-unique one
|
|
36
|
+
* (fine at runtime — the macro supplies a stable module-derived ns instead).
|
|
37
|
+
*/
|
|
38
|
+
let _nsCounter = 0
|
|
39
|
+
export function linkable(
|
|
40
|
+
rulesMap: Record<string, Combinator<unknown>>,
|
|
41
|
+
ns?: string,
|
|
42
|
+
trivia?: Combinator<unknown>,
|
|
43
|
+
// Compile-time host mode, same meaning as `compile(g, { hostMode })`: 'ast' (default)
|
|
44
|
+
// emits the grammar's own builders and NO positioned-CST branch; 'cst' builds every
|
|
45
|
+
// node through the host. A linked/fused artifact is version- and mode-locked, so a
|
|
46
|
+
// language service links its own 'cst' artifact rather than switching at parse time.
|
|
47
|
+
hostMode?: HostMode,
|
|
48
|
+
): LinkableTable {
|
|
49
|
+
const piece = compileLinkableTable(
|
|
50
|
+
Object.entries(rulesMap),
|
|
51
|
+
ns ?? `_lk${_nsCounter++}_`,
|
|
52
|
+
{ ...(trivia ? { trivia } : {}), ...(hostMode ? { hostMode } : {}) },
|
|
53
|
+
)
|
|
54
|
+
if (!piece) throw new Error('linkable(): this grammar cannot be compiled to a linkable artifact (contains a runtime-only parser fallback)')
|
|
55
|
+
return piece
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type CstBuildHostOptions = {
|
|
59
|
+
/**
|
|
60
|
+
* Collapse transparent one-child CST wrapper nodes at build time.
|
|
61
|
+
* - `true`: collapse any one-child node whose rawChildren also has exactly one
|
|
62
|
+
* entry, so trivia/error boundaries are not silently dropped.
|
|
63
|
+
* - `string[]`: collapse only these grammar node types.
|
|
64
|
+
* - predicate: final policy hook for language-specific public CSTs.
|
|
65
|
+
*/
|
|
66
|
+
collapse?: boolean | readonly string[] | CstCollapsePredicate
|
|
67
|
+
/**
|
|
68
|
+
* Materialize `node(..., { tags })` grammar metadata onto produced CST nodes.
|
|
69
|
+
* When omitted, tags stay in grammar reflection for zero per-node tree cost.
|
|
70
|
+
*/
|
|
71
|
+
tags?: boolean
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeCstCollapse(collapse: CstBuildHostOptions['collapse']): CstCollapsePredicate | undefined {
|
|
75
|
+
if (collapse === true) return () => true
|
|
76
|
+
if (Array.isArray(collapse)) {
|
|
77
|
+
const types = new Set(collapse)
|
|
78
|
+
return type => types.has(type)
|
|
79
|
+
}
|
|
80
|
+
return typeof collapse === 'function' ? collapse : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildCstNode(
|
|
84
|
+
type: string,
|
|
85
|
+
children: ReadonlyArray<unknown>,
|
|
86
|
+
_fields: unknown,
|
|
87
|
+
span: { start: number; end: number },
|
|
88
|
+
_rawChildren?: ReadonlyArray<unknown>,
|
|
89
|
+
_triviaLog?: readonly number[],
|
|
90
|
+
state?: unknown,
|
|
91
|
+
tags?: readonly string[] | undefined,
|
|
92
|
+
): unknown {
|
|
93
|
+
// Carry the grammar's `ctx.state` snapshot onto the node (null when unset) — the
|
|
94
|
+
// CST contract includes `state` and incremental re-parse replays it on edit.
|
|
95
|
+
return tags !== undefined && tags.length > 0
|
|
96
|
+
? { _tag: 'node', type, tags, span: { ...span }, state: state ?? null, children: [...children] }
|
|
97
|
+
: { _tag: 'node', type, span: { ...span }, state: state ?? null, children: [...children] }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A generic positioned-CST build host (RULE_ABI_PLAN §7). Pass as `ctx.build`
|
|
102
|
+
* (or `parseDoc(..., { build: cstBuildHost })`) to make ANY linkable/fused
|
|
103
|
+
* grammar produce a uniform CST — `{ _tag:'node', type, span, state, children }`
|
|
104
|
+
* — instead of its own eval-AST builders. This is the host the linter and IDE
|
|
105
|
+
* drivers use; the eval driver leaves `ctx.build` unset (grammar's own builders).
|
|
106
|
+
*
|
|
107
|
+
* For public syntax trees, call `cstBuildHost({ collapse })`: Parseman will skip
|
|
108
|
+
* allocating wrapper CST nodes whose single child should stand in for the rule.
|
|
109
|
+
*/
|
|
110
|
+
export function cstBuildHost(options?: CstBuildHostOptions): BuildHost
|
|
111
|
+
export function cstBuildHost(
|
|
112
|
+
type: string,
|
|
113
|
+
children: ReadonlyArray<unknown>,
|
|
114
|
+
fields: unknown,
|
|
115
|
+
span: { start: number; end: number },
|
|
116
|
+
rawChildren?: ReadonlyArray<unknown>,
|
|
117
|
+
triviaLog?: readonly number[],
|
|
118
|
+
state?: unknown,
|
|
119
|
+
tags?: readonly string[] | undefined,
|
|
120
|
+
): unknown
|
|
121
|
+
export function cstBuildHost(
|
|
122
|
+
typeOrOptions?: string | CstBuildHostOptions,
|
|
123
|
+
children?: ReadonlyArray<unknown>,
|
|
124
|
+
_fields?: unknown,
|
|
125
|
+
span?: { start: number; end: number },
|
|
126
|
+
rawChildren?: ReadonlyArray<unknown>,
|
|
127
|
+
triviaLog?: readonly number[],
|
|
128
|
+
state?: unknown,
|
|
129
|
+
_tags?: readonly string[] | undefined,
|
|
130
|
+
): unknown {
|
|
131
|
+
if (typeof typeOrOptions === 'string') {
|
|
132
|
+
return buildCstNode(typeOrOptions, children ?? [], _fields, span ?? { start: 0, end: 0 }, rawChildren, triviaLog, state)
|
|
133
|
+
}
|
|
134
|
+
const collapse = normalizeCstCollapse(typeOrOptions?.collapse)
|
|
135
|
+
const materializeTags = typeOrOptions?.tags === true
|
|
136
|
+
const host: BuildHost = (
|
|
137
|
+
type: string,
|
|
138
|
+
children: ReadonlyArray<unknown> | undefined,
|
|
139
|
+
fields: unknown,
|
|
140
|
+
span: { start: number; end: number },
|
|
141
|
+
rawChildren: ReadonlyArray<unknown>,
|
|
142
|
+
triviaLog: readonly number[],
|
|
143
|
+
state: unknown,
|
|
144
|
+
tags?: readonly string[] | undefined,
|
|
145
|
+
// A CST/collapse host always keeps `children` (chV) — the opt-out never
|
|
146
|
+
// applies — so `?? []` is unreachable defensive modeling for the widened type.
|
|
147
|
+
) => buildCstNode(type, children ?? [], fields, span, rawChildren, triviaLog, state, materializeTags ? tags : undefined)
|
|
148
|
+
;(host as typeof host & { _parsemanCstOutput?: true })._parsemanCstOutput = true
|
|
149
|
+
if (collapse) host._parsemanCstCollapse = collapse
|
|
150
|
+
return host
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// `cstBuildHost` itself is also accepted as a BuildHost (without options).
|
|
154
|
+
;(cstBuildHost as unknown as { _parsemanCstOutput?: true })._parsemanCstOutput = true
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A fused function receives the full ParseContext through `run()`. Direct
|
|
158
|
+
* callers historically supplied a plain context object, so keep that usage
|
|
159
|
+
* valid while making the function assignable to the public `Runnable` type.
|
|
160
|
+
* Generated code treats optional framework fields as absent when they are not
|
|
161
|
+
* provided, matching the interpreter's normal defaults.
|
|
162
|
+
*/
|
|
163
|
+
export type FusedRule = (
|
|
164
|
+
input: string,
|
|
165
|
+
pos: number,
|
|
166
|
+
ctx: ParseContext | Record<string, unknown>,
|
|
167
|
+
) => ParseResult<unknown> & { readonly value?: unknown }
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Fuse carried pieces into a runnable rule map — the TABLE equivalent of the
|
|
172
|
+
* textual splice this file used to perform.
|
|
173
|
+
*
|
|
174
|
+
* `fusedBody()` concatenated namespaced `_r_<Name>` function sources, picked a winning
|
|
175
|
+
* function per name, and patched `@FS:` dispatch placeholders with the winner's
|
|
176
|
+
* first-set condition — roughly 200 lines whose entire job was to make separately
|
|
177
|
+
* lowered SOURCE agree about names. A table has no text to splice, so the merge moves
|
|
178
|
+
* up one level onto the combinators: merge the rule maps (later piece wins), then
|
|
179
|
+
* `encodeTable` ONCE over the merged map. `enc.winners` binds every by-name reference,
|
|
180
|
+
* including a base piece's internal `g.Atom` that an override replaced, so open
|
|
181
|
+
* recursion across pieces resolves without relocating a single encoded offset.
|
|
182
|
+
*
|
|
183
|
+
* One encode, no pools to merge, and the result is the table the merged grammar would
|
|
184
|
+
* have produced had it been written as a single `rules()` call.
|
|
185
|
+
*/
|
|
186
|
+
function fuseCarried(
|
|
187
|
+
carried: ReadonlyArray<LinkableTable | IRPiece>,
|
|
188
|
+
trivia?: Combinator<unknown>,
|
|
189
|
+
hostMode?: HostMode,
|
|
190
|
+
): Record<string, FusedRule> {
|
|
191
|
+
const maps: Array<Array<[string, Combinator<unknown>]>> = []
|
|
192
|
+
for (const p of carried) {
|
|
193
|
+
// ARTIFACT VERSION LOCK. `fusedBody` enforced this and would have taken it with it:
|
|
194
|
+
// artifacts are version-locked and there is no cross-version read path, so an
|
|
195
|
+
// UNSTAMPED piece and a MISMATCHED one are both refused — a stale artifact that
|
|
196
|
+
// merely happens to still encode is exactly what this stops.
|
|
197
|
+
if (!isIRPiece(p)) {
|
|
198
|
+
if (typeof p.v !== 'string') {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`parseman: artifact "${p.ns}" is UNSTAMPED (compiled before the version-lock invariant). `
|
|
201
|
+
+ `Recompile the grammar with parseman ${PARSEMAN_VERSION}; parseman does not fuse unversioned or cross-version artifacts.`,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
if (p.v !== PARSEMAN_VERSION) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`parseman: artifact "${p.ns}" was compiled with parseman ${p.v}, but is being fused with parseman ${PARSEMAN_VERSION}. `
|
|
207
|
+
+ `Compiled grammar artifacts are version-locked — recompile the grammar with parseman ${PARSEMAN_VERSION}; parseman does not fuse across versions.`,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const rules = ruleMapOfCarried(p)
|
|
212
|
+
if (rules === undefined) {
|
|
213
|
+
throw new Error(`compose: carried piece "${p.ns}" has no re-lowerable IR and cannot be fused`)
|
|
214
|
+
}
|
|
215
|
+
maps.push(rules)
|
|
216
|
+
}
|
|
217
|
+
const merged = mergeCarriedRuleMaps(maps)
|
|
218
|
+
materializeDirectBuilders(merged)
|
|
219
|
+
// COMPOSING-WINS is an OVERRIDE, not a gap-fill: the composing grammar's trivia
|
|
220
|
+
// governs every fused rule INCLUDING inherited ones. `applyAmbient` inside
|
|
221
|
+
// `compileRuleMap` only fills a rule that declares none, which would leave an
|
|
222
|
+
// inherited rule still skipping its own base's whitespace after a delta re-declared
|
|
223
|
+
// it. Safe to mutate — `evalRuleMapIR` builds fresh combinators per fuse.
|
|
224
|
+
if (trivia) {
|
|
225
|
+
for (const [, rule] of merged) {
|
|
226
|
+
if (rule._meta.isTrivia) continue
|
|
227
|
+
;(rule._meta as { grammarTrivia?: Combinator<unknown> }).grammarTrivia = trivia
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// RUNNABLE, not printable. `compose()` returns a parser; it never emits source, so
|
|
231
|
+
// requiring a captured source per author callback would refuse every grammar built at
|
|
232
|
+
// runtime — which have live callbacks by construction.
|
|
233
|
+
const refusals: string[] = []
|
|
234
|
+
const compiled = compileRuleMapRunnable(merged, {
|
|
235
|
+
...(trivia ? { trivia } : {}),
|
|
236
|
+
...(hostMode ? { hostMode } : {}),
|
|
237
|
+
refusals,
|
|
238
|
+
})
|
|
239
|
+
if (compiled === null) {
|
|
240
|
+
throw new Error(`compose: the merged grammar could not be encoded to a table${refusals.length ? ` — ${refusals.join('; ')}` : ''}`)
|
|
241
|
+
}
|
|
242
|
+
// `tableRules`, NOT `exec.ts`'s same-named `tableRules`. Both return
|
|
243
|
+
// `Record<string, TableRule>`, so binding the interpreter here type-checked and ran
|
|
244
|
+
// correctly — it was just the SLOW engine, on the one path (compose/fuse) that never
|
|
245
|
+
// goes through `table/index.ts` and so never saw the `tableRules as tableRules`
|
|
246
|
+
// re-export. Import the assembler by its own name so the binding cannot go stale again.
|
|
247
|
+
const map = tableRules(compiled.prog) as unknown as Record<string, FusedRule>
|
|
248
|
+
// The host-mode stamp went on the fused closure before; a table carries nothing until
|
|
249
|
+
// it is stamped, and an UNSTAMPED map reads as `{ ast, false }` so every driver
|
|
250
|
+
// compatibility check passes vacuously. Stamped on the rule functions too, because
|
|
251
|
+
// `run(map.Rule, …)` is handed the rule and never sees the map.
|
|
252
|
+
for (const k of Object.keys(map)) {
|
|
253
|
+
Object.defineProperty(map[k]!, FUSED_HOST_MODE, { value: compiled.hostMode, enumerable: false })
|
|
254
|
+
Object.defineProperty(map[k]!, FUSED_HOST_ELIDED, { value: compiled.hostBranchElided, enumerable: false })
|
|
255
|
+
}
|
|
256
|
+
Object.defineProperty(map, FUSED_HOST_MODE, { value: compiled.hostMode, enumerable: false })
|
|
257
|
+
Object.defineProperty(map, FUSED_HOST_ELIDED, { value: compiled.hostBranchElided, enumerable: false })
|
|
258
|
+
// GRAMMAR REFLECTION, which `fusedBody` merged across pieces and stamped. A visitor
|
|
259
|
+
// built over a composed grammar reads it to know the node types; unstamped, it reads
|
|
260
|
+
// as an empty grammar and every visitor silently matches nothing.
|
|
261
|
+
Object.defineProperty(map, GRAMMAR_REFLECTION, { value: compiled.reflection, enumerable: false })
|
|
262
|
+
return map
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Re-attach a DIRECT node builder that came back from IR as an inert sentinel.
|
|
267
|
+
*
|
|
268
|
+
* `evalRuleMapIR` deliberately refuses to evaluate a captured `buildSrc`: raw IR
|
|
269
|
+
* interpretation must not run arbitrary source, so `_nd` installs a thrower and keeps
|
|
270
|
+
* the text. Only a COMPILER consumer may materialize it — and the runtime fuse is one.
|
|
271
|
+
* The source lowering did exactly this, less visibly: it inlined `buildSrc` into the
|
|
272
|
+
* fused body and `new Function` evaluated it. The table parks callbacks in a pool
|
|
273
|
+
* instead of inlining them, so the same materialization has to be explicit or the
|
|
274
|
+
* sentinel reaches the pool and throws on the first parse through that node.
|
|
275
|
+
*
|
|
276
|
+
* Scoped to the runtime fuse. The MACRO never comes here: its encoder captures
|
|
277
|
+
* `buildSrc` and PRINTS it, so it needs no live function at all.
|
|
278
|
+
*/
|
|
279
|
+
function materializeDirectBuilders(ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>): void {
|
|
280
|
+
const seen = new Set<Combinator<unknown>>()
|
|
281
|
+
const visit = (p: Combinator<unknown>): void => {
|
|
282
|
+
if (seen.has(p)) return
|
|
283
|
+
seen.add(p)
|
|
284
|
+
const d = p._def as { tag: string; build?: unknown; buildSrc?: string }
|
|
285
|
+
if (d.tag === 'node' && typeof d.buildSrc === 'string') {
|
|
286
|
+
try {
|
|
287
|
+
// eslint-disable-next-line no-eval
|
|
288
|
+
const fn = (0, eval)(`(${d.buildSrc})`) as unknown
|
|
289
|
+
if (typeof fn === 'function') d.build = fn
|
|
290
|
+
} catch { /* leave the sentinel: it throws with its own message on reach */ }
|
|
291
|
+
}
|
|
292
|
+
if (d.tag === 'lazy') {
|
|
293
|
+
let resolved: Combinator<unknown> | undefined
|
|
294
|
+
try { resolved = (d as unknown as { thunk: () => Combinator<unknown> }).thunk() } catch { return }
|
|
295
|
+
if (resolved) visit(resolved)
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
for (const child of childrenOf(p._def)) visit(child)
|
|
299
|
+
}
|
|
300
|
+
for (const [, rule] of ruleMap) visit(rule)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The combinator map behind a carried piece: IR is evaluated back, a table piece
|
|
304
|
+
* uses the IR it always carries. `undefined` only for a piece with neither. */
|
|
305
|
+
function ruleMapOfCarried(p: LinkableTable | IRPiece): Array<[string, Combinator<unknown>]> | undefined {
|
|
306
|
+
if (isIRPiece(p)) return evalRuleMapIR(p.ir)
|
|
307
|
+
// IR FIRST: re-evaluating it yields FRESH combinators, so seeding composing trivia
|
|
308
|
+
// onto their `_meta` cannot leak back into the artifact this piece came from. The live
|
|
309
|
+
// map is the fallback for a runtime-built grammar that has no serializable IR.
|
|
310
|
+
if (p.ir !== null) return evalRuleMapIR(p.ir)
|
|
311
|
+
return p.ruleMap.length > 0 ? p.ruleMap.map(([k, v]) => [k, v] as [string, Combinator<unknown>]) : undefined
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Fold ordered rule maps into the composed map: a later piece's name WINS.
|
|
315
|
+
*
|
|
316
|
+
* A REFERENCE IS NOT A DEFINITION. A `rules(g => …)` cache also holds every `g.X` that
|
|
317
|
+
* was merely ACCESSED, as an unresolvable lazy. Merged in order, such an entry lands
|
|
318
|
+
* last and SHADOWS the piece that actually defines the name — the encoder then finds a
|
|
319
|
+
* hole where the winner should be and refuses the whole grammar. The reference is not
|
|
320
|
+
* lost: it stays inside the referring piece's rule bodies, where `enc.winners` binds it
|
|
321
|
+
* by name to whichever piece supplies the definition.
|
|
322
|
+
*/
|
|
323
|
+
function mergeCarriedRuleMaps(
|
|
324
|
+
maps: ReadonlyArray<ReadonlyArray<readonly [string, Combinator<unknown>]>>,
|
|
325
|
+
): Array<[string, Combinator<unknown>]> {
|
|
326
|
+
const winners = new Map<string, Combinator<unknown>>()
|
|
327
|
+
for (const map of maps) {
|
|
328
|
+
for (const [name, rule] of map) {
|
|
329
|
+
if (rule._def.tag === 'lazy') {
|
|
330
|
+
try { rule._def.thunk() } catch { continue }
|
|
331
|
+
}
|
|
332
|
+
winners.set(name, rule)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return [...winners]
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
export { FUSED_HOST_MODE, FUSED_HOST_ELIDED } from '../cst/host-mode.ts'
|
|
340
|
+
|
|
341
|
+
/** The host mode a fused/composed rule map was built for. Defaults to 'ast'. */
|
|
342
|
+
export function fusedHostModeOf(registry: object): HostMode {
|
|
343
|
+
const m = (registry as Record<symbol, unknown>)[FUSED_HOST_MODE]
|
|
344
|
+
return m === 'cst' ? 'cst' : 'ast'
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Whether a fused/composed rule map dropped any direct builder's CST branch. */
|
|
348
|
+
export function fusedHostElidedOf(registry: object): boolean {
|
|
349
|
+
return (registry as Record<symbol, unknown>)[FUSED_HOST_ELIDED] === true
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Compose grammars/artifacts into a runnable parser map — the ONLY public
|
|
354
|
+
* composition entry point. `compose([base, ext, …])`: later entries override
|
|
355
|
+
* earlier ones by rule name, and because fusion re-binds every reference in one
|
|
356
|
+
* shared scope, an override reroutes the base's OWN calls too (open recursion).
|
|
357
|
+
*
|
|
358
|
+
* Each entry may be a **grammar** (a `rules()` result — a map of combinators,
|
|
359
|
+
* linkable-ified here) OR an already-compiled **linkable artifact** (what the
|
|
360
|
+
* macro emits and a package ships). So a package needs no opt-in wrapper to be
|
|
361
|
+
* composable — `compose([importedGrammar, myRules])` just works.
|
|
362
|
+
*
|
|
363
|
+
* The macro compiles `compose([...])` to STATIC fused source (no `new Function`).
|
|
364
|
+
* Called at runtime (no macro, like `compile()`) it fuses via `new Function`.
|
|
365
|
+
*/
|
|
366
|
+
/** A composed parser carries its flattened source pieces (non-enumerable) so it
|
|
367
|
+
* can be composed AGAIN — `compose([lessGrammar, delta])` where `lessGrammar` is
|
|
368
|
+
* itself a `compose([...])` result. */
|
|
369
|
+
export const COMPOSED_PIECES = Symbol.for('parseman.composedPieces')
|
|
370
|
+
|
|
371
|
+
/** The carried pieces a `compose()`/`composeLeaf()` result holds, or `undefined` when
|
|
372
|
+
* the value is not a composed grammar. This is what makes a fused grammar analysable:
|
|
373
|
+
* the pieces are re-lowerable IR even though the fused map itself is only functions. */
|
|
374
|
+
export function composedPiecesOf(
|
|
375
|
+
grammar: Record<string, unknown>,
|
|
376
|
+
): ReadonlyArray<LinkableTable | IRPiece> | undefined {
|
|
377
|
+
const pieces = (grammar as unknown as Record<symbol, unknown>)[COMPOSED_PIECES]
|
|
378
|
+
return Array.isArray(pieces) ? pieces as ReadonlyArray<LinkableTable | IRPiece> : undefined
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* A terminal fused grammar may be used to run a parser, but not as an input to
|
|
383
|
+
* another composition. Macro `composeLeaf()` uses this for a local semantic
|
|
384
|
+
* reduction over imported recognition-only IR: the local reductions stay in
|
|
385
|
+
* their lexical module and therefore never become carried IR.
|
|
386
|
+
*/
|
|
387
|
+
const LEAF_COMPOSED = Symbol.for('parseman.leafComposed')
|
|
388
|
+
|
|
389
|
+
/** The composing (outermost) trivia a runtime `compose()` applied — stored so a
|
|
390
|
+
* LATER `compose([thisResult, …])` that declares no trivia of its own still re-lowers
|
|
391
|
+
* these rules under the SAME trivia (composing-wins survives re-composition). The
|
|
392
|
+
* carried IR pieces hold no trivia of their own, so it must be remembered separately. */
|
|
393
|
+
const COMPOSED_TRIVIA = Symbol.for('parseman.composedTrivia')
|
|
394
|
+
|
|
395
|
+
/** Final winner map for semantic-coverage tooling. It exists only when every
|
|
396
|
+
* carried compose piece is re-lowerable IR; opaque precompiled artifacts have no
|
|
397
|
+
* combinator graph to inspect and therefore deliberately expose no fake map. */
|
|
398
|
+
const COMPOSED_COVERAGE_RULES = Symbol.for('parseman.composedCoverageRules')
|
|
399
|
+
|
|
400
|
+
/** The compact IR form a grammar carries instead of its lowered rule source: the
|
|
401
|
+
* combinator-construction expression, re-lowered here at fuse time. */
|
|
402
|
+
export type IRPiece = { ns: string; ir: string; trackLines?: true }
|
|
403
|
+
|
|
404
|
+
function isIRPiece(p: unknown): p is IRPiece {
|
|
405
|
+
return !!p && typeof p === 'object'
|
|
406
|
+
&& typeof (p as IRPiece).ir === 'string' && typeof (p as IRPiece).ns === 'string'
|
|
407
|
+
&& !('keys' in (p as object))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** A `linkable()` artifact — a TABLE piece. Distinguished from a bare IR piece by the
|
|
411
|
+
* fields only a compiled artifact has (`keys`/`external`), and from a plain `rules()`
|
|
412
|
+
* map by carrying `ns` at all. */
|
|
413
|
+
function isLinkableTable(p: unknown): p is LinkableTable {
|
|
414
|
+
return !!p && typeof p === 'object'
|
|
415
|
+
&& typeof (p as LinkableTable).ns === 'string'
|
|
416
|
+
&& Array.isArray((p as LinkableTable).keys)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Memoize a zero-arg thunk, keeping it LAZY. Used where two diagnostic thunks want
|
|
420
|
+
* the same carried-IR hydration: the work must not happen when the diagnostic is off,
|
|
421
|
+
* and must not happen twice when it is on. */
|
|
422
|
+
export function once<T>(fn: () => T): () => T {
|
|
423
|
+
let done = false
|
|
424
|
+
let value: T
|
|
425
|
+
return () => {
|
|
426
|
+
if (!done) { value = fn(); done = true }
|
|
427
|
+
return value
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** The re-lowerable carried pieces' rule maps, in compose order — the input to the
|
|
432
|
+
* gating analysis (`diagnoseGrammar`). An opaque precompiled artifact contributes no
|
|
433
|
+
* combinator graph, so it is skipped: a hole it would have bound stays unresolved
|
|
434
|
+
* and its choice stays deferred, never falsely warned.
|
|
435
|
+
*
|
|
436
|
+
* Skipping is not the same as having nothing to say. Use `carriedRuleMapsDetailed`
|
|
437
|
+
* where the skip must be REPORTED — a diagnostic that drops part of the grammar and
|
|
438
|
+
* then returns a clean result is indistinguishable from one that verified it. */
|
|
439
|
+
export function carriedRuleMaps(carried: ReadonlyArray<LinkableTable | IRPiece>): Array<Array<[string, Combinator<unknown>]>> {
|
|
440
|
+
return carriedRuleMapsDetailed(carried).maps
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** `carriedRuleMaps` plus the pieces it could NOT re-lower, named by namespace and
|
|
444
|
+
* rule count, so a caller can report exactly how much of the grammar went unseen. */
|
|
445
|
+
export function carriedRuleMapsDetailed(
|
|
446
|
+
carried: ReadonlyArray<LinkableTable | IRPiece>,
|
|
447
|
+
): { maps: Array<Array<[string, Combinator<unknown>]>>; opaque: Array<{ ns: string; ruleNames: string[] }> } {
|
|
448
|
+
const maps: Array<Array<[string, Combinator<unknown>]>> = []
|
|
449
|
+
const opaque: Array<{ ns: string; ruleNames: string[] }> = []
|
|
450
|
+
for (const p of carried) {
|
|
451
|
+
// A table artifact is recoverable far more often than a source one was: it carries
|
|
452
|
+
// IR, and failing that the live combinators. Ask for the map before declaring the
|
|
453
|
+
// piece opaque, or a perfectly analysable grammar is reported as unseen.
|
|
454
|
+
const rules = ruleMapOfCarried(p)
|
|
455
|
+
if (rules !== undefined) { maps.push(rules); continue }
|
|
456
|
+
// NAMED, not anonymous. `keys` is the artifact's rule-name list; the field this
|
|
457
|
+
// used to read (`ruleFns`) belonged to the source lowering, and reading a missing
|
|
458
|
+
// one degrades every opaque piece to `<artifact _lkN_>` — "reported, but uselessly",
|
|
459
|
+
// which is the exact failure this reporting exists to prevent.
|
|
460
|
+
opaque.push({ ns: p.ns, ruleNames: isIRPiece(p) ? [] : [...p.keys] })
|
|
461
|
+
}
|
|
462
|
+
return { maps, opaque }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Recover the override-winner COMBINATOR map behind a `compose()` result, plus the
|
|
467
|
+
* pieces that could not be recovered.
|
|
468
|
+
*
|
|
469
|
+
* A fused map holds rule functions, so any consumer that walks a combinator graph
|
|
470
|
+
* (gating analysis, the spec/EBNF/railroad model) cannot read it directly. The graph
|
|
471
|
+
* is not lost, though — `compose()` retains re-lowerable IR — so this is the single
|
|
472
|
+
* shared recovery both consumers use. Sharing it is the point: two copies of this
|
|
473
|
+
* logic is how one walker gets fixed and the other silently keeps failing.
|
|
474
|
+
*
|
|
475
|
+
* Returns `undefined` when `grammar` is not a composed result.
|
|
476
|
+
*/
|
|
477
|
+
export function recoverComposedRules(
|
|
478
|
+
grammar: Record<string, unknown>,
|
|
479
|
+
): { rules: Map<string, Combinator<unknown>>; opaque: Array<{ ns: string; ruleNames: string[] }> } | undefined {
|
|
480
|
+
const carried = composedPiecesOf(grammar)
|
|
481
|
+
if (carried === undefined) return undefined
|
|
482
|
+
const { maps, opaque } = carriedRuleMapsDetailed(carried)
|
|
483
|
+
const rules = new Map<string, Combinator<unknown>>()
|
|
484
|
+
// Later wins, matching the linker's own fuse semantics. An accessed-but-undefined
|
|
485
|
+
// `g.X` leaks in as an unresolved lazy — a REFERENCE, not a definition — and must
|
|
486
|
+
// never shadow the artifact that really defines X.
|
|
487
|
+
for (const map of maps) for (const [name, rule] of map) {
|
|
488
|
+
if (rule._def.tag === 'lazy') { try { rule._def.thunk() } catch { continue } }
|
|
489
|
+
rules.set(name, rule)
|
|
490
|
+
}
|
|
491
|
+
return { rules, opaque }
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function coverageRulesOf(carried: Array<LinkableTable | IRPiece>): Record<string, Combinator<unknown>> | undefined {
|
|
495
|
+
const winners: Record<string, Combinator<unknown>> = {}
|
|
496
|
+
for (const piece of carried) {
|
|
497
|
+
if (!isIRPiece(piece)) return undefined
|
|
498
|
+
const map = evalRuleMapIR(piece.ir)
|
|
499
|
+
for (const [name, rule] of map) {
|
|
500
|
+
// An accessed-but-undefined `g.Name` is an external reference, never a
|
|
501
|
+
// rule definition. Match compose's IR filtering rule exactly.
|
|
502
|
+
if (rule._def.tag === 'lazy') {
|
|
503
|
+
try { rule._def.thunk() } catch { continue }
|
|
504
|
+
}
|
|
505
|
+
winners[name] = rule
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return winners
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Return the final override-winner combinator map carried by runtime
|
|
512
|
+
* `compose()`, or `undefined` when a precompiled opaque artifact participated.
|
|
513
|
+
* This is intentionally internal: callers must not treat it as a parser API. */
|
|
514
|
+
export function composedCoverageRules(grammar: Record<string, unknown>): Record<string, Combinator<unknown>> | undefined {
|
|
515
|
+
return (grammar as Record<symbol, unknown>)[COMPOSED_COVERAGE_RULES] as Record<string, Combinator<unknown>> | undefined
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Flatten one `compose()` item to its pieces: a prior composed result → its
|
|
519
|
+
* carried list; an artifact → itself; a grammar (`rules()` map) → linkable-ified. */
|
|
520
|
+
function nextComposeNs(used: Set<string>): string {
|
|
521
|
+
let ns: string
|
|
522
|
+
do { ns = `_lk${_nsCounter++}_` } while (used.has(ns))
|
|
523
|
+
used.add(ns)
|
|
524
|
+
return ns
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** Flatten one `compose()` item to its RE-LOWERABLE carried items — the form stored
|
|
528
|
+
* on the composed result so it can be composed AGAIN under a NEW composing trivia.
|
|
529
|
+
* A grammar (`rules()` map) is carried as compact IR (`{ns, ir}`), NOT baked source,
|
|
530
|
+
* so a later `compose([thisResult, delta])` re-lowers it with the delta's trivia
|
|
531
|
+
* (multi-level composing-wins). A prior composed result contributes its OWN carried
|
|
532
|
+
* items (already IR); a pre-compiled artifact has no source, so it stays baked. */
|
|
533
|
+
function itemCarried(
|
|
534
|
+
item: LinkableTable | Record<string, unknown>,
|
|
535
|
+
used: Set<string>,
|
|
536
|
+
trivia?: Combinator<unknown>,
|
|
537
|
+
// Only reaches the non-serializable fallback below, where the grammar is baked
|
|
538
|
+
// immediately instead of carried as re-lowerable IR.
|
|
539
|
+
hostMode?: HostMode,
|
|
540
|
+
): Array<LinkableTable | IRPiece> {
|
|
541
|
+
const carried = (item as Record<symbol, unknown>)[COMPOSED_PIECES]
|
|
542
|
+
// A prior composed result (runtime or macro-compiled): its carried list is already
|
|
543
|
+
// re-lowerable (IR pieces, plus any pre-compiled artifacts). Pass it through so THIS
|
|
544
|
+
// compose re-lowers it under its own composing trivia. Reserve its namespaces so a
|
|
545
|
+
// sibling grammar map can't collide with them.
|
|
546
|
+
if (Array.isArray(carried)) {
|
|
547
|
+
const items = carried as Array<LinkableTable | IRPiece>
|
|
548
|
+
for (const p of items) used.add(p.ns)
|
|
549
|
+
return items
|
|
550
|
+
}
|
|
551
|
+
// A pre-compiled artifact (`linkable()`): a table piece. It ALWAYS carries its IR,
|
|
552
|
+
// which is what makes table-to-table composition a rule-map merge rather than a
|
|
553
|
+
// relocation of two encoded programs — so unlike a source artifact it stays
|
|
554
|
+
// re-lowerable under a new composing trivia.
|
|
555
|
+
if (isLinkableTable(item)) {
|
|
556
|
+
used.add(item.ns)
|
|
557
|
+
return [item]
|
|
558
|
+
}
|
|
559
|
+
// A grammar (`rules()` map): carry it as compact IR so a later compose re-lowers it
|
|
560
|
+
// under ITS trivia. Unserializable → bake now with this compose's trivia (can't
|
|
561
|
+
// re-lower later; acceptable fallback, mirrors the macro's full-pieces fallback).
|
|
562
|
+
const map = item as Record<string, Combinator<unknown>>
|
|
563
|
+
const ns = nextComposeNs(used)
|
|
564
|
+
// Drop EXTERNAL entries first (same filter as compileLinkable): a `rules()` cache
|
|
565
|
+
// also holds every ACCESSED-but-undefined `g.X` as an unresolved-lazy entry. Left in,
|
|
566
|
+
// serializeRuleMap would emit `X: g["X"]` — a self-referential rule that shadows the
|
|
567
|
+
// sibling artifact defining X and recurses forever. They resolve by name at fuse time.
|
|
568
|
+
const entries = Object.entries(map).filter(([, val]) => {
|
|
569
|
+
const d = val._def
|
|
570
|
+
if (d.tag !== 'lazy') return true
|
|
571
|
+
try { d.thunk(); return true } catch { return false }
|
|
572
|
+
})
|
|
573
|
+
// Carry this grammar's ambient `scanSkip` (per-piece — opaque units are
|
|
574
|
+
// dialect-specific, NOT composing-wins) into the IR so a re-lower stamps
|
|
575
|
+
// `grammarScanSkip` back on. `linkable()`'s fallback reads it off `_meta` directly.
|
|
576
|
+
const scanSkip = entries
|
|
577
|
+
.map(([, val]) => (val._meta as { grammarScanSkip?: Combinator<unknown>[] }).grammarScanSkip)
|
|
578
|
+
.find(Boolean)
|
|
579
|
+
const ir = serializeRuleMap(entries, scanSkip)
|
|
580
|
+
return ir ? [{ ns, ir }] : [linkable(map, ns, trivia, hostMode)]
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** The composed grammar's ambient trivia = the LAST composed item that declares a
|
|
584
|
+
* grammar-level trivia (via `rules({ trivia }, …)`, which tags `grammarTrivia` on its
|
|
585
|
+
* rules). Outermost wins: the composing grammar's trivia applies to every fused rule,
|
|
586
|
+
* including those inherited from a base — so e.g. an SCSS `rw` (which extends Less's)
|
|
587
|
+
* governs the inherited Less/CSS rules too. `parser`/`noTrivia` still override locally. */
|
|
588
|
+
function composingTriviaOf(items: Array<LinkableTable | Record<string, unknown>>): Combinator<unknown> | undefined {
|
|
589
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
590
|
+
const item = items[i] as Record<string, unknown> | undefined
|
|
591
|
+
if (!item || isLinkableTable(item)) continue
|
|
592
|
+
// A COMPOSED item states its trivia on the stamp rather than on its values (its
|
|
593
|
+
// rules live in carried IR, which does not carry `_meta`). Skipping such an item
|
|
594
|
+
// entirely — as this did — loses the trivia of every grammar that reached this
|
|
595
|
+
// compose through a PRIOR compose.
|
|
596
|
+
const stamped = (item as Record<symbol, unknown>)[COMPOSED_TRIVIA] as Combinator<unknown> | undefined
|
|
597
|
+
if (stamped) return stamped
|
|
598
|
+
if ((item as Record<symbol, unknown>)[COMPOSED_PIECES]) continue
|
|
599
|
+
for (const v of Object.values(item)) {
|
|
600
|
+
const t = (v as Combinator<unknown> | undefined)?._meta?.grammarTrivia
|
|
601
|
+
if (t) return t
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return undefined
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function compose(
|
|
608
|
+
items: Array<LinkableTable | Record<string, unknown>>,
|
|
609
|
+
/**
|
|
610
|
+
* Compile-time host mode for the fused artifact, same meaning as
|
|
611
|
+
* `compile(g, { hostMode })`. Omit (or `'ast'`) for the eval driver — the fused rules
|
|
612
|
+
* build through the grammar's own `build` callbacks and carry no positioned-CST
|
|
613
|
+
* branch. Pass `'cst'` to fuse a SECOND artifact from the same pieces for the linter /
|
|
614
|
+
* IDE / language-service driver. Two compilations of one grammar, decided here, rather
|
|
615
|
+
* than one artifact deciding per node on every parse.
|
|
616
|
+
*/
|
|
617
|
+
opts?: { hostMode?: HostMode },
|
|
618
|
+
): Record<string, FusedRule> {
|
|
619
|
+
if (items.some(item => (item as Record<symbol, unknown>)[LEAF_COMPOSED] === true)) {
|
|
620
|
+
throw new Error('compose: a composeLeaf() result is terminal and cannot be composed again')
|
|
621
|
+
}
|
|
622
|
+
const used = new Set<string>()
|
|
623
|
+
// The composed grammar's ambient trivia comes from the composing grammar itself —
|
|
624
|
+
// whatever the last piece declared via rules({ trivia }, …). No separate option:
|
|
625
|
+
// the trivia rides with the grammar that declared it.
|
|
626
|
+
const trivia = composingTriviaOf(items)
|
|
627
|
+
// Carried items are RE-LOWERABLE (IR); materialize them ONCE with this compose's
|
|
628
|
+
// trivia for the now-fuse, but STORE the un-materialized carried list so a later
|
|
629
|
+
// compose can re-lower it under a different trivia (multi-level composing-wins).
|
|
630
|
+
const carried = items.flatMap(item => itemCarried(item, used, trivia, opts?.hostMode))
|
|
631
|
+
const map = fuseCarried(carried, trivia, opts?.hostMode)
|
|
632
|
+
Object.defineProperty(map, COMPOSED_PIECES, { value: carried, enumerable: false })
|
|
633
|
+
if (trivia) Object.defineProperty(map, COMPOSED_TRIVIA, { value: trivia, enumerable: false })
|
|
634
|
+
const coverageRules = coverageRulesOf(carried)
|
|
635
|
+
if (coverageRules) Object.defineProperty(map, COMPOSED_COVERAGE_RULES, { value: coverageRules, enumerable: false })
|
|
636
|
+
return map
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Compose a terminal grammar. This is for a leaf parser that overlays local
|
|
641
|
+
* semantic reductions on reusable recognition rules.
|
|
642
|
+
*
|
|
643
|
+
* Under the macro this lowers to STATIC fused source (functions), exactly like
|
|
644
|
+
* `compose()`. It is still macro-only as a *compiled* artifact: without macro
|
|
645
|
+
* lowering there is no safe way to keep lexical builders out of carried IR, so it
|
|
646
|
+
* never falls back to runtime CODEGEN composition.
|
|
647
|
+
*
|
|
648
|
+
* Called at runtime (no macro) it returns the INTERPRETED fuse of the same items —
|
|
649
|
+
* a combinator map, not a map of compiled functions (`fuseInterpreted`), fused lazily
|
|
650
|
+
* per rule name.
|
|
651
|
+
*
|
|
652
|
+
* THE RETURN TYPE IS `Runnable`, NOT `FusedRule`, BECAUSE BOTH PATHS ARE REAL. A macro
|
|
653
|
+
* build yields fused functions; an un-macro'd call yields combinators. `Runnable` is
|
|
654
|
+
* already the library's name for "either of those" — it is what `run()` and
|
|
655
|
+
* `parseDoc()` take — so the declared type is TRUE on both paths and a caller needs no
|
|
656
|
+
* narrowing to use the result. This used to declare `Record<string, FusedRule>` and
|
|
657
|
+
* launder the runtime path through an `as unknown as`, which let a caller hold a
|
|
658
|
+
* combinator map while the type promised compiled functions.
|
|
659
|
+
*
|
|
660
|
+
* Do NOT "fix" this by deleting the runtime path. It is load-bearing: the `bench/jess`
|
|
661
|
+
* harness family and two differential-gate legs (`emit-identity-one`,
|
|
662
|
+
* `scan-shape-oracle-one`) import un-macro'd grammar modules and depend on this lazy
|
|
663
|
+
* interpreted fuse, one dialect per process. Whether an un-macro'd `composeLeaf()`
|
|
664
|
+
* should exist at all is a separate, open owner question — but while the gates depend
|
|
665
|
+
* on it, it exists, and the type says so.
|
|
666
|
+
*/
|
|
667
|
+
export function composeLeaf(
|
|
668
|
+
items: Array<LinkableTable | Record<string, unknown>>,
|
|
669
|
+
): Record<string, Runnable> {
|
|
670
|
+
const pieces = items.flatMap(interpretedPieces)
|
|
671
|
+
let fused: Record<string, Combinator<unknown>> | undefined
|
|
672
|
+
const map: Record<string, unknown> = {}
|
|
673
|
+
// LAZY on purpose. A grammar module typically builds SEVERAL leaf grammars over
|
|
674
|
+
// one shared recognition piece (`cssGrammar`, `cssLineGrammar`, `cssCstGrammar`,
|
|
675
|
+
// …). An interpreted fuse binds that shared piece IN PLACE, so only one of them
|
|
676
|
+
// can exist at a time — fusing all of them at import would make merely importing
|
|
677
|
+
// the module throw. Fusing on first ACCESS means the grammar you actually use
|
|
678
|
+
// works, and reaching for a second, conflicting one fails loudly at that point.
|
|
679
|
+
// (`trackLines`/`hostMode` are compile-time distinctions; the interpreter decides
|
|
680
|
+
// both per parse, so those variants are the same interpreted grammar anyway.)
|
|
681
|
+
for (const name of ruleNamesOf(pieces)) {
|
|
682
|
+
Object.defineProperty(map, name, {
|
|
683
|
+
enumerable: true,
|
|
684
|
+
configurable: true,
|
|
685
|
+
get: () => (fused ??= fusePieces(pieces))[name],
|
|
686
|
+
})
|
|
687
|
+
}
|
|
688
|
+
Object.defineProperty(map, LEAF_COMPOSED, { value: true, enumerable: false })
|
|
689
|
+
Object.defineProperty(map, INTERPRETED_PIECES, {
|
|
690
|
+
value: pieces.filter(p => p.plain).map(p => p.entries),
|
|
691
|
+
enumerable: false,
|
|
692
|
+
})
|
|
693
|
+
return map as Record<string, Runnable>
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* ── Interpreted fuse ─────────────────────────────────────────────────────────
|
|
697
|
+
*
|
|
698
|
+
* `compose()` fuses by CODEGEN: every piece is compiled to `_r_<Name>` functions
|
|
699
|
+
* dropped into one scope, so a reference resolves by NAME and an override reroutes
|
|
700
|
+
* the base piece's own calls (open recursion). None of that exists interpreted —
|
|
701
|
+
* the interpreter runs the combinator graph, and a cross-piece reference is an
|
|
702
|
+
* ordinary `ref()` placeholder that nobody ever `.define()`d. That is why a
|
|
703
|
+
* composed grammar could not be run interpreted at all, and why every diagnostic
|
|
704
|
+
* that must NOT reach codegen (profiling, gating analysis, coverage) had to be
|
|
705
|
+
* hand-fused in throwaway scripts.
|
|
706
|
+
*
|
|
707
|
+
* The interpreted fuse binds those placeholders directly, with the SAME semantics
|
|
708
|
+
* the compiled fuse gets from name resolution:
|
|
709
|
+
* - later piece wins per rule name (matching the compiled merge);
|
|
710
|
+
* - an override REPOINTS the slot every call site already holds, so a base
|
|
711
|
+
* piece's internal calls reroute too (open recursion);
|
|
712
|
+
* - the composing grammar's trivia governs every fused rule (`composingTriviaOf`);
|
|
713
|
+
* - a referenced-but-undefined rule is a fuse-time error, not a parse-time one.
|
|
714
|
+
*
|
|
715
|
+
* It is MUTATING by construction: a hole is a shared object, and binding it is the
|
|
716
|
+
* only way its call sites can see the answer. `repointRef` therefore records what
|
|
717
|
+
* it changed and refuses a CONFLICTING second bind, so two different fusions over
|
|
718
|
+
* one shared piece fail loudly instead of silently rewriting each other's parser.
|
|
719
|
+
* ───────────────────────────────────────────────────────────────────────────── */
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* A rule slot produced by `ref()` — what `rules()` stores for every rule that is
|
|
723
|
+
* referenced by name, and for every `g.X` hole a piece leaves for another piece to
|
|
724
|
+
* fill. `parse`/`_def.thunk` are OWN properties of that object, which is what lets
|
|
725
|
+
* the interpreted fuse repoint it in place.
|
|
726
|
+
*/
|
|
727
|
+
type RefSlot = Combinator<unknown> & {
|
|
728
|
+
_def: { tag: 'lazy'; thunk: () => Combinator<unknown> }
|
|
729
|
+
define(p: Combinator<unknown>): void
|
|
730
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<unknown>
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function isRefSlot(c: Combinator<unknown>): c is RefSlot {
|
|
734
|
+
return c._def.tag === 'lazy' && typeof (c as unknown as { define?: unknown }).define === 'function'
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** What a slot resolved to BEFORE any interpreted fuse touched it (`null` = it was
|
|
738
|
+
* an unbound cross-piece hole). Recorded on first repoint so a LATER fuse computes
|
|
739
|
+
* its winner from the grammar as authored, never from another fusion's binding. */
|
|
740
|
+
const FUSE_ORIGINAL = Symbol.for('parseman.interpretedFuseOriginal')
|
|
741
|
+
/** What an interpreted fuse repointed this slot at. */
|
|
742
|
+
const FUSE_TARGET = Symbol.for('parseman.interpretedFuseTarget')
|
|
743
|
+
/** The source rule maps behind a `fuseInterpreted()` result, so it can be fused
|
|
744
|
+
* again — the interpreted mirror of `COMPOSED_PIECES`. */
|
|
745
|
+
const INTERPRETED_PIECES = Symbol.for('parseman.interpretedPieces')
|
|
746
|
+
|
|
747
|
+
/** Whether `map` is an interpreted fuse (a combinator map) rather than a compiled
|
|
748
|
+
* `compose()` result (a map of fused functions). INTERNAL — not re-exported from
|
|
749
|
+
* `src/index.ts`. A consumer never has to ask this question: what it holds is
|
|
750
|
+
* whatever the macro built. Diagnostics that fuse interpreted on purpose do. */
|
|
751
|
+
export function isInterpretedFuse(map: object): boolean {
|
|
752
|
+
return Array.isArray((map as Record<symbol, unknown>)[INTERPRETED_PIECES])
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
type NamedEntries = ReadonlyArray<[string, Combinator<unknown>]>
|
|
756
|
+
/** `plain` = the item was an authored `rules()` map. Only plain maps declare the
|
|
757
|
+
* composing trivia, mirroring `composingTriviaOf`, which skips artifacts and prior
|
|
758
|
+
* composed results for exactly the same reason: their trivia was already applied. */
|
|
759
|
+
type FusePiece = { entries: NamedEntries; plain: boolean }
|
|
760
|
+
|
|
761
|
+
/** The rule a map entry DEFINES, or `undefined` when it is an external reference
|
|
762
|
+
* (an accessed-but-undefined `g.X`). Same local-vs-external test `compileLinkable`
|
|
763
|
+
* and `itemCarried` apply, so the two fuses agree on what a piece contributes. */
|
|
764
|
+
function definitionOf(entry: Combinator<unknown>): Combinator<unknown> | undefined {
|
|
765
|
+
if (isRefSlot(entry)) {
|
|
766
|
+
const original = (entry as unknown as Record<symbol, unknown>)[FUSE_ORIGINAL]
|
|
767
|
+
if (original !== undefined) return (original as Combinator<unknown> | null) ?? undefined
|
|
768
|
+
try { return entry._def.thunk() } catch { return undefined }
|
|
769
|
+
}
|
|
770
|
+
if (entry._def.tag === 'lazy') {
|
|
771
|
+
try { (entry._def as { thunk: () => Combinator<unknown> }).thunk() } catch { return undefined }
|
|
772
|
+
}
|
|
773
|
+
return entry
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/** Point `slot` at `target`, updating every call site that holds it. Mirrors
|
|
777
|
+
* `ref().define()`'s metadata propagation; refuses to overwrite a binding a
|
|
778
|
+
* DIFFERENT fusion already made (see the mutation note above). */
|
|
779
|
+
function repointRef(slot: RefSlot, target: Combinator<unknown>, name: string): void {
|
|
780
|
+
const tagged = slot as unknown as Record<symbol, unknown>
|
|
781
|
+
const bound = tagged[FUSE_TARGET] as Combinator<unknown> | undefined
|
|
782
|
+
const original = FUSE_ORIGINAL in tagged
|
|
783
|
+
? tagged[FUSE_ORIGINAL] as Combinator<unknown> | null
|
|
784
|
+
: (() => { try { return slot._def.thunk() } catch { return null } })()
|
|
785
|
+
if ((bound ?? original) === target) return
|
|
786
|
+
if (bound !== undefined) {
|
|
787
|
+
throw new Error(
|
|
788
|
+
`fuseInterpreted: rule "${name}" is already bound by a DIFFERENT interpreted fusion of the same grammar piece. `
|
|
789
|
+
+ `An interpreted fuse binds the shared placeholder objects in place, so two fusions cannot share a piece — `
|
|
790
|
+
+ `build a fresh instance of the piece (call its rules() factory again, or import the module under a distinct specifier) for the second fusion.`,
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
if (!(FUSE_ORIGINAL in tagged)) Object.defineProperty(slot, FUSE_ORIGINAL, { value: original, enumerable: false })
|
|
794
|
+
Object.defineProperty(slot, FUSE_TARGET, { value: target, enumerable: false })
|
|
795
|
+
slot._def.thunk = () => target
|
|
796
|
+
slot.parse = (input, pos, ctx) => target.parse(input, pos, ctx)
|
|
797
|
+
const meta = slot._meta
|
|
798
|
+
meta.firstSet = target._meta.firstSet
|
|
799
|
+
meta.canMatchNewline = target._meta.canMatchNewline
|
|
800
|
+
meta.isTrivia = target._meta.isTrivia
|
|
801
|
+
if (target._meta.triviaKindLabels !== undefined) meta.triviaKindLabels = target._meta.triviaKindLabels
|
|
802
|
+
else (meta as { triviaKindLabels: readonly string[] | undefined }).triviaKindLabels = undefined
|
|
803
|
+
if (target._meta.disjoint !== undefined) meta.disjoint = target._meta.disjoint
|
|
804
|
+
else (meta as { disjoint: boolean | undefined }).disjoint = undefined
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Flatten one `fuseInterpreted()` item to the rule maps it contributes, in order. */
|
|
808
|
+
function interpretedPieces(item: LinkableTable | Record<string, unknown>): FusePiece[] {
|
|
809
|
+
const fused = (item as Record<symbol, unknown>)[INTERPRETED_PIECES]
|
|
810
|
+
if (Array.isArray(fused)) return (fused as NamedEntries[]).map(entries => ({ entries, plain: false }))
|
|
811
|
+
const carried = composedPiecesOf(item as Record<string, unknown>)
|
|
812
|
+
if (carried !== undefined) {
|
|
813
|
+
// A compiled `compose()` result. Its carried IR re-lowers to combinators, but a
|
|
814
|
+
// piece that arrived already COMPILED has no combinator graph at all — fusing
|
|
815
|
+
// around it would silently drop its rules, which is the one failure mode a
|
|
816
|
+
// diagnostic must never have.
|
|
817
|
+
const { maps, opaque } = carriedRuleMapsDetailed(carried)
|
|
818
|
+
if (opaque.length > 0) {
|
|
819
|
+
throw new Error(
|
|
820
|
+
`fuseInterpreted: cannot interpret a composed grammar containing precompiled artifact(s) `
|
|
821
|
+
+ `${opaque.map(o => `"${o.ns}" (${o.ruleNames.length} rules)`).join(', ')} — they carry compiled functions, not a combinator graph. `
|
|
822
|
+
+ `Pass the source grammars (the same items you passed to compose()) instead.`,
|
|
823
|
+
)
|
|
824
|
+
}
|
|
825
|
+
return maps.map(entries => ({ entries, plain: false }))
|
|
826
|
+
}
|
|
827
|
+
if (isLinkableTable(item)) {
|
|
828
|
+
// A table piece DOES carry its IR, so this is recoverable rather than fatal: hydrate
|
|
829
|
+
// the combinator graph back out of it and interpret that.
|
|
830
|
+
const rules = ruleMapOfCarried(item)
|
|
831
|
+
if (rules === undefined) {
|
|
832
|
+
throw new Error('fuseInterpreted: a precompiled linkable artifact with no carried IR has no combinator graph to interpret; pass the source grammar (a rules() map) instead')
|
|
833
|
+
}
|
|
834
|
+
return [{ entries: rules, plain: false }]
|
|
835
|
+
}
|
|
836
|
+
return [{ entries: Object.entries(item as Record<string, Combinator<unknown>>), plain: true }]
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Materialize a composition as a RUNNABLE INTERPRETED rule map — the interpreted
|
|
841
|
+
* counterpart of `compose()`, with identical fuse semantics (later piece wins,
|
|
842
|
+
* override reroutes the base's own calls, composing trivia governs every rule).
|
|
843
|
+
* No codegen, no `new Function`, no macro build step: the result is a plain map of
|
|
844
|
+
* combinators that `run()` / `parseDoc()` accept exactly like a fused map.
|
|
845
|
+
*
|
|
846
|
+
* This is what diagnostics and profiling run against — they must stay in
|
|
847
|
+
* interpreted mode, and before this they could not see a composed grammar at all.
|
|
848
|
+
*
|
|
849
|
+
* Items are the SAME items `compose()`/`composeLeaf()` take: `rules()` maps
|
|
850
|
+
* (the intended input), a prior `fuseInterpreted()` result, or a runtime
|
|
851
|
+
* `compose()` result (re-lowered from its carried IR — note that carried IR cannot
|
|
852
|
+
* materialize direct `node()` builders, so prefer the source maps). A precompiled
|
|
853
|
+
* `linkable()` artifact is rejected: it has no combinator graph.
|
|
854
|
+
*
|
|
855
|
+
* MUTATION: binding a cross-piece hole rewrites the shared placeholder object every
|
|
856
|
+
* call site already holds — that IS how an override reaches a base piece's own
|
|
857
|
+
* calls. A second, DIFFERENT fusion over the same piece objects therefore throws
|
|
858
|
+
* rather than silently rewriting the first one's parser.
|
|
859
|
+
*/
|
|
860
|
+
export function fuseInterpreted(
|
|
861
|
+
items: Array<LinkableTable | Record<string, unknown>>,
|
|
862
|
+
opts?: { hostMode?: HostMode },
|
|
863
|
+
): Record<string, Combinator<unknown>> {
|
|
864
|
+
return fusePieces(items.flatMap(interpretedPieces), opts)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/** The rule names a fusion of these pieces defines, in winner order — computable
|
|
868
|
+
* WITHOUT binding anything, which is what lets `composeLeaf()` expose its key set
|
|
869
|
+
* before it fuses. */
|
|
870
|
+
function ruleNamesOf(pieces: FusePiece[]): string[] {
|
|
871
|
+
const names = new Set<string>()
|
|
872
|
+
for (const piece of pieces) {
|
|
873
|
+
for (const [name, value] of piece.entries) if (definitionOf(value) !== undefined) names.add(name)
|
|
874
|
+
}
|
|
875
|
+
return [...names]
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function fusePieces(
|
|
879
|
+
pieces: FusePiece[],
|
|
880
|
+
opts?: { hostMode?: HostMode },
|
|
881
|
+
): Record<string, Combinator<unknown>> {
|
|
882
|
+
// Composing-wins trivia, read exactly as `composingTriviaOf` reads it for compose():
|
|
883
|
+
// the LAST authored grammar that declared `rules({ trivia }, …)`.
|
|
884
|
+
let trivia: Combinator<unknown> | undefined
|
|
885
|
+
for (let i = pieces.length - 1; i >= 0 && trivia === undefined; i--) {
|
|
886
|
+
if (!pieces[i]!.plain) continue
|
|
887
|
+
for (const [, rule] of pieces[i]!.entries) {
|
|
888
|
+
const t = rule._meta.grammarTrivia
|
|
889
|
+
if (t) { trivia = t; break }
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Winner per rule name — later piece wins, matching `fuseRules`. The winner is the
|
|
894
|
+
// DEFINITION, never the slot holding it: a slot can be repointed, and a chain
|
|
895
|
+
// through one would make an override of X reroute into itself.
|
|
896
|
+
const winner = new Map<string, Combinator<unknown>>()
|
|
897
|
+
const entry = new Map<string, Combinator<unknown>>()
|
|
898
|
+
for (const piece of pieces) {
|
|
899
|
+
for (const [name, value] of piece.entries) {
|
|
900
|
+
const def = definitionOf(value)
|
|
901
|
+
if (def === undefined) continue
|
|
902
|
+
winner.set(name, def)
|
|
903
|
+
entry.set(name, value)
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Bind every hole (and repoint every overridden slot) before anything runs.
|
|
908
|
+
const missing = new Set<string>()
|
|
909
|
+
for (const piece of pieces) {
|
|
910
|
+
for (const [name, value] of piece.entries) {
|
|
911
|
+
if (!isRefSlot(value)) continue
|
|
912
|
+
const target = winner.get(name)
|
|
913
|
+
if (target === undefined) { missing.add(name); continue }
|
|
914
|
+
repointRef(value, target, name)
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (missing.size > 0) throw new Error(missingRuleMessage(pieces, missing))
|
|
918
|
+
|
|
919
|
+
const out: Record<string, Combinator<unknown>> = {}
|
|
920
|
+
for (const [name, value] of entry) {
|
|
921
|
+
out[name] = value
|
|
922
|
+
const meta = value._meta as {
|
|
923
|
+
isTrivia: boolean
|
|
924
|
+
grammarTrivia?: Combinator<unknown>
|
|
925
|
+
grammarHostMode?: HostMode
|
|
926
|
+
}
|
|
927
|
+
// A trivia rule must never carry the ambient trivia (it would recursively skip
|
|
928
|
+
// trivia within itself) — the same guard `compileLinkable` applies per rule.
|
|
929
|
+
if (trivia !== undefined && !meta.isTrivia) meta.grammarTrivia = trivia
|
|
930
|
+
// Host mode is PER PIECE, exactly as `compileLinkable` resolves it: an explicit
|
|
931
|
+
// option wins, otherwise the owning piece's own `rules({ hostMode })` stamp.
|
|
932
|
+
if (opts?.hostMode !== undefined && !meta.isTrivia) {
|
|
933
|
+
if (opts.hostMode === 'cst') meta.grammarHostMode = 'cst'
|
|
934
|
+
else (meta as { grammarHostMode: HostMode | undefined }).grammarHostMode = undefined
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
Object.defineProperty(out, INTERPRETED_PIECES, {
|
|
938
|
+
value: pieces.filter(p => p.plain).map(p => p.entries),
|
|
939
|
+
enumerable: false,
|
|
940
|
+
})
|
|
941
|
+
return out
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** Name-closure failure, reported the way the compiled fuse reports it: which rule
|
|
945
|
+
* referenced the missing name. Computed only on the error path. */
|
|
946
|
+
function missingRuleMessage(pieces: FusePiece[], missing: Set<string>): string {
|
|
947
|
+
for (const piece of pieces) {
|
|
948
|
+
const deps = ruleDependencies(piece.entries.filter(([, v]) => definitionOf(v) !== undefined))
|
|
949
|
+
for (const [name, ds] of deps) {
|
|
950
|
+
for (const d of ds) if (missing.has(d)) return `fuseInterpreted: rule "${name}" references missing rule "${d}"`
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return `fuseInterpreted: missing rule(s) ${[...missing].map(n => `"${n}"`).join(', ')}`
|
|
954
|
+
}
|