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,45 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runs `combinator` with `ctx.state` set to `extra` for the duration of the parse.
|
|
5
|
+
* The outer user context is restored on exit (lexical scoping).
|
|
6
|
+
*
|
|
7
|
+
* const functionBody = withCtx({ inFn: true },
|
|
8
|
+
* sequence(literal('{'), many(statement), literal('}'))
|
|
9
|
+
* )
|
|
10
|
+
*
|
|
11
|
+
* Read back with guard() or from within a transform's span argument.
|
|
12
|
+
*/
|
|
13
|
+
export function withCtx<U, T>(extra: U, combinator: Combinator<T>): Combinator<T> {
|
|
14
|
+
const meta: ParserMeta = {
|
|
15
|
+
firstSet: combinator._meta.firstSet,
|
|
16
|
+
canMatchNewline: combinator._meta.canMatchNewline,
|
|
17
|
+
isTrivia: false,
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
_tag: 'withCtx',
|
|
21
|
+
_meta: meta,
|
|
22
|
+
_def: { tag: 'withCtx', extra, parser: combinator as Combinator<unknown> },
|
|
23
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T> {
|
|
24
|
+
// SAVE / RESTORE, not a clone. This used to run the child against
|
|
25
|
+
// `{ ...ctx, state: extra }`, which scopes far more than the state: every
|
|
26
|
+
// scalar the child writes on `ctx` lands on the clone and dies with it —
|
|
27
|
+
// including `_fe` / `_fx`, the furthest-failure position and its expected
|
|
28
|
+
// set. A failing `withCtx` subtree therefore contributed NOTHING to the
|
|
29
|
+
// parent's expected set, silently. Nobody asked for that isolation; the
|
|
30
|
+
// combinator's contract is "set `state` for the duration", and the clone
|
|
31
|
+
// was an implementation detail with a semantic reach nobody consented to.
|
|
32
|
+
//
|
|
33
|
+
// (Array MUTATIONS were never affected either way — `_triviaLog` and the
|
|
34
|
+
// CST buffers are the same objects through a spread — so this changes the
|
|
35
|
+
// scalars only, which is exactly the part that was being dropped.)
|
|
36
|
+
//
|
|
37
|
+
// It also allocated a full context per entry, on the object every
|
|
38
|
+
// combinator reads on every step.
|
|
39
|
+
const saved = ctx.state
|
|
40
|
+
ctx.state = extra
|
|
41
|
+
try { return combinator.parse(input, pos, ctx) }
|
|
42
|
+
finally { ctx.state = saved }
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import type { ParserDef } from '../types.ts'
|
|
2
|
+
import { recordDegradation } from './degradation.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-node children/fields/raw/trivia/state capture is dead work when the build
|
|
6
|
+
* never declares the corresponding formal param. This module derives a build's
|
|
7
|
+
* *confirmed* formal-parameter arity so the compiler and macro can elide their
|
|
8
|
+
* direct-AST-only CST collectors without changing structural/CST output.
|
|
9
|
+
*
|
|
10
|
+
* Conservative by construction: any source we can't confidently parse — rest params,
|
|
11
|
+
* destructuring, `arguments`, an unrecognized shape — yields `null` (arity unknown),
|
|
12
|
+
* and callers KEEP full capture. We only ever return a number when the parameter list
|
|
13
|
+
* is a plain comma-separated list of simple identifiers.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type NodeDef = Extract<ParserDef, { tag: 'node' }>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Is a positioned-CST host installed for this parse?
|
|
20
|
+
*
|
|
21
|
+
* `cstBuildHost` (and the language-service hosts) mark themselves with
|
|
22
|
+
* `_parsemanCstOutput`. That flag is what re-routes a node with its OWN direct builder
|
|
23
|
+
* through the host — and therefore what makes the direct builder's formal arity the
|
|
24
|
+
* WRONG basis for eliding capture, since the consumer is no longer that builder.
|
|
25
|
+
*
|
|
26
|
+
* The COMPILED engine answers this at compile time (`hostMode`, 0.37.0). The interpreter
|
|
27
|
+
* has no compile step, so it asks per parse.
|
|
28
|
+
*/
|
|
29
|
+
export function cstOutputHost(build: unknown): boolean {
|
|
30
|
+
return (build as { _parsemanCstOutput?: true } | undefined)?._parsemanCstOutput === true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ── THE ONE RULE IN THIS FILE ────────────────────────────────────────────────────
|
|
35
|
+
* A WRONG NUMBER IS THE BUG. `null` (unknown) IS ALWAYS ACCEPTABLE.
|
|
36
|
+
*
|
|
37
|
+
* `null` fails open: the caller keeps every capture tier and records a degradation, so
|
|
38
|
+
* the output is correct and the cost is visible. A wrong NUMBER silently drops capture
|
|
39
|
+
* tiers — fields vanish from the AST with no diagnostic, because a confident answer
|
|
40
|
+
* never reaches `recordDegradation`. Every branch below that cannot decide with
|
|
41
|
+
* certainty must therefore return `null`, never a guess.
|
|
42
|
+
* ─────────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/** `x => ...` — a single unparenthesised identifier parameter. */
|
|
46
|
+
const SINGLE_IDENT_ARROW_RE = /^[A-Za-z_$][\w$]*\s*=>/
|
|
47
|
+
|
|
48
|
+
/** Start of a formal-parameter list: `(`, or `function name?(`. */
|
|
49
|
+
const PARAM_LIST_OPEN_RE = /^(?:function\b[^(]*)?\(/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A host builtin, a `Function.prototype.bind` result and a `Proxy` all stringify to
|
|
53
|
+
* `function () { [native code] }`. That has an EMPTY parameter list which says nothing
|
|
54
|
+
* whatever about the underlying function's arity — reading it as arity 0 is a confident
|
|
55
|
+
* lie that silently drops every capture tier. None of those are documented reducer
|
|
56
|
+
* shapes and parseman does not owe them support; it owes them an honest `null`.
|
|
57
|
+
*/
|
|
58
|
+
const NATIVE_CODE_RE = /\{\s*\[native code\]\s*\}/
|
|
59
|
+
|
|
60
|
+
/** Identifier head of a formal param, with its optional `?`. */
|
|
61
|
+
const PARAM_HEAD_RE = /^[A-Za-z_$][\w$]*\s*\??\s*/
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Confirmed count of simple formal parameters the build declares, or `null` when the
|
|
65
|
+
* source can't be parsed into a plain identifier list (→ caller keeps full capture).
|
|
66
|
+
*/
|
|
67
|
+
export function confirmedBuildArity(src: string): number | null {
|
|
68
|
+
const s = src.trim()
|
|
69
|
+
|
|
70
|
+
// Unreadable source → unknown. Checked FIRST: its parameter list is empty, and the
|
|
71
|
+
// empty-list answer below would otherwise report a confident 0.
|
|
72
|
+
if (NATIVE_CODE_RE.test(s)) return null
|
|
73
|
+
|
|
74
|
+
// A stray `arguments` reference defeats formal-arity reasoning entirely; only a
|
|
75
|
+
// full-source scan can rule it out. Cheap guard against the obvious cases.
|
|
76
|
+
//
|
|
77
|
+
// Checked BEFORE the empty-parameter-list answer below. It used to sit after it, which
|
|
78
|
+
// made it unreachable for exactly the shape it matters most for: `function () { return
|
|
79
|
+
// arguments[0] }` has an empty formal list and real arity ≥ 1, and was reported 0.
|
|
80
|
+
if (/\barguments\b/.test(s)) return null
|
|
81
|
+
|
|
82
|
+
// Single-ident arrow: `x => ...` → exactly one param.
|
|
83
|
+
if (SINGLE_IDENT_ARROW_RE.test(s)) return 1
|
|
84
|
+
|
|
85
|
+
const inner = paramListText(s)
|
|
86
|
+
if (inner === null) return null
|
|
87
|
+
if (inner.trim() === '') return 0
|
|
88
|
+
|
|
89
|
+
const parts = splitTopLevel(inner)
|
|
90
|
+
if (parts === null) return null
|
|
91
|
+
|
|
92
|
+
// Reject anything that isn't a flat list of simple identifiers. A rest param
|
|
93
|
+
// (`...args`), destructuring (`{a}`, `[a]`), or default (`a = 1`) all appear here.
|
|
94
|
+
for (const part of parts) {
|
|
95
|
+
if (!isConfirmableParam(part.trim())) return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return parts.length
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The text between a build's parameter parentheses, or `null` when it cannot be located
|
|
103
|
+
* with certainty.
|
|
104
|
+
*
|
|
105
|
+
* Found by a BALANCED scan rather than a character class. The previous `[^)]*` stopped at
|
|
106
|
+
* the first `)`, which in ordinary TypeScript is the one inside a function-typed
|
|
107
|
+
* annotation: `(children: (n: N) => N, fields) => …` yielded `children: (n: N` and
|
|
108
|
+
* reported arity 1 instead of 2 — a silent under-capture on precisely the typed shape the
|
|
109
|
+
* docs tell authors to write.
|
|
110
|
+
*/
|
|
111
|
+
function paramListText(s: string): string | null {
|
|
112
|
+
const open = PARAM_LIST_OPEN_RE.exec(s)
|
|
113
|
+
if (!open) return null
|
|
114
|
+
const start = open[0].length
|
|
115
|
+
const end = matchingParen(s, start)
|
|
116
|
+
if (end === null) return null
|
|
117
|
+
// An arrow's list must be followed by `=>`; a `function`'s is followed by its body.
|
|
118
|
+
// Without this a parenthesised expression could be read as a parameter list.
|
|
119
|
+
if (!s.startsWith('function') && !/^\s*=>/.test(s.slice(end + 1))) return null
|
|
120
|
+
return s.slice(start, end)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Index of the `)` closing the list opened just before `start`, else `null`. */
|
|
124
|
+
function matchingParen(s: string, start: number): number | null {
|
|
125
|
+
let depth = 0
|
|
126
|
+
for (let i = start; i < s.length; i++) {
|
|
127
|
+
const c = s[i]!
|
|
128
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
129
|
+
const close = skipString(s, i)
|
|
130
|
+
if (close === null) return null
|
|
131
|
+
i = close
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
if (c === '(' || c === '[' || c === '{') depth++
|
|
135
|
+
else if (c === ']' || c === '}') { if (--depth < 0) return null }
|
|
136
|
+
else if (c === ')') { if (depth === 0) return i; depth-- }
|
|
137
|
+
}
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Index of the closing quote of the literal opened at `open`, else `null`. */
|
|
142
|
+
function skipString(s: string, open: number): number | null {
|
|
143
|
+
const quote = s[open]!
|
|
144
|
+
for (let i = open + 1; i < s.length; i++) {
|
|
145
|
+
if (s[i] === '\\') { i++; continue }
|
|
146
|
+
if (s[i] === quote) return i
|
|
147
|
+
}
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Split a parameter list on TOP-LEVEL commas, or `null` if it cannot be split with
|
|
153
|
+
* certainty.
|
|
154
|
+
*
|
|
155
|
+
* Tracks `()`/`[]`/`{}` AND `<>`, because a comma inside type arguments belongs to the
|
|
156
|
+
* annotation, not the list: a plain `,`-split turned `(a: Map<K, V>, b) => …` into three
|
|
157
|
+
* fragments. `=>` is consumed as a unit so the `>` of an arrow type is not mistaken for
|
|
158
|
+
* the close of a type-argument list.
|
|
159
|
+
*/
|
|
160
|
+
function splitTopLevel(inner: string): string[] | null {
|
|
161
|
+
const parts: string[] = []
|
|
162
|
+
let depth = 0
|
|
163
|
+
let angle = 0
|
|
164
|
+
let last = 0
|
|
165
|
+
for (let i = 0; i < inner.length; i++) {
|
|
166
|
+
const c = inner[i]!
|
|
167
|
+
if (c === '=' && inner[i + 1] === '>') { i++; continue }
|
|
168
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
169
|
+
const close = skipString(inner, i)
|
|
170
|
+
if (close === null) return null
|
|
171
|
+
i = close
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
if (c === '(' || c === '[' || c === '{') depth++
|
|
175
|
+
else if (c === ')' || c === ']' || c === '}') { if (--depth < 0) return null }
|
|
176
|
+
else if (c === '<') angle++
|
|
177
|
+
else if (c === '>') { if (--angle < 0) return null }
|
|
178
|
+
else if (c === ',' && depth === 0 && angle === 0) { parts.push(inner.slice(last, i)); last = i + 1 }
|
|
179
|
+
}
|
|
180
|
+
if (depth !== 0 || angle !== 0) return null
|
|
181
|
+
parts.push(inner.slice(last))
|
|
182
|
+
return parts
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A confirmable formal param: a plain identifier, optionally `?`-optional and optionally
|
|
187
|
+
* carrying a TypeScript type annotation (`c`, `c?`, `c: any`, `c?: Map<K, V>`, `c: (n: N)
|
|
188
|
+
* => N`). The build source is sliced verbatim from the (possibly TS) grammar source, so
|
|
189
|
+
* typed params must be recognized or every typed grammar keeps full capture.
|
|
190
|
+
*
|
|
191
|
+
* A DEFAULT (`a = 1`, `a: number = 1`) stays unconfirmed: the parameter is optional, so
|
|
192
|
+
* the formal count overstates what the reducer is guaranteed to receive.
|
|
193
|
+
*/
|
|
194
|
+
function isConfirmableParam(part: string): boolean {
|
|
195
|
+
const head = PARAM_HEAD_RE.exec(part)
|
|
196
|
+
if (!head) return false // rest `...a`, destructuring `{a}` / `[a]`
|
|
197
|
+
const rest = part.slice(head[0].length)
|
|
198
|
+
if (rest === '') return true
|
|
199
|
+
if (rest[0] !== ':') return false // `a = 1`, or anything else we cannot read
|
|
200
|
+
return !hasDefaultAssign(rest.slice(1))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** An `=` that is not part of `=>`, `>=`, `<=`, `==` or `!=` — i.e. a default value. */
|
|
204
|
+
function hasDefaultAssign(annotation: string): boolean {
|
|
205
|
+
for (let i = 0; i < annotation.length; i++) {
|
|
206
|
+
if (annotation[i] !== '=') continue
|
|
207
|
+
if (annotation[i + 1] === '>' || annotation[i + 1] === '=') { i++; continue }
|
|
208
|
+
const prev = annotation[i - 1]
|
|
209
|
+
if (prev === '<' || prev === '>' || prev === '!' || prev === '=') continue
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
return false
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The source a def's arity/shape analysis must read.
|
|
217
|
+
*
|
|
218
|
+
* `buildSrc` is the source text of the EXPRESSION written at the `node(...)` call
|
|
219
|
+
* site. When the reducer is passed as a bare identifier — `node('Foo', p, { build:
|
|
220
|
+
* foldOperation })` — that text is just `"foldOperation"`, which matches no parameter
|
|
221
|
+
* list and used to fail open into all five capture tiers, silently. The macro plugin
|
|
222
|
+
* already holds the whole module's AST, so it resolves such an identifier to its
|
|
223
|
+
* module-scope declaration and parks that declaration's source here. `buildSigSrc` is
|
|
224
|
+
* ANALYSIS-ONLY: it is never emitted, so the generated builder reference is unchanged.
|
|
225
|
+
*/
|
|
226
|
+
export function buildAnalysisSrc(def: NodeDef): string {
|
|
227
|
+
return def.buildSigSrc ?? def.buildSrc ?? def.build!.toString()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Why a parameter list could not be read — the actionable half of the diagnostic.
|
|
232
|
+
*
|
|
233
|
+
* Only genuinely undecidable shapes reach here. The resolver (`plugin/reducer-resolver.
|
|
234
|
+
* ts`) handles named reducers, cross-module imports, namespace members, aliases,
|
|
235
|
+
* non-reassigned `let`/`var`, defaults and destructuring; a decline from it carries a
|
|
236
|
+
* machine-readable reason, which is what the author needs in order to fix or declare it.
|
|
237
|
+
*/
|
|
238
|
+
const RESOLVER_REASONS: Record<string, string> = {
|
|
239
|
+
'rest-parameter': 'its parameter list uses a rest parameter, so the declared arity is unbounded',
|
|
240
|
+
'arguments': 'its body references `arguments`',
|
|
241
|
+
'reassigned': 'the binding it names is reassigned, so the function read here is not decidable',
|
|
242
|
+
'not-a-function': 'the binding it names is not a function declaration',
|
|
243
|
+
'unresolved-import': 'its module could not be resolved or parsed',
|
|
244
|
+
'not-found': 'the name did not resolve to any binding',
|
|
245
|
+
'computed': 'it is a computed expression',
|
|
246
|
+
'foreign-source': 'it is named in a module the resolver was never told about, so its '
|
|
247
|
+
+ 'offsets index no scope tree the resolver holds',
|
|
248
|
+
'ambiguous-source': 'two distinct registered modules hold byte-identical text, so that '
|
|
249
|
+
+ 'text no longer names the module its offsets index',
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function unconfirmableReason(def: NodeDef, src: string): string {
|
|
253
|
+
const declared = def.buildArityUnresolved
|
|
254
|
+
if (declared !== undefined && RESOLVER_REASONS[declared]) return RESOLVER_REASONS[declared]!
|
|
255
|
+
const s = src.trim()
|
|
256
|
+
const params = s.slice(0, s.indexOf(')') + 1)
|
|
257
|
+
if (params.includes('...')) return RESOLVER_REASONS['rest-parameter']!
|
|
258
|
+
if (/\barguments\b/.test(s)) return RESOLVER_REASONS['arguments']!
|
|
259
|
+
if (/^[A-Za-z_$][\w$]*(\s*\.\s*[A-Za-z_$][\w$]*)?$/.test(s)) {
|
|
260
|
+
return 'it names a binding this build could not resolve to a function declaration'
|
|
261
|
+
}
|
|
262
|
+
return 'its parameter list could not be parsed'
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Confirmed formal arity for a node def, reporting once when it cannot be confirmed.
|
|
267
|
+
*
|
|
268
|
+
* Order of authority:
|
|
269
|
+
* 1. `node(..., { buildArity })` — the author DECLARED it. Nothing overrides that.
|
|
270
|
+
* 2. `def.buildArity` as resolved by the macro's reducer resolver (real scope analysis
|
|
271
|
+
* and cross-module import following).
|
|
272
|
+
* 3. Reading the parameter list out of whatever source we have.
|
|
273
|
+
*
|
|
274
|
+
* Fail-open is the correct BEHAVIOR — capturing too much is safe, capturing too little
|
|
275
|
+
* is a correctness bug. Fail-open with no diagnostic was the defect. But a diagnostic is
|
|
276
|
+
* the right answer only for the genuinely undecidable; for everything a static analysis
|
|
277
|
+
* can decide, the right answer is to decide it, which is why (2) exists and why (1) gives
|
|
278
|
+
* the author a way out of (3) entirely.
|
|
279
|
+
*/
|
|
280
|
+
export function confirmedArityForDef(def: NodeDef): number | null {
|
|
281
|
+
if (def.buildArity !== undefined) return def.buildArity
|
|
282
|
+
const src = buildAnalysisSrc(def)
|
|
283
|
+
const arity = confirmedBuildArity(src)
|
|
284
|
+
if (arity !== null) return arity
|
|
285
|
+
const subject = `build reducer \`${(def.buildSrc ?? '<runtime fn>').trim().split('\n')[0]!.slice(0, 60)}\``
|
|
286
|
+
recordDegradation({
|
|
287
|
+
code: 'build-arity-unconfirmed',
|
|
288
|
+
severity: 'warn',
|
|
289
|
+
where: `node("${def.type ?? '<inferred>'}")`,
|
|
290
|
+
subject,
|
|
291
|
+
fellBackTo: `could not confirm its declared arity — ${unconfirmableReason(def, src)} — `
|
|
292
|
+
+ 'so this node captures children, fields and raw children, logs trivia, and clones `_ctx.state` on every match',
|
|
293
|
+
otherwise: 'only the tiers the reducer actually declares would be captured '
|
|
294
|
+
+ '(arity >= 1 children, >= 2 fields, >= 4 raw, >= 5 trivia, >= 6 state). '
|
|
295
|
+
+ 'Declare it with `node(..., { buildArity: n })` if it cannot be derived',
|
|
296
|
+
})
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Build reads its 1st (`children`) arg? Unknown/unparseable → true (keep capture). */
|
|
301
|
+
export function buildReadsChildren(def: NodeDef): boolean {
|
|
302
|
+
if (!def.build) return true // structural node: CST output always owns children
|
|
303
|
+
const arity = confirmedArityForDef(def)
|
|
304
|
+
if (arity === null) return true
|
|
305
|
+
return arity >= 1
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Build reads its 4th (`rawChildren`) arg? Unknown/unparseable → true (keep capture). */
|
|
309
|
+
export function buildReadsRaw(def: NodeDef): boolean {
|
|
310
|
+
if (!def.build) return true // structural node: host/default CST may read raw children
|
|
311
|
+
const arity = confirmedArityForDef(def)
|
|
312
|
+
if (arity === null) return true
|
|
313
|
+
return arity >= 4
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Build reads the 5th (triviaLog) arg? Unknown/unparseable → true (keep capture). */
|
|
317
|
+
export function buildReadsTrivia(def: NodeDef): boolean {
|
|
318
|
+
if (!def.build) return true // structural node: host may read trivia — keep capture
|
|
319
|
+
const arity = confirmedArityForDef(def)
|
|
320
|
+
if (arity === null) return true
|
|
321
|
+
return arity >= 5
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Build reads the 6th (state) arg? Unknown/unparseable → true (keep state clone). */
|
|
325
|
+
export function buildReadsState(def: NodeDef): boolean {
|
|
326
|
+
if (!def.build) return true // structural node: host may read state — keep clone
|
|
327
|
+
const arity = confirmedArityForDef(def)
|
|
328
|
+
if (arity === null) return true
|
|
329
|
+
return arity >= 6
|
|
330
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { Combinator } from '../types.ts'
|
|
2
|
+
import type { HostMode } from '../cst/host-mode.ts'
|
|
3
|
+
import { childrenOf } from '../analysis/gating.ts'
|
|
4
|
+
import { collectGrammarReflection, type GrammarReflection } from '../cst/reflection.ts'
|
|
5
|
+
import { classifyRuleMap } from '../analysis/commitment.ts'
|
|
6
|
+
import { serializeRuleMap } from './ir-serialize.ts'
|
|
7
|
+
import { compileRuleMap, compileRuleMapRunnable, type TableRuleMapOptions } from '../table/compile-rule-map.ts'
|
|
8
|
+
import type { TableProgram, TableRule } from '../table/program.ts'
|
|
9
|
+
import { PARSEMAN_VERSION } from '../version.ts'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `compileLinkable()` FOR THE TABLE LOWERING — `linkable()` produces TABLES too.
|
|
13
|
+
*
|
|
14
|
+
* ── WHAT `compileLinkable` ACTUALLY RETURNS, AND WHY NONE OF IT TRANSFERS ─────
|
|
15
|
+
* `LinkablePieces` is `prelude` / `ruleFns` / `wrappers` / `firstSets` /
|
|
16
|
+
* `firstSetRecipes` / `nullable` / `deps` / `needsEmptyTl` — every field of it is
|
|
17
|
+
* SOURCE TEXT or an input to resolving source text. It exists because
|
|
18
|
+
* source-lowering composition is a TEXTUAL splice: `fusedBody()` concatenates
|
|
19
|
+
* namespaced preludes, picks a winning `_r_<Name>` per name, and substitutes
|
|
20
|
+
* `@FS:` dispatch placeholders with the winner's first-set condition. A table has
|
|
21
|
+
* no text to splice and no placeholders to resolve, so porting those fields would
|
|
22
|
+
* be porting the mechanism rather than the capability.
|
|
23
|
+
*
|
|
24
|
+
* The capability is: a piece must be RUNNABLE on its own, and COMPOSABLE with
|
|
25
|
+
* other pieces. This artifact carries exactly those two things.
|
|
26
|
+
*
|
|
27
|
+
* `prog` / `rules` — the piece AS A TABLE. Present whenever the piece is
|
|
28
|
+
* self-contained. This is the ruling: a `linkable()`
|
|
29
|
+
* artifact is a table, not a bag of compiled functions.
|
|
30
|
+
* `ir` — the piece's combinator graph, serialized. This is the
|
|
31
|
+
* composable half, and it is NOT new machinery: the plugin
|
|
32
|
+
* already carries pieces as IR (`plugin/index.ts:1772`) and
|
|
33
|
+
* the linker already re-lowers them (`linker.ts:640`,
|
|
34
|
+
* `compileLinkable(evalRuleMapIR(p.ir), …)`).
|
|
35
|
+
*
|
|
36
|
+
* ── WHY THAT MAKES COMPOSITION THE EASY KIND ─────────────────────────────────
|
|
37
|
+
* The open question was whether table-to-table composition has to merge two
|
|
38
|
+
* ALREADY-ENCODED programs — relocating code offsets and merging const, fn,
|
|
39
|
+
* class, expected and dispatch pools. It does not, as long as every piece
|
|
40
|
+
* carries its IR: the composer evaluates each piece's IR back to a rule map,
|
|
41
|
+
* merges the maps (later pieces overriding earlier names, which is what
|
|
42
|
+
* `compose()` means), and calls `encodeTable` ONCE over the merged map. One
|
|
43
|
+
* encode, no relocation, and the result is byte-for-byte the table the merged
|
|
44
|
+
* grammar would have produced if it had been written as one `rules()` call.
|
|
45
|
+
*
|
|
46
|
+
* The relocation route is only forced for a piece with NO IR, which is why `ir`
|
|
47
|
+
* being null is reported here as a first-class fact rather than folded into a
|
|
48
|
+
* bare `null` return.
|
|
49
|
+
*
|
|
50
|
+
* ── THE HOLE CASE IS NOT REFUSED ─────────────────────────────────────────────
|
|
51
|
+
* A piece that references a rule it does not define (`g.Value` with no
|
|
52
|
+
* `.define()` — `hasExternalRuleRef`'s SHARED-SHAPE signature) cannot be encoded
|
|
53
|
+
* standalone: `encodeTable` resolves a `lazy` by calling its thunk, and that
|
|
54
|
+
* thunk throws. Such a piece gets `prog: null` and `rules: null` and keeps its
|
|
55
|
+
* `ir`, which is the correct answer rather than a refusal: a shape with a hole
|
|
56
|
+
* is not a parser in ANY lowering, and it composes through the merge above,
|
|
57
|
+
* where the hole is filled by whichever piece supplies the name.
|
|
58
|
+
*/
|
|
59
|
+
export type LinkableTableOptions = Omit<TableRuleMapOptions, 'runtimeRef'>
|
|
60
|
+
|
|
61
|
+
export type LinkableTable = {
|
|
62
|
+
/**
|
|
63
|
+
* The parseman version that produced this artifact — the same ARTIFACT VERSION
|
|
64
|
+
* LOCK `LinkablePieces.v` carries, for the same reason: artifacts are
|
|
65
|
+
* version-locked and the format has no back-compat read path.
|
|
66
|
+
*/
|
|
67
|
+
v: string
|
|
68
|
+
ns: string
|
|
69
|
+
/** Local rule names, external (undefined) entries already dropped. */
|
|
70
|
+
keys: string[]
|
|
71
|
+
/** Rule names this piece REFERENCES but does not define. */
|
|
72
|
+
external: string[]
|
|
73
|
+
/** The piece as a table — null when it has holes (see `external`). */
|
|
74
|
+
prog: TableProgram | null
|
|
75
|
+
/** The runnable form of `prog` — null for the same reason. */
|
|
76
|
+
rules: Record<string, TableRule> | null
|
|
77
|
+
/** The emitted expression for `prog`, or null when there is no `prog`. */
|
|
78
|
+
replacement: string | null
|
|
79
|
+
/**
|
|
80
|
+
* The piece's combinator graph, serialized — the COMPOSABLE half. Null when
|
|
81
|
+
* the map cannot be faithfully serialized (a callback with no captured
|
|
82
|
+
* source); a null here is what would force composition to merge encoded
|
|
83
|
+
* programs instead of merging rule maps.
|
|
84
|
+
*/
|
|
85
|
+
ir: string | null
|
|
86
|
+
/**
|
|
87
|
+
* The piece's rule map as LIVE COMBINATORS — present only for an in-process
|
|
88
|
+
* artifact, never serialized.
|
|
89
|
+
*
|
|
90
|
+
* `ir` is the portable composable half, and a grammar BUILT AT RUNTIME often has no
|
|
91
|
+
* `ir` at all (a live callback has no recoverable source). Such a piece still has to
|
|
92
|
+
* compose, and in-process it can: the combinators themselves are right there. Without
|
|
93
|
+
* this the runtime `compose([linkable(g)])` refused a grammar the source lowering
|
|
94
|
+
* composed happily.
|
|
95
|
+
*/
|
|
96
|
+
ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>
|
|
97
|
+
hostMode: HostMode
|
|
98
|
+
hostBranchElided: boolean
|
|
99
|
+
/**
|
|
100
|
+
* Whether any rule carries a DIRECT builder, and whether the piece is free of
|
|
101
|
+
* semantic reduction. `composeLeaf` gates on both, and they are predicates over the
|
|
102
|
+
* combinator graph rather than products of lowering it — so they belong on the
|
|
103
|
+
* artifact regardless of which lowering produced it.
|
|
104
|
+
*/
|
|
105
|
+
hasDirectBuilders: boolean
|
|
106
|
+
isRecognitionOnly: boolean
|
|
107
|
+
reflection: GrammarReflection
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Local vs external, decided exactly as `compileLinkable` decides it: a
|
|
112
|
+
* `rules(g => …)` cache also holds every `g.X` that was merely ACCESSED, so an
|
|
113
|
+
* accessed-but-undefined rule leaks into `Object.entries` as an unresolvable
|
|
114
|
+
* lazy. Those are not local rules.
|
|
115
|
+
*/
|
|
116
|
+
function isLocal(rule: Combinator<unknown>): boolean {
|
|
117
|
+
const d = rule._def
|
|
118
|
+
if (d.tag !== 'lazy') return true
|
|
119
|
+
try { d.thunk(); return true } catch { return false }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Every named rule this map references but does not define, in first-seen order. */
|
|
123
|
+
function externalNames(
|
|
124
|
+
ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>,
|
|
125
|
+
): string[] {
|
|
126
|
+
const out: string[] = []
|
|
127
|
+
const seen = new Set<Combinator<unknown>>()
|
|
128
|
+
const visit = (p: Combinator<unknown>): void => {
|
|
129
|
+
if (seen.has(p)) return
|
|
130
|
+
seen.add(p)
|
|
131
|
+
const d = p._def
|
|
132
|
+
if (d.tag === 'lazy') {
|
|
133
|
+
let resolved: Combinator<unknown> | undefined
|
|
134
|
+
try { resolved = d.thunk() } catch { resolved = undefined }
|
|
135
|
+
if (resolved === undefined) {
|
|
136
|
+
const name = (p as unknown as { _ruleName?: string })._ruleName
|
|
137
|
+
if (name !== undefined && !out.includes(name)) out.push(name)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
visit(resolved)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
for (const child of childrenOf(d)) visit(child)
|
|
144
|
+
}
|
|
145
|
+
for (const [, rule] of ruleMap) visit(rule)
|
|
146
|
+
return out
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function compileLinkableTable(
|
|
150
|
+
ruleMapArg: ReadonlyArray<readonly [string, Combinator<unknown>]>,
|
|
151
|
+
ns: string,
|
|
152
|
+
opts: LinkableTableOptions = {},
|
|
153
|
+
): LinkableTable | null {
|
|
154
|
+
if (!ns) throw new Error('compileLinkableTable: ns must be a non-empty namespace')
|
|
155
|
+
const ruleMap = ruleMapArg.filter(([, rule]) => isLocal(rule))
|
|
156
|
+
if (ruleMap.length === 0) return null
|
|
157
|
+
const external = externalNames(ruleMap)
|
|
158
|
+
// The duplication diagnostic is OPT-IN here, unlike at the owning `rules()` site: the
|
|
159
|
+
// macro lowers the same map through both, so letting `PARSEMAN_DUPLICATION` alone
|
|
160
|
+
// reach this call would repeat every finding the owning site already reported. An
|
|
161
|
+
// explicit option still runs. `'off'` is passed rather than omitted because omitting
|
|
162
|
+
// it is what resolves to the env var.
|
|
163
|
+
const dedupOpts = { ...opts, duplication: opts.duplication ?? ('off' as const) }
|
|
164
|
+
const compiled = external.length === 0 ? compileRuleMap(ruleMap, dedupOpts) : null
|
|
165
|
+
// Serialized BEFORE anything else needs it and independently of whether the
|
|
166
|
+
// piece encoded: a shape with a hole is precisely the case that has no table
|
|
167
|
+
// and must still compose.
|
|
168
|
+
const ir = serializeRuleMap(ruleMap, opts.scanSkip ?? null)
|
|
169
|
+
// RUNNABLE IS NOT PRINTABLE, and `linkable()` only needs the first.
|
|
170
|
+
//
|
|
171
|
+
// `compileRuleMap` refuses a map whose reducers have no captured SOURCE, because
|
|
172
|
+
// printing one would emit `() => {}` and silently return the wrong tree. That gate is
|
|
173
|
+
// right for the macro, which prints — and wrong here: a grammar BUILT AT RUNTIME (every
|
|
174
|
+
// interpreter-path `linkable()` caller) has live callbacks and no sources by
|
|
175
|
+
// construction, and it is never printed. Treating "cannot print" as "cannot compile"
|
|
176
|
+
// made `linkable()` reject the ordinary runtime grammar the source lowering accepted.
|
|
177
|
+
//
|
|
178
|
+
// So: encode again for RUNNING only. `prog` still holds the live callbacks in its
|
|
179
|
+
// pool, `tableRules` binds them, and `replacement` stays null — which is the
|
|
180
|
+
// artifact honestly saying it cannot be emitted as source.
|
|
181
|
+
const runnable = compiled === null && external.length === 0
|
|
182
|
+
? compileRuleMapRunnable(ruleMap, dedupOpts)
|
|
183
|
+
: null
|
|
184
|
+
// NO REFUSAL PAST THIS POINT. A piece with neither a table nor IR is still USABLE
|
|
185
|
+
// in-process, because `ruleMap` carries the live combinators — which is exactly the
|
|
186
|
+
// case a grammar with holes AND unserializable callbacks lands in. Refusing it here
|
|
187
|
+
// made `compose()` throw for a grammar the interpreter fuses happily; the fields below
|
|
188
|
+
// already state, individually, what this piece can and cannot do.
|
|
189
|
+
|
|
190
|
+
const hostMode = compiled?.hostMode
|
|
191
|
+
?? opts.hostMode
|
|
192
|
+
?? ruleMap.map(([, r]) => r._meta.grammarHostMode).find(Boolean)
|
|
193
|
+
?? 'ast'
|
|
194
|
+
return {
|
|
195
|
+
v: PARSEMAN_VERSION,
|
|
196
|
+
ns,
|
|
197
|
+
keys: ruleMap.map(([key]) => key),
|
|
198
|
+
external,
|
|
199
|
+
prog: compiled?.prog ?? runnable?.prog ?? null,
|
|
200
|
+
rules: compiled?.rules ?? runnable?.rules ?? null,
|
|
201
|
+
replacement: compiled?.replacement ?? null,
|
|
202
|
+
ir,
|
|
203
|
+
ruleMap,
|
|
204
|
+
hostMode,
|
|
205
|
+
hostBranchElided: hostMode === 'ast',
|
|
206
|
+
...classifyRuleMap(ruleMap),
|
|
207
|
+
reflection: compiled?.reflection ?? collectGrammarReflection(ruleMap),
|
|
208
|
+
}
|
|
209
|
+
}
|