parseman 0.45.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-gate.d.ts +158 -0
- package/dist/analysis/choice-cost-gate.d.ts.map +1 -0
- package/dist/analysis/choice-cost-render.d.ts +96 -0
- package/dist/analysis/choice-cost-render.d.ts.map +1 -0
- package/dist/analysis/choice-cost.d.ts +383 -0
- package/dist/analysis/choice-cost.d.ts.map +1 -0
- package/dist/analysis/commitment.d.ts +131 -0
- package/dist/analysis/commitment.d.ts.map +1 -0
- package/dist/analysis/corpus.d.ts +73 -0
- package/dist/analysis/corpus.d.ts.map +1 -0
- package/dist/analysis/diagnose-render.d.ts +65 -0
- package/dist/analysis/diagnose-render.d.ts.map +1 -0
- package/dist/analysis/diagnose.d.ts +15 -0
- package/dist/analysis/diagnose.d.ts.map +1 -1
- package/dist/analysis/diagnostics.cjs +13736 -0
- package/dist/analysis/diagnostics.cjs.map +6 -0
- package/dist/analysis/diagnostics.d.ts +27 -0
- package/dist/analysis/diagnostics.d.ts.map +1 -0
- package/dist/analysis/diagnostics.js +13713 -0
- package/dist/analysis/diagnostics.js.map +6 -0
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/fix-render.d.ts +43 -0
- package/dist/analysis/fix-render.d.ts.map +1 -0
- package/dist/analysis/fix.d.ts +172 -0
- package/dist/analysis/fix.d.ts.map +1 -0
- 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 +47 -2
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/analysis/rebuild.d.ts +61 -0
- package/dist/analysis/rebuild.d.ts.map +1 -0
- package/dist/analysis/terminal.d.ts +145 -0
- package/dist/analysis/terminal.d.ts.map +1 -0
- package/dist/cli/index.cjs +14837 -0
- package/dist/cli/index.cjs.map +6 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +14814 -0
- package/dist/cli/index.js.map +6 -0
- 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/build-arity.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 +79 -47
- 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 +10499 -7478
- package/dist/index.cjs.map +3 -4
- package/dist/index.d.ts +24 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10499 -7478
- 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 +41 -2
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/free-identifiers.d.ts +23 -0
- package/dist/plugin/free-identifiers.d.ts.map +1 -0
- package/dist/plugin/index.cjs +11381 -9442
- package/dist/plugin/index.cjs.map +3 -4
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +11381 -9442
- package/dist/plugin/index.js.map +3 -4
- package/dist/plugin/reducer-resolver.d.ts +17 -1
- package/dist/plugin/reducer-resolver.d.ts.map +1 -1
- 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 -1622
- package/dist/spec/index.cjs.map +3 -4
- package/dist/spec/index.js +2898 -1622
- 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 +41 -3
- 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/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,2281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parseman unplugin — macro transform
|
|
3
|
+
*
|
|
4
|
+
* Handles: import { ... } from 'parseman' with { type: 'macro' }
|
|
5
|
+
*
|
|
6
|
+
* For each such import, walks the file's AST, finds variable declarations
|
|
7
|
+
* whose RHS is a pure parseman combinator call, evaluates them at build time,
|
|
8
|
+
* compiles the result to an optimized inline function, and replaces the
|
|
9
|
+
* declaration — removing the import entirely.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* // vite.config.ts
|
|
13
|
+
* import parseman from 'parseman/plugin'
|
|
14
|
+
* export default { plugins: [parseman()] }
|
|
15
|
+
*
|
|
16
|
+
* // rollup.config.js
|
|
17
|
+
* import parseman from 'parseman/plugin'
|
|
18
|
+
* export default { plugins: [parseman.rollup()] }
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs'
|
|
21
|
+
import * as path from 'node:path'
|
|
22
|
+
import { createUnplugin } from 'unplugin'
|
|
23
|
+
import { parseSync } from 'oxc-parser'
|
|
24
|
+
import { ResolverFactory } from 'oxc-resolver'
|
|
25
|
+
import MagicString from 'magic-string'
|
|
26
|
+
import { evaluateExpr, evaluateCombinatorArray, evaluateParserFactory, evaluateStaticValue, evaluateWordFactory, evaluateWhenFactory, evaluateRefDeclaration, applyDefineStatement, referencesAny, setReducerResolver, propName, type Scope, type ScopeEntry } from './evaluator.ts'
|
|
27
|
+
import { classifyRuleMap } from '../analysis/commitment.ts'
|
|
28
|
+
import { compile } from '../table/compile.ts'
|
|
29
|
+
import { compileRuleMap } from '../table/compile-rule-map.ts'
|
|
30
|
+
import { compileLinkableTable } from '../compiler/compile-linkable-table.ts'
|
|
31
|
+
import { createReducerResolver } from './reducer-resolver.ts'
|
|
32
|
+
import { findFreeIdentifiers } from './free-identifiers.ts'
|
|
33
|
+
import {
|
|
34
|
+
beginDegradationCapture, endDegradationCapture, formatDegradation, formatDegradations,
|
|
35
|
+
resolveDegradationLevel, recordDegradation, degradationCaptureDepth, unwindDegradationCapture,
|
|
36
|
+
} from '../compiler/degradation.ts'
|
|
37
|
+
import type { HostMode } from '../cst/host-mode.ts'
|
|
38
|
+
import { COMPOSED_PIECES } from '../compiler/linker.ts'
|
|
39
|
+
import { evalRuleMapIR, serializeRuleMap } from '../compiler/ir-serialize.ts'
|
|
40
|
+
import { PARSEMAN_VERSION } from '../version.ts'
|
|
41
|
+
import { grammarReflectionSource, type GrammarReflection } from '../cst/reflection.ts'
|
|
42
|
+
import { createHash } from 'node:crypto'
|
|
43
|
+
import type { Combinator } from '../types.ts'
|
|
44
|
+
import type {
|
|
45
|
+
ImportDeclaration,
|
|
46
|
+
VariableDeclarator,
|
|
47
|
+
VariableDeclaration,
|
|
48
|
+
Expression,
|
|
49
|
+
Statement,
|
|
50
|
+
ExportNamedDeclaration,
|
|
51
|
+
} from '@oxc-project/types'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Where a lowered module imports the shared table driver from.
|
|
55
|
+
*
|
|
56
|
+
* The PUBLIC subpath, not a deep path into `src/`: this string is written into a
|
|
57
|
+
* consumer's build output, so it has to be a specifier their resolver can see.
|
|
58
|
+
* `parseman/table` is declared in package.json `exports` and re-exports
|
|
59
|
+
* `tableRules` as `tableRules` (src/table/index.ts:28).
|
|
60
|
+
*/
|
|
61
|
+
const TABLE_RUNTIME_SPECIFIER = 'parseman/table'
|
|
62
|
+
|
|
63
|
+
export type ParsecraftPluginOptions = {
|
|
64
|
+
/** Extra module specifiers to treat as parseman re-exports */
|
|
65
|
+
moduleAliases?: string[]
|
|
66
|
+
/**
|
|
67
|
+
* Warn when a regex terminal can't LOWER to a fast `charCodeAt` scan and falls
|
|
68
|
+
* back to `RegExp.exec`. **Default `false` (opt-in).** `RegExp.exec` is an
|
|
69
|
+
* accepted, JIT-fast compiled path — most un-lowered regexes are perfectly fine
|
|
70
|
+
* and lowering them often shows no real gain — so this is a diagnostic you turn
|
|
71
|
+
* ON when specifically auditing lowering coverage, not a build-time nag.
|
|
72
|
+
*/
|
|
73
|
+
warnUnloweredRegex?: boolean
|
|
74
|
+
/**
|
|
75
|
+
* Bake automatic list recovery into the macro-compiled output. **Default `false`.**
|
|
76
|
+
* When on, `many`/`oneOrMore`/`sepBy` in every compiled grammar gain a
|
|
77
|
+
* `_ctx._tolerant`-gated recovery branch (dormant on strict parses — a normal
|
|
78
|
+
* parse is unaffected and allocates nothing). The output stays macro-inlinable
|
|
79
|
+
* (sentinels build via `_ctx._rec`, not `_rp`). Turn it on for grammars you drive
|
|
80
|
+
* from an editor/language service so `run(g, src, { tolerant: true })` recovers on
|
|
81
|
+
* the fast compiled path; leave it off for build-only grammars.
|
|
82
|
+
*/
|
|
83
|
+
recovery?: boolean
|
|
84
|
+
/** Emit grammar-coverage hooks in macro output. Off by default, so ordinary
|
|
85
|
+
* macro output remains byte-identical. */
|
|
86
|
+
grammarCoverage?: boolean
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const PARSEMAN_MODULE = 'parseman'
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The REASON a lowering refused, appended to the warning that reports it.
|
|
93
|
+
*
|
|
94
|
+
* A grammar that fails to lower falls back to the interpreter with the parseman
|
|
95
|
+
* import surviving — it still parses, so no test notices, and the only symptom is
|
|
96
|
+
* ~79 ms against ~14 ms on `benchmark.less`. "Couldn't be inlined" alone does not
|
|
97
|
+
* tell an author which construct to change; the compiler knows, so it says.
|
|
98
|
+
*/
|
|
99
|
+
function reasonSuffix(runtimeOnly: readonly string[] | undefined): string {
|
|
100
|
+
return runtimeOnly === undefined || runtimeOnly.length === 0
|
|
101
|
+
? ''
|
|
102
|
+
: ` — ${runtimeOnly.join('; ')}`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** THE reader for a build-time options object — `rules({ … }, f)`, `compose(…, { … })`.
|
|
106
|
+
*
|
|
107
|
+
* Returns the value expression for `name`, or undefined when the object does not set it.
|
|
108
|
+
* Keys are compared through `propName`, the plugin's single property-key reader, so a
|
|
109
|
+
* QUOTED key is the same key and a COMPUTED key is no key at all.
|
|
110
|
+
*
|
|
111
|
+
* This existed three times, inline, each reading only `key.name`. The runtime path reads
|
|
112
|
+
* a real object and cannot tell `{ hostMode: 'cst' }` from `{ 'hostMode': 'cst' }`; the
|
|
113
|
+
* macro could, and dropped the second — shipping an 'ast' artifact for a source that
|
|
114
|
+
* asked for 'cst', with no warning, which `assertHostModeCompatible` then passed because
|
|
115
|
+
* the artifact genuinely WAS 'ast'. Two correct copies would not have been a fix here;
|
|
116
|
+
* there were three copies and all three were wrong the same way. */
|
|
117
|
+
function optionProp(optExpr: AnyNode | undefined, name: string): Expression | undefined {
|
|
118
|
+
if (optExpr?.type !== 'ObjectExpression') return undefined
|
|
119
|
+
const found = ((optExpr.properties as AnyNode[] | undefined) ?? [])
|
|
120
|
+
.find(p => propName(p as never) === name)
|
|
121
|
+
return (found as { value?: Expression } | undefined)?.value
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function unwrapStaticExpression(expr: Expression): Expression {
|
|
125
|
+
let cur = expr as unknown as { type?: string; expression?: Expression }
|
|
126
|
+
while (cur.type === 'TSAsExpression'
|
|
127
|
+
|| cur.type === 'TSSatisfiesExpression'
|
|
128
|
+
|| cur.type === 'TSNonNullExpression'
|
|
129
|
+
|| cur.type === 'TSTypeAssertion'
|
|
130
|
+
|| cur.type === 'TSInstantiationExpression'
|
|
131
|
+
|| cur.type === 'ParenthesizedExpression') {
|
|
132
|
+
if (!cur.expression) break
|
|
133
|
+
cur = cur.expression as unknown as typeof cur
|
|
134
|
+
}
|
|
135
|
+
return cur as Expression
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isStaticNullishExpression(expr: Expression): boolean {
|
|
139
|
+
const unwrapped = unwrapStaticExpression(expr) as Expression & { name?: string; value?: unknown }
|
|
140
|
+
return (unwrapped.type === 'Identifier' && unwrapped.name === 'undefined')
|
|
141
|
+
|| (unwrapped.type === 'Literal' && unwrapped.value === null)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// A grammar's carried linkable pieces live ONLY in its COMPILED output (the macro
|
|
145
|
+
// embeds them there), never in its `.ts` source. So resolving an imported grammar
|
|
146
|
+
// to read its pieces must prefer the built `import`/`require` entry — NOT the
|
|
147
|
+
// `source` condition (which would land on un-compiled `.ts` with no pieces).
|
|
148
|
+
let _compiledResolver: ResolverFactory | null = null
|
|
149
|
+
function getCompiledResolver(): ResolverFactory {
|
|
150
|
+
return _compiledResolver ??= new ResolverFactory({
|
|
151
|
+
extensions: ['.js', '.mjs', '.cjs'],
|
|
152
|
+
conditionNames: ['import', 'require', 'default'],
|
|
153
|
+
mainFields: ['module', 'main'],
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type ResolvedGrammarModule = {
|
|
158
|
+
file: string
|
|
159
|
+
/** A private TypeScript source module, not a published macro artifact. */
|
|
160
|
+
source: boolean
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const PRIVATE_SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts'])
|
|
164
|
+
|
|
165
|
+
function nearestPackageRoot(file: string): string | null {
|
|
166
|
+
let dir = path.dirname(file)
|
|
167
|
+
for (;;) {
|
|
168
|
+
try {
|
|
169
|
+
if (fs.statSync(path.join(dir, 'package.json')).isFile()) return fs.realpathSync(dir)
|
|
170
|
+
} catch { /* keep walking upward */ }
|
|
171
|
+
const parent = path.dirname(dir)
|
|
172
|
+
if (parent === dir) return null
|
|
173
|
+
dir = parent
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function isWithinPackageRoot(file: string, root: string): boolean {
|
|
178
|
+
const relative = path.relative(root, file)
|
|
179
|
+
return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve only a relative/absolute source-private import. Published package
|
|
184
|
+
* imports deliberately remain compiled-artifact-only: source is allowed here
|
|
185
|
+
* solely so `import { syntax } from './recognition.js'` can consume the
|
|
186
|
+
* colocated `recognition.ts` before a bundler has emitted that JS file.
|
|
187
|
+
*/
|
|
188
|
+
function resolvePrivateSourceModule(from: string, specifier: string): string | null {
|
|
189
|
+
if (!specifier.startsWith('.') && !path.isAbsolute(specifier)) return null
|
|
190
|
+
const packageRoot = nearestPackageRoot(from)
|
|
191
|
+
if (!packageRoot) return null
|
|
192
|
+
const base = path.resolve(path.dirname(from), specifier)
|
|
193
|
+
const ext = path.extname(base)
|
|
194
|
+
const candidates = [
|
|
195
|
+
base,
|
|
196
|
+
...(ext ? ['.ts', '.tsx', '.mts', '.cts'].map(next => `${base.slice(0, -ext.length)}${next}`) : []),
|
|
197
|
+
...(!ext ? ['.ts', '.tsx', '.mts', '.cts'].map(next => `${base}${next}`) : []),
|
|
198
|
+
...(!ext ? ['index.ts', 'index.tsx', 'index.mts', 'index.cts'].map(next => path.join(base, next)) : []),
|
|
199
|
+
]
|
|
200
|
+
for (const candidate of candidates) {
|
|
201
|
+
try {
|
|
202
|
+
if (!fs.statSync(candidate).isFile()) continue
|
|
203
|
+
// Both paths are real, so a symlink cannot smuggle a source dependency out
|
|
204
|
+
// of the importing package after the lexical `../` check passed.
|
|
205
|
+
const real = fs.realpathSync(candidate)
|
|
206
|
+
if (isWithinPackageRoot(real, packageRoot)) return real
|
|
207
|
+
} catch { /* try the next source spelling */ }
|
|
208
|
+
}
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolveGrammarModule(from: string, specifier: string): ResolvedGrammarModule | null {
|
|
213
|
+
// An explicit source import means source semantics even when a stale/generated
|
|
214
|
+
// sibling JS artifact exists. Ordinary `.js` imports keep compiled precedence.
|
|
215
|
+
if (PRIVATE_SOURCE_EXTENSIONS.has(path.extname(specifier))) {
|
|
216
|
+
const file = resolvePrivateSourceModule(from, specifier)
|
|
217
|
+
return file ? { file, source: true } : null
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const file = getCompiledResolver().resolveFileSync(from, specifier).path
|
|
221
|
+
if (file) return { file, source: false }
|
|
222
|
+
} catch { /* private source fallback below */ }
|
|
223
|
+
const file = resolvePrivateSourceModule(from, specifier)
|
|
224
|
+
return file ? { file, source: true } : null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Recursive source lowering is a build-time operation. This guard makes an
|
|
228
|
+
// import cycle fail closed rather than recursing into a runtime composition.
|
|
229
|
+
const _sourceLowering = new Set<string>()
|
|
230
|
+
const _sourceLoweringCache = new Map<string, { mtimeMs: number; parsed: ParsedModule | null }>()
|
|
231
|
+
|
|
232
|
+
function sourceLoweringCacheKey(
|
|
233
|
+
file: string,
|
|
234
|
+
moduleAliases: Set<string>,
|
|
235
|
+
warnUnloweredRegex: boolean,
|
|
236
|
+
recovery: boolean,
|
|
237
|
+
): string {
|
|
238
|
+
return `${file}\0${[...moduleAliases].sort().join('\0')}\0${warnUnloweredRegex ? 'warn' : ''}\0${recovery ? 'recovery' : ''}`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Module-level parse cache for imported COMPILED grammar modules (read to recover
|
|
242
|
+
// a grammar's carried pieces for compose()), keyed by absolute file path. Shared
|
|
243
|
+
// across transformMacro invocations so a grammar imported by many consumer files
|
|
244
|
+
// is read + parsed once per build. The mtime guard invalidates a stale entry so a
|
|
245
|
+
// watch-mode rebuild picks up a rebuilt module. A null `parsed` (unreadable / parse
|
|
246
|
+
// error) is cached too, to avoid retrying a known-bad file for every consumer.
|
|
247
|
+
type ParsedModule = { body: unknown[]; src: string }
|
|
248
|
+
const _moduleParseCache = new Map<string, { mtimeMs: number; parsed: ParsedModule | null }>()
|
|
249
|
+
|
|
250
|
+
function parseModuleCached(filePath: string): ParsedModule | null {
|
|
251
|
+
let mtimeMs: number
|
|
252
|
+
try {
|
|
253
|
+
mtimeMs = fs.statSync(filePath).mtimeMs
|
|
254
|
+
} catch {
|
|
255
|
+
return null // file vanished — don't cache under a stale key
|
|
256
|
+
}
|
|
257
|
+
const hit = _moduleParseCache.get(filePath)
|
|
258
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.parsed
|
|
259
|
+
let parsed: ParsedModule | null = null
|
|
260
|
+
try {
|
|
261
|
+
const src = fs.readFileSync(filePath, 'utf8')
|
|
262
|
+
const r = parseSync(filePath, src)
|
|
263
|
+
if (r.errors.length === 0) parsed = { body: r.program.body as unknown[], src }
|
|
264
|
+
} catch { /* leave null */ }
|
|
265
|
+
_moduleParseCache.set(filePath, { mtimeMs, parsed })
|
|
266
|
+
return parsed
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export default createUnplugin((opts: ParsecraftPluginOptions = {}) => ({
|
|
270
|
+
name: 'parseman',
|
|
271
|
+
// Run BEFORE the bundler's TS/JS transform — otherwise esbuild (Vite) strips
|
|
272
|
+
// the `with { type: 'macro' }` import attribute before we ever see it, and the
|
|
273
|
+
// macro silently never fires.
|
|
274
|
+
enforce: 'pre' as const,
|
|
275
|
+
|
|
276
|
+
transformInclude(id: string) {
|
|
277
|
+
return /\.[jt]sx?$/.test(id) && !id.includes('node_modules')
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
transform(this: { warn?: (msg: string) => void }, code: string, id: string) {
|
|
281
|
+
if (!code.includes('parseman')) return null
|
|
282
|
+
if (!code.includes('macro')) return null
|
|
283
|
+
const moduleAliases = new Set([PARSEMAN_MODULE, ...(opts.moduleAliases ?? [])])
|
|
284
|
+
const result = transformMacro(code, id, moduleAliases, opts.warnUnloweredRegex === true, opts.recovery === true, opts.grammarCoverage === true)
|
|
285
|
+
if (result?.warnings.length) {
|
|
286
|
+
for (const w of result.warnings) {
|
|
287
|
+
if (typeof this?.warn === 'function') this.warn(`[parseman] ${w}`)
|
|
288
|
+
else console.warn(`[parseman] ${w}`)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!result) return null
|
|
292
|
+
return { code: result.code, map: result.map }
|
|
293
|
+
},
|
|
294
|
+
}))
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Reading carried pieces back out of a COMPILED grammar module
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// The macro embeds a grammar's linkable pieces inside its exported const's
|
|
300
|
+
// initializer (see `withCarriedPieces`). To compose an imported grammar we parse
|
|
301
|
+
// its compiled module, find that const, and pull the pieces literal back out —
|
|
302
|
+
// these three pure helpers do exactly that (no runtime, no source recompile).
|
|
303
|
+
|
|
304
|
+
type AnyNode = { type: string; start: number; end: number } & Record<string, unknown>
|
|
305
|
+
|
|
306
|
+
/** Map an export NAME → the local binding it refers to. Handles both
|
|
307
|
+
* `export const X = …` (local === exported) and a bundler's rename
|
|
308
|
+
* `const X$1 = …; export { X$1 as X }`. */
|
|
309
|
+
function exportLocalName(body: AnyNode[], exported: string): string | null {
|
|
310
|
+
for (const st of body) {
|
|
311
|
+
if (st.type !== 'ExportNamedDeclaration') continue
|
|
312
|
+
const decl = st.declaration as AnyNode | undefined
|
|
313
|
+
if (decl?.type === 'VariableDeclaration') {
|
|
314
|
+
for (const d of (decl.declarations as AnyNode[] | undefined) ?? []) {
|
|
315
|
+
const idn = d.id as { type?: string; name?: string } | undefined
|
|
316
|
+
if (idn?.type === 'Identifier' && idn.name === exported) return exported
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const sp of (st.specifiers as AnyNode[] | undefined) ?? []) {
|
|
320
|
+
const exp = (sp.exported as { name?: string } | undefined)?.name
|
|
321
|
+
const loc = (sp.local as { name?: string } | undefined)?.name
|
|
322
|
+
if (exp === exported && loc) return loc
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Find the top-level `const <name> = <init>` initializer node. */
|
|
329
|
+
function topLevelInit(body: AnyNode[], name: string): AnyNode | null {
|
|
330
|
+
for (const st of body) {
|
|
331
|
+
const vd = st.type === 'VariableDeclaration' ? st
|
|
332
|
+
: st.type === 'ExportNamedDeclaration' && (st.declaration as AnyNode | undefined)?.type === 'VariableDeclaration'
|
|
333
|
+
? (st.declaration as AnyNode) : null
|
|
334
|
+
if (!vd) continue
|
|
335
|
+
for (const d of (vd.declarations as AnyNode[] | undefined) ?? []) {
|
|
336
|
+
const idn = d.id as { type?: string; name?: string } | undefined
|
|
337
|
+
if (idn?.type === 'Identifier' && idn.name === name && d.init) return d.init as AnyNode
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return null
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** `Symbol.for('parseman.composedPieces')` ? */
|
|
344
|
+
function isComposedPiecesSymbol(n: AnyNode | undefined): boolean {
|
|
345
|
+
if (!n || n.type !== 'CallExpression') return false
|
|
346
|
+
const callee = n.callee as AnyNode | undefined
|
|
347
|
+
const obj = (callee?.object as { name?: string } | undefined)?.name
|
|
348
|
+
const prop = (callee?.property as { name?: string } | undefined)?.name
|
|
349
|
+
const arg0 = (n.arguments as AnyNode[] | undefined)?.[0] as { value?: unknown } | undefined
|
|
350
|
+
return obj === 'Symbol' && prop === 'for' && arg0?.value === 'parseman.composedPieces'
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Walk an initializer subtree for the `Object.defineProperty(_,
|
|
354
|
+
* Symbol.for('parseman.composedPieces'), { value: <LITERAL> })` the macro emits,
|
|
355
|
+
* and return the source range of <LITERAL>. */
|
|
356
|
+
function findCarriedPiecesLiteral(root: AnyNode): { start: number; end: number } | null {
|
|
357
|
+
let found: { start: number; end: number } | null = null
|
|
358
|
+
const visit = (n: unknown): void => {
|
|
359
|
+
if (found || !n || typeof n !== 'object') return
|
|
360
|
+
if (Array.isArray(n)) { for (const c of n) visit(c); return }
|
|
361
|
+
const node = n as AnyNode
|
|
362
|
+
if (node.type === 'CallExpression') {
|
|
363
|
+
const callee = node.callee as AnyNode | undefined
|
|
364
|
+
const obj = (callee?.object as { name?: string } | undefined)?.name
|
|
365
|
+
const prop = (callee?.property as { name?: string } | undefined)?.name
|
|
366
|
+
const args = node.arguments as AnyNode[] | undefined
|
|
367
|
+
// Table lowering: `tableRules(program, { [Symbol.for('…composedPieces')]:
|
|
368
|
+
// <literal>, … })`. The metadata object becomes the returned map's
|
|
369
|
+
// prototype, so the symbol remains readable without becoming an own key.
|
|
370
|
+
if ((callee as { type?: string; name?: string } | undefined)?.type === 'Identifier'
|
|
371
|
+
&& (callee as { name?: string }).name === 'tableRules'
|
|
372
|
+
&& (args?.[1] as AnyNode | undefined)?.type === 'ObjectExpression') {
|
|
373
|
+
for (const p of ((args![1] as AnyNode).properties as AnyNode[] | undefined) ?? []) {
|
|
374
|
+
if ((p as { computed?: boolean }).computed && isComposedPiecesSymbol(p.key as AnyNode) && p.value) {
|
|
375
|
+
found = { start: (p.value as AnyNode).start, end: (p.value as AnyNode).end }
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Also accepted: `Object.assign(_g, { [Symbol.for('…composedPieces')]: <literal> })`
|
|
381
|
+
// (transitional; the current emitter uses the non-enumerable defineProperty form below).
|
|
382
|
+
if (obj === 'Object' && prop === 'assign' && args && (args[1] as AnyNode | undefined)?.type === 'ObjectExpression') {
|
|
383
|
+
for (const p of ((args[1] as AnyNode).properties as AnyNode[] | undefined) ?? []) {
|
|
384
|
+
if ((p as { computed?: boolean }).computed && isComposedPiecesSymbol(p.key as AnyNode) && p.value) {
|
|
385
|
+
found = { start: (p.value as AnyNode).start, end: (p.value as AnyNode).end }
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Current form: `Object.defineProperty(_g, sym, { value: …, enumerable: false })`.
|
|
391
|
+
if (obj === 'Object' && prop === 'defineProperty' && args && isComposedPiecesSymbol(args[1])) {
|
|
392
|
+
const descriptor = args[2] as AnyNode | undefined
|
|
393
|
+
for (const p of (descriptor?.properties as AnyNode[] | undefined) ?? []) {
|
|
394
|
+
const key = p.key as { name?: string; value?: string } | undefined
|
|
395
|
+
if ((key?.name === 'value' || key?.value === 'value') && p.value) {
|
|
396
|
+
found = { start: (p.value as AnyNode).start, end: (p.value as AnyNode).end }
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const k in node) {
|
|
403
|
+
if (k === 'type' || k === 'start' || k === 'end') continue
|
|
404
|
+
const v = (node as Record<string, unknown>)[k]
|
|
405
|
+
if (v && typeof v === 'object') visit(v)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
visit(root)
|
|
409
|
+
return found
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Local-binding → { source, imported } for every `import { … }` in a parsed
|
|
413
|
+
* module body. Used to resolve an ancestor-pieces spread (`...(binding[Symbol…])`)
|
|
414
|
+
* in a compiled grammar back to the module that binding was imported from. */
|
|
415
|
+
function extractImportBindings(body: AnyNode[]): Map<string, { source: string; imported: string }> {
|
|
416
|
+
const out = new Map<string, { source: string; imported: string }>()
|
|
417
|
+
for (const stmt of body) {
|
|
418
|
+
if (stmt.type !== 'ImportDeclaration') continue
|
|
419
|
+
const s = stmt as unknown as ImportDeclaration
|
|
420
|
+
for (const spec of s.specifiers) {
|
|
421
|
+
if (spec.type === 'ImportSpecifier') {
|
|
422
|
+
const imported = (spec.imported as { name?: string }).name ?? spec.local.name
|
|
423
|
+
out.set(spec.local.name, { source: s.source.value, imported })
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return out
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// Core transform (exported for testing)
|
|
432
|
+
// ---------------------------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
type ImportInfo = {
|
|
435
|
+
start: number
|
|
436
|
+
end: number
|
|
437
|
+
names: Set<string>
|
|
438
|
+
fullyResolved: boolean // mutated after evaluation
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export type TransformMacroResult = {
|
|
442
|
+
code: string
|
|
443
|
+
map: ReturnType<MagicString['generateMap']>
|
|
444
|
+
/** Diagnostics for macro-referencing shapes that fell back to the interpreter. */
|
|
445
|
+
warnings: string[]
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Read a source-private grammar as if Vite had already run this macro on it.
|
|
450
|
+
* The result is still only an in-memory artifact for the importing transform:
|
|
451
|
+
* no source module is executed, emitted, or made available at runtime.
|
|
452
|
+
*/
|
|
453
|
+
function lowerPrivateSourceModule(
|
|
454
|
+
file: string,
|
|
455
|
+
moduleAliases: Set<string>,
|
|
456
|
+
warnUnloweredRegex: boolean,
|
|
457
|
+
recovery: boolean,
|
|
458
|
+
): ParsedModule | null {
|
|
459
|
+
if (_sourceLowering.has(file)) return null
|
|
460
|
+
let mtimeMs: number
|
|
461
|
+
try {
|
|
462
|
+
mtimeMs = fs.statSync(file).mtimeMs
|
|
463
|
+
} catch {
|
|
464
|
+
return null
|
|
465
|
+
}
|
|
466
|
+
const cacheKey = sourceLoweringCacheKey(file, moduleAliases, warnUnloweredRegex, recovery)
|
|
467
|
+
const hit = _sourceLoweringCache.get(cacheKey)
|
|
468
|
+
if (hit && hit.mtimeMs === mtimeMs) return hit.parsed
|
|
469
|
+
_sourceLowering.add(file)
|
|
470
|
+
let parsedModule: ParsedModule | null = null
|
|
471
|
+
try {
|
|
472
|
+
const source = fs.readFileSync(file, 'utf8')
|
|
473
|
+
const transformed = transformMacro(source, file, moduleAliases, warnUnloweredRegex, recovery)
|
|
474
|
+
if (!transformed || transformed.warnings.length > 0) return null
|
|
475
|
+
const parsed = parseSync(file, transformed.code)
|
|
476
|
+
parsedModule = parsed.errors.length === 0
|
|
477
|
+
? { body: parsed.program.body as unknown[], src: transformed.code }
|
|
478
|
+
: null
|
|
479
|
+
return parsedModule
|
|
480
|
+
} catch {
|
|
481
|
+
return null
|
|
482
|
+
} finally {
|
|
483
|
+
_sourceLoweringCache.set(cacheKey, { mtimeMs, parsed: parsedModule })
|
|
484
|
+
_sourceLowering.delete(file)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Transform one module, ALWAYS releasing the process-global capture state.
|
|
490
|
+
*
|
|
491
|
+
* `beginDegradationCapture()`, `beginLoweringCapture()` and `setReducerResolver()` set
|
|
492
|
+
* module-level globals that the body's straight-line path released at the end. The body
|
|
493
|
+
* also THROWS — `composeLeaf() must macro-fuse` at the `compileComposeLeafCall` site is
|
|
494
|
+
* one of several — and every one of those throws jumped over the release.
|
|
495
|
+
*
|
|
496
|
+
* The consequence was process-wide and silent: one failed macro transform left the
|
|
497
|
+
* degradation sink OPEN forever, so every later `recordDegradation` — including from an
|
|
498
|
+
* unrelated runtime `compile()` in the same process — was filed into an orphaned Map that
|
|
499
|
+
* nobody would ever drain, and printed nothing. Measured: 0 `console.warn` calls after an
|
|
500
|
+
* aborted capture, 1 finding stranded in the orphan.
|
|
501
|
+
*
|
|
502
|
+
* So the release lives in a `finally`, and whatever the failed frame had collected is
|
|
503
|
+
* printed rather than dropped — a degradation that was real before the abort is still
|
|
504
|
+
* real after it.
|
|
505
|
+
*/
|
|
506
|
+
export function transformMacro(
|
|
507
|
+
code: string,
|
|
508
|
+
id: string,
|
|
509
|
+
moduleAliases = new Set([PARSEMAN_MODULE]),
|
|
510
|
+
warnUnloweredRegex = false,
|
|
511
|
+
recovery = false,
|
|
512
|
+
grammarCoverage = false,
|
|
513
|
+
): TransformMacroResult | null {
|
|
514
|
+
const depth = degradationCaptureDepth()
|
|
515
|
+
try {
|
|
516
|
+
return transformMacroImpl(code, id, moduleAliases, warnUnloweredRegex, recovery, grammarCoverage)
|
|
517
|
+
} finally {
|
|
518
|
+
setReducerResolver(null)
|
|
519
|
+
// Both are idempotent: on the success path the body already released them and these
|
|
520
|
+
// are no-ops. On an aborted transform they are what stops the leak.
|
|
521
|
+
for (const d of unwindDegradationCapture(depth)) console.warn(formatDegradation(d))
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function transformMacroImpl(
|
|
526
|
+
code: string,
|
|
527
|
+
id: string,
|
|
528
|
+
moduleAliases: Set<string>,
|
|
529
|
+
warnUnloweredRegex: boolean,
|
|
530
|
+
recovery: boolean,
|
|
531
|
+
grammarCoverage: boolean,
|
|
532
|
+
): TransformMacroResult | null {
|
|
533
|
+
let result: ReturnType<typeof parseSync>
|
|
534
|
+
try {
|
|
535
|
+
result = parseSync(id, code)
|
|
536
|
+
} catch {
|
|
537
|
+
return null
|
|
538
|
+
}
|
|
539
|
+
if (result.errors.length > 0) return null
|
|
540
|
+
|
|
541
|
+
const body = result.program.body
|
|
542
|
+
|
|
543
|
+
// --- Pass 1: collect macro imports + non-macro import bindings ---
|
|
544
|
+
const macroImports: ImportInfo[] = []
|
|
545
|
+
const allNames = new Set<string>()
|
|
546
|
+
// local name -> { source module, imported name }, for every NON-macro named
|
|
547
|
+
// import. Lets a `...frag(g)` spread whose factory is imported (tier 2) be
|
|
548
|
+
// resolved from the exporting module's source at build time.
|
|
549
|
+
const importBindings = new Map<string, { source: string; imported: string }>()
|
|
550
|
+
const ordinaryImports: Array<{
|
|
551
|
+
start: number
|
|
552
|
+
end: number
|
|
553
|
+
specifiers: Array<{ type: string; local: string; start: number; end: number }>
|
|
554
|
+
}> = []
|
|
555
|
+
|
|
556
|
+
for (const stmt of body) {
|
|
557
|
+
if (stmt.type !== 'ImportDeclaration') continue
|
|
558
|
+
const s = stmt as ImportDeclaration
|
|
559
|
+
|
|
560
|
+
// A macro import is a parseman-alias import carrying `with { type: 'macro' }`
|
|
561
|
+
// (oxc exposes this as ImportDeclaration.attributes).
|
|
562
|
+
const isMacro = moduleAliases.has(s.source.value) && s.attributes.some(a => {
|
|
563
|
+
const key = a.key.type === 'Identifier' ? a.key.name : String(a.key.value)
|
|
564
|
+
return key === 'type' && a.value.value === 'macro'
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
if (!isMacro) {
|
|
568
|
+
const specifiers: Array<{ type: string; local: string; start: number; end: number }> = []
|
|
569
|
+
for (const spec of s.specifiers) {
|
|
570
|
+
if (spec.type === 'ImportSpecifier') {
|
|
571
|
+
const imported = (spec.imported as { name?: string }).name ?? spec.local.name
|
|
572
|
+
importBindings.set(spec.local.name, { source: s.source.value, imported })
|
|
573
|
+
specifiers.push({ type: spec.type, local: spec.local.name, start: spec.start, end: spec.end })
|
|
574
|
+
} else {
|
|
575
|
+
specifiers.push({ type: spec.type, local: spec.local.name, start: spec.start, end: spec.end })
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
ordinaryImports.push({ start: s.start, end: s.end, specifiers })
|
|
579
|
+
continue
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const names = new Set<string>()
|
|
583
|
+
for (const spec of s.specifiers) {
|
|
584
|
+
if (spec.type === 'ImportSpecifier') names.add(spec.local.name)
|
|
585
|
+
}
|
|
586
|
+
macroImports.push({ start: s.start, end: s.end, names, fullyResolved: false })
|
|
587
|
+
for (const n of names) allNames.add(n)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (macroImports.length === 0) return null
|
|
591
|
+
|
|
592
|
+
const unwrapVd = (stmt: Statement): VariableDeclaration | null =>
|
|
593
|
+
stmt.type === 'VariableDeclaration'
|
|
594
|
+
? (stmt as unknown as VariableDeclaration)
|
|
595
|
+
: stmt.type === 'ExportNamedDeclaration'
|
|
596
|
+
&& (stmt as unknown as ExportNamedDeclaration).declaration?.type === 'VariableDeclaration'
|
|
597
|
+
? ((stmt as unknown as ExportNamedDeclaration).declaration as unknown as VariableDeclaration)
|
|
598
|
+
: null
|
|
599
|
+
|
|
600
|
+
/*
|
|
601
|
+
* Module-level `const <name> = (g) => …` factory declarations, so a `rules()` call
|
|
602
|
+
* can be handed its factory BY NAME instead of inline.
|
|
603
|
+
*
|
|
604
|
+
* This is what makes one grammar source compilable twice:
|
|
605
|
+
*
|
|
606
|
+
* const factory = (g) => ({ … })
|
|
607
|
+
* export const grammar = rules({ trivia: rw }, factory)
|
|
608
|
+
* export const cstGrammar = rules({ trivia: rw, hostMode: 'cst' }, factory)
|
|
609
|
+
*
|
|
610
|
+
* Without it, `rules({ hostMode: 'cst' }, factory)` reports "factory isn't statically
|
|
611
|
+
* evaluable" and falls back to the interpreter, and the ONE thing compile-time host
|
|
612
|
+
* mode exists to enable — two artifacts from one source — cannot be written down. The
|
|
613
|
+
* only alternative is duplicating the whole factory at both call sites.
|
|
614
|
+
*
|
|
615
|
+
* Deliberately narrow: a top-level `const` function-valued binding, matched by name.
|
|
616
|
+
* Anything else still takes the existing inline path.
|
|
617
|
+
*
|
|
618
|
+
* The narrowness is load-bearing, because this substitutes a stored initializer for a
|
|
619
|
+
* NAME and so has to reproduce the binding semantics the name actually has:
|
|
620
|
+
*
|
|
621
|
+
* - `const` ONLY. A `let`/`var` factory can be reassigned between the declaration and
|
|
622
|
+
* the `rules()` call, and substituting the initializer would compile a grammar the
|
|
623
|
+
* program does not have. `const` makes reassignment a syntax error, so matching only
|
|
624
|
+
* `const` removes the case rather than trying to detect it.
|
|
625
|
+
* - DECLARED BEFORE USE. The map is built from the whole module body up front, so
|
|
626
|
+
* without a position check a `rules(…, factory)` sitting ABOVE `const factory = …`
|
|
627
|
+
* would resolve here while the real program throws a temporal-dead-zone
|
|
628
|
+
* ReferenceError. Suppressing an error the source would raise is the same class of
|
|
629
|
+
* defect as emitting the wrong grammar. `declaredAt` is compared against the call
|
|
630
|
+
* site below.
|
|
631
|
+
*/
|
|
632
|
+
const factoryDecls = new Map<string, { fn: Expression; declaredAt: number; code?: string; scope?: Scope; imported?: boolean }>()
|
|
633
|
+
for (const stmt of body) {
|
|
634
|
+
const decl = stmt.type === 'ExportNamedDeclaration'
|
|
635
|
+
? (stmt as unknown as { declaration?: unknown }).declaration
|
|
636
|
+
: stmt
|
|
637
|
+
if ((decl as { type?: string } | undefined)?.type !== 'VariableDeclaration') continue
|
|
638
|
+
// `let`/`var` are rebindable; only `const` can be substituted by name safely.
|
|
639
|
+
if ((decl as unknown as { kind?: string }).kind !== 'const') continue
|
|
640
|
+
for (const d of (decl as unknown as VariableDeclaration).declarations) {
|
|
641
|
+
const id = d.id as unknown as { type: string; name?: string }
|
|
642
|
+
const init = d.init as Expression | null | undefined
|
|
643
|
+
if (id.type !== 'Identifier' || !id.name || !init) continue
|
|
644
|
+
if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') {
|
|
645
|
+
factoryDecls.set(id.name, {
|
|
646
|
+
fn: init,
|
|
647
|
+
declaredAt: (d as unknown as { end: number }).end,
|
|
648
|
+
})
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/*
|
|
654
|
+
* Reducer resolution, for `node(..., build)` where `build` is a NAME rather than an
|
|
655
|
+
* inline function.
|
|
656
|
+
*
|
|
657
|
+
* `buildSrc` is the call site's EXPRESSION text, so a named reducer arrives as just the
|
|
658
|
+
* name — no parameter list — and the capture-tier analysis used to fail open and charge
|
|
659
|
+
* the node all five facilities. Sharing reducers across a grammar and importing them
|
|
660
|
+
* from another module is ordinary grammar authoring, so the resolver does the analysis
|
|
661
|
+
* (lexical scope tree, cross-module import following) rather than reporting that it
|
|
662
|
+
* won't. See `reducer-resolver.ts` for what it decides and what genuinely declines.
|
|
663
|
+
*
|
|
664
|
+
* Everything it produces — `_def.buildArity`, `_def.buildSigSrc` — is ANALYSIS-ONLY and
|
|
665
|
+
* never emitted, so the generated builder reference is unchanged either way.
|
|
666
|
+
*/
|
|
667
|
+
const reducerResolver = createReducerResolver(id, body as unknown[], code)
|
|
668
|
+
setReducerResolver(reducerResolver, code)
|
|
669
|
+
|
|
670
|
+
const topLevelFunction = (moduleBody: AnyNode[], name: string): AnyNode | null => {
|
|
671
|
+
for (const st of moduleBody) {
|
|
672
|
+
const decl = st.type === 'ExportNamedDeclaration'
|
|
673
|
+
? (st as unknown as { declaration?: AnyNode }).declaration
|
|
674
|
+
: st
|
|
675
|
+
const idn = decl?.type === 'FunctionDeclaration'
|
|
676
|
+
? (decl as unknown as { id?: { name?: string } }).id
|
|
677
|
+
: undefined
|
|
678
|
+
if (idn?.name === name) return decl ?? null
|
|
679
|
+
}
|
|
680
|
+
return null
|
|
681
|
+
}
|
|
682
|
+
const isParsemanMacroImport = (stmt: AnyNode): boolean => {
|
|
683
|
+
if (stmt.type !== 'ImportDeclaration') return false
|
|
684
|
+
const s = stmt as unknown as ImportDeclaration
|
|
685
|
+
return moduleAliases.has(s.source.value) && s.attributes.some(a => {
|
|
686
|
+
const key = a.key.type === 'Identifier' ? a.key.name : String(a.key.value)
|
|
687
|
+
return key === 'type' && a.value.value === 'macro'
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
const sourceScopeUntil = (moduleBody: AnyNode[], source: string, until: number): Scope | null => {
|
|
691
|
+
const out: Scope = new Map()
|
|
692
|
+
for (const stmt of moduleBody as Statement[]) {
|
|
693
|
+
if (stmt.type === 'ImportDeclaration') {
|
|
694
|
+
if (!isParsemanMacroImport(stmt as unknown as AnyNode)) return null
|
|
695
|
+
continue
|
|
696
|
+
}
|
|
697
|
+
if (stmt.type === 'FunctionDeclaration') continue
|
|
698
|
+
if (stmt.type === 'ExportNamedDeclaration' && (stmt as unknown as ExportNamedDeclaration).declaration?.type === 'FunctionDeclaration') continue
|
|
699
|
+
if (stmt.type === 'ExportNamedDeclaration' && !(stmt as unknown as ExportNamedDeclaration).declaration) {
|
|
700
|
+
if ((stmt as unknown as { source?: unknown }).source) return null
|
|
701
|
+
continue
|
|
702
|
+
}
|
|
703
|
+
const vd = unwrapVd(stmt)
|
|
704
|
+
if (!vd || (vd as unknown as { kind?: string }).kind !== 'const') return null
|
|
705
|
+
for (const decl of vd.declarations) {
|
|
706
|
+
const d = decl as VariableDeclarator
|
|
707
|
+
const idn = d.id as unknown as { type?: string; name?: string }
|
|
708
|
+
const init = d.init as Expression | null | undefined
|
|
709
|
+
if ((idn.type !== 'Identifier' && idn.type !== 'BindingIdentifier') || !idn.name || !init) return null
|
|
710
|
+
if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') continue
|
|
711
|
+
const mapFnSources: string[] = []
|
|
712
|
+
const combi = evaluateExpr(init, out, source, mapFnSources)
|
|
713
|
+
if (combi) {
|
|
714
|
+
if (stmt.start < until) out.set(idn.name, { combi, mfSrcs: mapFnSources })
|
|
715
|
+
continue
|
|
716
|
+
}
|
|
717
|
+
const wordFactory = evaluateWordFactory(init, out, source)
|
|
718
|
+
if (wordFactory) {
|
|
719
|
+
if (stmt.start < until) {
|
|
720
|
+
;(out as Map<string, unknown>).set(idn.name, wordFactory)
|
|
721
|
+
}
|
|
722
|
+
continue
|
|
723
|
+
}
|
|
724
|
+
const whenFactory = evaluateWhenFactory(init, out, source)
|
|
725
|
+
if (whenFactory) {
|
|
726
|
+
if (stmt.start < until) {
|
|
727
|
+
;(out as Map<string, unknown>).set(idn.name, whenFactory)
|
|
728
|
+
}
|
|
729
|
+
continue
|
|
730
|
+
}
|
|
731
|
+
const combiArray = evaluateCombinatorArray(init, out, source)
|
|
732
|
+
if (combiArray) {
|
|
733
|
+
if (stmt.start < until) {
|
|
734
|
+
;(out as Map<string, unknown>).set(idn.name, combiArray)
|
|
735
|
+
}
|
|
736
|
+
continue
|
|
737
|
+
}
|
|
738
|
+
return null
|
|
739
|
+
}
|
|
740
|
+
continue
|
|
741
|
+
}
|
|
742
|
+
return out
|
|
743
|
+
}
|
|
744
|
+
const usedImportedFactories = new Set<string>()
|
|
745
|
+
const markUsedImportedFactories = (names: readonly string[] | undefined): void => {
|
|
746
|
+
for (const name of names ?? []) usedImportedFactories.add(name)
|
|
747
|
+
}
|
|
748
|
+
for (const [localName, binding] of importBindings) {
|
|
749
|
+
const file = resolvePrivateSourceModule(id, binding.source)
|
|
750
|
+
if (!file) continue
|
|
751
|
+
const mod = parseModuleCached(file)
|
|
752
|
+
if (!mod) continue
|
|
753
|
+
const localFor = exportLocalName(mod.body as AnyNode[], binding.imported)
|
|
754
|
+
const init = localFor ? topLevelInit(mod.body as AnyNode[], localFor) ?? topLevelFunction(mod.body as AnyNode[], localFor) : null
|
|
755
|
+
if (!init || (init.type !== 'ArrowFunctionExpression' && init.type !== 'FunctionExpression' && init.type !== 'FunctionDeclaration')) continue
|
|
756
|
+
const factoryScope = sourceScopeUntil(mod.body as AnyNode[], mod.src, init.start)
|
|
757
|
+
if (!factoryScope) continue
|
|
758
|
+
factoryDecls.set(localName, {
|
|
759
|
+
fn: init as unknown as Expression,
|
|
760
|
+
declaredAt: -1,
|
|
761
|
+
code: mod.src,
|
|
762
|
+
scope: factoryScope,
|
|
763
|
+
imported: true,
|
|
764
|
+
})
|
|
765
|
+
// This factory is evaluated with `code: mod.src`, so every `node(…, build)` inside
|
|
766
|
+
// it hands the resolver an offset into THIS file. Tell the resolver the file exists
|
|
767
|
+
// so it answers against that module's own scope tree instead of declining
|
|
768
|
+
// `foreign-source` — a decline is sound but charges the node full capture.
|
|
769
|
+
reducerResolver.register(file)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// --- Pass 2: evaluate declarations in source order ---
|
|
773
|
+
// Scope stores enriched ScopeEntry objects so evaluateParserFactory can
|
|
774
|
+
// replay mfSrcs when outer-scope combinators are referenced inside factories.
|
|
775
|
+
const scope: Scope = new Map<string, ScopeEntry>()
|
|
776
|
+
const replacements: Array<{ start: number; end: number; replacement: string }> = []
|
|
777
|
+
const warnings: string[] = []
|
|
778
|
+
// Collect degradations instead of printing them, so they arrive on the SAME channel
|
|
779
|
+
// as every other macro warning (the bundler's `this.warn`) with a `file:line` anchor.
|
|
780
|
+
beginDegradationCapture()
|
|
781
|
+
let anyUnresolved = false
|
|
782
|
+
// A declaration whose emitted value still CALLS a macro import, without anything
|
|
783
|
+
// having failed: a shared shape keeps its `rules(…)` source (the interpreter map is
|
|
784
|
+
// the only correct standalone value for a grammar with a hole) while still carrying
|
|
785
|
+
// fully compiled pieces for downstream composition. The import must survive, but
|
|
786
|
+
// this is not an unresolved shape, so it neither warns nor blocks other cleanups.
|
|
787
|
+
let keepMacroImport = false
|
|
788
|
+
/**
|
|
789
|
+
* Set when any emitted replacement is a TABLE, which references the shared
|
|
790
|
+
* driver `tableRules` by name.
|
|
791
|
+
*
|
|
792
|
+
* This is the one CONTRACT DIVERGENCE the table lowering states openly
|
|
793
|
+
* (`table/compile.ts`): a table expression is not self-contained, because the
|
|
794
|
+
* driver being external is exactly why the artifact is 0.56 MB rather than
|
|
795
|
+
* 2.10 MB. Inlining the driver per grammar rebuilds the size this lowering
|
|
796
|
+
* exists to remove. So the plugin owns the import, and the free-identifier net
|
|
797
|
+
* at the end of this transform is what proves it was actually emitted.
|
|
798
|
+
*/
|
|
799
|
+
let usedTableRuntime = false
|
|
800
|
+
/** Exported `rules()` factories, whose bodies are left verbatim. See the push site. */
|
|
801
|
+
const exportedFactories: Array<{ name: string; pos: number }> = []
|
|
802
|
+
let runtimeComposeFallback = false
|
|
803
|
+
// Unique per rules() call site in this file — holds the ONE shared compiled
|
|
804
|
+
// rule-map object; each destructured local name reads its property off it.
|
|
805
|
+
let ruleMapHolderCounter = 0
|
|
806
|
+
|
|
807
|
+
// Surface a shape the macro couldn't compile (it silently runs via the
|
|
808
|
+
// interpreter otherwise). Includes a file:line anchor so it's actionable.
|
|
809
|
+
const lineOf = (pos: number): number => {
|
|
810
|
+
let line = 1
|
|
811
|
+
for (let i = 0; i < pos && i < code.length; i++) if (code.charCodeAt(i) === 10) line++
|
|
812
|
+
return line
|
|
813
|
+
}
|
|
814
|
+
const warn = (pos: number, msg: string): void => {
|
|
815
|
+
anyUnresolved = true
|
|
816
|
+
warnings.push(`${id}:${lineOf(pos)} — ${msg} (running via the interpreter; add the plugin or simplify the declaration to compile it)`)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Compile a `rules(factory)` call into ONE shared replacement expression
|
|
821
|
+
* for the whole call — see compileRuleMap() in codegen.ts for why this is
|
|
822
|
+
* one shared codegen pass instead of one `compile()` per entry (a `rules()`
|
|
823
|
+
* factory's entries commonly share large reachable sub-rule graphs; a
|
|
824
|
+
* shared pass compiles each shared sub-rule once instead of once per entry
|
|
825
|
+
* that reaches it).
|
|
826
|
+
*/
|
|
827
|
+
const evaluateRulesFactory = (
|
|
828
|
+
init: Expression,
|
|
829
|
+
label: string,
|
|
830
|
+
): { ruleMap: Map<string, Combinator<unknown>>; trivia?: Combinator<unknown>; scanSkip?: Combinator<unknown>[]; trackLines?: boolean; importedFactory?: string } | null => {
|
|
831
|
+
const args = (init as unknown as { arguments: unknown[] }).arguments
|
|
832
|
+
// Options-first: rules({ trivia }, factory). Disambiguate by type — an
|
|
833
|
+
// ObjectExpression first arg means options lead; otherwise the factory leads
|
|
834
|
+
// (bare rules(factory), or the tolerated legacy rules(factory, { trivia })).
|
|
835
|
+
const arg0 = args[0] as AnyNode | undefined
|
|
836
|
+
const arg1 = args[1] as AnyNode | undefined
|
|
837
|
+
const optionsFirst = arg0?.type === 'ObjectExpression'
|
|
838
|
+
const factoryArgRaw = (optionsFirst ? arg1 : arg0) as Expression | undefined
|
|
839
|
+
if (!factoryArgRaw) { warn(init.start, `${label}: rules() needs a factory argument`); return null }
|
|
840
|
+
/* A factory named by identifier resolves to its module-level declaration, so two
|
|
841
|
+
* `rules()` call sites can SHARE one factory — see `factoryDecls`. Only when the
|
|
842
|
+
* declaration PRECEDES this call: a later one is a temporal dead zone in the real
|
|
843
|
+
* program, and resolving it here would compile a grammar out of a binding that does
|
|
844
|
+
* not exist yet. Falling through leaves `factoryArgRaw`, which takes the ordinary
|
|
845
|
+
* inline path and reports "isn't statically evaluable" rather than inventing an answer. */
|
|
846
|
+
const namedFactory = factoryArgRaw.type === 'Identifier'
|
|
847
|
+
? factoryDecls.get((factoryArgRaw as unknown as { name: string }).name)
|
|
848
|
+
: undefined
|
|
849
|
+
const factoryArg = namedFactory !== undefined && namedFactory.declaredAt <= init.start
|
|
850
|
+
? namedFactory.fn
|
|
851
|
+
: factoryArgRaw
|
|
852
|
+
const factoryScope = namedFactory?.scope ?? scope
|
|
853
|
+
const factoryCode = namedFactory?.code ?? code
|
|
854
|
+
const importedFactory = namedFactory?.imported === true && factoryArg === namedFactory.fn
|
|
855
|
+
? (factoryArgRaw as unknown as { name?: string }).name
|
|
856
|
+
: undefined
|
|
857
|
+
|
|
858
|
+
// Grammar-level options object — evaluate `trivia` / `scanSkip` so the compiled
|
|
859
|
+
// map seeds them as the ambient defaults (build-time mirror of rules() tagging
|
|
860
|
+
// grammarTrivia / grammarScanSkip at runtime).
|
|
861
|
+
const optionsArg = (optionsFirst ? arg0 : arg1) as AnyNode | undefined
|
|
862
|
+
const optionValue = (name: string): Expression | undefined => optionProp(optionsArg, name)
|
|
863
|
+
|
|
864
|
+
// Read and VALIDATE hostMode before evaluating the factory, so a mode the macro
|
|
865
|
+
// cannot honour is reported even when the factory also fails to evaluate. Getting
|
|
866
|
+
// this order wrong means the one option whose silent loss is a wrong TREE gets
|
|
867
|
+
// swallowed by an unrelated warning.
|
|
868
|
+
const hostModeValue = optionValue('hostMode')
|
|
869
|
+
const gHostMode = hostModeValue?.type === 'Literal'
|
|
870
|
+
? (hostModeValue as unknown as { value?: unknown }).value
|
|
871
|
+
: undefined
|
|
872
|
+
if (hostModeValue !== undefined && gHostMode !== 'ast' && gHostMode !== 'cst') {
|
|
873
|
+
warn(init.start, `${label}: rules({ hostMode }) must be the literal 'ast' or 'cst'`)
|
|
874
|
+
return null
|
|
875
|
+
}
|
|
876
|
+
const trackLinesValue = optionValue('trackLines')
|
|
877
|
+
const gTrackLines = trackLinesValue?.type === 'Literal'
|
|
878
|
+
? (trackLinesValue as unknown as { value?: unknown }).value
|
|
879
|
+
: undefined
|
|
880
|
+
if (trackLinesValue !== undefined && typeof gTrackLines !== 'boolean') {
|
|
881
|
+
warn(init.start, `${label}: rules({ trackLines }) must be a boolean literal`)
|
|
882
|
+
return null
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// A specific reason when the factory BODY is what failed. The generic message
|
|
886
|
+
// points nowhere near the cause, and the dominant real cause — a forward
|
|
887
|
+
// reference to a const declared lower down — is one the interpreter reports
|
|
888
|
+
// precisely. Two lanes lost a round to the generic text.
|
|
889
|
+
/*
|
|
890
|
+
* THE OPTIONS GO IN, THEY ARE NOT REAPPLIED AFTERWARDS.
|
|
891
|
+
*
|
|
892
|
+
* Three loops used to live below this point, stamping `grammarScanSkip`,
|
|
893
|
+
* `grammarHostMode` and `grammarTrackLines` onto the evaluated rules, each
|
|
894
|
+
* carrying a comment that the macro "evaluates the FACTORY directly and never
|
|
895
|
+
* calls `rules()`". It does now, so the options are threaded as the
|
|
896
|
+
* `RulesOptions` argument and `rules()` applies them — the same code, on the
|
|
897
|
+
* same rules, at the same point, for the runtime and the build.
|
|
898
|
+
*
|
|
899
|
+
* That also fixes the `trackLines` half, which the stamp-only copy got wrong:
|
|
900
|
+
* `rules()` does not merely mark the rules, it WRAPS each non-trivia rule in a
|
|
901
|
+
* `grammarParser({ trackLines: true })` scope (`parser.ts:228-242`). The macro
|
|
902
|
+
* carried the setting to the encoder by a different route instead, so the two
|
|
903
|
+
* routes built structurally different maps for the same source.
|
|
904
|
+
*
|
|
905
|
+
* Evaluated BEFORE the factory because they are now inputs to it.
|
|
906
|
+
*/
|
|
907
|
+
const triviaValue = optionValue('trivia')
|
|
908
|
+
const gTrivia = triviaValue ? evaluateExpr(triviaValue, scope, code, []) : undefined
|
|
909
|
+
const scanSkipValue = optionValue('scanSkip')
|
|
910
|
+
const gScanSkip = scanSkipValue
|
|
911
|
+
? (evaluateCombinatorArray(scanSkipValue, scope, code) ?? undefined)
|
|
912
|
+
: undefined
|
|
913
|
+
|
|
914
|
+
const why: { reason?: string } = {}
|
|
915
|
+
const ruleMap = evaluateParserFactory(factoryArg, factoryScope, factoryCode, [], why, {
|
|
916
|
+
...(gTrivia ? { trivia: gTrivia as Combinator<unknown> } : {}),
|
|
917
|
+
...(gScanSkip ? { scanSkip: gScanSkip } : {}),
|
|
918
|
+
...(gHostMode === 'cst' ? { hostMode: 'cst' as const } : {}),
|
|
919
|
+
...(gTrackLines === true ? { trackLines: true } : {}),
|
|
920
|
+
})
|
|
921
|
+
if (!ruleMap) {
|
|
922
|
+
warn(init.start, why.reason === undefined
|
|
923
|
+
? `${label}: rules(...) factory isn't statically evaluable`
|
|
924
|
+
: `${label}: rules(...) factory isn't statically evaluable — ${why.reason}`)
|
|
925
|
+
return null
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return {
|
|
929
|
+
ruleMap,
|
|
930
|
+
...(gTrivia ? { trivia: gTrivia } : {}),
|
|
931
|
+
...(gScanSkip ? { scanSkip: gScanSkip } : {}),
|
|
932
|
+
...(gTrackLines === true ? { trackLines: true } : {}),
|
|
933
|
+
...(importedFactory ? { importedFactory } : {}),
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/** null → the factory itself isn't statically evaluable (already warned).
|
|
938
|
+
* `replacement: null` → the map evaluated fine but `compileRuleMap` couldn't
|
|
939
|
+
* inline it; the caller decides whether that is a fallback-to-interpreter
|
|
940
|
+
* warning or the SHARED-SHAPE case (an unresolved external `g.` ref, which is
|
|
941
|
+
* un-inlinable by construction but still linkable — see `hasExternalRef`). */
|
|
942
|
+
const compileRulesFactory = (
|
|
943
|
+
init: Expression,
|
|
944
|
+
label: string,
|
|
945
|
+
): { replacement: string | null; replacementWithMetadata?: (metadataSource: string) => string; refusals?: readonly string[]; ruleMap: Map<string, Combinator<unknown>>; hostMode: HostMode; hostBranchElided: boolean; reflection: GrammarReflection; coverageDefinitions?: readonly { id: string; kind: string }[]; trivia?: Combinator<unknown>; scanSkip?: Combinator<unknown>[]; trackLines?: boolean; importedFactory?: string } | null => {
|
|
946
|
+
const evaluated = evaluateRulesFactory(init, label)
|
|
947
|
+
if (!evaluated) return null
|
|
948
|
+
// The reasons, out of the encoder and into the warning. A bare "couldn't be
|
|
949
|
+
// inlined" leaves the author with a ~5x silent perf regression and no lead.
|
|
950
|
+
const refusals: string[] = []
|
|
951
|
+
const compiled = compileRuleMap([...evaluated.ruleMap], { ...(evaluated.trivia ? { trivia: evaluated.trivia } : {}), ...(evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}), ...(evaluated.trackLines ? { trackLines: true } : {}), recovery, coverage: grammarCoverage, refusals })
|
|
952
|
+
// A table replacement names `tableRules`, which nothing in the consumer's
|
|
953
|
+
// module binds. That reference is the whole reason the artifact is small (the
|
|
954
|
+
// driver is SHARED, not inlined per grammar), so the import is owned here.
|
|
955
|
+
if (compiled !== null) usedTableRuntime = true
|
|
956
|
+
return {
|
|
957
|
+
replacement: compiled?.replacement ?? null,
|
|
958
|
+
...(compiled === null ? {} : { replacementWithMetadata: compiled.replacementWithMetadata }),
|
|
959
|
+
...(compiled === null ? { refusals } : {}),
|
|
960
|
+
ruleMap: evaluated.ruleMap,
|
|
961
|
+
hostMode: compiled?.hostMode ?? 'ast',
|
|
962
|
+
hostBranchElided: compiled?.hostBranchElided ?? false,
|
|
963
|
+
reflection: compiled?.reflection ?? { nodes: [] },
|
|
964
|
+
...(compiled?.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}),
|
|
965
|
+
...(evaluated.trivia ? { trivia: evaluated.trivia } : {}),
|
|
966
|
+
...(evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}),
|
|
967
|
+
...(evaluated.trackLines ? { trackLines: true } : {}),
|
|
968
|
+
...(evaluated.importedFactory ? { importedFactory: evaluated.importedFactory } : {}),
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const isRulesCall = (init: Expression): boolean =>
|
|
973
|
+
init.type === 'CallExpression' &&
|
|
974
|
+
(init as unknown as { callee: { type: string; name?: string } }).callee.type === 'Identifier' &&
|
|
975
|
+
(init as unknown as { callee: { name?: string } }).callee.name === 'rules'
|
|
976
|
+
|
|
977
|
+
const isComposeCall = (init: Expression): boolean =>
|
|
978
|
+
init.type === 'CallExpression' &&
|
|
979
|
+
(init as unknown as { callee: { type: string; name?: string } }).callee.type === 'Identifier' &&
|
|
980
|
+
(init as unknown as { callee: { name?: string } }).callee.name === 'compose'
|
|
981
|
+
|
|
982
|
+
const isComposeLeafCall = (init: Expression): boolean =>
|
|
983
|
+
init.type === 'CallExpression'
|
|
984
|
+
&& (init as unknown as { callee: { type: string; name?: string } }).callee.type === 'Identifier'
|
|
985
|
+
&& (init as unknown as { callee: { name?: string } }).callee.name === 'composeLeaf'
|
|
986
|
+
|
|
987
|
+
// Local `rules()` grammars, so a same-file `compose([myRules, …])` can recover
|
|
988
|
+
// the pieces to fuse (name → the rule map evaluated at build). A grammar stays a
|
|
989
|
+
// usable parser AND is composable — no opt-in wrapper.
|
|
990
|
+
const localRuleMaps = new Map<string, Map<string, Combinator<unknown>>>()
|
|
991
|
+
// Local plain grammars (`const g = rules({ trivia }, …)`) that declared a
|
|
992
|
+
// grammar-level trivia — read by composingTrivia() so a same-file
|
|
993
|
+
// `compose([…, g])` can adopt g's trivia (composing-wins). A grammar declared
|
|
994
|
+
// WITHOUT trivia is absent here, so composingTrivia keeps scanning earlier items.
|
|
995
|
+
const localGrammarTrivia = new Map<string, Combinator<unknown>>()
|
|
996
|
+
// …and its grammar-level scanSkip, so a same-file `composeLeaf([g, …])` where the
|
|
997
|
+
// final local grammar is an IDENTIFIER bound to `rules({ scanSkip })` still threads
|
|
998
|
+
// that opaque-unit set to the fused local piece (mirrors the inline-rules() case).
|
|
999
|
+
const localGrammarScanSkip = new Map<string, Combinator<unknown>[]>()
|
|
1000
|
+
|
|
1001
|
+
// Stable, reproducible per-artifact namespace: hash of module id + a label
|
|
1002
|
+
// (binding name / arg position) — never a counter, so rebuilds are byte-stable
|
|
1003
|
+
// and two artifacts never collide when fused.
|
|
1004
|
+
const nsFor = (label: string): string =>
|
|
1005
|
+
`_${createHash('sha1').update(`${id}#${label}`).digest('hex').slice(0, 8)}_`
|
|
1006
|
+
|
|
1007
|
+
/** Serialize a pieces LIST — one entry for a `rules()` grammar, the flattened
|
|
1008
|
+
* list for a `compose()` result. */
|
|
1009
|
+
// A carried list entry is either a grammar's own pieces (serialized in full) or
|
|
1010
|
+
// an IMPORT MARKER standing in for an imported ancestor grammar's pieces. The
|
|
1011
|
+
// marker keeps a descendant from re-serializing everything it imported (Less
|
|
1012
|
+
// re-shipping CSS, SCSS re-shipping css+less, …): at read time the marker is
|
|
1013
|
+
// resolved back to the ancestor's compiled file transitively. Build-time only —
|
|
1014
|
+
// importedPieces resolves it via the module resolver; runtime never reads it.
|
|
1015
|
+
// An imported ancestor is referenced by SPREADING its live carried pieces off the
|
|
1016
|
+
// imported binding — `...(cssGrammar[Symbol.for('parseman.composedPieces')] ?? [])`.
|
|
1017
|
+
// At runtime the ES import is a live value, so this self-resolves (transitively:
|
|
1018
|
+
// the ancestor's own value already spread ITS ancestors). At macro-compile time
|
|
1019
|
+
// the plugin resolves the spread statically by following the import. Either way
|
|
1020
|
+
// the ancestor isn't re-serialized. `local` is the binding name as it appears in
|
|
1021
|
+
// THIS module's source (the bundler renames import + references together).
|
|
1022
|
+
type ImportSpread = { __spreadLocal: string }
|
|
1023
|
+
type IRItem = { ns: string; ir: string; trackLines?: true }
|
|
1024
|
+
type CarriedItem = ImportSpread | IRItem
|
|
1025
|
+
const isSpread = (it: CarriedItem): it is ImportSpread => '__spreadLocal' in it
|
|
1026
|
+
const isIR = (it: CarriedItem): it is IRItem => 'ir' in it
|
|
1027
|
+
// A carried entry is: an import SPREAD (live ref to an ancestor's pieces) or an IR
|
|
1028
|
+
// PIECE (this grammar's own rules as a compact combinator expression, re-lowered at
|
|
1029
|
+
// fuse). There is no third form — a piece that cannot serialize is not carried.
|
|
1030
|
+
const serializeItem = (it: CarriedItem): string =>
|
|
1031
|
+
isSpread(it)
|
|
1032
|
+
? `...(${it.__spreadLocal}[Symbol.for('parseman.composedPieces')] ?? [])`
|
|
1033
|
+
: `{ ns: ${JSON.stringify(it.ns)}, ir: ${JSON.stringify(it.ir)}${it.trackLines ? ', trackLines: true' : ''} }`
|
|
1034
|
+
const serializeList = (list: CarriedItem[]): string => `[${list.map(serializeItem).join(', ')}]`
|
|
1035
|
+
|
|
1036
|
+
type StaticTableMetadata = {
|
|
1037
|
+
carried?: CarriedItem[]
|
|
1038
|
+
reflection?: GrammarReflection
|
|
1039
|
+
leaf?: true
|
|
1040
|
+
}
|
|
1041
|
+
/** Metadata passed into `tableRules` at construction. `stampRuleMap` uses this
|
|
1042
|
+
* object as the returned map's prototype, so these ordinary symbol fields are
|
|
1043
|
+
* readable but never copied by object spread/Object.assign. Coverage comes
|
|
1044
|
+
* directly from the emitted program's `cv` pool and host mode from `h`. */
|
|
1045
|
+
const staticTableMetadataSource = (metadata: StaticTableMetadata): string => {
|
|
1046
|
+
const fields = [
|
|
1047
|
+
...(metadata.carried === undefined ? [] : [
|
|
1048
|
+
`[Symbol.for('parseman.composedPieces')]: ${serializeList(metadata.carried)}`,
|
|
1049
|
+
]),
|
|
1050
|
+
...(metadata.reflection === undefined ? [] : [
|
|
1051
|
+
`[Symbol.for('parseman.grammarReflection')]: ${grammarReflectionSource(metadata.reflection)}`,
|
|
1052
|
+
]),
|
|
1053
|
+
...(metadata.leaf === true ? [`[Symbol.for('parseman.leafComposed')]: true`] : []),
|
|
1054
|
+
]
|
|
1055
|
+
return `{ ${fields.join(', ')} }`
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Attach a compiled grammar's linkable pieces onto the value, under
|
|
1060
|
+
* `Symbol.for('parseman.composedPieces')` — the same symbol `compose()` reads at
|
|
1061
|
+
* runtime. This is why `import { cssGrammar }` is all a downstream package needs:
|
|
1062
|
+
* the pieces travel WITH the grammar (no detached, tree-shakeable `__pieces`
|
|
1063
|
+
* export). `Object.defineProperty` returns the grammar, so it stays one
|
|
1064
|
+
* expression that can't be shaken off the exported const without dropping the
|
|
1065
|
+
* grammar; `importedPieces()` reads it back out. The descriptor mirrors the
|
|
1066
|
+
* runtime attach (`linker.ts`) — `enumerable: false` so the (large) carried IR
|
|
1067
|
+
* isn't dragged along by `Object.assign(target, grammar)` or `{ ...grammar }`.
|
|
1068
|
+
*/
|
|
1069
|
+
const withCarriedPieces = (grammarExpr: string, list: CarriedItem[]): string =>
|
|
1070
|
+
`/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.composedPieces'), { value: ${serializeList(list)}, enumerable: false })`
|
|
1071
|
+
/** Coverage-only macro output carries the exact IDs emitted in its generated
|
|
1072
|
+
* hooks. The metadata is non-enumerable so grammar maps keep their ordinary
|
|
1073
|
+
* public shape, and it is absent entirely from production builds. */
|
|
1074
|
+
const withCoverageDefinitions = (grammarExpr: string, definitions: readonly { id: string; kind: string }[]): string =>
|
|
1075
|
+
!grammarCoverage ? grammarExpr
|
|
1076
|
+
: `/* @__PURE__ */ Object.defineProperty(${grammarExpr}, Symbol.for('parseman.grammarCoverageDefinitions'), { value: Object.freeze(${JSON.stringify(definitions)}.map(Object.freeze)), enumerable: false })`
|
|
1077
|
+
/**
|
|
1078
|
+
* Recover the coverage DENOMINATOR by scraping the IDs out of the generated hooks.
|
|
1079
|
+
*
|
|
1080
|
+
* This is a fallback: it reads emitted source with a regex, so it silently returns `[]`
|
|
1081
|
+
* if the emitted hook shape ever changes or the hooks are absent. An empty denominator
|
|
1082
|
+
* is NOT zero coverage — it is NO MEASUREMENT — and it used to travel all the way to a
|
|
1083
|
+
* consumer's gate as 100%. `'coverage-definitions-unavailable'` is the declared code for
|
|
1084
|
+
* exactly this and had no record site anywhere; this is it.
|
|
1085
|
+
*/
|
|
1086
|
+
const emittedCoverageDefinitions = (source: string, where: string): Array<{ id: string; kind: 'rule' | 'choice-arm' | 'dispatch-arm' | 'label' }> => {
|
|
1087
|
+
const ids = new Set<string>()
|
|
1088
|
+
for (const match of source.matchAll(/id:\s*"([^"]+)"/g)) ids.add(match[1]!)
|
|
1089
|
+
if (grammarCoverage && ids.size === 0) {
|
|
1090
|
+
recordDegradation({
|
|
1091
|
+
code: 'coverage-definitions-unavailable',
|
|
1092
|
+
severity: 'warn',
|
|
1093
|
+
where,
|
|
1094
|
+
subject: 'generated coverage hooks',
|
|
1095
|
+
fellBackTo: 'no coverage definitions could be read out of the generated source, so the '
|
|
1096
|
+
+ 'grammar carries an EMPTY definition set — which is no measurement, not full coverage',
|
|
1097
|
+
otherwise: 'the emitted hook IDs would form the coverage denominator',
|
|
1098
|
+
})
|
|
1099
|
+
}
|
|
1100
|
+
return [...ids].sort().map(id => ({
|
|
1101
|
+
id,
|
|
1102
|
+
kind: id.startsWith('rule:') ? 'rule' : id.startsWith('label:') ? 'label' : id.startsWith('dispatch:') ? 'dispatch-arm' : 'choice-arm',
|
|
1103
|
+
}))
|
|
1104
|
+
}
|
|
1105
|
+
// Same-file `const X = compose([...])` → its carried (re-lowerable) list, so a
|
|
1106
|
+
// later same-file compose can chain it AND re-lower it under that compose's own
|
|
1107
|
+
// composing trivia (composing-wins holds at every level).
|
|
1108
|
+
const localComposedCarried = new Map<string, CarriedItem[]>()
|
|
1109
|
+
|
|
1110
|
+
// Cache of RE-LOWERABLE pieces lists read from imported COMPILED grammars'
|
|
1111
|
+
// carried pieces (IR pieces + spreads left un-materialized, so the composing
|
|
1112
|
+
// grammar's trivia can be applied when they are finally lowered).
|
|
1113
|
+
type RawItem = IRItem
|
|
1114
|
+
const importedPiecesCache = new Map<string, RawItem[] | null>()
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Read an imported grammar's pieces LIST straight off its COMPILED value —
|
|
1118
|
+
* the `Symbol.for('parseman.composedPieces')` literal the macro embeds inside
|
|
1119
|
+
* the exported const's initializer (see `withCarriedPieces`). No `__pieces`
|
|
1120
|
+
* export, no TS source, no recompile. `import { <name> }` carries everything.
|
|
1121
|
+
* null if the grammar wasn't macro-compiled (or isn't source-free composable).
|
|
1122
|
+
*/
|
|
1123
|
+
// Resolve the FULLY-EXPANDED pieces list from a compiled grammar's carried
|
|
1124
|
+
// literal. The literal may SPREAD imported ancestors' pieces —
|
|
1125
|
+
// `...(binding[Symbol.for('parseman.composedPieces')] ?? [])` — which the runtime
|
|
1126
|
+
// resolves off the live import. Here (build time, no live module) we resolve each
|
|
1127
|
+
// spread by following that module's own import of `binding` to its compiled file
|
|
1128
|
+
// and recursing, then eval the literal with the resolved ancestors stubbed in so
|
|
1129
|
+
// the spreads splice their pieces. `seen` guards against an import cycle.
|
|
1130
|
+
const resolveModulePieces = (module: ResolvedGrammarModule, exportName: string, seen: Set<string>): RawItem[] | null => {
|
|
1131
|
+
const { file } = module
|
|
1132
|
+
const mod = module.source
|
|
1133
|
+
? lowerPrivateSourceModule(file, moduleAliases, warnUnloweredRegex, recovery)
|
|
1134
|
+
: parseModuleCached(file)
|
|
1135
|
+
if (!mod) return null
|
|
1136
|
+
const localFor = exportLocalName(mod.body as AnyNode[], exportName)
|
|
1137
|
+
const initNode = localFor ? topLevelInit(mod.body as AnyNode[], localFor) : null
|
|
1138
|
+
const literalRange = initNode ? findCarriedPiecesLiteral(initNode) : null
|
|
1139
|
+
if (!literalRange) return null
|
|
1140
|
+
const literal = mod.src.slice(literalRange.start, literalRange.end)
|
|
1141
|
+
|
|
1142
|
+
const imports = extractImportBindings(mod.body as AnyNode[])
|
|
1143
|
+
const stubNames: string[] = []
|
|
1144
|
+
const stubVals: unknown[] = []
|
|
1145
|
+
const spreadRe = /\.\.\.\s*\(?\s*([A-Za-z_$][\w$]*)\s*\[\s*Symbol\s*\.\s*for\s*\(\s*['"]parseman\.composedPieces['"]/g
|
|
1146
|
+
const done = new Set<string>()
|
|
1147
|
+
for (let m: RegExpExecArray | null; (m = spreadRe.exec(literal)); ) {
|
|
1148
|
+
const local = m[1]!
|
|
1149
|
+
if (done.has(local)) continue
|
|
1150
|
+
done.add(local)
|
|
1151
|
+
const b = imports.get(local)
|
|
1152
|
+
let subPieces: RawItem[] = []
|
|
1153
|
+
if (b) {
|
|
1154
|
+
const subModule = resolveGrammarModule(file, b.source)
|
|
1155
|
+
if (subModule && !seen.has(subModule.file)) {
|
|
1156
|
+
subPieces = resolveModulePieces(subModule, b.imported, new Set(seen).add(subModule.file)) ?? []
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
stubNames.push(local)
|
|
1160
|
+
// The KEY comes from the linker, which owns it. Spelling `Symbol.for('…')`
|
|
1161
|
+
// here made the string the contract between a build-time module and a
|
|
1162
|
+
// runtime one, with nothing checking: renaming it in `linker.ts` would have
|
|
1163
|
+
// left this stub keying on the old name, and a carried-pieces lookup that
|
|
1164
|
+
// silently finds nothing produces a grammar that composes to less than it
|
|
1165
|
+
// should — no type error, no failing test. (The `Symbol.for('…')` spellings
|
|
1166
|
+
// further down are EMITTED SOURCE TEXT, not this module's own key: they are
|
|
1167
|
+
// strings by necessity and the runtime resolves them through the registry.)
|
|
1168
|
+
stubVals.push({ [COMPOSED_PIECES]: subPieces })
|
|
1169
|
+
}
|
|
1170
|
+
try {
|
|
1171
|
+
// Build-time eval of the carried literal with ancestor spreads stubbed — NOT
|
|
1172
|
+
// runtime. `Symbol.for`/`Map` are globals; `stubNames` provide the imports.
|
|
1173
|
+
// The result stays RE-LOWERABLE: IR pieces ({ns, ir}) are NOT materialized here,
|
|
1174
|
+
// so the composing grammar's trivia can be seeded when they are finally lowered
|
|
1175
|
+
// (materializeCarried). Ancestor spreads were already resolved (also un-lowered).
|
|
1176
|
+
// eslint-disable-next-line no-new-func
|
|
1177
|
+
const list = new Function(...stubNames, `return (${literal})`)(...stubVals) as RawItem[]
|
|
1178
|
+
return list
|
|
1179
|
+
} catch { return null }
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const importedPieces = (localName: string): RawItem[] | null => {
|
|
1183
|
+
if (importedPiecesCache.has(localName)) return importedPiecesCache.get(localName)!
|
|
1184
|
+
let result: RawItem[] | null = null
|
|
1185
|
+
const binding = importBindings.get(localName)
|
|
1186
|
+
if (binding) {
|
|
1187
|
+
const module = resolveGrammarModule(id, binding.source)
|
|
1188
|
+
result = module ? resolveModulePieces(module, binding.imported, new Set([module.file])) : null
|
|
1189
|
+
}
|
|
1190
|
+
importedPiecesCache.set(localName, result)
|
|
1191
|
+
return result
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* MERGE a carried list to ONE rule map — the table lowering's entire composition
|
|
1196
|
+
* mechanism, and the reason it needs no linker.
|
|
1197
|
+
*
|
|
1198
|
+
* Source composition is a TEXTUAL splice: each piece is lowered on its own to
|
|
1199
|
+
* namespaced `_r_<Name>` functions, and `fusedBody()` picks a winner per name and
|
|
1200
|
+
* substitutes the `@FS:` dispatch placeholders with that winner's first-set
|
|
1201
|
+
* condition. A table has no text to splice and no placeholders to resolve, so the
|
|
1202
|
+
* merge moves one level UP, onto the combinators: evaluate each piece's IR back to
|
|
1203
|
+
* a rule map, let a later piece's name override an earlier one — which is exactly
|
|
1204
|
+
* what `compose()` means — and hand the single merged map to
|
|
1205
|
+
* `compileRuleMap`, which encodes ONCE.
|
|
1206
|
+
*
|
|
1207
|
+
* This is what makes table composition the easy kind: no relocation of code
|
|
1208
|
+
* offsets, no merging of const / fn / class / expected / dispatch pools between
|
|
1209
|
+
* two already-encoded programs. `encodeTableProgram` points `enc.winners` at the
|
|
1210
|
+
* merged map (`table/encode.ts:1383`), so a base piece's internal `g.Atom`
|
|
1211
|
+
* resolves BY NAME to the override (`:1036`) rather than through a thunk that
|
|
1212
|
+
* closes over the base. Open recursion — much of the point of compose — therefore
|
|
1213
|
+
* survives the merge, and the result is the table the merged grammar would have
|
|
1214
|
+
* produced had it been written as a single `rules()` call.
|
|
1215
|
+
*
|
|
1216
|
+
* `null` means some piece cannot contribute combinators: a FULL BAKED piece (the
|
|
1217
|
+
* un-serializable fallback at `localCarried`) carries lowered SOURCE and nothing
|
|
1218
|
+
* else. That shape is codegen-only, so the caller falls back to fused source
|
|
1219
|
+
* rather than refusing the grammar.
|
|
1220
|
+
*/
|
|
1221
|
+
type CarriedRuleMap = { ns: string; rules: Array<[string, Combinator<unknown>]> }
|
|
1222
|
+
const carriedRuleMaps = (
|
|
1223
|
+
items: CarriedItem[],
|
|
1224
|
+
): { pieces: CarriedRuleMap[]; trackLines: boolean } | null => {
|
|
1225
|
+
const pieces: CarriedRuleMap[] = []
|
|
1226
|
+
let trackLines = false
|
|
1227
|
+
const add = (item: RawItem): boolean => {
|
|
1228
|
+
if (!isIR(item)) return false
|
|
1229
|
+
// `trackLines` rides on the CARRIED ITEM, not inside the IR (`IRPiece.trackLines`
|
|
1230
|
+
// is a sibling field of `ir`), so it has to be lifted here. Left behind, the
|
|
1231
|
+
// merged encode resolves `trackLines` from `_meta` alone and silently drops line
|
|
1232
|
+
// tracking for a grammar that asked for it.
|
|
1233
|
+
if (item.trackLines === true) trackLines = true
|
|
1234
|
+
pieces.push({ ns: item.ns, rules: evalRuleMapIR(item.ir) })
|
|
1235
|
+
return true
|
|
1236
|
+
}
|
|
1237
|
+
for (const item of items) {
|
|
1238
|
+
if (isSpread(item)) {
|
|
1239
|
+
const imported = importedPieces(item.__spreadLocal)
|
|
1240
|
+
if (!imported || !imported.every(add)) return null
|
|
1241
|
+
} else if (!add(item as RawItem)) return null
|
|
1242
|
+
}
|
|
1243
|
+
return { pieces, trackLines }
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Fold ordered rule maps into the composed map: a later piece's name WINS, and
|
|
1248
|
+
* `Map` keeps each name at its first-sighting position so the encoded rule order
|
|
1249
|
+
* tracks the source lowering's rather than drifting per compose.
|
|
1250
|
+
*
|
|
1251
|
+
* A REFERENCE IS NOT A DEFINITION. A `rules(g => …)` cache also holds every `g.X`
|
|
1252
|
+
* that was merely ACCESSED, so a delta that calls `g.Pair` carries a `Pair` entry
|
|
1253
|
+
* that is an unresolved named lazy. Merged in name order that entry lands LAST and
|
|
1254
|
+
* shadows the base piece which actually defines `Pair` — the encoder then finds a
|
|
1255
|
+
* hole where the winner should be and refuses the whole grammar with "ref() used
|
|
1256
|
+
* before .define()". The same filter guards `compileLinkableTable`'s `isLocal`
|
|
1257
|
+
* (`compile-linkable-table.ts:96`), `itemCarried` (`linker.ts:696`) and
|
|
1258
|
+
* `recoverComposedRules`; this is the fourth site that needs it, for the same
|
|
1259
|
+
* reason, and skipping it is why cross-piece composition looked blocked.
|
|
1260
|
+
*
|
|
1261
|
+
* The reference itself is not lost: it stays inside the delta's own rule bodies and
|
|
1262
|
+
* `enc.winners` binds it BY NAME to whichever piece supplies the definition.
|
|
1263
|
+
*/
|
|
1264
|
+
const mergeRuleMaps = (
|
|
1265
|
+
maps: ReadonlyArray<ReadonlyArray<readonly [string, Combinator<unknown>]>>,
|
|
1266
|
+
): Array<[string, Combinator<unknown>]> => {
|
|
1267
|
+
const winners = new Map<string, Combinator<unknown>>()
|
|
1268
|
+
for (const map of maps) {
|
|
1269
|
+
for (const [name, rule] of map) {
|
|
1270
|
+
if (rule._def.tag === 'lazy') {
|
|
1271
|
+
try { rule._def.thunk() } catch { continue }
|
|
1272
|
+
}
|
|
1273
|
+
winners.set(name, rule)
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return [...winners]
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const mergedCarriedRules = (
|
|
1280
|
+
items: CarriedItem[],
|
|
1281
|
+
): { rules: Array<[string, Combinator<unknown>]>; trackLines: boolean } | null => {
|
|
1282
|
+
const carriedMaps = carriedRuleMaps(items)
|
|
1283
|
+
if (carriedMaps === null) return null
|
|
1284
|
+
return { rules: mergeRuleMaps(carriedMaps.pieces.map(p => p.rules)), trackLines: carriedMaps.trackLines }
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* COMPOSING-WINS, as an OVERRIDE rather than a gap-fill.
|
|
1289
|
+
*
|
|
1290
|
+
* `compileRuleMap`'s `applyAmbient` only fills a rule that carries no trivia of
|
|
1291
|
+
* its own — correct for `composeLeaf`, whose pieces may legitimately disagree, and
|
|
1292
|
+
* WRONG for `compose`, where the composing grammar's trivia governs every fused rule
|
|
1293
|
+
* INCLUDING the inherited ones. Gap-filling leaves a base rule that declared its own
|
|
1294
|
+
* `rules({ trivia }, …)` still skipping the base's trivia after a delta re-declared
|
|
1295
|
+
* it, so `compose([css, less])` silently parses the inherited rules under css's
|
|
1296
|
+
* whitespace — the multi-level composing-wins contract, inverted.
|
|
1297
|
+
*
|
|
1298
|
+
* Safe to mutate: every rule here came from `evalRuleMapIR`, which constructs FRESH
|
|
1299
|
+
* combinators per piece per compose, so this cannot leak into another compose of the
|
|
1300
|
+
* same base grammar.
|
|
1301
|
+
*/
|
|
1302
|
+
const applyComposingTrivia = (
|
|
1303
|
+
ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>,
|
|
1304
|
+
composing: Combinator<unknown>,
|
|
1305
|
+
): void => {
|
|
1306
|
+
for (const [, rule] of ruleMap) {
|
|
1307
|
+
if (rule._meta.isTrivia) continue
|
|
1308
|
+
;(rule._meta as { grammarTrivia?: Combinator<unknown> }).grammarTrivia = composing
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/** Resolve one `compose([...])` argument to its RE-LOWERABLE carried items (IR
|
|
1313
|
+
* pieces / import spreads / baked-pieces fallback). The composing grammar's trivia
|
|
1314
|
+
* is applied later, uniformly, in materializeCarried — never per-piece here. */
|
|
1315
|
+
// A local rule map → its compact IR ({ns, ir}) for carrying; the trivia is NOT baked
|
|
1316
|
+
// into the IR (it is seeded at re-lower time). When the map can't be faithfully
|
|
1317
|
+
// serialized, fall back to full lowered pieces WITH the composing trivia baked in.
|
|
1318
|
+
const localCarried = (rm: Iterable<readonly [string, unknown]>, label: string, composing?: Combinator<unknown>, scanSkip?: Combinator<unknown>[]): { carried: CarriedItem[] } | null => {
|
|
1319
|
+
const entries = [...rm]
|
|
1320
|
+
const ns = nsFor(label)
|
|
1321
|
+
const trackLines = entries.some(([, rule]) => (rule as Combinator<unknown> | undefined)?._meta?.grammarTrackLines === true)
|
|
1322
|
+
// scanSkip is PER-PIECE (opaque units are dialect-specific — NOT composing-wins
|
|
1323
|
+
// like trivia), so it rides WITH this element's IR: it is emitted into the carried
|
|
1324
|
+
// `rules({ scanSkip }, …)` options, which stamps `_meta.grammarScanSkip` when the IR
|
|
1325
|
+
// is re-lowered (materializePiece → compileLinkable picks it up), and survives to a
|
|
1326
|
+
// downstream re-compose. The full-pieces fallback bakes it via the compile option.
|
|
1327
|
+
const ir = serializeRuleMap(entries as never, scanSkip)
|
|
1328
|
+
if (ir) return { carried: [{ ns, ir, ...(trackLines ? { trackLines: true as const } : {}) }] }
|
|
1329
|
+
// NO FULL-PIECES FALLBACK. It used to bake the grammar to lowered source when the IR
|
|
1330
|
+
// would not serialize; with one lowering there is nothing to bake to, and IR is not
|
|
1331
|
+
// an optimization here but the representation composition is built on.
|
|
1332
|
+
//
|
|
1333
|
+
// This is not a capability loss. The branch had no test fixture and not for want of
|
|
1334
|
+
// trying: every callback-source trigger in `serializeRuleMap` is pre-empted by the
|
|
1335
|
+
// macro's own stricter guard (a direct builder must be "macro-static and
|
|
1336
|
+
// self-contained", which throws first), and every "unsupported tag" trigger is a
|
|
1337
|
+
// ChoiceStrategy tag (`types.ts:405-408`), not a ParserDef tag, so it never reaches
|
|
1338
|
+
// that switch. It fired ZERO times across jess's whole five-package compose chain.
|
|
1339
|
+
// Refusing here leaves the runtime `compose()` in place, which is correct.
|
|
1340
|
+
warn(0, `${label}: rule map could not be serialized to IR, so it cannot be carried for composition. `
|
|
1341
|
+
+ 'Re-run with PARSEMAN_IR_DEBUG=1 to print the exact combinator that blocked serialization.')
|
|
1342
|
+
return null
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const argPieces = (arg: Expression, label: string, composing?: Combinator<unknown>): { carried: CarriedItem[]; importedFactories?: string[] } | null => {
|
|
1346
|
+
// Inline `rules(g => …)` or `rules({ trivia }, g => …)` (options-first). The
|
|
1347
|
+
// element's OWN trivia option is ignored for lowering — composing-wins means the
|
|
1348
|
+
// composing grammar's trivia (computed in compileComposeCall) governs every
|
|
1349
|
+
// fused rule, this element's included. It only matters as a CANDIDATE for the
|
|
1350
|
+
// composing trivia itself, which composingTrivia() reads directly off the AST.
|
|
1351
|
+
if (isRulesCall(arg)) {
|
|
1352
|
+
// This element's OWN scanSkip DOES thread (per-piece, unlike composing-wins
|
|
1353
|
+
// trivia): evaluateRulesFactory returns both the rule map and the grammar-level
|
|
1354
|
+
// scanSkip option, which localCarried carries with this piece's IR.
|
|
1355
|
+
const evaluated = evaluateRulesFactory(arg, label)
|
|
1356
|
+
const carried = evaluated ? localCarried(evaluated.ruleMap, label, composing, evaluated.scanSkip) : null
|
|
1357
|
+
return carried
|
|
1358
|
+
? { ...carried, ...(evaluated?.importedFactory ? { importedFactories: [evaluated.importedFactory] } : {}) }
|
|
1359
|
+
: null
|
|
1360
|
+
}
|
|
1361
|
+
if (arg.type === 'Identifier') {
|
|
1362
|
+
const name = (arg as unknown as { name: string }).name
|
|
1363
|
+
// Local grammar var (`const myRules = rules(...)`).
|
|
1364
|
+
const rm = localRuleMaps.get(name)
|
|
1365
|
+
if (rm) return localCarried(rm, label, composing, localGrammarScanSkip.get(name))
|
|
1366
|
+
// Local composed var (`const g = compose([...])`) → its own carried list, which
|
|
1367
|
+
// materializeCarried re-lowers under THIS compose's composing trivia.
|
|
1368
|
+
const composed = localComposedCarried.get(name)
|
|
1369
|
+
if (composed) return { carried: composed }
|
|
1370
|
+
// Imported grammar — carry a live spread of the imported binding (so we don't
|
|
1371
|
+
// re-serialize an ancestor); resolved + re-lowered in materializeCarried.
|
|
1372
|
+
const pieces = importedPieces(name)
|
|
1373
|
+
if (!pieces) return null
|
|
1374
|
+
return { carried: importBindings.has(name) ? [{ __spreadLocal: name }] : [...pieces] }
|
|
1375
|
+
}
|
|
1376
|
+
return null
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** The composing (outermost) grammar's trivia: the LAST array element that is a
|
|
1380
|
+
* plain grammar declaring `trivia` — an inline `rules({ trivia }, …)` (options-first
|
|
1381
|
+
* OR legacy trailing-options) or a local `const g = rules({ trivia }, …)`. Composed
|
|
1382
|
+
* sub-results and imported compiled grammars do NOT contribute (they carry no
|
|
1383
|
+
* re-surfaced trivia identity) — mirrors runtime composingTriviaOf skipping
|
|
1384
|
+
* COMPOSED_PIECES / already-compiled artifacts. */
|
|
1385
|
+
const rulesCallTrivia = (el: Expression): Combinator<unknown> | undefined => {
|
|
1386
|
+
const rulesArgs = (el as unknown as { arguments: unknown[] }).arguments
|
|
1387
|
+
const a0 = rulesArgs[0] as AnyNode | undefined
|
|
1388
|
+
const a1 = rulesArgs[1] as AnyNode | undefined
|
|
1389
|
+
const optExpr = (a0?.type === 'ObjectExpression' ? a0 : a1?.type === 'ObjectExpression' ? a1 : undefined) as AnyNode | undefined
|
|
1390
|
+
const triviaValue = optionProp(optExpr, 'trivia')
|
|
1391
|
+
if (!triviaValue) return undefined
|
|
1392
|
+
return (evaluateExpr(triviaValue, scope, code, []) as Combinator<unknown> | null) ?? undefined
|
|
1393
|
+
}
|
|
1394
|
+
const composingTrivia = (elements: ReadonlyArray<Expression | null>): Combinator<unknown> | undefined => {
|
|
1395
|
+
for (let i = elements.length - 1; i >= 0; i--) {
|
|
1396
|
+
const el = elements[i]
|
|
1397
|
+
if (!el) continue
|
|
1398
|
+
if (isRulesCall(el)) {
|
|
1399
|
+
const t = rulesCallTrivia(el)
|
|
1400
|
+
if (t) return t
|
|
1401
|
+
continue
|
|
1402
|
+
}
|
|
1403
|
+
if (el.type === 'Identifier') {
|
|
1404
|
+
const t = localGrammarTrivia.get((el as unknown as { name: string }).name)
|
|
1405
|
+
if (t) return t
|
|
1406
|
+
// local composed / imported compiled grammar → contributes rules, not trivia.
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return undefined
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/** Compile `compose([...])` to STATIC fused source (eval-free) + its carried
|
|
1413
|
+
* (re-lowerable) list (for a sidecar / same-file chaining). null → leave the
|
|
1414
|
+
* runtime `compose()` in place (correct, just not build-fused). */
|
|
1415
|
+
const compileComposeCall = (init: Expression): { replacement: string; exportedReplacement: string; carried: CarriedItem[]; trivia?: Combinator<unknown>; importedFactories?: string[]; coverageDefinitions?: readonly { id: string; kind: string }[] } | null => {
|
|
1416
|
+
const args = (init as unknown as { arguments: Expression[] }).arguments
|
|
1417
|
+
const arr = args[0]
|
|
1418
|
+
if (!arr || arr.type !== 'ArrayExpression') {
|
|
1419
|
+
warn(init.start, 'compose(): expected a static array of grammars/artifacts')
|
|
1420
|
+
return null
|
|
1421
|
+
}
|
|
1422
|
+
const elements = (arr as unknown as { elements: Expression[] }).elements
|
|
1423
|
+
// `compose(items, { hostMode })` — read and VALIDATE it, mirroring `rules()`. This
|
|
1424
|
+
// argument used to be ignored entirely: `args[1]` was never read, so a macro build of
|
|
1425
|
+
// `compose(items, { hostMode: 'cst' })` silently produced an 'ast' artifact while the
|
|
1426
|
+
// runtime path honoured the option. Silently is the operative word — the caller asked
|
|
1427
|
+
// for a CST artifact, got an eval-AST one, and the compatibility assertion then passed
|
|
1428
|
+
// because the artifact genuinely WAS 'ast'. Same vacuous-classification shape this
|
|
1429
|
+
// change exists to remove, one call site over.
|
|
1430
|
+
const cOptions = (init as unknown as { arguments: Expression[] }).arguments[1] as AnyNode | undefined
|
|
1431
|
+
const cHostModeValue = optionProp(cOptions, 'hostMode')
|
|
1432
|
+
const cHostMode = cHostModeValue?.type === 'Literal'
|
|
1433
|
+
? (cHostModeValue as unknown as { value?: unknown }).value
|
|
1434
|
+
: undefined
|
|
1435
|
+
if (cHostModeValue !== undefined && cHostMode !== 'ast' && cHostMode !== 'cst') {
|
|
1436
|
+
warn(init.start, "compose({ hostMode }) must be the literal 'ast' or 'cst'")
|
|
1437
|
+
return null
|
|
1438
|
+
}
|
|
1439
|
+
// Composing-wins (B): ONE composing trivia, from the last plain grammar in the
|
|
1440
|
+
// list that declares one, governs EVERY fused rule — including inherited ones.
|
|
1441
|
+
const composing = composingTrivia(elements)
|
|
1442
|
+
const carried: CarriedItem[] = [] // re-lowerable; also SERIALIZED onto the value
|
|
1443
|
+
const importedFactories: string[] = []
|
|
1444
|
+
for (let i = 0; i < elements.length; i++) {
|
|
1445
|
+
const r = argPieces(elements[i]!, `compose${init.start}_${i}`, composing)
|
|
1446
|
+
if (!r) { warn(init.start, `compose(): argument ${i} isn't a build-resolvable grammar; falling back to runtime`); return null }
|
|
1447
|
+
carried.push(...r.carried)
|
|
1448
|
+
importedFactories.push(...(r.importedFactories ?? []))
|
|
1449
|
+
}
|
|
1450
|
+
// Lower the whole list ONCE, seeding the composing trivia into every re-lowerable
|
|
1451
|
+
// piece (composing-wins), then fuse.
|
|
1452
|
+
// TABLE FIRST. The merged map IS the composed grammar (see `mergedCarriedRules`),
|
|
1453
|
+
// so `compose()` lowers through the SAME `compileRuleMap` a plain `rules()`
|
|
1454
|
+
// does — one encode, one `tableRules(…)` expression, no linker. `carried` is
|
|
1455
|
+
// unchanged either way: it is the re-lowerable IR list, not a lowering artifact,
|
|
1456
|
+
// so a downstream re-compose behaves identically whichever engine emitted here.
|
|
1457
|
+
const merged = mergedCarriedRules(carried)
|
|
1458
|
+
if (merged !== null) {
|
|
1459
|
+
if (composing) applyComposingTrivia(merged.rules, composing)
|
|
1460
|
+
const refusals: string[] = []
|
|
1461
|
+
const compiled = compileRuleMap(merged.rules, {
|
|
1462
|
+
...(composing ? { trivia: composing } : {}),
|
|
1463
|
+
...(merged.trackLines ? { trackLines: true } : {}),
|
|
1464
|
+
...(cHostMode ? { hostMode: cHostMode as HostMode } : {}),
|
|
1465
|
+
recovery,
|
|
1466
|
+
coverage: grammarCoverage,
|
|
1467
|
+
refusals,
|
|
1468
|
+
})
|
|
1469
|
+
if (compiled !== null) {
|
|
1470
|
+
usedTableRuntime = true
|
|
1471
|
+
const reflectionMetadata = staticTableMetadataSource({ reflection: compiled.reflection })
|
|
1472
|
+
const exportedMetadata = staticTableMetadataSource({
|
|
1473
|
+
carried,
|
|
1474
|
+
reflection: compiled.reflection,
|
|
1475
|
+
})
|
|
1476
|
+
return {
|
|
1477
|
+
replacement: compiled.replacementWithMetadata(reflectionMetadata),
|
|
1478
|
+
exportedReplacement: compiled.replacementWithMetadata(exportedMetadata),
|
|
1479
|
+
carried,
|
|
1480
|
+
...(composing ? { trivia: composing } : {}),
|
|
1481
|
+
...(importedFactories.length ? { importedFactories } : {}),
|
|
1482
|
+
// The AUTHORITATIVE denominator, from `buildGrammarPlan` via
|
|
1483
|
+
// `compileRuleMapTable`. It was computed here and dropped, leaving the
|
|
1484
|
+
// compose() call site with nothing but the regex scrape — see the call site.
|
|
1485
|
+
...(compiled.coverageDefinitions ? { coverageDefinitions: compiled.coverageDefinitions } : {}),
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
// There is no second lowering to fall back TO. Leaving the runtime `compose()`
|
|
1489
|
+
// call in place is correct (the runtime path fuses the same merged map), just not
|
|
1490
|
+
// build-fused — so name the reason rather than reporting a bare refusal.
|
|
1491
|
+
warn(init.start, `compose(): could not be lowered to a table; leaving the runtime compose() in place${reasonSuffix(refusals)}`)
|
|
1492
|
+
return null
|
|
1493
|
+
}
|
|
1494
|
+
warn(init.start, 'compose(): a carried piece has no re-lowerable IR; leaving the runtime compose() in place')
|
|
1495
|
+
return null
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Compile a terminal composition. Imported/base pieces still travel as normal
|
|
1500
|
+
* re-lowerable IR, but the final local rules map is compiled directly in this
|
|
1501
|
+
* module. Its direct builders may therefore refer to lexical AST constructors;
|
|
1502
|
+
* they are inlined into the fused output and are never serialized or carried.
|
|
1503
|
+
*/
|
|
1504
|
+
const compileComposeLeafCall = (init: Expression): { replacement: string; importedFactories?: string[] } | null => {
|
|
1505
|
+
const args = (init as unknown as { arguments: Expression[] }).arguments
|
|
1506
|
+
const arr = args[0]
|
|
1507
|
+
if (!arr || arr.type !== 'ArrayExpression') {
|
|
1508
|
+
warn(init.start, 'composeLeaf(): expected a static array of grammars')
|
|
1509
|
+
return null
|
|
1510
|
+
}
|
|
1511
|
+
const elements = (arr as unknown as { elements: Expression[] }).elements
|
|
1512
|
+
if (elements.length < 2) {
|
|
1513
|
+
warn(init.start, 'composeLeaf(): needs imported recognition grammar(s) and one local rules() map')
|
|
1514
|
+
return null
|
|
1515
|
+
}
|
|
1516
|
+
const localArg = elements[elements.length - 1]!
|
|
1517
|
+
let localRules: Iterable<readonly [string, unknown]> | null = null
|
|
1518
|
+
// The LOCAL leaf grammar's own ambient scanSkip (from its `rules({ scanSkip })`)
|
|
1519
|
+
// governs the scanTo/balanced sites IT defines — unlike trivia (composing-wins),
|
|
1520
|
+
// opaque units are dialect-specific, so the local declaration is threaded to the
|
|
1521
|
+
// local piece's compile.
|
|
1522
|
+
let localScanSkip: Combinator<unknown>[] | undefined
|
|
1523
|
+
const importedFactories: string[] = []
|
|
1524
|
+
if (isRulesCall(localArg)) {
|
|
1525
|
+
const evaluated = evaluateRulesFactory(localArg, `composeLeaf${init.start}`)
|
|
1526
|
+
localRules = evaluated?.ruleMap ?? null
|
|
1527
|
+
localScanSkip = evaluated?.scanSkip
|
|
1528
|
+
if (evaluated?.importedFactory) importedFactories.push(evaluated.importedFactory)
|
|
1529
|
+
} else if (localArg.type === 'Identifier') {
|
|
1530
|
+
const name = (localArg as unknown as { name: string }).name
|
|
1531
|
+
localRules = localRuleMaps.get(name) ?? null
|
|
1532
|
+
localScanSkip = localGrammarScanSkip.get(name)
|
|
1533
|
+
}
|
|
1534
|
+
if (!localRules) {
|
|
1535
|
+
// Distinguish "the argument is the wrong SHAPE" from "the argument is the right
|
|
1536
|
+
// shape and failed to evaluate". Only the first is a composeLeaf problem. When a
|
|
1537
|
+
// `rules()` call is present it has already warned with the specific cause (a
|
|
1538
|
+
// forward reference, a non-static callback); repeating a shape complaint here
|
|
1539
|
+
// buries it and sends the reader looking at the composeLeaf argument list. That
|
|
1540
|
+
// is exactly how this signature got reported twice as a grammar defect.
|
|
1541
|
+
const named = localArg.type === 'Identifier' ? (localArg as unknown as { name: string }).name : null
|
|
1542
|
+
warn(init.start, isRulesCall(localArg)
|
|
1543
|
+
? 'composeLeaf(): the final rules() map failed to evaluate — see the warning above for the cause'
|
|
1544
|
+
: named !== null
|
|
1545
|
+
? `composeLeaf(): final argument \`${named}\` did not resolve to a local rules() map.`
|
|
1546
|
+
+ ` It must be a \`const ${named} = rules(...)\` in THIS module, declared before this call`
|
|
1547
|
+
+ ` — an imported or re-exported grammar cannot be the leaf.`
|
|
1548
|
+
: 'composeLeaf(): final argument must be a local rules() map')
|
|
1549
|
+
return null
|
|
1550
|
+
}
|
|
1551
|
+
const composing = composingTrivia(elements)
|
|
1552
|
+
const carried: CarriedItem[] = []
|
|
1553
|
+
for (let i = 0; i < elements.length - 1; i++) {
|
|
1554
|
+
const r = argPieces(elements[i]!, `composeLeaf${init.start}_${i}`, composing)
|
|
1555
|
+
if (!r) {
|
|
1556
|
+
warn(init.start, `composeLeaf(): argument ${i} isn't a build-resolvable recognition grammar`)
|
|
1557
|
+
return null
|
|
1558
|
+
}
|
|
1559
|
+
carried.push(...r.carried)
|
|
1560
|
+
importedFactories.push(...(r.importedFactories ?? []))
|
|
1561
|
+
}
|
|
1562
|
+
try {
|
|
1563
|
+
// The imported pieces are recognition-only, but the local leaf grammar
|
|
1564
|
+
// may place one beneath a direct node(). Re-lower their terminals with
|
|
1565
|
+
// capture enabled so that node receives the imported token values in its
|
|
1566
|
+
// normal child collector; the pieces still contain no semantic callback.
|
|
1567
|
+
const localNs = nsFor(`composeLeaf${init.start}`)
|
|
1568
|
+
const localEntries = [...localRules] as Array<[string, Combinator<unknown>]>
|
|
1569
|
+
|
|
1570
|
+
// TABLE FIRST — same merge as `compose()`, with the local leaf map appended LAST
|
|
1571
|
+
// so it wins every name and binds the imported shapes' holes (`enc.winners`
|
|
1572
|
+
// resolves those by name; see `mergedCarriedRules`).
|
|
1573
|
+
//
|
|
1574
|
+
// The recognition-only gate is preserved EXACTLY, and is the reason this path
|
|
1575
|
+
// needs no `compileLinkable`: `hasDirectBuilders` / `isRecognitionOnly` are
|
|
1576
|
+
// predicates over the combinator graph, not products of lowering it, so
|
|
1577
|
+
// `classifyRuleMap` answers both from the piece's own rule map. Lowering every
|
|
1578
|
+
// piece to read two booleans off `LinkablePieces` was the only thing making this
|
|
1579
|
+
// gate look codegen-shaped.
|
|
1580
|
+
//
|
|
1581
|
+
// Coverage needs no `materializeLeafCoverage` counterpart here. That helper
|
|
1582
|
+
// exists because coverage ids are WeakMap-keyed and planning from a second IR
|
|
1583
|
+
// hydration would leave the EMITTED pieces uninstrumented; the table plans from
|
|
1584
|
+
// the merged map it then encodes, so the planned identities and the encoded
|
|
1585
|
+
// identities are the same objects by construction.
|
|
1586
|
+
const carriedMaps = carriedRuleMaps(carried)
|
|
1587
|
+
if (carriedMaps !== null) {
|
|
1588
|
+
const unproven = carriedMaps.pieces.find(p => {
|
|
1589
|
+
const c = classifyRuleMap(p.rules)
|
|
1590
|
+
return c.hasDirectBuilders || !c.isRecognitionOnly
|
|
1591
|
+
})
|
|
1592
|
+
if (unproven) {
|
|
1593
|
+
warn(init.start, 'composeLeaf(): every pre-final grammar must explicitly prove recognition-only')
|
|
1594
|
+
return null
|
|
1595
|
+
}
|
|
1596
|
+
// The local grammar's OWN ambient scanSkip is stamped onto the LOCAL entries
|
|
1597
|
+
// only. Passing it as a merged-map option instead would let `applyAmbient`
|
|
1598
|
+
// hand it to every imported rule that happens to carry no stamp of its own —
|
|
1599
|
+
// opaque units are dialect-specific, and that is precisely the leak the
|
|
1600
|
+
// per-piece threading in the source path exists to avoid.
|
|
1601
|
+
if (localScanSkip) {
|
|
1602
|
+
for (const [, rule] of localEntries) {
|
|
1603
|
+
if (rule._meta.isTrivia) continue
|
|
1604
|
+
const meta = rule._meta as { grammarScanSkip?: Combinator<unknown>[] }
|
|
1605
|
+
if (meta.grammarScanSkip === undefined) meta.grammarScanSkip = localScanSkip
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const leafMerged = mergeRuleMaps([...carriedMaps.pieces.map(p => p.rules), localEntries])
|
|
1609
|
+
// Composing-wins governs the leaf fuse too. The local entries are NOT freshly
|
|
1610
|
+
// evaluated (they come from this module's own `rules()` factory), so they are
|
|
1611
|
+
// excluded from the override — their declared trivia IS the composing candidate,
|
|
1612
|
+
// and rewriting `_meta` on them would mutate the module-level grammar object.
|
|
1613
|
+
if (composing) applyComposingTrivia(carriedMaps.pieces.flatMap(p => p.rules), composing)
|
|
1614
|
+
const refusals: string[] = []
|
|
1615
|
+
const compiled = compileRuleMap(
|
|
1616
|
+
leafMerged,
|
|
1617
|
+
{
|
|
1618
|
+
...(composing ? { trivia: composing } : {}),
|
|
1619
|
+
...(carriedMaps.trackLines ? { trackLines: true } : {}),
|
|
1620
|
+
recovery,
|
|
1621
|
+
coverage: grammarCoverage,
|
|
1622
|
+
refusals,
|
|
1623
|
+
},
|
|
1624
|
+
)
|
|
1625
|
+
if (compiled !== null) {
|
|
1626
|
+
usedTableRuntime = true
|
|
1627
|
+
return {
|
|
1628
|
+
replacement: compiled.replacementWithMetadata(staticTableMetadataSource({
|
|
1629
|
+
reflection: compiled.reflection,
|
|
1630
|
+
leaf: true,
|
|
1631
|
+
})),
|
|
1632
|
+
...(importedFactories.length ? { importedFactories } : {}),
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
warn(init.start, `composeLeaf(): could not be lowered to a table${reasonSuffix(refusals)}`)
|
|
1636
|
+
return null
|
|
1637
|
+
}
|
|
1638
|
+
// `composeLeaf` is TERMINAL and macro-only: there is no runtime composition to
|
|
1639
|
+
// fall back to (the caller turns this null into a hard throw), and now no second
|
|
1640
|
+
// lowering either. A piece with no re-lowerable IR is the end of the road.
|
|
1641
|
+
warn(init.start, 'composeLeaf(): a recognition piece has no re-lowerable IR')
|
|
1642
|
+
return null
|
|
1643
|
+
} catch (e) {
|
|
1644
|
+
warn(init.start, `composeLeaf(): ${(e as Error).message}`)
|
|
1645
|
+
return null
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// --- Pre-pass: resolve standalone ref() recursion clusters ---
|
|
1650
|
+
// `const x = ref()` … `x.define(expr)` is the interpreter/compile() recursion
|
|
1651
|
+
// mechanism. The macro must support it for parity: evaluate every ref slot and
|
|
1652
|
+
// apply every `.define(...)` into scope BEFORE the main loop compiles anything,
|
|
1653
|
+
// so codegen's emitLazy sees a defined thunk (otherwise it falls back to the
|
|
1654
|
+
// interpreter). `.define(...)` statements are stripped from the output since
|
|
1655
|
+
// they reference the now-removed import. Only a ref whose define resolves is
|
|
1656
|
+
// treated as a macro ref; an unresolved one falls through to the normal warn().
|
|
1657
|
+
// First detect whether any ref() cluster exists; only then do the (more
|
|
1658
|
+
// involved) full-scope pre-pass, keeping ordinary macro files on the fast path.
|
|
1659
|
+
const refNames = new Set<string>()
|
|
1660
|
+
for (const stmt of body as Statement[]) {
|
|
1661
|
+
const innerVd = unwrapVd(stmt)
|
|
1662
|
+
if (!innerVd) continue
|
|
1663
|
+
for (const decl of innerVd.declarations) {
|
|
1664
|
+
const d = decl as VariableDeclarator
|
|
1665
|
+
if (!d.init || (d.id as unknown as { type: string }).type !== 'Identifier') continue
|
|
1666
|
+
const init = d.init as Expression
|
|
1667
|
+
if (init.type === 'CallExpression'
|
|
1668
|
+
&& (init as unknown as { callee: { type: string; name?: string } }).callee.type === 'Identifier'
|
|
1669
|
+
&& (init as unknown as { callee: { name?: string } }).callee.name === 'ref') {
|
|
1670
|
+
refNames.add((d.id as unknown as { name: string }).name)
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// For a ref cluster we must fully populate scope (refs AND the regular consts a
|
|
1676
|
+
// `.define(expr)` references) in source order, so each define resolves before
|
|
1677
|
+
// the main loop compiles. `.define(...)` statements are stripped from the
|
|
1678
|
+
// output since they reference the now-removed import. A ref whose define never
|
|
1679
|
+
// resolves falls through to the normal warn() path in the main loop.
|
|
1680
|
+
const defineRemovals: Array<{ start: number; end: number }> = []
|
|
1681
|
+
if (refNames.size > 0) {
|
|
1682
|
+
for (const stmt of body as Statement[]) {
|
|
1683
|
+
if (stmt.type === 'ExpressionStatement') {
|
|
1684
|
+
const expr = (stmt as unknown as { expression: Expression }).expression
|
|
1685
|
+
if (applyDefineStatement(expr, scope, code)) {
|
|
1686
|
+
defineRemovals.push({ start: stmt.start, end: stmt.end })
|
|
1687
|
+
}
|
|
1688
|
+
continue
|
|
1689
|
+
}
|
|
1690
|
+
const innerVd = unwrapVd(stmt)
|
|
1691
|
+
if (!innerVd) continue
|
|
1692
|
+
for (const decl of innerVd.declarations) {
|
|
1693
|
+
const d = decl as VariableDeclarator
|
|
1694
|
+
if (!d.init || (d.id as unknown as { type: string }).type !== 'Identifier') continue
|
|
1695
|
+
const name = (d.id as unknown as { name: string }).name
|
|
1696
|
+
const init = d.init as Expression
|
|
1697
|
+
if (evaluateRefDeclaration(init, name, scope)) continue
|
|
1698
|
+
if (scope.has(name)) continue
|
|
1699
|
+
if (!referencesAny(init, allNames, scope)) continue
|
|
1700
|
+
// Evaluate into scope so a subsequent `.define()` can reference it. The
|
|
1701
|
+
// main loop re-evaluates and compiles; refs stay shared, so this is safe.
|
|
1702
|
+
const combi = evaluateExpr(init, scope, code, [])
|
|
1703
|
+
if (combi) scope.set(name, { combi, mfSrcs: [] })
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
for (const stmt of body as Statement[]) {
|
|
1709
|
+
// Handle both direct VariableDeclarations and exported ones
|
|
1710
|
+
let vd: VariableDeclaration | null = null
|
|
1711
|
+
let stmtStart = stmt.start
|
|
1712
|
+
let stmtEnd = stmt.end
|
|
1713
|
+
let exportPrefix = ''
|
|
1714
|
+
|
|
1715
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
1716
|
+
vd = stmt as unknown as VariableDeclaration
|
|
1717
|
+
} else if (stmt.type === 'ExportNamedDeclaration') {
|
|
1718
|
+
const expStmt = stmt as unknown as ExportNamedDeclaration
|
|
1719
|
+
if (expStmt.declaration?.type === 'VariableDeclaration') {
|
|
1720
|
+
vd = expStmt.declaration as unknown as VariableDeclaration
|
|
1721
|
+
exportPrefix = 'export '
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (!vd) continue
|
|
1726
|
+
|
|
1727
|
+
for (const decl of vd.declarations) {
|
|
1728
|
+
const d = decl as VariableDeclarator
|
|
1729
|
+
if (!d.init) continue
|
|
1730
|
+
const init = d.init as Expression
|
|
1731
|
+
const kind = (vd as unknown as { kind: string }).kind ?? 'const'
|
|
1732
|
+
|
|
1733
|
+
if ((d.id as unknown as { type: string }).type === 'Identifier') {
|
|
1734
|
+
// ── Simple binding: const name = <expr> ──────────────────────────
|
|
1735
|
+
const varName = (d.id as unknown as { name: string }).name
|
|
1736
|
+
if (!referencesAny(init, allNames, scope)) {
|
|
1737
|
+
const staticValue = evaluateStaticValue(init, scope, code)
|
|
1738
|
+
if (staticValue !== null && staticValue !== undefined || isStaticNullishExpression(init)) {
|
|
1739
|
+
;(scope as Map<string, unknown>).set(varName, staticValue)
|
|
1740
|
+
}
|
|
1741
|
+
continue
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// const name = ref() — resolved by the pre-pass. Compile the (now
|
|
1745
|
+
// defined) ref combinator in place; codegen inlines the whole recursive
|
|
1746
|
+
// cluster behind a named function. The `.define(...)` statements are
|
|
1747
|
+
// removed separately.
|
|
1748
|
+
if (refNames.has(varName)) {
|
|
1749
|
+
const refEntry = scope.get(varName)
|
|
1750
|
+
const refCombi = refEntry?.combi ?? null
|
|
1751
|
+
if (refCombi) {
|
|
1752
|
+
// `mfSrcs` is the POSITIONAL fallback the evaluator collected while it
|
|
1753
|
+
// built this combinator. The encoder's def-carried sources win; this only
|
|
1754
|
+
// fills holes. Passing `undefined` here is what made a ref()'s reducers
|
|
1755
|
+
// print as `() => {}`.
|
|
1756
|
+
const compiled = compile(refCombi, refEntry?.mfSrcs, { recovery, coverage: grammarCoverage })
|
|
1757
|
+
if (compiled.inlineExpression === null) {
|
|
1758
|
+
warn(init.start, `"${varName}" is a ref() that couldn't be inlined (was .define() called with a static combinator?)`
|
|
1759
|
+
+ reasonSuffix(compiled.runtimeOnly))
|
|
1760
|
+
continue
|
|
1761
|
+
}
|
|
1762
|
+
usedTableRuntime = true
|
|
1763
|
+
replacements.push({ start: init.start, end: init.end, replacement: compiled.inlineExpression })
|
|
1764
|
+
continue
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// const name = rules(factory) → the ONE shared compiled-rule-map
|
|
1769
|
+
// expression, so `name.RuleX(...)` resolves to the compiled function
|
|
1770
|
+
// at runtime (the map's own values are already plain functions).
|
|
1771
|
+
if (isRulesCall(init)) {
|
|
1772
|
+
const compiledRules = compileRulesFactory(init, varName)
|
|
1773
|
+
if (!compiledRules) continue
|
|
1774
|
+
// Remember the rule map so a same-file `compose([varName, …])` can fuse it,
|
|
1775
|
+
// and its grammar-level trivia so that compose can adopt it (composing-wins).
|
|
1776
|
+
localRuleMaps.set(varName, compiledRules.ruleMap)
|
|
1777
|
+
if (compiledRules.trivia) localGrammarTrivia.set(varName, compiledRules.trivia)
|
|
1778
|
+
if (compiledRules.scanSkip) localGrammarScanSkip.set(varName, compiledRules.scanSkip)
|
|
1779
|
+
// Carry the grammar's linkable pieces ON the value so a downstream package
|
|
1780
|
+
// composes it via `import { <name> }` alone. Needed when EXPORTED — and
|
|
1781
|
+
// computed unconditionally for a SHARED SHAPE, because the pieces are the
|
|
1782
|
+
// proof that the shape really did compile (see below).
|
|
1783
|
+
//
|
|
1784
|
+
// Thread `scanSkip` explicitly: it is what a downstream package composes.
|
|
1785
|
+
// (The `_meta` stamp in evaluateRulesFactory also covers it; passing it
|
|
1786
|
+
// here keeps the intent local and independent of that.) `trivia` is NOT
|
|
1787
|
+
// threaded — it is composing-wins, so the downstream compose supplies it;
|
|
1788
|
+
// `scanSkip` is per-piece (opaque units are dialect-specific) and must
|
|
1789
|
+
// travel WITH the grammar or the downstream loses ambient skipping.
|
|
1790
|
+
//
|
|
1791
|
+
// `compileLinkableTable` is the piece-artifact producer — the job this call
|
|
1792
|
+
// site actually has. It reports `external` as a first-class field, which is
|
|
1793
|
+
// what the SHARED-SHAPE check below needs, and it encodes a piece WITH holes
|
|
1794
|
+
// to `prog: null` while keeping its IR, so a hole is a described state rather
|
|
1795
|
+
// than a refusal.
|
|
1796
|
+
const ns = nsFor(varName)
|
|
1797
|
+
const pieces = exportPrefix || compiledRules.replacement === null
|
|
1798
|
+
? compileLinkableTable([...compiledRules.ruleMap], ns, {
|
|
1799
|
+
...(compiledRules.scanSkip ? { scanSkip: compiledRules.scanSkip } : {}),
|
|
1800
|
+
...(compiledRules.trackLines ? { trackLines: true } : {}),
|
|
1801
|
+
recovery,
|
|
1802
|
+
})
|
|
1803
|
+
: null
|
|
1804
|
+
// A SHARED SHAPE — a map with an unresolved external `g.` ref, e.g.
|
|
1805
|
+
// `Ratio: sequence(g.Value, literal('/'), g.Value)` where the consuming
|
|
1806
|
+
// dialect defines `Value`. It can't be inlined as a standalone parser (the
|
|
1807
|
+
// hole has no body here), so its runtime value stays the `rules(…)` call —
|
|
1808
|
+
// the interpreter map, which is the only correct standalone value for a
|
|
1809
|
+
// grammar with a hole. `compileLinkable` DOES compile it (it emits a by-name
|
|
1810
|
+
// `_r_<Name>` call for each hole, bound at fuse time), so a downstream
|
|
1811
|
+
// `compose()` / `composeLeaf()` still fully macro-fuses it. Nothing failed,
|
|
1812
|
+
// so this doesn't warn — it only pins the macro import, since the emitted
|
|
1813
|
+
// source still calls `rules`. Requiring `pieces` here is what keeps this
|
|
1814
|
+
// from swallowing a map that failed to inline for some OTHER reason and
|
|
1815
|
+
// merely happens to also reference an external rule.
|
|
1816
|
+
if (compiledRules.replacement === null) {
|
|
1817
|
+
if (!pieces || pieces.external.length === 0) {
|
|
1818
|
+
warn(init.start, `${varName}: rule map couldn't be inlined` + reasonSuffix(compiledRules.refusals))
|
|
1819
|
+
// THE WHOLE MODULE FALLS BACK, for the same reason a runtime `compose()`
|
|
1820
|
+
// does. This map keeps its `rules(…)` source and is therefore built by the
|
|
1821
|
+
// INTERPRETER, from combinators — but its ambient trivia is an ordinary
|
|
1822
|
+
// declaration that this pass may already have lowered to a compiled rule
|
|
1823
|
+
// function. Mixing the two hands `rules()` a function where it needs a
|
|
1824
|
+
// combinator, and it throws walking it for reflection. Partial lowering is
|
|
1825
|
+
// only ever safe when every consumer of a lowered declaration also lowered.
|
|
1826
|
+
runtimeComposeFallback = true
|
|
1827
|
+
continue
|
|
1828
|
+
}
|
|
1829
|
+
keepMacroImport = true
|
|
1830
|
+
}
|
|
1831
|
+
const source = compiledRules.replacement ?? code.slice(init.start, init.end)
|
|
1832
|
+
// Carry only when the pieces are fully static (no runtime-only callbacks) —
|
|
1833
|
+
// otherwise the grammar isn't source-free composable and we ship it as a
|
|
1834
|
+
// plain map.
|
|
1835
|
+
let carriedMetadata: CarriedItem[] | undefined
|
|
1836
|
+
if (exportPrefix && pieces) {
|
|
1837
|
+
// Carry the compact IR. Thread the grammar's scanSkip into it so a
|
|
1838
|
+
// downstream compose of this imported grammar re-lowers its
|
|
1839
|
+
// scanTo/balanced sites ambiently.
|
|
1840
|
+
//
|
|
1841
|
+
// IR OR NOTHING. The old alternative was to carry the fully lowered pieces,
|
|
1842
|
+
// which a downstream compose could link but never RE-LOWER — so it silently
|
|
1843
|
+
// froze that grammar's trivia against any later composing-wins. With one
|
|
1844
|
+
// lowering there is no such artifact, and a grammar that cannot serialize
|
|
1845
|
+
// simply is not carried: an absent carried list makes the downstream compose
|
|
1846
|
+
// say so, where a frozen one did not.
|
|
1847
|
+
const ir = serializeRuleMap([...compiledRules.ruleMap] as never, compiledRules.scanSkip)
|
|
1848
|
+
if (ir) carriedMetadata = [{ ns, ir, ...(compiledRules.trackLines ? { trackLines: true as const } : {}) }]
|
|
1849
|
+
// NOT SILENT. An export with no carried IR cannot be composed downstream at
|
|
1850
|
+
// all — the consumer's `compose()` will fall back to runtime and say so, but
|
|
1851
|
+
// by then the cause is a module away. The source lowering carried fully
|
|
1852
|
+
// lowered pieces here instead; the table has no such form, because merging
|
|
1853
|
+
// two ALREADY-ENCODED programs means relocating code offsets and merging
|
|
1854
|
+
// every pool, which is the one route `compile-linkable-table.ts` documents
|
|
1855
|
+
// as deferred. Naming it at the origin is the least this can do.
|
|
1856
|
+
else warn(init.start, `${varName}: exported grammar could not be serialized to IR, so it carries no `
|
|
1857
|
+
+ 'composable pieces — a downstream compose() of this grammar will fall back to runtime. '
|
|
1858
|
+
+ 'Re-run with PARSEMAN_IR_DEBUG=1 to print the combinator that blocked serialization.')
|
|
1859
|
+
}
|
|
1860
|
+
const replacement = compiledRules.replacement !== null
|
|
1861
|
+
? compiledRules.replacementWithMetadata!(staticTableMetadataSource({
|
|
1862
|
+
...(carriedMetadata === undefined ? {} : { carried: carriedMetadata }),
|
|
1863
|
+
reflection: compiledRules.reflection,
|
|
1864
|
+
}))
|
|
1865
|
+
: (() => {
|
|
1866
|
+
const covered = withCoverageDefinitions(
|
|
1867
|
+
source,
|
|
1868
|
+
compiledRules.coverageDefinitions?.length
|
|
1869
|
+
? compiledRules.coverageDefinitions
|
|
1870
|
+
: emittedCoverageDefinitions(source, `${id} rules()`),
|
|
1871
|
+
)
|
|
1872
|
+
return carriedMetadata === undefined ? covered : withCarriedPieces(covered, carriedMetadata)
|
|
1873
|
+
})()
|
|
1874
|
+
replacements.push({ start: init.start, end: init.end, replacement })
|
|
1875
|
+
if (compiledRules.replacement !== null) {
|
|
1876
|
+
markUsedImportedFactories(compiledRules.importedFactory ? [compiledRules.importedFactory] : undefined)
|
|
1877
|
+
}
|
|
1878
|
+
continue
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// const name = compose([...]) → STATIC fused source (eval-free); the macro
|
|
1882
|
+
// fuses at build, never emitting `new Function`.
|
|
1883
|
+
if (isComposeCall(init)) {
|
|
1884
|
+
const fused = compileComposeCall(init)
|
|
1885
|
+
if (!fused) {
|
|
1886
|
+
runtimeComposeFallback = true
|
|
1887
|
+
continue // leave the runtime compose() call in place
|
|
1888
|
+
}
|
|
1889
|
+
// Remember for a same-file downstream compose, and (if exported) carry the
|
|
1890
|
+
// re-lowerable list on the value so another package can compose this composed
|
|
1891
|
+
// grammar via `import { <name> }` (re-composition, no source) and re-lower it
|
|
1892
|
+
// under ITS composing trivia.
|
|
1893
|
+
localComposedCarried.set(varName, fused.carried)
|
|
1894
|
+
const replacement = exportPrefix ? fused.exportedReplacement : fused.replacement
|
|
1895
|
+
// Coverage definitions already ride in the table program's `cv` pool;
|
|
1896
|
+
// `tableRules` exposes that same pool through the metadata prototype.
|
|
1897
|
+
replacements.push({ start: init.start, end: init.end, replacement })
|
|
1898
|
+
markUsedImportedFactories(fused.importedFactories)
|
|
1899
|
+
continue
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// `composeLeaf([...recognition, localRules])` is static and terminal:
|
|
1903
|
+
// local direct builders stay lexical and are not carried/recomposable.
|
|
1904
|
+
if (isComposeLeafCall(init)) {
|
|
1905
|
+
const fused = compileComposeLeafCall(init)
|
|
1906
|
+
if (!fused) {
|
|
1907
|
+
const reasons = warnings.slice(-4)
|
|
1908
|
+
throw new Error(
|
|
1909
|
+
`${id}:${lineOf(init.start)} — composeLeaf() must macro-fuse; runtime composition is forbidden`
|
|
1910
|
+
+ (reasons.length ? `\n causes:\n - ${reasons.join('\n - ')}` : ''),
|
|
1911
|
+
)
|
|
1912
|
+
}
|
|
1913
|
+
replacements.push({ start: init.start, end: init.end, replacement: fused.replacement })
|
|
1914
|
+
markUsedImportedFactories(fused.importedFactories)
|
|
1915
|
+
continue
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const mapFnSources: string[] = []
|
|
1919
|
+
const parser = evaluateExpr(init, scope, code, mapFnSources)
|
|
1920
|
+
if (parser === null) {
|
|
1921
|
+
const wordFactory = evaluateWordFactory(init, scope, code)
|
|
1922
|
+
if (wordFactory) {
|
|
1923
|
+
;(scope as Map<string, unknown>).set(varName, wordFactory)
|
|
1924
|
+
replacements.push({ start: init.start, end: init.end, replacement: 'undefined' })
|
|
1925
|
+
continue
|
|
1926
|
+
}
|
|
1927
|
+
const whenFactory = evaluateWhenFactory(init, scope, code)
|
|
1928
|
+
if (whenFactory) {
|
|
1929
|
+
;(scope as Map<string, unknown>).set(varName, whenFactory)
|
|
1930
|
+
replacements.push({ start: init.start, end: init.end, replacement: 'undefined' })
|
|
1931
|
+
continue
|
|
1932
|
+
}
|
|
1933
|
+
// A shared array-of-combinators const (e.g. a reusable `skip` set) — store
|
|
1934
|
+
// it in scope so `{ skip: name }` on a later scanTo/balanced resolves the
|
|
1935
|
+
// array. Left in the emitted output; it references other emitted consts.
|
|
1936
|
+
const combiArray = evaluateCombinatorArray(init, scope, code)
|
|
1937
|
+
if (combiArray) {
|
|
1938
|
+
;(scope as Map<string, unknown>).set(varName, combiArray)
|
|
1939
|
+
continue
|
|
1940
|
+
}
|
|
1941
|
+
const staticValue = evaluateStaticValue(init, scope, code)
|
|
1942
|
+
if (staticValue !== null && staticValue !== undefined || isStaticNullishExpression(init)) {
|
|
1943
|
+
;(scope as Map<string, unknown>).set(varName, staticValue)
|
|
1944
|
+
continue
|
|
1945
|
+
}
|
|
1946
|
+
// A `rules()` FACTORY is not a combinator and never was one — it is a function
|
|
1947
|
+
// that returns the rule map. It only reaches this branch now that a factory can
|
|
1948
|
+
// be shared by name (see `factoryDecls`), and warning here would tell the author
|
|
1949
|
+
// to "simplify" the one declaration the two-artifact pattern requires.
|
|
1950
|
+
//
|
|
1951
|
+
// Its text is left VERBATIM, which is fine for a local `const` — nothing
|
|
1952
|
+
// references it after lowering, so it is dead code the bundler drops. An
|
|
1953
|
+
// EXPORTED one cannot be dropped, so the macro-only identifiers in its body
|
|
1954
|
+
// (`node`, `sequence`, `literal`, …) survive into the artifact with nothing
|
|
1955
|
+
// binding them once the macro import is removed. Record it; whether that is
|
|
1956
|
+
// actually fatal depends on whether the import IS removed, which is only
|
|
1957
|
+
// decided once every declaration has been seen.
|
|
1958
|
+
if (factoryDecls.has(varName)) {
|
|
1959
|
+
if (exportPrefix) exportedFactories.push({ name: varName, pos: init.start })
|
|
1960
|
+
continue
|
|
1961
|
+
}
|
|
1962
|
+
warn(init.start, `"${varName}" references a parseman macro import but isn't a statically-evaluable combinator`)
|
|
1963
|
+
continue
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// Sources are carried on each transform's def (set by the evaluator) AND
|
|
1967
|
+
// collected positionally into `mapFnSources`. Codegen only needed the former;
|
|
1968
|
+
// the TABLE ENCODER needs whichever is present, so hand it both — the def
|
|
1969
|
+
// sources win and the positional list fills holes. Passing `undefined` here
|
|
1970
|
+
// is what made every macro-lowered reducer print as `() => {}`.
|
|
1971
|
+
const compiled = compile(parser, mapFnSources, { recovery, coverage: grammarCoverage })
|
|
1972
|
+
if (compiled.inlineExpression === null) {
|
|
1973
|
+
warn(init.start, `"${varName}" couldn't be inlined (likely closes over a runtime value)`
|
|
1974
|
+
+ reasonSuffix(compiled.runtimeOnly))
|
|
1975
|
+
continue
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
usedTableRuntime = true
|
|
1979
|
+
replacements.push({
|
|
1980
|
+
start: init.start,
|
|
1981
|
+
end: init.end,
|
|
1982
|
+
replacement: compiled.inlineExpression,
|
|
1983
|
+
})
|
|
1984
|
+
|
|
1985
|
+
// Store enriched scope entry so factories can replay mfSrcs
|
|
1986
|
+
scope.set(varName, { combi: parser, mfSrcs: mapFnSources })
|
|
1987
|
+
|
|
1988
|
+
} else if ((d.id as unknown as { type: string }).type === 'ObjectPattern') {
|
|
1989
|
+
// ── Destructured binding: const { a, b } = rules(g => { ... }) ──
|
|
1990
|
+
// Only handle rules() factory calls
|
|
1991
|
+
if (!referencesAny(init, allNames, scope)) continue
|
|
1992
|
+
if (!isRulesCall(init)) {
|
|
1993
|
+
warn(init.start, `destructured macro binding must come from rules(...)`)
|
|
1994
|
+
continue
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
const compiledRules = compileRulesFactory(init, '{ … }')
|
|
1998
|
+
if (!compiledRules) continue
|
|
1999
|
+
// A destructured binding names INDIVIDUAL rules, so it has to inline —
|
|
2000
|
+
// there is no shared-shape path here (a hole has no standalone value).
|
|
2001
|
+
if (compiledRules.replacement === null) { warn(init.start, `{ … }: rule map couldn't be inlined` + reasonSuffix(compiledRules.refusals)); continue }
|
|
2002
|
+
|
|
2003
|
+
// Walk the ObjectPattern properties, validating each destructured key
|
|
2004
|
+
// exists on the compiled rule map — collect bindings before emitting
|
|
2005
|
+
// any replacement text (uniform with the previous all-or-nothing
|
|
2006
|
+
// per-declaration behavior).
|
|
2007
|
+
const pattern = d.id as unknown as { properties: unknown[] }
|
|
2008
|
+
const bindings: Array<{ ruleKey: string; localName: string }> = []
|
|
2009
|
+
let allOk = true
|
|
2010
|
+
|
|
2011
|
+
for (const prop of pattern.properties) {
|
|
2012
|
+
const p = prop as { type: string; key: { type: string; name?: string; value?: unknown }; value: { type: string; name?: string } }
|
|
2013
|
+
if (p.type === 'RestElement' || p.type === 'BindingRestElement') {
|
|
2014
|
+
warn(init.start, `rest element in a rules() destructure isn't supported`)
|
|
2015
|
+
allOk = false; break
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
const ruleKey = p.key.type === 'Identifier' ? p.key.name!
|
|
2019
|
+
: p.key.type === 'StringLiteral' ? String(p.key.value)
|
|
2020
|
+
: null
|
|
2021
|
+
const localName = (p.value.type === 'Identifier' || p.value.type === 'BindingIdentifier') ? p.value.name!
|
|
2022
|
+
: ruleKey
|
|
2023
|
+
if (!ruleKey || !localName) { allOk = false; break }
|
|
2024
|
+
|
|
2025
|
+
if (!compiledRules.ruleMap.has(ruleKey)) {
|
|
2026
|
+
warn(init.start, `destructured rule "${ruleKey}" isn't returned by the rules() factory`)
|
|
2027
|
+
allOk = false; break
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
bindings.push({ ruleKey, localName })
|
|
2031
|
+
// Store under the local name so a later macro declaration can reference it.
|
|
2032
|
+
const rule = compiledRules.ruleMap.get(ruleKey)
|
|
2033
|
+
if (rule) scope.set(localName, { combi: rule, mfSrcs: [] })
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
if (!allOk) continue
|
|
2037
|
+
|
|
2038
|
+
// ONE shared holder (not exported — an internal implementation
|
|
2039
|
+
// detail) evaluates the compiled rule map exactly once; each
|
|
2040
|
+
// destructured local name is just a property read off it, preserving
|
|
2041
|
+
// that name's own export-ness from the original declaration.
|
|
2042
|
+
const holderVar = `__rules${ruleMapHolderCounter++}`
|
|
2043
|
+
const lines = [
|
|
2044
|
+
`const ${holderVar} = ${compiledRules.replacement}`,
|
|
2045
|
+
...bindings.map(({ ruleKey, localName }) =>
|
|
2046
|
+
`${exportPrefix}${kind} ${localName} = ${holderVar}[${JSON.stringify(ruleKey)}]`),
|
|
2047
|
+
]
|
|
2048
|
+
|
|
2049
|
+
replacements.push({
|
|
2050
|
+
start: stmtStart,
|
|
2051
|
+
end: stmtEnd,
|
|
2052
|
+
replacement: lines.join('\n'),
|
|
2053
|
+
})
|
|
2054
|
+
markUsedImportedFactories(compiledRules.importedFactory ? [compiledRules.importedFactory] : undefined)
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// Nothing to rewrite and nothing to report — leave the file untouched.
|
|
2060
|
+
if (replacements.length === 0 && warnings.length === 0) return null
|
|
2061
|
+
|
|
2062
|
+
// If every declaration referencing an imported name was successfully inlined,
|
|
2063
|
+
// the import is no longer needed. Otherwise downgrade to runtime (strip just
|
|
2064
|
+
// the macro attribute so the import stays valid for the interpreter). A shared
|
|
2065
|
+
// shape also keeps the import — its emitted value still calls `rules(…)` — but
|
|
2066
|
+
// without the "fell back to the interpreter" warning, because nothing failed.
|
|
2067
|
+
for (const imp of macroImports) {
|
|
2068
|
+
imp.fullyResolved = !anyUnresolved && !keepMacroImport
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// An exported `rules()` factory whose macro import is about to be removed is an
|
|
2072
|
+
// UNRUNNABLE binding: lowering erases the call sites, but an export must still hold a
|
|
2073
|
+
// value, so the factory body ships verbatim naming `node`/`sequence`/`literal`/… that
|
|
2074
|
+
// nothing imports. It does not fail at build time and it does not fail at import time
|
|
2075
|
+
// — it throws `ReferenceError` the first time anything calls it, which in practice is
|
|
2076
|
+
// in a consumer's process, long after the artifact was published.
|
|
2077
|
+
//
|
|
2078
|
+
// There is no correct value to emit instead. Pinning the macro import would make the
|
|
2079
|
+
// binding run, but only by dragging the whole parseman runtime into an artifact whose
|
|
2080
|
+
// entire point is not to need it — a silent trade the author never asked for. So this
|
|
2081
|
+
// refuses to emit the shape at all. The fix is the author's: drop the `export`, since
|
|
2082
|
+
// a factory is macro-only by construction and no runtime consumer can use one.
|
|
2083
|
+
if (exportedFactories.length > 0 && macroImports.some(imp => imp.fullyResolved)) {
|
|
2084
|
+
const which = exportedFactories.map(f => ` - ${id}:${lineOf(f.pos)} — export const ${f.name}`).join('\n')
|
|
2085
|
+
throw new Error(
|
|
2086
|
+
`${id} — a rules() factory cannot be exported; lowering leaves its body verbatim, so the `
|
|
2087
|
+
+ `export would ship a binding that throws ReferenceError on first call:\n${which}\n`
|
|
2088
|
+
+ ` Drop the \`export\`. The factory is macro-only; \`rules(${exportedFactories[0]!.name})\` `
|
|
2089
|
+
+ `is lowered in place and needs no runtime value.`,
|
|
2090
|
+
)
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
const ms = new MagicString(code)
|
|
2094
|
+
const replacementRanges = runtimeComposeFallback
|
|
2095
|
+
? []
|
|
2096
|
+
: replacements.map(({ start, end }) => ({ start, end }))
|
|
2097
|
+
const isInsideReplacement = (start: number, end: number): boolean =>
|
|
2098
|
+
replacementRanges.some(r => start >= r.start && end <= r.end)
|
|
2099
|
+
const importedBindingStillReferenced = (local: string): boolean => {
|
|
2100
|
+
const walk = (node: unknown, parent?: AnyNode, parentKey?: string): boolean => {
|
|
2101
|
+
if (!node || typeof node !== 'object') return false
|
|
2102
|
+
const rec = node as AnyNode
|
|
2103
|
+
if (rec.type === 'ImportDeclaration') return false
|
|
2104
|
+
if (typeof rec.start === 'number' && typeof rec.end === 'number' && isInsideReplacement(rec.start, rec.end)) return false
|
|
2105
|
+
if ((rec.type === 'Identifier' || rec.type === 'BindingIdentifier') && (rec as { name?: string }).name === local) {
|
|
2106
|
+
if (
|
|
2107
|
+
parentKey === 'key' &&
|
|
2108
|
+
(parent?.type === 'ObjectProperty' || parent?.type === 'Property' || parent?.type === 'PropertyDefinition') &&
|
|
2109
|
+
(parent as { computed?: boolean }).computed !== true
|
|
2110
|
+
) return false
|
|
2111
|
+
if (parentKey === 'property' && parent?.type === 'StaticMemberExpression') return false
|
|
2112
|
+
if (parentKey === 'property' && parent?.type === 'MemberExpression' && (parent as { computed?: boolean }).computed !== true) return false
|
|
2113
|
+
return true
|
|
2114
|
+
}
|
|
2115
|
+
for (const key of Object.keys(rec)) {
|
|
2116
|
+
if (key === 'type' || key === 'start' || key === 'end') continue
|
|
2117
|
+
const value = (rec as Record<string, unknown>)[key]
|
|
2118
|
+
if (Array.isArray(value)) {
|
|
2119
|
+
if (value.some(child => walk(child, rec, key))) return true
|
|
2120
|
+
} else if (walk(value, rec, key)) {
|
|
2121
|
+
return true
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
return false
|
|
2125
|
+
}
|
|
2126
|
+
return (body as unknown[]).some(stmt => walk(stmt))
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
for (const imp of ordinaryImports) {
|
|
2130
|
+
if (
|
|
2131
|
+
!runtimeComposeFallback &&
|
|
2132
|
+
imp.specifiers.length > 0 &&
|
|
2133
|
+
imp.specifiers.every(spec =>
|
|
2134
|
+
spec.type === 'ImportSpecifier' &&
|
|
2135
|
+
usedImportedFactories.has(spec.local) &&
|
|
2136
|
+
!importedBindingStillReferenced(spec.local),
|
|
2137
|
+
)
|
|
2138
|
+
) {
|
|
2139
|
+
ms.remove(imp.start, imp.end)
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// Strip `x.define(...)` statements — only when every ref() cluster was inlined.
|
|
2144
|
+
// If anything fell back to the interpreter the import stays, and those
|
|
2145
|
+
// statements are still needed to wire the ref at runtime. (A shared shape holds
|
|
2146
|
+
// no ref() cluster of its own, so it doesn't gate this.)
|
|
2147
|
+
if (!anyUnresolved) {
|
|
2148
|
+
for (const { start, end } of defineRemovals) ms.remove(start, end)
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
for (const imp of macroImports) {
|
|
2152
|
+
if (imp.fullyResolved) {
|
|
2153
|
+
ms.remove(imp.start, imp.end)
|
|
2154
|
+
} else {
|
|
2155
|
+
// Strip only the macro attribute, keep the import
|
|
2156
|
+
const original = code.slice(imp.start, imp.end)
|
|
2157
|
+
const stripped = original
|
|
2158
|
+
.replace(/\s+with\s*\{[^}]*\}/gs, '')
|
|
2159
|
+
.replace(/\s+assert\s*\{[^}]*\}/gs, '')
|
|
2160
|
+
ms.overwrite(imp.start, imp.end, stripped)
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// Runtime compose consumes combinator objects. A partially lowered module mixes
|
|
2165
|
+
// compiled parser functions with those objects (for example `trivia(ws)` after
|
|
2166
|
+
// `ws` was lowered), so an unresolved compose makes the whole module runtime.
|
|
2167
|
+
const applied = runtimeComposeFallback ? [] : replacements
|
|
2168
|
+
// NO MODULE HOIST. It existed to deduplicate declarations across the fused IIFEs the
|
|
2169
|
+
// source lowering emitted — `_pfFail` and friends, one copy per variant. A table
|
|
2170
|
+
// replacement is a `tableRules(...)` call over a data literal: there are no emitted
|
|
2171
|
+
// function declarations to share, so there is nothing to hoist and no marker to
|
|
2172
|
+
// resolve.
|
|
2173
|
+
for (const { start, end, replacement } of applied.slice().sort((a, b) => b.start - a.start)) {
|
|
2174
|
+
ms.overwrite(start, end, replacement)
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// Stamp the generated artifact with a version-lock banner. This is the exact spot a
|
|
2178
|
+
// stale artifact is inspected, and the version here IS the stamp fusedBody's
|
|
2179
|
+
// version-lock assertion compares (see src/version.ts).
|
|
2180
|
+
// THE SHARED DRIVER. A table replacement names `tableRules` and nothing in the
|
|
2181
|
+
// consumer's module binds it — the macro import it might have come from is
|
|
2182
|
+
// exactly what lowering deletes. Emitted only when a table was actually applied,
|
|
2183
|
+
// so a module that lowered nothing (or fell back to runtime compose) does not
|
|
2184
|
+
// acquire an import it never uses, and so the artifact of a source-lowered
|
|
2185
|
+
// module is unchanged.
|
|
2186
|
+
if (usedTableRuntime && applied.length > 0) {
|
|
2187
|
+
ms.prepend(`import { tableRules } from ${JSON.stringify(TABLE_RUNTIME_SPECIFIER)}\n`)
|
|
2188
|
+
}
|
|
2189
|
+
if (applied.length > 0) {
|
|
2190
|
+
ms.prepend(
|
|
2191
|
+
`// Generated by parseman v${PARSEMAN_VERSION} — DO NOT EDIT.\n` +
|
|
2192
|
+
`// Version-locked: compile AND fuse/link this artifact with parseman v${PARSEMAN_VERSION} ONLY.\n` +
|
|
2193
|
+
`// Parseman does not read artifacts across versions; recompile if the version differs.\n`,
|
|
2194
|
+
)
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
/*
|
|
2198
|
+
* REFUSE TO EMIT a module that names something nothing binds.
|
|
2199
|
+
*
|
|
2200
|
+
* Every deletion lowering performs — the `rules(…)` call sites, the `x.define(…)`
|
|
2201
|
+
* statements, and last the macro import — is only safe because the text that read
|
|
2202
|
+
* the deleted binding went with it. Where a shape slips through where it did not,
|
|
2203
|
+
* the artifact builds clean, imports clean, and throws `ReferenceError` the first
|
|
2204
|
+
* time a consumer calls the binding: jess shipped exactly that for three days
|
|
2205
|
+
* across three grammars, 26 undefined identifiers in the css parser alone, and had
|
|
2206
|
+
* to write this check itself downstream. It belongs here.
|
|
2207
|
+
*
|
|
2208
|
+
* This is the NET, not the diagnostic. Shapes we recognise are refused where they
|
|
2209
|
+
* are recognised, with an error that says what to do — an exported `rules()`
|
|
2210
|
+
* factory throws above, before this point, so it is never double-reported. What
|
|
2211
|
+
* reaches here is a name that escaped by a route nobody enumerated, which is the
|
|
2212
|
+
* whole reason for checking the emitted text instead of guessing at shapes.
|
|
2213
|
+
*
|
|
2214
|
+
* Gated on `applied.length > 0` — the same condition as the version banner. A
|
|
2215
|
+
* module that lowered nothing had nothing deleted from it, so it cannot have
|
|
2216
|
+
* acquired a free name, and the scan (one extra parse of the emitted module, at
|
|
2217
|
+
* macro time) is not worth paying for. Nothing here runs in, or is emitted into,
|
|
2218
|
+
* the artifact.
|
|
2219
|
+
*/
|
|
2220
|
+
if (applied.length > 0) {
|
|
2221
|
+
/*
|
|
2222
|
+
* Only names lowering MADE free. A module that already read something nothing binds
|
|
2223
|
+
* — a host that injects the name some other way, an ambient the author knows about
|
|
2224
|
+
* — was written that way, and reporting it here would be parseman failing a build
|
|
2225
|
+
* over a decision it had no part in. Subtracting the source's own free names is what
|
|
2226
|
+
* keeps this a check on parseman's output rather than a lint on the author's input.
|
|
2227
|
+
* The source scan runs ONLY when the emitted module already looks wrong, so the
|
|
2228
|
+
* clean path costs one parse, not two.
|
|
2229
|
+
*/
|
|
2230
|
+
let sourceFree: Set<string> | null = null
|
|
2231
|
+
const freeInSource = (): Set<string> =>
|
|
2232
|
+
sourceFree ??= new Set(findFreeIdentifiers(code, id).map(f => f.name))
|
|
2233
|
+
const free = findFreeIdentifiers(ms.toString(), id)
|
|
2234
|
+
.filter(f => !freeInSource().has(f.name))
|
|
2235
|
+
if (free.length > 0) {
|
|
2236
|
+
const macroProvided = free.filter(f => allNames.has(f.name))
|
|
2237
|
+
const where = (f: (typeof free)[number]): string =>
|
|
2238
|
+
` - ${id}:${f.line}:${f.column} — \`${f.name}\``
|
|
2239
|
+
+ (f.enclosing ? `, inside \`${f.enclosing}\`` : '')
|
|
2240
|
+
const cause = macroProvided.length > 0
|
|
2241
|
+
? `\n ${macroProvided.length === free.length ? 'All' : `${macroProvided.length}`} of these came from the `
|
|
2242
|
+
+ `\`with { type: 'macro' }\` import, which was removed because every macro declaration lowered. `
|
|
2243
|
+
+ `Something still names them verbatim — a declaration parseman left as text rather than compiling. `
|
|
2244
|
+
+ `Make that declaration macro-buildable, or stop exporting it so lowering can drop it.`
|
|
2245
|
+
: ''
|
|
2246
|
+
throw new Error(
|
|
2247
|
+
`${id} — parseman will not emit this module: ${free.length} identifier(s) are read but bound by `
|
|
2248
|
+
+ `nothing, so it would throw ReferenceError at runtime (positions are in the EMITTED module):\n`
|
|
2249
|
+
+ `${free.map(where).join('\n')}${cause}`,
|
|
2250
|
+
)
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// NOTE: `warnUnloweredRegex` and the inline-expansion cap were both properties of the
|
|
2255
|
+
// SOURCE lowering — "this regex became RegExp.exec instead of a charCodeAt scan", and
|
|
2256
|
+
// "this emitted function hit INLINE_MAX_NODES". The table has no emitted function to
|
|
2257
|
+
// bound and no per-regex emission choice to report, so both diagnostics are gone
|
|
2258
|
+
// rather than reporting a value nothing computes. The option is still accepted so a
|
|
2259
|
+
// caller's config does not break; it is now inert.
|
|
2260
|
+
|
|
2261
|
+
setReducerResolver(null)
|
|
2262
|
+
// Every place the compiler chose a correct-but-slower path for this module. Reported
|
|
2263
|
+
// on the ordinary warning channel and greppable on `[parseman] degraded`, so a
|
|
2264
|
+
// consumer's build gate can assert zero of them the way jess's `check:macro` already
|
|
2265
|
+
// asserts zero `"falling back to runtime"` lines. `PARSEMAN_DEGRADATION=error` turns
|
|
2266
|
+
// the assertion on here instead of in the consumer.
|
|
2267
|
+
const degradations = endDegradationCapture()
|
|
2268
|
+
if (degradations.length > 0) {
|
|
2269
|
+
const lines = formatDegradations(degradations)
|
|
2270
|
+
if (resolveDegradationLevel() === 'error') {
|
|
2271
|
+
throw new Error(`parseman: ${degradations.length} degraded compilation path(s) in ${id}\n${lines.join('\n')}`)
|
|
2272
|
+
}
|
|
2273
|
+
for (const l of lines) warnings.push(`${id}: ${l}`)
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
return {
|
|
2277
|
+
code: ms.toString(),
|
|
2278
|
+
map: ms.generateMap({ hires: true }),
|
|
2279
|
+
warnings,
|
|
2280
|
+
}
|
|
2281
|
+
}
|