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,1505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statically evaluates parseman combinator call expressions from an oxc AST
|
|
3
|
+
* into actual Combinator<unknown> objects by calling the real library functions.
|
|
4
|
+
*
|
|
5
|
+
* Returns null for anything unresolvable (external variables, template literals,
|
|
6
|
+
* computed keys, etc.) — callers leave those as-is.
|
|
7
|
+
*/
|
|
8
|
+
import type {
|
|
9
|
+
Expression, Node,
|
|
10
|
+
ArrowFunctionExpression, Function as OxcFunction,
|
|
11
|
+
ReturnStatement,
|
|
12
|
+
VariableDeclaration, VariableDeclarator,
|
|
13
|
+
StaticMemberExpression,
|
|
14
|
+
ObjectExpression, ObjectProperty,
|
|
15
|
+
} from '@oxc-project/types'
|
|
16
|
+
import type { Combinator } from '../types.ts'
|
|
17
|
+
import type { DispatchArm } from '../combinators/dispatch.ts'
|
|
18
|
+
import { ref } from '../combinators/ref.ts'
|
|
19
|
+
import { rules, type RulesOptions } from '../combinators/parser.ts'
|
|
20
|
+
import * as parseman from '../index.ts'
|
|
21
|
+
import { directBuilderUnsupportedBindings } from './direct-builder-static.ts'
|
|
22
|
+
import type { ReducerResolver } from './reducer-resolver.ts'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Emit an AST subtree's source with TypeScript-only syntax removed. A gate source
|
|
26
|
+
* is sliced verbatim from the grammar's `.ts` and may carry a type annotation (e.g.
|
|
27
|
+
* `(s: any) => …`, unavoidable for a gate under a `g: any` factory with
|
|
28
|
+
* noImplicitAny). Where the macro INLINES the source, downstream TS→JS transpilation
|
|
29
|
+
* strips that — but a gated choice also round-trips through the `serializeRuleMap`
|
|
30
|
+
* IR string, which is re-lowered with `new Function` VERBATIM, where TS syntax is a
|
|
31
|
+
* hard parse error. So blank out every TS-only span (param/return/variable type
|
|
32
|
+
* annotations, generic type args, and `as`/`satisfies`/`!` cast suffixes) using the
|
|
33
|
+
* spans the oxc parser already gave us — no extra transpiler dependency. A subtree
|
|
34
|
+
* with no TS syntax (every existing untyped callback) is returned byte-for-byte, so
|
|
35
|
+
* standalone codegen output is unchanged.
|
|
36
|
+
*/
|
|
37
|
+
function stripTsFromSource(node: Node, code: string): string {
|
|
38
|
+
const cuts: Array<[number, number]> = []
|
|
39
|
+
const walk = (n: unknown): void => {
|
|
40
|
+
if (!n || typeof n !== 'object') return
|
|
41
|
+
const rec = n as Record<string, unknown> & { type?: string; start?: number; end?: number; expression?: { start?: number; end?: number } }
|
|
42
|
+
// A whole TS-only node (a type annotation, type-argument list, etc.): drop it.
|
|
43
|
+
if (typeof rec.type === 'string' && rec.type.startsWith('TS') && typeof rec.start === 'number' && typeof rec.end === 'number') {
|
|
44
|
+
const ex = rec.expression
|
|
45
|
+
// SUFFIX wrappers keep their expression; only the trailing TS is dropped:
|
|
46
|
+
// `x as T` / `x satisfies T` / `x!` (after the expression) and `f<T>` (the
|
|
47
|
+
// `<T>` type-argument list after the callee expression).
|
|
48
|
+
if ((rec.type === 'TSAsExpression' || rec.type === 'TSSatisfiesExpression' || rec.type === 'TSNonNullExpression' || rec.type === 'TSInstantiationExpression') && ex && typeof ex.end === 'number') {
|
|
49
|
+
cuts.push([ex.end, rec.end])
|
|
50
|
+
walk(rec.expression)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
// PREFIX wrapper: `<T>x` (angle-bracket assertion) — cut the leading `<T>`,
|
|
54
|
+
// keep the wrapped expression.
|
|
55
|
+
if (rec.type === 'TSTypeAssertion' && ex && typeof ex.start === 'number') {
|
|
56
|
+
cuts.push([rec.start, ex.start])
|
|
57
|
+
walk(rec.expression)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
// Everything else (annotations, bare type-argument lists) is dropped whole.
|
|
61
|
+
cuts.push([rec.start, rec.end])
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
for (const key of Object.keys(rec)) {
|
|
65
|
+
if (key === 'type' || key === 'start' || key === 'end') continue
|
|
66
|
+
const v = rec[key]
|
|
67
|
+
if (Array.isArray(v)) { for (const item of v) walk(item) }
|
|
68
|
+
else if (v && typeof v === 'object') walk(v)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
walk(node)
|
|
72
|
+
const start = (node as { start: number }).start
|
|
73
|
+
const end = (node as { end: number }).end
|
|
74
|
+
if (cuts.length === 0) return code.slice(start, end)
|
|
75
|
+
cuts.sort((a, b) => a[0] - b[0])
|
|
76
|
+
let out = ''
|
|
77
|
+
let cur = start
|
|
78
|
+
for (const [s, e] of cuts) {
|
|
79
|
+
if (s < cur) continue // nested cut already covered
|
|
80
|
+
out += code.slice(cur, s)
|
|
81
|
+
cur = e
|
|
82
|
+
}
|
|
83
|
+
return out + code.slice(cur, end)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The stand-in for a `withCtx(extra, …)` argument the macro could not evaluate.
|
|
88
|
+
*
|
|
89
|
+
* It is a CLASS INSTANCE on purpose. `{}` would be indistinguishable from an
|
|
90
|
+
* author's own empty state object, so the table encoder would intern it, print
|
|
91
|
+
* it, and ship a grammar whose every state gate is silently false. A non-plain
|
|
92
|
+
* prototype fails `emittableConst`, which is what turns "we don't know the state"
|
|
93
|
+
* into a named `runtimeOnly` refusal instead of a wrong artifact.
|
|
94
|
+
*/
|
|
95
|
+
class UnevaluatedExtra {}
|
|
96
|
+
const UNEVALUATED_EXTRA: unknown = new UnevaluatedExtra()
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Reducer resolution
|
|
100
|
+
//
|
|
101
|
+
// `buildSrc` is the source text of the EXPRESSION at the `node(...)` call site, so a
|
|
102
|
+
// reducer passed as a bare identifier — `node('Foo', p, { build: foldOperation })` —
|
|
103
|
+
// arrives as the string `"foldOperation"`. That matches no parameter list, so
|
|
104
|
+
// `confirmedBuildArity` returned `null` and every capture tier stayed on: the runtime
|
|
105
|
+
// cost of a rule depended on how its reducer was SPELLED.
|
|
106
|
+
//
|
|
107
|
+
// `reducer-resolver.ts` does the real work — lexical scope analysis over this module,
|
|
108
|
+
// plus cross-module import following — and this is where its answer is attached. The
|
|
109
|
+
// resolved arity lands on `_def.buildArity` and the resolved source on
|
|
110
|
+
// `_def.buildSigSrc`; both are ANALYSIS-ONLY and never emitted, so the generated builder
|
|
111
|
+
// reference is byte-identical either way.
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
let _reducers: ReducerResolver | null = null
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The source of the module actually being EMITTED, so a factory evaluated out of some
|
|
117
|
+
* OTHER file can be told apart from one written here. See `setReducerResolver`.
|
|
118
|
+
*/
|
|
119
|
+
let _entrySource: string | null = null
|
|
120
|
+
|
|
121
|
+
/** Install (or clear, with `null`) the resolver for the module being transformed. */
|
|
122
|
+
export function setReducerResolver(r: ReducerResolver | null, entrySource: string | null = null): void {
|
|
123
|
+
_reducers = r
|
|
124
|
+
_entrySource = entrySource
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Scope types
|
|
129
|
+
//
|
|
130
|
+
// Each scope entry is either a raw Combinator, or an enriched entry that
|
|
131
|
+
// carries the mapFnSources this combinator will contribute when the codegen
|
|
132
|
+
// traverses its subtree. The enriched form is needed so that anyValue can
|
|
133
|
+
// "replay" those sources when the combinator is referenced by another
|
|
134
|
+
// expression — keeping mapFnSources aligned with what ctx.mapFns builds.
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
export type ScopeEntry = {
|
|
137
|
+
combi: Combinator<unknown>
|
|
138
|
+
mfSrcs: string[]
|
|
139
|
+
}
|
|
140
|
+
export type Scope = Map<string, ScopeEntry>
|
|
141
|
+
|
|
142
|
+
// Internal XScope also holds non-Combinator values (g proxy objects etc.)
|
|
143
|
+
type XScopeVal = ScopeEntry | unknown
|
|
144
|
+
type XScope = Map<string, XScopeVal>
|
|
145
|
+
|
|
146
|
+
type WordFactoryEntry = { tag: 'wordFactory'; boundary: string; caseInsensitive: boolean }
|
|
147
|
+
type WhenFactoryEntry = { tag: 'whenFactory'; caseInsensitive: boolean }
|
|
148
|
+
type StaticValueEntry = { value: unknown; mfSrcs: string[] }
|
|
149
|
+
|
|
150
|
+
function isWordFactory(v: unknown): v is WordFactoryEntry {
|
|
151
|
+
return !!v && typeof v === 'object' && (v as WordFactoryEntry).tag === 'wordFactory'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isWhenFactory(v: unknown): v is WhenFactoryEntry {
|
|
155
|
+
return !!v && typeof v === 'object' && (v as WhenFactoryEntry).tag === 'whenFactory'
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isStaticValueEntry(v: unknown): v is StaticValueEntry {
|
|
159
|
+
return !!v && typeof v === 'object' && 'value' in v && 'mfSrcs' in v
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function wordFactoryFromArgs(args: readonly (Expression | { type: 'SpreadElement' })[], scope: XScope, code?: string, mfs?: string[]): WordFactoryEntry | null {
|
|
163
|
+
const [boundaryOrOptsArg, optsArg] = args
|
|
164
|
+
if (boundaryOrOptsArg?.type === 'SpreadElement' || optsArg?.type === 'SpreadElement') return null
|
|
165
|
+
|
|
166
|
+
const rawBoundaryOrOpts = boundaryOrOptsArg === undefined
|
|
167
|
+
? undefined
|
|
168
|
+
: anyValue(boundaryOrOptsArg as Expression, scope, code, mfs)
|
|
169
|
+
const boundaryOrOpts = rawBoundaryOrOpts === undefined ? '_0-9A-Za-z' : rawBoundaryOrOpts
|
|
170
|
+
const opts = optsArg === undefined
|
|
171
|
+
? undefined
|
|
172
|
+
: anyValue(optsArg as Expression, scope, code, mfs)
|
|
173
|
+
|
|
174
|
+
if (typeof boundaryOrOpts === 'string') {
|
|
175
|
+
if (opts !== undefined && (typeof opts !== 'object' || opts === null || Array.isArray(opts))) return null
|
|
176
|
+
const caseInsensitive = typeof opts === 'object' && opts !== null && 'caseInsensitive' in opts
|
|
177
|
+
? (opts as { caseInsensitive?: unknown }).caseInsensitive
|
|
178
|
+
: false
|
|
179
|
+
if (typeof caseInsensitive !== 'boolean') return null
|
|
180
|
+
return { tag: 'wordFactory', boundary: boundaryOrOpts, caseInsensitive }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (typeof boundaryOrOpts !== 'object' || boundaryOrOpts === null || Array.isArray(boundaryOrOpts) || opts !== undefined) {
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
const caseInsensitive = 'caseInsensitive' in boundaryOrOpts
|
|
187
|
+
? (boundaryOrOpts as { caseInsensitive?: unknown }).caseInsensitive
|
|
188
|
+
: false
|
|
189
|
+
if (typeof caseInsensitive !== 'boolean') return null
|
|
190
|
+
return { tag: 'wordFactory', boundary: '_0-9A-Za-z', caseInsensitive }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function dispatchWhenOptions(v: unknown): parseman.DispatchWhenOptions | null {
|
|
194
|
+
if (v === undefined) return {}
|
|
195
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v)) return null
|
|
196
|
+
for (const key of Object.keys(v)) {
|
|
197
|
+
if (key !== 'caseInsensitive') return null
|
|
198
|
+
}
|
|
199
|
+
const caseInsensitive = 'caseInsensitive' in v
|
|
200
|
+
? (v as { caseInsensitive?: unknown }).caseInsensitive
|
|
201
|
+
: false
|
|
202
|
+
if (typeof caseInsensitive !== 'boolean') return null
|
|
203
|
+
return { caseInsensitive }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function whenFactoryFromArgs(args: readonly (Expression | { type: 'SpreadElement' })[], scope: XScope, code?: string, mfs?: string[]): WhenFactoryEntry | null {
|
|
207
|
+
const [optsArg, extra] = args
|
|
208
|
+
if (optsArg?.type === 'SpreadElement' || extra !== undefined) return null
|
|
209
|
+
const rawOpts = optsArg === undefined
|
|
210
|
+
? undefined
|
|
211
|
+
: anyValue(optsArg as Expression, scope, code, mfs)
|
|
212
|
+
const opts = dispatchWhenOptions(rawOpts)
|
|
213
|
+
return opts === null ? null : { tag: 'whenFactory', caseInsensitive: opts.caseInsensitive ?? false }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Generic fallback table, consulted LAST (see the dispatch at the end of
|
|
218
|
+
* `exprToCombi`). A combinator with an UNCONDITIONAL explicit branch above must
|
|
219
|
+
* not appear here — the entry would be unreachable. `many`/`oneOrMore`/`sepBy`/
|
|
220
|
+
* `oneOrMoreSep`/`peek`/`not` are all handled explicitly, because their emitters
|
|
221
|
+
* traverse the item more than once (mfSrcs replay) or take options that must not
|
|
222
|
+
* be silently dropped.
|
|
223
|
+
*/
|
|
224
|
+
const SUPPORTED: Record<string, (...args: unknown[]) => Combinator<unknown>> = {
|
|
225
|
+
literal: (...a) => parseman.literal(a[0] as string, a[1] as parseman.LiteralOptions | undefined),
|
|
226
|
+
regex: (...a) => parseman.regex(a[0] as RegExp, a[1] as string | undefined),
|
|
227
|
+
keywords: (...a) => parseman.keywords(a[0] as readonly string[], a[1] as parseman.KeywordsOptions | undefined),
|
|
228
|
+
word: (...a) => parseman.word(a[0] as string, a[1] as string | undefined, a[2] as Omit<parseman.KeywordsOptions, 'boundary'> | undefined),
|
|
229
|
+
sequence: (...a) => (parseman.sequence as (...p: Combinator<unknown>[]) => Combinator<unknown[]>)(...(a as Combinator<unknown>[])),
|
|
230
|
+
choice: (...a) => (parseman.choice as (...p: Combinator<unknown>[]) => Combinator<unknown>)(...(a as Combinator<unknown>[])),
|
|
231
|
+
attempt: (...a) => parseman.attempt(a[0] as Combinator<unknown>),
|
|
232
|
+
optional: (...a) => parseman.optional(a[0] as Combinator<unknown>),
|
|
233
|
+
trivia: (...a) => parseman.trivia(a[0] as Combinator<unknown>),
|
|
234
|
+
classifiedTrivia: (...a) =>
|
|
235
|
+
parseman.classifiedTrivia(
|
|
236
|
+
a[0] as Readonly<Record<string, Combinator<unknown>>>,
|
|
237
|
+
),
|
|
238
|
+
label: (...a) => parseman.label(a[0] as string, a[1] as Combinator<unknown>),
|
|
239
|
+
field: (...a) => parseman.field(a[0] as string, a[1] as Combinator<unknown>),
|
|
240
|
+
noTrivia: (...a) => parseman.noTrivia(a[0] as Combinator<unknown>),
|
|
241
|
+
token: (...a) => parseman.token(a[0] as Combinator<unknown>),
|
|
242
|
+
// `routed(fallback)` — the fallback must be forwarded. A zero-arg entry here read
|
|
243
|
+
// as "routed takes no arguments" and SILENTLY built a bare `routed()`, so a
|
|
244
|
+
// production written to work both inside and outside a dispatch branch lost its
|
|
245
|
+
// out-of-branch behaviour under the macro while keeping it under the interpreter.
|
|
246
|
+
// `undefined` reproduces the bare def exactly (see `routed()`), so `routed()` is
|
|
247
|
+
// byte-identical.
|
|
248
|
+
routed: (...a) => parseman.routed(a[0] as Combinator<unknown> | undefined),
|
|
249
|
+
leaf: (...a) => parseman.leaf(a[0] as Combinator<unknown>, a[1] as (value: unknown, span: { start: number; end: number }) => unknown),
|
|
250
|
+
expect: (...a) => parseman.expect(a[0] as Combinator<unknown>, a[1] as string | undefined),
|
|
251
|
+
// Adjacency assertions carry only plain data (a polarity and an optional list of
|
|
252
|
+
// category names), so the macro reproduces them exactly — no source capture, no
|
|
253
|
+
// interpreter fallback.
|
|
254
|
+
adjacent: () => parseman.adjacent(),
|
|
255
|
+
notAdjacent: (...a) => parseman.notAdjacent(a[0] as { kinds?: readonly string[] } | undefined),
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Internal helpers
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
function isScopeEntry(v: unknown): v is ScopeEntry {
|
|
263
|
+
return !!v && typeof v === 'object' && 'combi' in v && 'mfSrcs' in v
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** THE reader for an object-literal property key, for every consumer in the plugin.
|
|
267
|
+
*
|
|
268
|
+
* Returns the key a JavaScript engine would use, or null when the property does not
|
|
269
|
+
* name a static key at all — a spread, a rest, or a COMPUTED key whose value is not
|
|
270
|
+
* known until runtime.
|
|
271
|
+
*
|
|
272
|
+
* Both halves are load-bearing and each was got wrong somewhere:
|
|
273
|
+
* - a quoted key is a `Literal`, not an `Identifier`. Reading only `key.name` sees
|
|
274
|
+
* `{ 'hostMode': 'cst' }` as having no hostMode, and drops the option SILENTLY.
|
|
275
|
+
* - `key.name` is also populated for a COMPUTED key `{ [hostMode]: … }`, where the
|
|
276
|
+
* identifier is a variable and the actual key is its value. Reading only
|
|
277
|
+
* `key.name` there invents an option the source never set.
|
|
278
|
+
*
|
|
279
|
+
* So an Identifier-only reader both misses keys that are present and matches keys that
|
|
280
|
+
* are not. Three option readers in plugin/index.ts each re-derived one wrong half; the
|
|
281
|
+
* fix is this one function, imported. */
|
|
282
|
+
export function propName(p: { type?: string; computed?: boolean; key?: unknown }): string | null {
|
|
283
|
+
if ((p.type !== undefined && p.type !== 'Property') || p.computed || !p.key) return null
|
|
284
|
+
const key = p.key as { type?: string; name?: unknown; value?: unknown }
|
|
285
|
+
return key.type === 'Identifier' ? (typeof key.name === 'string' ? key.name : null)
|
|
286
|
+
: key.type === 'Literal' ? String(key.value)
|
|
287
|
+
: null
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Is this a gated-choice arm object literal — `{ gate, combinator }`? */
|
|
291
|
+
function isGatedArmExpr(e: { type: string }): boolean {
|
|
292
|
+
if (e.type !== 'ObjectExpression') return false
|
|
293
|
+
let hasGate = false, hasCombinator = false
|
|
294
|
+
for (const prop of (e as ObjectExpression).properties) {
|
|
295
|
+
if (prop.type !== 'Property') continue
|
|
296
|
+
const name = propName(prop as unknown as ObjectProperty)
|
|
297
|
+
if (name === 'gate') hasGate = true
|
|
298
|
+
else if (name === 'combinator') hasCombinator = true
|
|
299
|
+
}
|
|
300
|
+
return hasGate && hasCombinator
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Extract the `gate` / `combinator` value expressions from a gated-arm object.
|
|
304
|
+
* Returns null on any unexpected shape (spread, computed key, extra key). */
|
|
305
|
+
function gatedArmParts(e: ObjectExpression): { gate: Expression; combinator: Expression } | null {
|
|
306
|
+
let gate: Expression | undefined
|
|
307
|
+
let combinator: Expression | undefined
|
|
308
|
+
for (const prop of e.properties) {
|
|
309
|
+
if (prop.type !== 'Property') return null
|
|
310
|
+
const op = prop as unknown as ObjectProperty
|
|
311
|
+
const name = propName(op)
|
|
312
|
+
if (name === 'gate') gate = op.value as Expression
|
|
313
|
+
else if (name === 'combinator') combinator = op.value as Expression
|
|
314
|
+
else return null
|
|
315
|
+
}
|
|
316
|
+
return gate && combinator ? { gate, combinator } : null
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function dispatchArmValue(node: Expression, scope: XScope, code?: string, mfs?: string[]): DispatchArm<unknown> | null {
|
|
320
|
+
if (node.type === 'Identifier') {
|
|
321
|
+
const value = anyValue(node, scope, code, mfs)
|
|
322
|
+
return isDispatchArm(value) ? value : null
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (node.type !== 'CallExpression') return null
|
|
326
|
+
const callee = node.callee
|
|
327
|
+
if (callee.type !== 'Identifier') return null
|
|
328
|
+
|
|
329
|
+
const factory = scope.get(callee.name)
|
|
330
|
+
if (isWhenFactory(factory)) {
|
|
331
|
+
const [keyArg, parserArg, extraArg] = node.arguments
|
|
332
|
+
if (!keyArg || !parserArg || extraArg !== undefined || keyArg.type === 'SpreadElement' || parserArg.type === 'SpreadElement') return null
|
|
333
|
+
const key = anyValue(keyArg as Expression, scope, code, mfs)
|
|
334
|
+
const parserValue = anyValue(parserArg as Expression, scope, code, mfs)
|
|
335
|
+
if (!isCombinator(parserValue)) return null
|
|
336
|
+
const opts = { caseInsensitive: factory.caseInsensitive }
|
|
337
|
+
if (typeof key === 'string') return parseman.when(key, parserValue, opts)
|
|
338
|
+
if (Array.isArray(key) && key.every(item => typeof item === 'string')) return parseman.when(key, parserValue, opts)
|
|
339
|
+
// `makeWhen(opts)` returns `(key, parser) => when(key, parser, opts)`, so it accepts
|
|
340
|
+
// EVERY key `when` accepts — including a startsWith/endsWith/matches matcher. Omitting
|
|
341
|
+
// that case here made the aliased form a hard macro failure ("factory isn't statically
|
|
342
|
+
// evaluable") for an arm the interpreter builds, so the alias silently carried a
|
|
343
|
+
// narrower contract than the constructor it forwards to.
|
|
344
|
+
if (isDispatchMatcher(key)) return parseman.when(key, parserValue, opts)
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (callee.name === 'otherwise') {
|
|
349
|
+
const [parserArg] = node.arguments
|
|
350
|
+
if (!parserArg || parserArg.type === 'SpreadElement') return null
|
|
351
|
+
const parserValue = anyValue(parserArg as Expression, scope, code, mfs)
|
|
352
|
+
if (!isCombinator(parserValue)) return null
|
|
353
|
+
return parseman.otherwise(parserValue)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (callee.name === 'when') {
|
|
357
|
+
const [keyArg, parserArg, optsArg] = node.arguments
|
|
358
|
+
if (!keyArg || !parserArg || keyArg.type === 'SpreadElement' || parserArg.type === 'SpreadElement' || optsArg?.type === 'SpreadElement') return null
|
|
359
|
+
const key = anyValue(keyArg as Expression, scope, code, mfs)
|
|
360
|
+
const parserValue = anyValue(parserArg as Expression, scope, code, mfs)
|
|
361
|
+
const opts = dispatchWhenOptions(optsArg === undefined ? undefined : anyValue(optsArg as Expression, scope, code, mfs))
|
|
362
|
+
if (opts === null) return null
|
|
363
|
+
if (!isCombinator(parserValue)) return null
|
|
364
|
+
if (typeof key === 'string') return parseman.when(key, parserValue, opts)
|
|
365
|
+
if (Array.isArray(key) && key.every(item => typeof item === 'string')) return parseman.when(key, parserValue, opts)
|
|
366
|
+
if (isDispatchMatcher(key)) return parseman.when(key, parserValue, opts)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return null
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function isCombinator(v: unknown): v is Combinator<unknown> {
|
|
373
|
+
return !!v && typeof v === 'object' && '_def' in v
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isDispatchArm(v: unknown): v is DispatchArm<unknown> {
|
|
377
|
+
if (!v || typeof v !== 'object') return false
|
|
378
|
+
const rec = v as { kind?: unknown; keys?: unknown; matcher?: unknown; parser?: unknown; caseInsensitive?: unknown }
|
|
379
|
+
if (rec.kind === 'otherwise') return isCombinator(rec.parser)
|
|
380
|
+
if (rec.kind === 'whenMatcher') {
|
|
381
|
+
return isDispatchMatcher(rec.matcher) &&
|
|
382
|
+
typeof rec.caseInsensitive === 'boolean' &&
|
|
383
|
+
isCombinator(rec.parser)
|
|
384
|
+
}
|
|
385
|
+
return rec.kind === 'when' &&
|
|
386
|
+
Array.isArray(rec.keys) &&
|
|
387
|
+
rec.keys.every(key => typeof key === 'string') &&
|
|
388
|
+
typeof rec.caseInsensitive === 'boolean' &&
|
|
389
|
+
isCombinator(rec.parser)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isDispatchMatcher(v: unknown): v is ReturnType<typeof parseman.startsWith> {
|
|
393
|
+
if (!v || typeof v !== 'object') return false
|
|
394
|
+
const rec = v as { kind?: unknown; value?: unknown; flags?: unknown }
|
|
395
|
+
if ((rec.kind === 'startsWith' || rec.kind === 'endsWith') && typeof rec.value === 'string') return true
|
|
396
|
+
return rec.kind === 'matches' &&
|
|
397
|
+
typeof rec.value === 'string' &&
|
|
398
|
+
typeof rec.flags === 'string'
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Resolve an identifier from scope.
|
|
403
|
+
* If the entry carries mfSrcs, replay them into `mfs` so that the
|
|
404
|
+
* overall accumulator stays aligned with what codegen will push.
|
|
405
|
+
*/
|
|
406
|
+
function scopeGet(scope: XScope, name: string, mfs?: string[]): Combinator<unknown> | null {
|
|
407
|
+
const entry = scope.get(name)
|
|
408
|
+
if (!entry) return null
|
|
409
|
+
if (isScopeEntry(entry)) {
|
|
410
|
+
if (mfs && entry.mfSrcs.length > 0) mfs.push(...entry.mfSrcs)
|
|
411
|
+
return entry.combi
|
|
412
|
+
}
|
|
413
|
+
if (isCombinator(entry)) return entry
|
|
414
|
+
return null
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Core evaluators
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
/** Read static node opts that affect generated grammar shape. */
|
|
422
|
+
function unwrapStaticExpr<T extends { type?: string }>(expr: T): T {
|
|
423
|
+
let cur = expr as unknown as { type?: string; expression?: T }
|
|
424
|
+
while (cur.type === 'TSAsExpression'
|
|
425
|
+
|| cur.type === 'TSSatisfiesExpression'
|
|
426
|
+
|| cur.type === 'TSNonNullExpression'
|
|
427
|
+
|| cur.type === 'TSTypeAssertion'
|
|
428
|
+
|| cur.type === 'TSInstantiationExpression'
|
|
429
|
+
|| cur.type === 'ParenthesizedExpression') {
|
|
430
|
+
if (!cur.expression) break
|
|
431
|
+
cur = cur.expression as unknown as typeof cur
|
|
432
|
+
}
|
|
433
|
+
return cur as unknown as T
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function staticLiteralValue(expr: unknown): unknown {
|
|
437
|
+
const val = unwrapStaticExpr(expr as { type?: string; value?: unknown })
|
|
438
|
+
return val.type === 'Literal' || val.type === 'BooleanLiteral' || val.type === 'NumericLiteral'
|
|
439
|
+
? val.value
|
|
440
|
+
: undefined
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function staticStringArray(expr: unknown, scope?: XScope): readonly string[] | undefined {
|
|
444
|
+
const id = unwrapStaticExpr(expr as { type?: string; name?: string })
|
|
445
|
+
if (id.type === 'Identifier' && scope !== undefined && id.name !== undefined) {
|
|
446
|
+
const scoped = scope.get(id.name)
|
|
447
|
+
const value = isStaticValueEntry(scoped) ? scoped.value : scoped
|
|
448
|
+
return Array.isArray(value) && value.every(v => typeof v === 'string') ? value : undefined
|
|
449
|
+
}
|
|
450
|
+
const arr = id as { type?: string; elements?: unknown[] }
|
|
451
|
+
if (arr.type !== 'ArrayExpression' || !Array.isArray(arr.elements)) return undefined
|
|
452
|
+
const out: string[] = []
|
|
453
|
+
for (const el of arr.elements) {
|
|
454
|
+
if (!el || (el as { type?: string }).type === 'SpreadElement') return undefined
|
|
455
|
+
const value = staticLiteralValue(el)
|
|
456
|
+
if (typeof value !== 'string') return undefined
|
|
457
|
+
out.push(value)
|
|
458
|
+
}
|
|
459
|
+
return out
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
type StaticNodeProject = { ok: true; value: number } | { ok: false }
|
|
463
|
+
const STATIC_NODE_OPTIONS_FAILED = Symbol('parseman.staticNodeOptions.failed')
|
|
464
|
+
const STATIC_NODE_OPTIONS_NOT_OPTIONS = Symbol('parseman.staticNodeOptions.notOptions')
|
|
465
|
+
type StaticNodeOptions =
|
|
466
|
+
| parseman.NodeOptions<readonly string[]>
|
|
467
|
+
| undefined
|
|
468
|
+
| typeof STATIC_NODE_OPTIONS_FAILED
|
|
469
|
+
| typeof STATIC_NODE_OPTIONS_NOT_OPTIONS
|
|
470
|
+
|
|
471
|
+
function staticNodeProject(expr: Expression): StaticNodeProject | undefined {
|
|
472
|
+
const literalValue = staticLiteralValue(expr)
|
|
473
|
+
if (typeof literalValue === 'number' && Number.isInteger(literalValue) && literalValue >= 0) {
|
|
474
|
+
return { ok: true, value: literalValue }
|
|
475
|
+
}
|
|
476
|
+
if (typeof literalValue === 'number' || literalValue !== undefined) return { ok: false }
|
|
477
|
+
return { ok: false }
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function scopedStaticValue(expr: Expression, scope: XScope): { found: true; value: unknown } | { found: false } {
|
|
481
|
+
const unwrapped = unwrapStaticExpr(expr)
|
|
482
|
+
if (unwrapped.type !== 'Identifier' || unwrapped.name === 'undefined') return { found: false }
|
|
483
|
+
if (!scope.has(unwrapped.name)) return { found: false }
|
|
484
|
+
const scoped = scope.get(unwrapped.name)
|
|
485
|
+
const value = isStaticValueEntry(scoped) ? scoped.value : scoped
|
|
486
|
+
return { found: true, value }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function staticNodeOptionsFromValue(value: unknown): parseman.NodeOptions<readonly string[]> | undefined | typeof STATIC_NODE_OPTIONS_FAILED | typeof STATIC_NODE_OPTIONS_NOT_OPTIONS {
|
|
490
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value) || isCombinator(value)) return STATIC_NODE_OPTIONS_NOT_OPTIONS
|
|
491
|
+
const opts: parseman.NodeOptions<readonly string[]> = {}
|
|
492
|
+
const rec = value as Record<string, unknown>
|
|
493
|
+
for (const name of Object.keys(rec)) {
|
|
494
|
+
const v = rec[name]
|
|
495
|
+
if (name === 'unwrap' || name === 'collapse' || name === 'captureTrivia' || name === 'trailingTrivia') {
|
|
496
|
+
if (v === true) opts[name] = true
|
|
497
|
+
else if (v !== false && v !== undefined) return STATIC_NODE_OPTIONS_FAILED
|
|
498
|
+
} else if (name === 'project') {
|
|
499
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0) return STATIC_NODE_OPTIONS_FAILED
|
|
500
|
+
opts.project = v
|
|
501
|
+
} else if (name === 'tags') {
|
|
502
|
+
if (!Array.isArray(v) || !v.every(item => typeof item === 'string')) return STATIC_NODE_OPTIONS_FAILED
|
|
503
|
+
opts.tags = v
|
|
504
|
+
} else if (name === 'buildArity') {
|
|
505
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0 || v > 6) return STATIC_NODE_OPTIONS_FAILED
|
|
506
|
+
opts.buildArity = v
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return opts.unwrap || opts.collapse || opts.project !== undefined || opts.captureTrivia || opts.trailingTrivia || opts.tags !== undefined || opts.buildArity !== undefined ? opts : undefined
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function staticNodeOptions(expr: Expression, scope: XScope): StaticNodeOptions {
|
|
513
|
+
const unwrapped = unwrapStaticExpr(expr)
|
|
514
|
+
const scoped = scopedStaticValue(unwrapped, scope)
|
|
515
|
+
if (scoped.found) return staticNodeOptionsFromValue(scoped.value)
|
|
516
|
+
if (unwrapped.type !== 'ObjectExpression') return STATIC_NODE_OPTIONS_NOT_OPTIONS
|
|
517
|
+
const opts: parseman.NodeOptions<readonly string[]> = {}
|
|
518
|
+
for (const prop of (unwrapped as ObjectExpression).properties) {
|
|
519
|
+
if ((prop as { type?: string }).type !== 'Property') return STATIC_NODE_OPTIONS_FAILED
|
|
520
|
+
const p = prop as unknown as ObjectProperty
|
|
521
|
+
if (p.computed) return STATIC_NODE_OPTIONS_FAILED
|
|
522
|
+
const name = propName(p as never)
|
|
523
|
+
if (name === 'unwrap' || name === 'collapse' || name === 'captureTrivia' || name === 'trailingTrivia') {
|
|
524
|
+
const value = staticLiteralValue(p.value)
|
|
525
|
+
if (value === true) opts[name] = true
|
|
526
|
+
else if (value !== false && value !== undefined) return STATIC_NODE_OPTIONS_FAILED
|
|
527
|
+
} else if (name === 'project') {
|
|
528
|
+
const project = staticNodeProject(p.value as Expression)
|
|
529
|
+
if (project?.ok === false) return STATIC_NODE_OPTIONS_FAILED
|
|
530
|
+
if (project !== undefined) opts.project = project.value
|
|
531
|
+
} else if (name === 'tags') {
|
|
532
|
+
const tags = staticStringArray(p.value, scope)
|
|
533
|
+
if (tags === undefined) return STATIC_NODE_OPTIONS_FAILED
|
|
534
|
+
opts.tags = tags
|
|
535
|
+
} else if (name === 'buildArity') {
|
|
536
|
+
const arity = staticLiteralValue(p.value)
|
|
537
|
+
if (typeof arity !== 'number' || !Number.isInteger(arity) || arity < 0 || arity > 6) return STATIC_NODE_OPTIONS_FAILED
|
|
538
|
+
opts.buildArity = arity
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return opts.unwrap || opts.collapse || opts.project !== undefined || opts.captureTrivia || opts.trailingTrivia || opts.tags !== undefined || opts.buildArity !== undefined ? opts : undefined
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Evaluate a call expression to a Combinator.
|
|
546
|
+
* `mfs` accumulates mapFn source texts in depth-first order — must match
|
|
547
|
+
* what codegen pushes to ctx.mapFns when it traverses the same tree.
|
|
548
|
+
*/
|
|
549
|
+
function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: string[]): Combinator<unknown> | null {
|
|
550
|
+
if (node.type === 'Identifier') return scopeGet(scope, node.name, mfs)
|
|
551
|
+
|
|
552
|
+
if (node.type !== 'CallExpression') return null
|
|
553
|
+
|
|
554
|
+
const callee = node.callee
|
|
555
|
+
|
|
556
|
+
// makeWord(boundary?, opts?)(str)
|
|
557
|
+
if (callee.type === 'CallExpression'
|
|
558
|
+
&& callee.callee.type === 'Identifier'
|
|
559
|
+
&& callee.callee.name === 'makeWord') {
|
|
560
|
+
const strArg = node.arguments[0]
|
|
561
|
+
if (!strArg || strArg.type === 'SpreadElement') return null
|
|
562
|
+
const factory = wordFactoryFromArgs(callee.arguments, scope, code, mfs)
|
|
563
|
+
const str = anyValue(strArg as Expression, scope, code, mfs)
|
|
564
|
+
if (factory === null || typeof str !== 'string') return null
|
|
565
|
+
try { return parseman.word(str, factory.boundary, { caseInsensitive: factory.caseInsensitive }) } catch { return null }
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (callee.type === 'Identifier') {
|
|
569
|
+
const factory = scope.get(callee.name)
|
|
570
|
+
if (isWordFactory(factory)) {
|
|
571
|
+
const [strArg] = node.arguments
|
|
572
|
+
if (!strArg || strArg.type === 'SpreadElement') return null
|
|
573
|
+
const str = anyValue(strArg as Expression, scope, code, mfs)
|
|
574
|
+
if (typeof str !== 'string') return null
|
|
575
|
+
try { return parseman.word(str, factory.boundary, { caseInsensitive: factory.caseInsensitive }) } catch { return null }
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (callee.type !== 'Identifier') return null
|
|
580
|
+
|
|
581
|
+
// transform(inner, fn) — capture fn source text before pushing to mfs
|
|
582
|
+
if (callee.name === 'transform' && code !== undefined && mfs !== undefined) {
|
|
583
|
+
const [parserArg, fnArg] = node.arguments
|
|
584
|
+
if (!parserArg || !fnArg || parserArg.type === 'SpreadElement' || fnArg.type === 'SpreadElement') return null
|
|
585
|
+
const inner = anyValue(parserArg as Expression, scope, code, mfs)
|
|
586
|
+
if (!isCombinator(inner)) return null
|
|
587
|
+
const fnSrc = stripTsFromSource(fnArg as Node, code)
|
|
588
|
+
mfs.push(fnSrc)
|
|
589
|
+
try {
|
|
590
|
+
const combi = parseman.transform(inner, (v: unknown) => v)
|
|
591
|
+
// Carry the callback source on the def so codegen can pull it in traversal
|
|
592
|
+
// order (order-independent across rules that share sub-combinators).
|
|
593
|
+
if (combi._def.tag === 'transform') combi._def.fnSrc = fnSrc
|
|
594
|
+
return combi
|
|
595
|
+
} catch { return null }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// leaf(inner, fn) — like transform(), but suppresses inner CST captures and
|
|
599
|
+
// publishes one reducer-selected terminal leaf to its parent.
|
|
600
|
+
if (callee.name === 'leaf' && code !== undefined && mfs !== undefined) {
|
|
601
|
+
const [parserArg, fnArg] = node.arguments
|
|
602
|
+
if (!parserArg || !fnArg || parserArg.type === 'SpreadElement' || fnArg.type === 'SpreadElement') return null
|
|
603
|
+
const inner = anyValue(parserArg as Expression, scope, code, mfs)
|
|
604
|
+
if (!isCombinator(inner)) return null
|
|
605
|
+
const fnSrc = stripTsFromSource(fnArg as Node, code)
|
|
606
|
+
mfs.push(fnSrc)
|
|
607
|
+
try {
|
|
608
|
+
const combi = parseman.leaf(inner, (v: unknown) => v)
|
|
609
|
+
if (combi._def.tag === 'leaf') combi._def.fnSrc = fnSrc
|
|
610
|
+
return combi
|
|
611
|
+
} catch { return null }
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// node(parser, build?, opts?) / node(type, parser, build?, opts?) — CST node rule.
|
|
615
|
+
// Capture the build callback source (like transform) so codegen inlines it; the
|
|
616
|
+
// inner parser carries the capture. Options may be the trailing argument or the
|
|
617
|
+
// third argument when no build callback is present.
|
|
618
|
+
if (callee.name === 'node' && code !== undefined) {
|
|
619
|
+
const [firstArg, secondArg, thirdArg, fourthArg] = node.arguments
|
|
620
|
+
if (!firstArg || firstArg.type === 'SpreadElement') return null
|
|
621
|
+
const firstVal = anyValue(firstArg as Expression, scope, code, mfs)
|
|
622
|
+
const explicitType = typeof firstVal === 'string' ? firstVal : undefined
|
|
623
|
+
const parserArg = explicitType !== undefined ? secondArg : firstArg
|
|
624
|
+
const buildArg = explicitType !== undefined ? thirdArg : secondArg
|
|
625
|
+
const optsArg = explicitType !== undefined ? fourthArg : thirdArg
|
|
626
|
+
if (!parserArg || parserArg.type === 'SpreadElement') return null
|
|
627
|
+
const inner = anyValue(parserArg as Expression, scope, code, mfs)
|
|
628
|
+
if (!isCombinator(inner)) return null
|
|
629
|
+
// `build` is OPTIONAL — a structural node() omits it (or passes the literal
|
|
630
|
+
// `undefined` to reach the 4th opts arg). Structural nodes build via the
|
|
631
|
+
// injected `ctx.build` host; codegen keys that off `def.build === undefined`.
|
|
632
|
+
const be = buildArg as { type: string; start: number; end: number; name?: string } | undefined
|
|
633
|
+
const buildExpr = be === undefined || be.type === 'SpreadElement' ? undefined : unwrapStaticExpr(be as unknown as Expression)
|
|
634
|
+
const buildArgOptions = be !== undefined && be.type !== 'SpreadElement'
|
|
635
|
+
? staticNodeOptions(buildArg as Expression, scope)
|
|
636
|
+
: STATIC_NODE_OPTIONS_NOT_OPTIONS
|
|
637
|
+
if (buildArgOptions === STATIC_NODE_OPTIONS_FAILED) return null
|
|
638
|
+
const buildArgIsOptions = buildArgOptions !== STATIC_NODE_OPTIONS_NOT_OPTIONS
|
|
639
|
+
const scopedBuild = buildExpr === undefined
|
|
640
|
+
? { found: false } as const
|
|
641
|
+
: scopedStaticValue(buildExpr, scope)
|
|
642
|
+
const absentBuild = (buildExpr?.type === 'Identifier' && buildExpr.name === 'undefined')
|
|
643
|
+
|| (buildExpr !== undefined && staticLiteralValue(buildExpr) === null)
|
|
644
|
+
|| (scopedBuild.found && (scopedBuild.value === undefined || scopedBuild.value === null))
|
|
645
|
+
const hasBuild = be !== undefined && be.type !== 'SpreadElement'
|
|
646
|
+
&& !buildArgIsOptions
|
|
647
|
+
&& !absentBuild
|
|
648
|
+
const buildSrc = hasBuild ? stripTsFromSource(be! as Node, code) : undefined
|
|
649
|
+
let opts: parseman.NodeOptions<readonly string[]> | undefined
|
|
650
|
+
if (buildArgIsOptions) {
|
|
651
|
+
opts = buildArgOptions as parseman.NodeOptions<readonly string[]> | undefined
|
|
652
|
+
} else if (optsArg !== undefined) {
|
|
653
|
+
if (optsArg.type === 'SpreadElement') return null
|
|
654
|
+
const optsResult = staticNodeOptions(optsArg as Expression, scope)
|
|
655
|
+
if (optsResult === STATIC_NODE_OPTIONS_FAILED || optsResult === STATIC_NODE_OPTIONS_NOT_OPTIONS) return null
|
|
656
|
+
opts = optsResult
|
|
657
|
+
}
|
|
658
|
+
try {
|
|
659
|
+
const combi = explicitType !== undefined
|
|
660
|
+
? parseman.node(explicitType, inner, hasBuild ? () => null : undefined, opts as parseman.NodeOptions | undefined)
|
|
661
|
+
: parseman.node(inner, hasBuild ? () => null : undefined, opts as parseman.NodeOptions | undefined)
|
|
662
|
+
if (combi._def.tag === 'node' && buildSrc !== undefined) {
|
|
663
|
+
combi._def.buildSrc = buildSrc
|
|
664
|
+
// The type argument's IDENTIFIER, when it was written as one. A `node(type, …)`
|
|
665
|
+
// inside a factory resolves `type` to a string here, which loses the fact that
|
|
666
|
+
// the reducer's `mk(type, …)` names the SAME binding — and losing it is what
|
|
667
|
+
// made every factory-built node miss the inline-`mk` path.
|
|
668
|
+
if (explicitType !== undefined && firstArg.type === 'Identifier') {
|
|
669
|
+
combi._def.typeSrc = (firstArg as unknown as { name: string }).name
|
|
670
|
+
}
|
|
671
|
+
// A NAMED reducer (`foldOperation`, `helpers.fold`, an import): resolve it so the
|
|
672
|
+
// capture-tier analysis reads the REAL parameter list instead of failing open.
|
|
673
|
+
// `null` means the expression was an inline function, which is self-describing.
|
|
674
|
+
const resolved = _reducers?.resolve(buildSrc, be!.start, code)
|
|
675
|
+
if (resolved) {
|
|
676
|
+
if (resolved.src !== null) combi._def.buildSigSrc = resolved.src
|
|
677
|
+
/*
|
|
678
|
+
* A reducer named from a FOREIGN factory has to be emitted as its SOURCE, not
|
|
679
|
+
* its name. `buildSrc` is the call site's expression text, and the call site is
|
|
680
|
+
* in the factory's module — so `node('Fold', …, fold)` in an imported factory
|
|
681
|
+
* emitted `const _build = [fold]` into the CONSUMING module, where `fold` is a
|
|
682
|
+
* module-private const of a file that was never imported. That artifact threw
|
|
683
|
+
* `ReferenceError: fold is not defined` on import, and nothing noticed: the
|
|
684
|
+
* shape has tests, but they assert on the emitted TEXT and never run it. The
|
|
685
|
+
* emit-time scope check below `ms.toString()` is what found it.
|
|
686
|
+
*
|
|
687
|
+
* The resolver has already read the declaration out of the right module, so the
|
|
688
|
+
* substitution is exact for a self-contained reducer. One that closes over more
|
|
689
|
+
* of its own module's privates is NOT fixed by this — it is caught by that same
|
|
690
|
+
* scope check, which refuses to emit rather than shipping the next ReferenceError.
|
|
691
|
+
*/
|
|
692
|
+
if (_entrySource !== null && code !== _entrySource
|
|
693
|
+
&& resolved.src !== null && be!.type === 'Identifier') {
|
|
694
|
+
combi._def.buildSrc = resolved.src
|
|
695
|
+
}
|
|
696
|
+
// An author-declared `node(..., { buildArity })` is authority 1 in
|
|
697
|
+
// `confirmedArityForDef`; the resolver is authority 2. Both land in the SAME
|
|
698
|
+
// field, so writing unconditionally here demoted the declaration to whatever
|
|
699
|
+
// scope analysis happened to find.
|
|
700
|
+
if (combi._def.buildArity === undefined && resolved.arity !== null) combi._def.buildArity = resolved.arity
|
|
701
|
+
if (resolved.reason !== undefined) combi._def.buildArityUnresolved = resolved.reason
|
|
702
|
+
}
|
|
703
|
+
const staticError = directBuilderUnsupportedBindings(buildSrc)
|
|
704
|
+
if (staticError.length > 0) combi._def.buildStaticError = staticError
|
|
705
|
+
}
|
|
706
|
+
return combi
|
|
707
|
+
} catch { return null }
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// rules(factory) — handled separately by evaluateParserFactory; signal null here
|
|
711
|
+
if (callee.name === 'rules') return null
|
|
712
|
+
|
|
713
|
+
// ref() — forward-declared recursion slot. Standalone refs (declared, then
|
|
714
|
+
// resolved later via `x.define(...)`) are the interpreter/compile() recursion
|
|
715
|
+
// mechanism; the macro must support them too for parity. We return a REAL ref
|
|
716
|
+
// placeholder here; index.ts pre-resolves all `x.define(...)` statements into
|
|
717
|
+
// scope before compilation so codegen's emitLazy sees a defined thunk.
|
|
718
|
+
if (callee.name === 'ref') {
|
|
719
|
+
if (node.arguments.length !== 0) return null
|
|
720
|
+
return ref<unknown>() as Combinator<unknown>
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// parser(opts, root) — bakes trivia/trackLines into a `grammar` combinator so
|
|
724
|
+
// the compiled output skips whitespace between sequence terms identically to
|
|
725
|
+
// the interpreter. opts.trivia is itself a combinator; evaluate it with a
|
|
726
|
+
// throwaway mfs accumulator since the trivia parser is emitted out-of-band
|
|
727
|
+
// (ensureTriviaFn) and its sources are pulled via def.fnSrc, not positionally.
|
|
728
|
+
if (callee.name === 'parser') {
|
|
729
|
+
const [optsArg, rootArg] = node.arguments
|
|
730
|
+
if (!optsArg || !rootArg || optsArg.type === 'SpreadElement' || rootArg.type === 'SpreadElement') return null
|
|
731
|
+
const opts = anyValue(optsArg as Expression, scope, code, [])
|
|
732
|
+
if (!opts || typeof opts !== 'object') return null
|
|
733
|
+
const root = anyValue(rootArg as Expression, scope, code, mfs)
|
|
734
|
+
if (!isCombinator(root)) return null
|
|
735
|
+
try {
|
|
736
|
+
return parseman.parser(opts as parseman.ParserOptions, root)
|
|
737
|
+
} catch { return null }
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// sepBy(item, sep, opts?) — emitSepBy traverses: item (first probe), sep, item
|
|
741
|
+
// (loop body). We must push item's mfSrcs twice to stay aligned with ctx.mapFns.
|
|
742
|
+
// `opts` (notably `{ min: 1 }`) MUST be honored: dropping it would silently
|
|
743
|
+
// compile a NULLABLE list where the source asked for a non-empty one.
|
|
744
|
+
if (callee.name === 'sepBy' || callee.name === 'oneOrMoreSep') {
|
|
745
|
+
const [itemArg, sepArg, optsArg] = node.arguments
|
|
746
|
+
if (!itemArg || !sepArg || itemArg.type === 'SpreadElement' || sepArg.type === 'SpreadElement') return null
|
|
747
|
+
const itemMfs: string[] = []
|
|
748
|
+
const itemCombi = anyValue(itemArg as Expression, scope, code, itemMfs)
|
|
749
|
+
if (!isCombinator(itemCombi)) return null
|
|
750
|
+
const sepMfs: string[] = []
|
|
751
|
+
const sepCombi = anyValue(sepArg as Expression, scope, code, sepMfs)
|
|
752
|
+
if (!isCombinator(sepCombi)) return null
|
|
753
|
+
let opts: parseman.SepByOptions | undefined
|
|
754
|
+
if (optsArg) {
|
|
755
|
+
if (optsArg.type === 'SpreadElement') return null
|
|
756
|
+
const v = anyValue(optsArg as Expression, scope, code, [])
|
|
757
|
+
if (!v || typeof v !== 'object') return null
|
|
758
|
+
opts = v as parseman.SepByOptions
|
|
759
|
+
}
|
|
760
|
+
if (mfs) mfs.push(...itemMfs, ...sepMfs, ...itemMfs)
|
|
761
|
+
try {
|
|
762
|
+
return callee.name === 'sepBy'
|
|
763
|
+
? parseman.sepBy(itemCombi, sepCombi, opts)
|
|
764
|
+
: parseman.oneOrMoreSep(itemCombi, sepCombi, opts)
|
|
765
|
+
} catch { return null }
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// many(item, opts?) / oneOrMore(item, opts?) — emitMany traverses `min` mandatory
|
|
769
|
+
// items then the loop body, so item's mfSrcs are replayed min+1 times. `opts`
|
|
770
|
+
// MUST be honored: dropping `{ min }` would silently compile a NULLABLE repeat.
|
|
771
|
+
if (callee.name === 'oneOrMore' || callee.name === 'many') {
|
|
772
|
+
const [itemArg, optsArg] = node.arguments
|
|
773
|
+
if (!itemArg || itemArg.type === 'SpreadElement') return null
|
|
774
|
+
const itemMfs: string[] = []
|
|
775
|
+
const itemCombi = anyValue(itemArg as Expression, scope, code, itemMfs)
|
|
776
|
+
if (!isCombinator(itemCombi)) return null
|
|
777
|
+
let opts: parseman.RepeatOptions | undefined
|
|
778
|
+
if (optsArg) {
|
|
779
|
+
if (optsArg.type === 'SpreadElement') return null
|
|
780
|
+
const v = anyValue(optsArg as Expression, scope, code, [])
|
|
781
|
+
if (!v || typeof v !== 'object') return null
|
|
782
|
+
opts = v as parseman.RepeatOptions
|
|
783
|
+
}
|
|
784
|
+
let combi: Combinator<unknown>
|
|
785
|
+
try {
|
|
786
|
+
combi = callee.name === 'many' ? parseman.many(itemCombi, opts) : parseman.oneOrMore(itemCombi, opts)
|
|
787
|
+
} catch { return null }
|
|
788
|
+
const min = combi._def.tag === 'oneOrMore' ? combi._def.min : 0
|
|
789
|
+
if (mfs) for (let i = 0; i <= min; i++) mfs.push(...itemMfs)
|
|
790
|
+
return combi
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// not(parser) — negative lookahead (consumes nothing).
|
|
794
|
+
if (callee.name === 'not') {
|
|
795
|
+
const [innerArg] = node.arguments
|
|
796
|
+
if (!innerArg || innerArg.type === 'SpreadElement') return null
|
|
797
|
+
const inner = anyValue(innerArg as Expression, scope, code, mfs)
|
|
798
|
+
if (!isCombinator(inner)) return null
|
|
799
|
+
try { return parseman.not(inner) } catch { return null }
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// peek(parser) — POSITIVE lookahead (consumes nothing), carrying the body's
|
|
803
|
+
// first-set so a leading peek() still gates its choice arm.
|
|
804
|
+
if (callee.name === 'peek') {
|
|
805
|
+
const [innerArg] = node.arguments
|
|
806
|
+
if (!innerArg || innerArg.type === 'SpreadElement') return null
|
|
807
|
+
const inner = anyValue(innerArg as Expression, scope, code, mfs)
|
|
808
|
+
if (!isCombinator(inner)) return null
|
|
809
|
+
try { return parseman.peek(inner) } catch { return null }
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// balanced(open, close, opts?) — like scanTo, opts (notably opts.skip, an array
|
|
813
|
+
// of combinators) MUST be honored. The interpreter and compile() build the full
|
|
814
|
+
// combinator structure from opts; the macro must evaluate and pass opts too —
|
|
815
|
+
// dropping it silently produces wrong (parity-breaking) behavior.
|
|
816
|
+
if (callee.name === 'balanced') {
|
|
817
|
+
const [openArg, closeArg, optsArg] = node.arguments
|
|
818
|
+
if (!openArg || !closeArg || openArg.type === 'SpreadElement' || closeArg.type === 'SpreadElement') return null
|
|
819
|
+
const open = anyValue(openArg as Expression, scope, code, [])
|
|
820
|
+
const close = anyValue(closeArg as Expression, scope, code, [])
|
|
821
|
+
if (typeof open !== 'string' || typeof close !== 'string') return null
|
|
822
|
+
const opts = optsArg && optsArg.type !== 'SpreadElement'
|
|
823
|
+
? anyValue(optsArg as Expression, scope, code, [])
|
|
824
|
+
: undefined
|
|
825
|
+
try { return parseman.balanced(open, close, opts as parseman.ScanToOptions | undefined) } catch { return null }
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// scanTo(sentinel, opts?) — consume up to (and including) a sentinel, optionally
|
|
829
|
+
// skipping balanced pairs. opts.skip is an array of combinators.
|
|
830
|
+
if (callee.name === 'scanTo') {
|
|
831
|
+
const [sentinelArg, optsArg] = node.arguments
|
|
832
|
+
if (!sentinelArg || sentinelArg.type === 'SpreadElement') return null
|
|
833
|
+
const sentinel = anyValue(sentinelArg as Expression, scope, code, [])
|
|
834
|
+
if (!isCombinator(sentinel)) return null
|
|
835
|
+
const opts = optsArg && optsArg.type !== 'SpreadElement'
|
|
836
|
+
? anyValue(optsArg as Expression, scope, code, [])
|
|
837
|
+
: undefined
|
|
838
|
+
try { return parseman.scanTo(sentinel, opts as parseman.ScanToOptions | undefined) } catch { return null }
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// gate(pred) (formerly guard(pred)) — context assertion. Capture the predicate
|
|
842
|
+
// source (like transform's fn) so codegen inlines it into `_mf`; build a
|
|
843
|
+
// placeholder gate and stash the source on `_def.predSrc`. Without source-capture
|
|
844
|
+
// context we return null → the whole rule falls back to the (correct)
|
|
845
|
+
// interpreter, never dropping the predicate.
|
|
846
|
+
if (callee.name === 'gate' || callee.name === 'guard') {
|
|
847
|
+
if (code === undefined || mfs === undefined) return null
|
|
848
|
+
const [predArg] = node.arguments
|
|
849
|
+
if (!predArg || predArg.type === 'SpreadElement') return null
|
|
850
|
+
const predSrc = stripTsFromSource(predArg as Node, code)
|
|
851
|
+
mfs.push(predSrc)
|
|
852
|
+
try {
|
|
853
|
+
const combi = parseman.gate(() => true)
|
|
854
|
+
if (combi._def.tag !== 'guard') return null
|
|
855
|
+
combi._def.predSrc = predSrc
|
|
856
|
+
return combi
|
|
857
|
+
} catch { return null }
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// withCtx(extra, inner) — run `inner` with ctx.state = extra. Capture the
|
|
861
|
+
// `extra` argument source; codegen wraps it as `() => (extra)` in `_mf`. The
|
|
862
|
+
// extra getter is emitted BEFORE the inner parser's own mapFns (matching
|
|
863
|
+
// emitWithCtx's push order), so push the extra token first, then eval inner.
|
|
864
|
+
if (callee.name === 'withCtx') {
|
|
865
|
+
if (code === undefined || mfs === undefined) return null
|
|
866
|
+
const [extraArg, innerArg] = node.arguments
|
|
867
|
+
if (!extraArg || !innerArg || extraArg.type === 'SpreadElement' || innerArg.type === 'SpreadElement') return null
|
|
868
|
+
const extraSrc = stripTsFromSource(extraArg as Node, code)
|
|
869
|
+
mfs.push(extraSrc)
|
|
870
|
+
const inner = anyValue(innerArg as Expression, scope, code, mfs)
|
|
871
|
+
if (!isCombinator(inner)) return null
|
|
872
|
+
try {
|
|
873
|
+
// THE VALUE, not just its source text. Codegen only ever needed `extraSrc`
|
|
874
|
+
// — it prints `() => (extra)` into `_mf` — so `{}` was an adequate stand-in
|
|
875
|
+
// for the def's own `extra`. The TABLE ENCODER reads `d.extra` and interns
|
|
876
|
+
// it in the const pool, so the placeholder became the artifact: the pool
|
|
877
|
+
// held a bare `{}` and every `withCtx` gate predicate (`s => !!(s && s.inner)`)
|
|
878
|
+
// was present and permanently false. Evaluate the argument; the placeholder
|
|
879
|
+
// survives only when it cannot be evaluated, and then it is `emittableConst`
|
|
880
|
+
// that decides — a plain `{}` extras object is indistinguishable from an
|
|
881
|
+
// author's `{}`, so an unevaluable one must NOT masquerade as empty state.
|
|
882
|
+
const evaluated = anyValue(extraArg as Expression, scope, code, [])
|
|
883
|
+
const usable = typeof evaluated === 'object' && evaluated !== null && !Array.isArray(evaluated)
|
|
884
|
+
&& Object.getPrototypeOf(evaluated) === Object.prototype
|
|
885
|
+
const combi = parseman.withCtx(usable ? evaluated : UNEVALUATED_EXTRA, inner)
|
|
886
|
+
if (combi._def.tag !== 'withCtx') return null
|
|
887
|
+
combi._def.extraSrc = extraSrc
|
|
888
|
+
return combi
|
|
889
|
+
} catch { return null }
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (callee.name === 'dispatch') {
|
|
893
|
+
const [selectorArg, ...armArgs] = node.arguments
|
|
894
|
+
if (!selectorArg || selectorArg.type === 'SpreadElement') return null
|
|
895
|
+
const selector = anyValue(selectorArg as Expression, scope, code, mfs)
|
|
896
|
+
if (!isCombinator(selector)) return null
|
|
897
|
+
const arms: DispatchArm<unknown>[] = []
|
|
898
|
+
for (const arg of armArgs) {
|
|
899
|
+
if (arg.type === 'SpreadElement') return null
|
|
900
|
+
const arm = dispatchArmValue(arg as Expression, scope, code, mfs)
|
|
901
|
+
if (!isDispatchArm(arm)) return null
|
|
902
|
+
arms.push(arm)
|
|
903
|
+
}
|
|
904
|
+
try {
|
|
905
|
+
const unsafeDispatch = parseman.dispatch as (selector: Combinator<string>, ...items: DispatchArm<unknown>[]) => Combinator<unknown>
|
|
906
|
+
return unsafeDispatch(selector as Combinator<string>, ...arms)
|
|
907
|
+
} catch { return null }
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// choice(...) WITH at least one gated arm `{ gate, combinator }`. The generic
|
|
911
|
+
// SUPPORTED path evaluates a gated-arm ObjectExpression via anyValue, whose
|
|
912
|
+
// arrow-gate evaluates to `null` — so `choice` would treat the arm as UNGATED
|
|
913
|
+
// and emit it unconditionally (a SILENT semantic miscompile vs the interpreter).
|
|
914
|
+
// Handle gated choices explicitly: capture each gate's source, build the REAL
|
|
915
|
+
// gated arm, and stash the per-arm sources on `_def.gateSrcs`. If any gate can't
|
|
916
|
+
// be source-captured, return null for the WHOLE choice → safe interpreter
|
|
917
|
+
// fallback. (Non-gated choices fall through to the generic path → byte-identical.)
|
|
918
|
+
if (callee.name === 'choice' && node.arguments.some(a => a.type !== 'SpreadElement' && isGatedArmExpr(a as { type: string }))) {
|
|
919
|
+
if (code === undefined || mfs === undefined) return null
|
|
920
|
+
const arms: Array<Combinator<unknown> | { gate: (s: unknown) => boolean; combinator: Combinator<unknown> }> = []
|
|
921
|
+
const gateSrcs: (string | null)[] = []
|
|
922
|
+
for (const argNode of node.arguments) {
|
|
923
|
+
if (argNode.type === 'SpreadElement') return null
|
|
924
|
+
if (isGatedArmExpr(argNode as { type: string })) {
|
|
925
|
+
const parts = gatedArmParts(argNode as unknown as ObjectExpression)
|
|
926
|
+
if (!parts) return null
|
|
927
|
+
// Gate mapFn is pushed BEFORE the arm body's mapFns (matches emitFirstMatch).
|
|
928
|
+
const gateSrc = stripTsFromSource(parts.gate as unknown as Node, code)
|
|
929
|
+
mfs.push(gateSrc)
|
|
930
|
+
const combi = anyValue(parts.combinator, scope, code, mfs)
|
|
931
|
+
if (!isCombinator(combi)) return null
|
|
932
|
+
arms.push({ gate: () => true, combinator: combi })
|
|
933
|
+
gateSrcs.push(gateSrc)
|
|
934
|
+
} else {
|
|
935
|
+
const combi = anyValue(argNode as Expression, scope, code, mfs)
|
|
936
|
+
if (!isCombinator(combi)) return null
|
|
937
|
+
arms.push(combi)
|
|
938
|
+
gateSrcs.push(null)
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
const combi = (parseman.choice as (...p: unknown[]) => Combinator<unknown>)(...arms)
|
|
943
|
+
if (combi._def.tag !== 'choice') return null
|
|
944
|
+
combi._def.gateSrcs = gateSrcs
|
|
945
|
+
// Guard: a real gate MUST align with a captured source, and vice-versa —
|
|
946
|
+
// no predicate-bearing arm may reach codegen with a dropped source.
|
|
947
|
+
for (let i = 0; i < combi._def.gates.length; i++) {
|
|
948
|
+
if ((combi._def.gates[i] !== null) !== (gateSrcs[i] !== null)) return null
|
|
949
|
+
}
|
|
950
|
+
return combi
|
|
951
|
+
} catch { return null }
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const factory = SUPPORTED[callee.name]
|
|
955
|
+
if (!factory) return null
|
|
956
|
+
|
|
957
|
+
const args = node.arguments.map(arg => {
|
|
958
|
+
if (arg.type === 'SpreadElement') return null
|
|
959
|
+
return anyValue(arg as Expression, scope, code, mfs)
|
|
960
|
+
})
|
|
961
|
+
if (args.some(a => a === null)) return null
|
|
962
|
+
|
|
963
|
+
try {
|
|
964
|
+
return factory(...(args as unknown[]))
|
|
965
|
+
} catch { return null }
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/** Evaluate any expression to its JS value (not necessarily a Combinator). */
|
|
969
|
+
function anyValue(node: Expression, scope: XScope, code?: string, mfs?: string[]): unknown {
|
|
970
|
+
if (node.type === 'TSAsExpression'
|
|
971
|
+
|| node.type === 'TSSatisfiesExpression'
|
|
972
|
+
|| node.type === 'TSNonNullExpression'
|
|
973
|
+
|| node.type === 'TSTypeAssertion'
|
|
974
|
+
|| node.type === 'TSInstantiationExpression'
|
|
975
|
+
|| node.type === 'ParenthesizedExpression') {
|
|
976
|
+
const inner = (node as unknown as { expression?: Expression }).expression
|
|
977
|
+
return inner ? anyValue(inner, scope, code, mfs) : null
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (node.type === 'Literal') {
|
|
981
|
+
if ('regex' in node && node.regex !== null && node.regex !== undefined) {
|
|
982
|
+
return new RegExp(node.regex.pattern, node.regex.flags)
|
|
983
|
+
}
|
|
984
|
+
return node.value
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (node.type === 'ArrayExpression') {
|
|
988
|
+
const arr = node as unknown as { elements: Array<Expression | null> }
|
|
989
|
+
const out: unknown[] = []
|
|
990
|
+
for (const el of arr.elements) {
|
|
991
|
+
if (el === null) { out.push(null); continue }
|
|
992
|
+
if ((el as { type: string }).type === 'SpreadElement') return null
|
|
993
|
+
out.push(anyValue(el as Expression, scope, code, mfs))
|
|
994
|
+
}
|
|
995
|
+
return out
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (node.type === 'ObjectExpression') {
|
|
999
|
+
const obj: Record<string, unknown> = {}
|
|
1000
|
+
for (const prop of node.properties) {
|
|
1001
|
+
const key = propName(prop as never)
|
|
1002
|
+
if (key === null) return null
|
|
1003
|
+
obj[key] = anyValue((prop as unknown as ObjectProperty).value as Expression, scope, code, mfs)
|
|
1004
|
+
}
|
|
1005
|
+
return obj
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (node.type === 'Identifier') {
|
|
1009
|
+
if (node.name === 'undefined') return undefined
|
|
1010
|
+
const entry = scope.get(node.name) ?? null
|
|
1011
|
+
if (isScopeEntry(entry)) {
|
|
1012
|
+
if (mfs && entry.mfSrcs.length > 0) mfs.push(...entry.mfSrcs)
|
|
1013
|
+
return entry.combi
|
|
1014
|
+
}
|
|
1015
|
+
if (isStaticValueEntry(entry)) {
|
|
1016
|
+
if (mfs && entry.mfSrcs.length > 0) mfs.push(...entry.mfSrcs)
|
|
1017
|
+
return entry.value
|
|
1018
|
+
}
|
|
1019
|
+
return entry
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// MemberExpression — handles g.ruleName references inside parser() factories
|
|
1023
|
+
if (node.type === 'MemberExpression') {
|
|
1024
|
+
const mem = node as unknown as StaticMemberExpression
|
|
1025
|
+
const obj = anyValue(mem.object as Expression, scope, code, mfs)
|
|
1026
|
+
if (!obj || typeof obj !== 'object') return null
|
|
1027
|
+
if ((node as unknown as { computed: boolean }).computed) {
|
|
1028
|
+
const key = anyValue((node as unknown as { property: Expression }).property, scope, code, mfs)
|
|
1029
|
+
if (typeof key !== 'string' && typeof key !== 'number') return null
|
|
1030
|
+
return (obj as Record<string | number, unknown>)[key] ?? null
|
|
1031
|
+
}
|
|
1032
|
+
// A member ACCESS name (`obj.foo`), not an object-literal key — a different thing
|
|
1033
|
+
// from `propName`, and named apart from it so it cannot shadow the shared reader.
|
|
1034
|
+
const memberName = (mem.property as { name?: string }).name
|
|
1035
|
+
if (!memberName) return null
|
|
1036
|
+
return (obj as Record<string, unknown>)[memberName] ?? null
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (node.type === 'CallExpression') {
|
|
1040
|
+
const callee = node.callee
|
|
1041
|
+
if (callee.type === 'Identifier' && callee.name === 'makeWord') {
|
|
1042
|
+
return wordFactoryFromArgs(node.arguments, scope, code, mfs)
|
|
1043
|
+
}
|
|
1044
|
+
if (callee.type === 'Identifier' && callee.name === 'makeWhen') {
|
|
1045
|
+
return whenFactoryFromArgs(node.arguments, scope, code, mfs)
|
|
1046
|
+
}
|
|
1047
|
+
if (callee.type === 'Identifier' && (callee.name === 'startsWith' || callee.name === 'endsWith')) {
|
|
1048
|
+
if (node.arguments.length !== 1 || node.arguments[0]?.type === 'SpreadElement') return null
|
|
1049
|
+
const value = anyValue(node.arguments[0] as Expression, scope, code, mfs)
|
|
1050
|
+
if (typeof value !== 'string') return null
|
|
1051
|
+
return callee.name === 'startsWith' ? parseman.startsWith(value) : parseman.endsWith(value)
|
|
1052
|
+
}
|
|
1053
|
+
if (callee.type === 'Identifier' && callee.name === 'matches') {
|
|
1054
|
+
if (node.arguments.length !== 1 || node.arguments[0]?.type === 'SpreadElement') return null
|
|
1055
|
+
const value = anyValue(node.arguments[0] as Expression, scope, code, mfs)
|
|
1056
|
+
if (!(value instanceof RegExp)) return null
|
|
1057
|
+
return parseman.matches(value)
|
|
1058
|
+
}
|
|
1059
|
+
if (callee.type === 'Identifier' && (callee.name === 'when' || callee.name === 'otherwise')) {
|
|
1060
|
+
return dispatchArmValue(node, scope, code, mfs)
|
|
1061
|
+
}
|
|
1062
|
+
if (callee.type === 'Identifier' && isWhenFactory(scope.get(callee.name))) {
|
|
1063
|
+
return dispatchArmValue(node, scope, code, mfs)
|
|
1064
|
+
}
|
|
1065
|
+
return exprToCombi(node, scope, code, mfs)
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
return null
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// ---------------------------------------------------------------------------
|
|
1072
|
+
// Public API
|
|
1073
|
+
// ---------------------------------------------------------------------------
|
|
1074
|
+
|
|
1075
|
+
/** Evaluate makeWord(boundary?, opts?) to a factory entry (not a combinator). */
|
|
1076
|
+
export function evaluateWordFactory(
|
|
1077
|
+
node: Expression,
|
|
1078
|
+
scope: Scope,
|
|
1079
|
+
code?: string,
|
|
1080
|
+
): WordFactoryEntry | null {
|
|
1081
|
+
if (node.type !== 'CallExpression') return null
|
|
1082
|
+
const callee = node.callee
|
|
1083
|
+
if (callee.type !== 'Identifier' || callee.name !== 'makeWord') return null
|
|
1084
|
+
return wordFactoryFromArgs(node.arguments, scope as XScope, code)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** Evaluate makeWhen(opts?) to a dispatch-arm factory entry (not a combinator). */
|
|
1088
|
+
export function evaluateWhenFactory(
|
|
1089
|
+
node: Expression,
|
|
1090
|
+
scope: Scope,
|
|
1091
|
+
code?: string,
|
|
1092
|
+
): WhenFactoryEntry | null {
|
|
1093
|
+
if (node.type !== 'CallExpression') return null
|
|
1094
|
+
const callee = node.callee
|
|
1095
|
+
if (callee.type !== 'Identifier' || callee.name !== 'makeWhen') return null
|
|
1096
|
+
return whenFactoryFromArgs(node.arguments, scope as XScope, code)
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/** Evaluate a single combinator expression. Returns null if unresolvable. */
|
|
1100
|
+
export function evaluateExpr(
|
|
1101
|
+
node: Expression,
|
|
1102
|
+
scope: Scope,
|
|
1103
|
+
code?: string,
|
|
1104
|
+
mapFnSources?: string[],
|
|
1105
|
+
): Combinator<unknown> | null {
|
|
1106
|
+
return exprToCombi(node, scope as XScope, code, mapFnSources)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Evaluate a `const X = [combinator, …]` array literal into an array of
|
|
1111
|
+
* Combinators. Lets a shared option array (e.g. a `skip` set reused across
|
|
1112
|
+
* `scanTo`/`balanced` calls) be referenced by name — `{ skip: X }` — instead of
|
|
1113
|
+
* inlining the array at every call site. Returns null when `node` isn't an array
|
|
1114
|
+
* literal of statically-resolvable combinators.
|
|
1115
|
+
*/
|
|
1116
|
+
export function evaluateCombinatorArray(
|
|
1117
|
+
node: Expression,
|
|
1118
|
+
scope: Scope,
|
|
1119
|
+
code?: string,
|
|
1120
|
+
): Combinator<unknown>[] | null {
|
|
1121
|
+
if (node.type !== 'ArrayExpression') return null
|
|
1122
|
+
const val = anyValue(node, scope as XScope, code, [])
|
|
1123
|
+
if (!Array.isArray(val) || val.length === 0) return null
|
|
1124
|
+
if (!val.every(isCombinator)) return null
|
|
1125
|
+
return val as Combinator<unknown>[]
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// ---------------------------------------------------------------------------
|
|
1129
|
+
// A rules() factory's returned object is a flat map of `key: combinator` — the
|
|
1130
|
+
// ONLY composition mechanism is compose() (see linker.ts). `...frag(g)` spreads
|
|
1131
|
+
// are not supported: a spread property makes the factory non-statically-evaluable
|
|
1132
|
+
// (propName returns null below), so it falls back to the interpreter.
|
|
1133
|
+
// ---------------------------------------------------------------------------
|
|
1134
|
+
|
|
1135
|
+
/** Collect every rule key from a rules() return object. A non-`key: value`
|
|
1136
|
+
* property (spread / computed / rest) → null → the caller falls back. */
|
|
1137
|
+
function collectRuleKeys(retObj: ObjectExpression): string[] | null {
|
|
1138
|
+
const out: string[] = []
|
|
1139
|
+
for (const prop of (retObj as unknown as { properties: Array<{ type: string }> }).properties) {
|
|
1140
|
+
const key = propName(prop as never)
|
|
1141
|
+
if (!key) return null
|
|
1142
|
+
out.push(key)
|
|
1143
|
+
}
|
|
1144
|
+
return out
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
export function evaluateStaticValue(
|
|
1148
|
+
node: Expression,
|
|
1149
|
+
scope: Scope,
|
|
1150
|
+
code?: string,
|
|
1151
|
+
): unknown {
|
|
1152
|
+
return anyValue(node, scope as XScope, code, [])
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
type RuleEntry = { key: string; value: Expression; scope: XScope; code: string }
|
|
1156
|
+
|
|
1157
|
+
/** Flatten a rules() return object into ordered (key, valueExpr, evalScope).
|
|
1158
|
+
* A non-`key: value` property → null → interpreter fallback. */
|
|
1159
|
+
function flattenRuleEntries(retObj: ObjectExpression, scope: XScope, code: string): RuleEntry[] | null {
|
|
1160
|
+
const out: RuleEntry[] = []
|
|
1161
|
+
for (const prop of (retObj as unknown as { properties: Array<{ type: string; value?: unknown }> }).properties) {
|
|
1162
|
+
const key = propName(prop as never)
|
|
1163
|
+
if (!key) return null
|
|
1164
|
+
out.push({ key, value: (prop as { value: Expression }).value, scope, code })
|
|
1165
|
+
}
|
|
1166
|
+
return out
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** Every `Identifier` name appearing anywhere under an expression node. */
|
|
1170
|
+
function identifierNamesIn(node: unknown, out: Set<string> = new Set()): Set<string> {
|
|
1171
|
+
if (!node || typeof node !== 'object') return out
|
|
1172
|
+
if (Array.isArray(node)) { for (const n of node) identifierNamesIn(n, out); return out }
|
|
1173
|
+
const rec = node as Record<string, unknown>
|
|
1174
|
+
if (rec.type === 'Identifier' && typeof rec.name === 'string') out.add(rec.name)
|
|
1175
|
+
for (const k of Object.keys(rec)) {
|
|
1176
|
+
if (k === 'type' || k === 'start' || k === 'end') continue
|
|
1177
|
+
identifierNamesIn(rec[k], out)
|
|
1178
|
+
}
|
|
1179
|
+
return out
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Evaluate a factory body's `const` declarations into `scope`. Returns false on failure,
|
|
1184
|
+
* and writes a specific reason into `out.reason`.
|
|
1185
|
+
*
|
|
1186
|
+
* The reason matters more than it looks. A body binding that fails takes the whole
|
|
1187
|
+
* factory down, and the caller's generic "isn't statically evaluable" — or, through
|
|
1188
|
+
* `composeLeaf`, a message about the ARGUMENT SHAPE — points nowhere near the cause.
|
|
1189
|
+
* The dominant real cause is a forward reference: `const A = node('A', B, …)` above
|
|
1190
|
+
* `const B = …`. That is a JavaScript temporal dead zone, not a macro limitation — the
|
|
1191
|
+
* interpreter throws `ReferenceError: Cannot access 'B' before initialization` on the
|
|
1192
|
+
* very same source — so the macro should say so as plainly as the interpreter does.
|
|
1193
|
+
* `g.B` is order-free (the proxy mints a ref and defines it in phase 2), which is why
|
|
1194
|
+
* converting a `g.` reference to a bare const can only move DOWN the file.
|
|
1195
|
+
*/
|
|
1196
|
+
function evalBodyStatements(
|
|
1197
|
+
statements: VariableDeclaration[],
|
|
1198
|
+
scope: XScope,
|
|
1199
|
+
code: string,
|
|
1200
|
+
out?: { reason?: string },
|
|
1201
|
+
): boolean {
|
|
1202
|
+
const bodyMfs: string[] = []
|
|
1203
|
+
// Names bound later in this body, for forward-reference attribution.
|
|
1204
|
+
const laterNames: Array<Set<string>> = []
|
|
1205
|
+
{
|
|
1206
|
+
const all: string[][] = statements.map(stmt =>
|
|
1207
|
+
stmt.declarations.map(d => ((d as unknown as VariableDeclarator).id as unknown as { name?: string }).name ?? '')
|
|
1208
|
+
)
|
|
1209
|
+
const flat = all.flat()
|
|
1210
|
+
let seen = 0
|
|
1211
|
+
for (const names of all) {
|
|
1212
|
+
laterNames.push(new Set(flat.slice(seen + names.length)))
|
|
1213
|
+
seen += names.length
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
let si = -1
|
|
1217
|
+
for (const stmt of statements) {
|
|
1218
|
+
si++
|
|
1219
|
+
for (const d of stmt.declarations) {
|
|
1220
|
+
const decl = d as unknown as VariableDeclarator
|
|
1221
|
+
const id = decl.id as unknown as { type: string; name?: string }
|
|
1222
|
+
const name = id.name ?? '<destructured>'
|
|
1223
|
+
if (!decl.init) { if (out) out.reason = `\`${name}\` has no initializer`; return false }
|
|
1224
|
+
if (id.type !== 'Identifier' && id.type !== 'BindingIdentifier') {
|
|
1225
|
+
if (out) out.reason = 'a destructuring binding in the factory body'
|
|
1226
|
+
return false
|
|
1227
|
+
}
|
|
1228
|
+
const before = bodyMfs.length
|
|
1229
|
+
const val = anyValue(decl.init as unknown as Expression, scope, code, bodyMfs)
|
|
1230
|
+
if (val === null) {
|
|
1231
|
+
if (out) {
|
|
1232
|
+
const used = identifierNamesIn(decl.init)
|
|
1233
|
+
const forward = [...(laterNames[si] ?? [])].filter(n => n !== '' && used.has(n))
|
|
1234
|
+
out.reason = forward.length > 0
|
|
1235
|
+
? `\`${name}\` references ${forward.map(n => `\`${n}\``).join(', ')} before ${forward.length > 1 ? 'their declarations' : 'its declaration'}`
|
|
1236
|
+
+ ` — a temporal dead zone (the interpreter throws "Cannot access '${forward[0]}' before initialization" on this source too);`
|
|
1237
|
+
+ ` move the declaration above \`${name}\`, or use \`g.${forward[0]}\`, which is order-free`
|
|
1238
|
+
: `\`${name}\` isn't a statically-evaluable combinator`
|
|
1239
|
+
}
|
|
1240
|
+
return false
|
|
1241
|
+
}
|
|
1242
|
+
const thisDeclMfSrcs = bodyMfs.slice(before)
|
|
1243
|
+
if (isCombinator(val)) scope.set(name, { combi: val, mfSrcs: thisDeclMfSrcs } satisfies ScopeEntry)
|
|
1244
|
+
else if (isDispatchArm(val)) scope.set(name, { value: val, mfSrcs: thisDeclMfSrcs } satisfies StaticValueEntry)
|
|
1245
|
+
else scope.set(name, val)
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return true
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Evaluate a `parser(g => { ... return { ruleName: combinator, ... } })` call.
|
|
1253
|
+
* Returns a map of rule names → defined Combinators, or null if the factory
|
|
1254
|
+
* can't be statically evaluated.
|
|
1255
|
+
*
|
|
1256
|
+
* mapFnSources is populated with the sources for mapFns that codegen will push
|
|
1257
|
+
* when compiling each returned rule — each rule's entry in the map will produce
|
|
1258
|
+
* a sub-slice of mapFnSources aligned to its specific ctx.mapFns.
|
|
1259
|
+
*
|
|
1260
|
+
* Important: this function uses a SEPARATE accumulator for body statement
|
|
1261
|
+
* evaluation so that only the return-expression phase adds entries to the
|
|
1262
|
+
* caller-provided mapFnSources (which is what compile() will receive).
|
|
1263
|
+
* The body-phase entries are stored as `mfSrcs` on localScope entries and
|
|
1264
|
+
* replayed when those entries are referenced during return evaluation.
|
|
1265
|
+
*/
|
|
1266
|
+
export function evaluateParserFactory(
|
|
1267
|
+
factoryNode: Expression,
|
|
1268
|
+
scope: Scope,
|
|
1269
|
+
code: string,
|
|
1270
|
+
mapFnSources: string[], // receives ONLY the return-expression mfSrcs
|
|
1271
|
+
out?: { reason?: string }, // receives a SPECIFIC failure reason (see evalBodyStatements)
|
|
1272
|
+
/**
|
|
1273
|
+
* The `rules({ … }, factory)` options this call site declared, THREADED THROUGH
|
|
1274
|
+
* rather than reapplied afterwards.
|
|
1275
|
+
*
|
|
1276
|
+
* `plugin/index.ts` used to stamp `grammarScanSkip`, `grammarHostMode` and
|
|
1277
|
+
* `grammarTrackLines` onto the evaluated rules in three loops of its own,
|
|
1278
|
+
* carrying a comment that it had to "because the macro evaluates the FACTORY
|
|
1279
|
+
* directly and never calls `rules()`". It calls `rules()` now, so the options
|
|
1280
|
+
* belong where every other caller puts them: in the argument. That also gets
|
|
1281
|
+
* the `trackLines` half right for the first time — `rules()` does not merely
|
|
1282
|
+
* stamp it, it WRAPS each rule in a `grammarParser({ trackLines: true })` scope
|
|
1283
|
+
* (`parser.ts:228-242`), which the macro's stamp-only copy never did.
|
|
1284
|
+
*/
|
|
1285
|
+
options?: RulesOptions,
|
|
1286
|
+
): Map<string, Combinator<unknown>> | null {
|
|
1287
|
+
if (factoryNode.type !== 'ArrowFunctionExpression' && factoryNode.type !== 'FunctionDeclaration' && factoryNode.type !== 'FunctionExpression') return null
|
|
1288
|
+
|
|
1289
|
+
const factory = factoryNode as unknown as ArrowFunctionExpression | OxcFunction
|
|
1290
|
+
const params = factory.params
|
|
1291
|
+
if (params.length !== 1) return null
|
|
1292
|
+
const param = params[0] as unknown as { type: string; name?: string }
|
|
1293
|
+
// FormalParameter is { decorators? } & BindingPattern — BindingIdentifier has type "Identifier"
|
|
1294
|
+
const proxyName = param.type === 'Identifier' ? param.name ?? null : null
|
|
1295
|
+
if (!proxyName) return null
|
|
1296
|
+
|
|
1297
|
+
const body = factory.body
|
|
1298
|
+
if (!body) return null
|
|
1299
|
+
const statements: VariableDeclaration[] = []
|
|
1300
|
+
let returnExpr: Expression | null = null
|
|
1301
|
+
|
|
1302
|
+
if ((body as unknown as { type: string }).type === 'BlockStatement') {
|
|
1303
|
+
const stmts = (body as unknown as { body: unknown[] }).body
|
|
1304
|
+
for (const stmt of stmts) {
|
|
1305
|
+
const s = stmt as { type: string }
|
|
1306
|
+
if (s.type === 'ReturnStatement') {
|
|
1307
|
+
returnExpr = ((s as unknown as ReturnStatement).argument ?? null) as Expression | null
|
|
1308
|
+
break
|
|
1309
|
+
}
|
|
1310
|
+
if (s.type === 'VariableDeclaration') {
|
|
1311
|
+
statements.push(s as unknown as VariableDeclaration)
|
|
1312
|
+
} else {
|
|
1313
|
+
return null // unsupported statement type
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
} else {
|
|
1317
|
+
// Concise arrow body: g => ({ ... })
|
|
1318
|
+
returnExpr = body as unknown as Expression
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (!returnExpr) return null
|
|
1322
|
+
|
|
1323
|
+
// Unwrap parenthesized expression if needed
|
|
1324
|
+
const retObj = returnExpr.type === 'ParenthesizedExpression'
|
|
1325
|
+
? (returnExpr as unknown as { expression: Expression }).expression
|
|
1326
|
+
: returnExpr
|
|
1327
|
+
if (retObj.type !== 'ObjectExpression') return null
|
|
1328
|
+
|
|
1329
|
+
// Pre-scan the return object for rule names. A non-`key: value` property (spread,
|
|
1330
|
+
// computed, rest) → null → the caller falls back to the interpreter. One ref per
|
|
1331
|
+
// UNIQUE key (first occurrence).
|
|
1332
|
+
const keys = collectRuleKeys(retObj as unknown as ObjectExpression)
|
|
1333
|
+
if (!keys) return null
|
|
1334
|
+
|
|
1335
|
+
/*
|
|
1336
|
+
* ── ONE GRAMMAR-EVALUATION PATH ────────────────────────────────────────────
|
|
1337
|
+
*
|
|
1338
|
+
* Everything below this point used to be a SECOND IMPLEMENTATION of `rules()`:
|
|
1339
|
+
* mint a `ref()` per key, build a `g` proxy that hands back a placeholder for
|
|
1340
|
+
* any name, evaluate, define each slot, tag each rule. `rules()`
|
|
1341
|
+
* (`combinators/parser.ts:136`) does exactly that, and the two had drifted —
|
|
1342
|
+
* the copy never ran the closing `markUnusedValues`, so every macro-lowered
|
|
1343
|
+
* grammar reached the encoder with `valueUnused` unset and the shipped artifact
|
|
1344
|
+
* built 318 sequence tuples and 90 repeat arrays per parse of `benchmark.less`
|
|
1345
|
+
* that nothing reads. That was fixed by calling the real pass; this removes the
|
|
1346
|
+
* copy that made the omission possible, so there is nothing left to omit.
|
|
1347
|
+
*
|
|
1348
|
+
* `rules()` takes a FACTORY, which is precisely what this function has — not as
|
|
1349
|
+
* a JS closure, but as an AST it can evaluate on demand. So the collapse is to
|
|
1350
|
+
* hand `rules()` a closure that evaluates that AST against whatever proxy
|
|
1351
|
+
* `rules()` supplies, and let `rules()` own every step it already owned:
|
|
1352
|
+
*
|
|
1353
|
+
* - the `g` proxy, INCLUDING the external-ref behaviour. `rules()`'s proxy
|
|
1354
|
+
* mints a tagged placeholder for ANY name touched, which is what the local
|
|
1355
|
+
* `externalRefs` map was reproducing — a `g.X` this grammar references but
|
|
1356
|
+
* does not define, bound later by the fuse.
|
|
1357
|
+
* - the define loop, the self-alias check, `tagRule` (byte-identical to the
|
|
1358
|
+
* `tagRef` that lived here, `_ruleName` plus the untyped-`node()` type).
|
|
1359
|
+
* - the ambient `trivia` / `scanSkip` stamps and the `hostMode` / `trackLines`
|
|
1360
|
+
* stamps, which `plugin/index.ts` was applying itself in three more loops
|
|
1361
|
+
* with a comment saying it had to "because the macro never calls `rules()`".
|
|
1362
|
+
* - `markUnusedValues`, `RULE_ORDER`, and the grammar reflection.
|
|
1363
|
+
*
|
|
1364
|
+
* WHAT THIS CHANGES ABOUT THE RESULT, deliberately: a key the factory never
|
|
1365
|
+
* referenced through `g` now comes back as the parser itself rather than a
|
|
1366
|
+
* `lazy` wrapping it, because that is what `rules()` produces and the runtime
|
|
1367
|
+
* shape is the one the encoder is measured good on. A key that IS referenced
|
|
1368
|
+
* still comes back as its placeholder, so recursion is unchanged.
|
|
1369
|
+
*
|
|
1370
|
+
* A failure inside the factory cannot `return null` from here — it is running
|
|
1371
|
+
* under `rules()` — so it throws `ABORT` and is caught below. `rules()`'s own
|
|
1372
|
+
* self-alias `Error` is caught by the same handler, preserving this function's
|
|
1373
|
+
* "return null and let the caller fall back to the interpreter" contract rather
|
|
1374
|
+
* than turning a tolerated shape into a build failure.
|
|
1375
|
+
*/
|
|
1376
|
+
const ABORT = Symbol('parseman: factory not statically evaluable')
|
|
1377
|
+
let built: Record<string, Combinator<unknown>>
|
|
1378
|
+
try {
|
|
1379
|
+
built = rules(options ?? {}, (g: Record<string, Combinator<unknown>>) => {
|
|
1380
|
+
// Outer ScopeEntry values carry their mfSrcs and are replayed by scopeGet()
|
|
1381
|
+
// when body statements or return expressions reference them.
|
|
1382
|
+
const localScope: XScope = new Map(scope as XScope)
|
|
1383
|
+
localScope.set(proxyName, g)
|
|
1384
|
+
|
|
1385
|
+
// ── Phase 1: the factory's own body statements ────────────────────────
|
|
1386
|
+
if (!evalBodyStatements(statements, localScope, code, out)) throw ABORT
|
|
1387
|
+
|
|
1388
|
+
// ── Phase 2: flatten the return object → dedup last-wins → evaluate ────
|
|
1389
|
+
// `flattenRuleEntries` returns the ordered (key, valueExpr, scope). A later
|
|
1390
|
+
// property of the same name wins.
|
|
1391
|
+
const entries = flattenRuleEntries(retObj as unknown as ObjectExpression, localScope, code)
|
|
1392
|
+
if (!entries) throw ABORT
|
|
1393
|
+
const finalByKey = new Map<string, RuleEntry>()
|
|
1394
|
+
for (const e of entries) finalByKey.set(e.key, e) // keeps first position, updates value → last wins
|
|
1395
|
+
const definitions: Record<string, Combinator<unknown>> = {}
|
|
1396
|
+
for (const [key, e] of finalByKey) {
|
|
1397
|
+
const val = anyValue(e.value, e.scope, e.code, mapFnSources)
|
|
1398
|
+
if (!isCombinator(val)) throw ABORT
|
|
1399
|
+
definitions[key] = val as Combinator<unknown>
|
|
1400
|
+
}
|
|
1401
|
+
return definitions
|
|
1402
|
+
}) as unknown as Record<string, Combinator<unknown>>
|
|
1403
|
+
} catch (e) {
|
|
1404
|
+
if (e === ABORT) return null
|
|
1405
|
+
// `rules()` throws on a rule that is a direct alias to itself. This function's
|
|
1406
|
+
// contract is `null` — "leave it interpreted" — not a thrown build failure, and
|
|
1407
|
+
// the interpreter accepts the same shape, so the caller's existing fallback is
|
|
1408
|
+
// the right answer. Anything else is a real defect and must not be swallowed.
|
|
1409
|
+
if (e instanceof Error && /cannot be a direct alias to itself/.test(e.message)) return null
|
|
1410
|
+
throw e
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/*
|
|
1414
|
+
* DECLARED KEYS ONLY, in DECLARATION order.
|
|
1415
|
+
*
|
|
1416
|
+
* `rules()` returns its whole cache, which also holds a placeholder for every
|
|
1417
|
+
* EXTERNAL name the factory touched — a `g.X` provided by another piece. Those
|
|
1418
|
+
* are references, not rules of this map, and handing them back would mint
|
|
1419
|
+
* `rule:` ids this grammar does not define, widen the coverage denominator and
|
|
1420
|
+
* put an undefined slot in the emitted map. The previous implementation
|
|
1421
|
+
* returned only its own `ruleRefs` for the same reason; this preserves that
|
|
1422
|
+
* contract exactly while letting `rules()` own everything else.
|
|
1423
|
+
*/
|
|
1424
|
+
const map = new Map<string, Combinator<unknown>>()
|
|
1425
|
+
for (const key of keys) map.set(key, built[key]!)
|
|
1426
|
+
return map
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/** A combinator slot created by ref() — has a callable `define`. */
|
|
1430
|
+
type DefinableRef = Combinator<unknown> & { define(p: Combinator<unknown>): void }
|
|
1431
|
+
|
|
1432
|
+
function isDefinableRef(v: unknown): v is DefinableRef {
|
|
1433
|
+
return isCombinator(v)
|
|
1434
|
+
&& typeof (v as { define?: unknown }).define === 'function'
|
|
1435
|
+
&& (v as { _def: { tag?: string } })._def.tag === 'lazy'
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* If `init` is a bare `ref()` call, evaluate it to a real ref placeholder and
|
|
1440
|
+
* register it in scope under `name`. Returns the ref, or null if `init` isn't
|
|
1441
|
+
* a `ref()` call. Used by the macro pre-pass so standalone refs resolve before
|
|
1442
|
+
* compilation (parity with the interpreter / compile()).
|
|
1443
|
+
*/
|
|
1444
|
+
export function evaluateRefDeclaration(
|
|
1445
|
+
init: Expression,
|
|
1446
|
+
name: string,
|
|
1447
|
+
scope: Scope,
|
|
1448
|
+
): DefinableRef | null {
|
|
1449
|
+
if (init.type !== 'CallExpression') return null
|
|
1450
|
+
const callee = (init as unknown as { callee: { type: string; name?: string } }).callee
|
|
1451
|
+
if (callee.type !== 'Identifier' || callee.name !== 'ref') return null
|
|
1452
|
+
if ((init as unknown as { arguments: unknown[] }).arguments.length !== 0) return null
|
|
1453
|
+
const slot = ref<unknown>() as DefinableRef
|
|
1454
|
+
;(scope as XScope).set(name, { combi: slot, mfSrcs: [] } satisfies ScopeEntry)
|
|
1455
|
+
return slot
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Apply a `someRef.define(expr)` statement: resolve the target ref from scope,
|
|
1460
|
+
* evaluate the argument to a combinator, and call `.define()`. Returns true on
|
|
1461
|
+
* success. The macro removes the original statement from the output (it would
|
|
1462
|
+
* otherwise reference the stripped import); returning false signals "leave it".
|
|
1463
|
+
*/
|
|
1464
|
+
export function applyDefineStatement(
|
|
1465
|
+
callExpr: Expression,
|
|
1466
|
+
scope: Scope,
|
|
1467
|
+
code: string,
|
|
1468
|
+
): boolean {
|
|
1469
|
+
if (callExpr.type !== 'CallExpression') return false
|
|
1470
|
+
const callee = (callExpr as unknown as { callee: { type: string } }).callee
|
|
1471
|
+
if (callee.type !== 'MemberExpression') return false
|
|
1472
|
+
const mem = callee as unknown as { object: { type: string; name?: string }; property: { type: string; name?: string }; computed: boolean }
|
|
1473
|
+
if (mem.computed) return false
|
|
1474
|
+
if (mem.property.type !== 'Identifier' || mem.property.name !== 'define') return false
|
|
1475
|
+
if (mem.object.type !== 'Identifier' || !mem.object.name) return false
|
|
1476
|
+
|
|
1477
|
+
const target = (scope as XScope).get(mem.object.name)
|
|
1478
|
+
const refCombi = isScopeEntry(target) ? target.combi : (isCombinator(target) ? target : null)
|
|
1479
|
+
if (!refCombi || !isDefinableRef(refCombi)) return false
|
|
1480
|
+
|
|
1481
|
+
const args = (callExpr as unknown as { arguments: Array<{ type: string }> }).arguments
|
|
1482
|
+
if (args.length !== 1 || args[0]!.type === 'SpreadElement') return false
|
|
1483
|
+
const inner = anyValue(args[0] as unknown as Expression, scope as XScope, code, [])
|
|
1484
|
+
if (!isCombinator(inner)) return false
|
|
1485
|
+
|
|
1486
|
+
try { refCombi.define(inner) } catch { return false }
|
|
1487
|
+
return true
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/** Check if an AST node references any name from the given scope or names set. */
|
|
1491
|
+
export function referencesAny(node: Node, names: Set<string>, scope: Scope): boolean {
|
|
1492
|
+
if (node.type === 'Identifier') {
|
|
1493
|
+
return names.has(node.name) || scope.has(node.name)
|
|
1494
|
+
}
|
|
1495
|
+
for (const key of Object.keys(node) as (keyof typeof node)[]) {
|
|
1496
|
+
const child = node[key]
|
|
1497
|
+
if (!child || typeof child !== 'object') continue
|
|
1498
|
+
if (Array.isArray(child)) {
|
|
1499
|
+
if (child.some(c => c && typeof c === 'object' && 'type' in c && referencesAny(c as Node, names, scope))) return true
|
|
1500
|
+
} else if ('type' in child) {
|
|
1501
|
+
if (referencesAny(child as Node, names, scope)) return true
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return false
|
|
1505
|
+
}
|