parseman 0.52.0 → 0.54.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/dist/analysis/diagnostics.js +636 -126
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +6191 -6274
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/field-trivia.d.ts +40 -0
- package/dist/combinators/field-trivia.d.ts.map +1 -0
- package/dist/combinators/map.d.ts +19 -2
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/parser.d.ts +34 -0
- package/dist/combinators/parser.d.ts.map +1 -1
- package/dist/combinators/ref.d.ts +0 -13
- package/dist/combinators/ref.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/compiler/borrowed-rules.d.ts +30 -0
- package/dist/compiler/borrowed-rules.d.ts.map +1 -0
- package/dist/compiler/compile-linkable-table.d.ts +1 -1
- package/dist/compiler/fields.d.ts +2 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts +10 -4
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts +48 -91
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/cst/trivia-charscan.d.ts +11 -3
- package/dist/cst/trivia-charscan.d.ts.map +1 -1
- package/dist/cst/trivia-kinds.d.ts +11 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +764 -888
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js.map +1 -1
- package/dist/plugin/evaluator.d.ts +38 -2
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1443 -729
- package/dist/plugin/index.js.map +3 -3
- package/dist/plugin/ir-eval.d.ts +21 -0
- package/dist/plugin/ir-eval.d.ts.map +1 -0
- package/dist/plugin/reducer-resolver.d.ts +6 -0
- package/dist/plugin/reducer-resolver.d.ts.map +1 -1
- package/dist/spec/index.js +555 -6109
- package/dist/spec/index.js.map +3 -3
- package/dist/spec/railroad-lib.d.ts +7 -2
- package/dist/spec/railroad-lib.d.ts.map +1 -1
- package/dist/spec/railroad.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +9 -0
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +11 -4
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/compile.d.ts +16 -0
- package/dist/table/compile.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +1 -1
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts +6 -2
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/field-spec.d.ts +13 -0
- package/dist/table/field-spec.d.ts.map +1 -0
- package/dist/table/index.js +480 -199
- package/dist/table/index.js.map +2 -2
- package/dist/table/scalar-terminal.d.ts +11 -0
- package/dist/table/scalar-terminal.d.ts.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/analysis/duplication.ts +1 -1
- package/src/analysis/rebuild.ts +1 -1
- package/src/combinators/field-trivia.ts +44 -0
- package/src/combinators/map.ts +48 -6
- package/src/combinators/parser.ts +133 -67
- package/src/combinators/ref.ts +10 -1
- package/src/combinators/repeat.ts +6 -1
- package/src/combinators/sequence.ts +9 -1
- package/src/compiler/borrowed-rules.ts +88 -0
- package/src/compiler/compile-linkable-table.ts +1 -1
- package/src/compiler/fields.ts +5 -2
- package/src/compiler/grammar-coverage-ids.ts +35 -6
- package/src/compiler/ir-serialize.ts +19 -119
- package/src/compiler/linker.ts +190 -666
- package/src/coverage.ts +7 -5
- package/src/cst/reflection.ts +6 -1
- package/src/cst/trivia-charscan.ts +17 -56
- package/src/cst/trivia-kinds.ts +24 -8
- package/src/index.ts +1 -0
- package/src/plugin/evaluator.ts +114 -14
- package/src/plugin/index.ts +588 -110
- package/src/plugin/ir-eval.ts +165 -0
- package/src/plugin/reducer-resolver.ts +20 -3
- package/src/spec/railroad-lib.ts +18 -8
- package/src/spec/railroad.ts +3 -4
- package/src/table/assemble.ts +126 -22
- package/src/table/compile-rule-map.ts +18 -8
- package/src/table/compile.ts +61 -0
- package/src/table/emit-assembly.ts +38 -18
- package/src/table/emit.ts +15 -4
- package/src/table/encode.ts +18 -2
- package/src/table/exec.ts +15 -1
- package/src/table/field-spec.ts +17 -0
- package/src/table/scalar-terminal.ts +25 -0
- package/src/types.ts +12 -2
- package/src/version.ts +1 -1
package/src/plugin/index.ts
CHANGED
|
@@ -23,12 +23,12 @@ import { createUnplugin } from 'unplugin'
|
|
|
23
23
|
import { parseSync } from 'oxc-parser'
|
|
24
24
|
import { ResolverFactory } from 'oxc-resolver'
|
|
25
25
|
import MagicString from 'magic-string'
|
|
26
|
-
import { evaluateExpr, evaluateCombinatorArray, evaluateParserFactory, evaluateStaticValue, evaluateWordFactory, evaluateWhenFactory, evaluateRefDeclaration, applyDefineStatement, referencesAny, setReducerResolver, setBuilderImportResolver, propName, type Scope, type ScopeEntry } from './evaluator.ts'
|
|
26
|
+
import { evaluateExpr, evaluateCombinatorArray, evaluateParserFactory, evaluateStaticValue, evaluateWordFactory, evaluateWhenFactory, evaluateRefDeclaration, applyDefineStatement, referencesAny, setReducerResolver, setBuilderImportResolver, propName, ModuleScope, type FreeNameResolution, type Scope, type ScopeEntry } from './evaluator.ts'
|
|
27
27
|
import { classifyRuleMap } from '../analysis/commitment.ts'
|
|
28
28
|
import { compile } from '../table/compile.ts'
|
|
29
29
|
import { compileRuleMap } from '../table/compile-rule-map.ts'
|
|
30
30
|
import { compileLinkableTable } from '../compiler/compile-linkable-table.ts'
|
|
31
|
-
import { createReducerResolver } from './reducer-resolver.ts'
|
|
31
|
+
import { createReducerResolver, topLevelIsStable } from './reducer-resolver.ts'
|
|
32
32
|
import { findFreeIdentifiers } from './free-identifiers.ts'
|
|
33
33
|
import { supercompileRuleMapReplacement } from './entry-supercompiler.ts'
|
|
34
34
|
import {
|
|
@@ -37,7 +37,9 @@ import {
|
|
|
37
37
|
} from '../compiler/degradation.ts'
|
|
38
38
|
import type { HostMode } from '../cst/host-mode.ts'
|
|
39
39
|
import { COMPOSED_PIECES } from '../compiler/linker.ts'
|
|
40
|
-
import {
|
|
40
|
+
import { serializeCombinator, serializeRuleMap } from '../compiler/ir-serialize.ts'
|
|
41
|
+
import { evalCombinatorIR, evalRulesIR } from './ir-eval.ts'
|
|
42
|
+
import { linkRules, RULES_RECIPE, type LinkOptions, type RulesRecipe } from '../combinators/parser.ts'
|
|
41
43
|
import { PARSEMAN_VERSION } from '../version.ts'
|
|
42
44
|
import { grammarReflectionSource, type GrammarReflection } from '../cst/reflection.ts'
|
|
43
45
|
import { createHash } from 'node:crypto'
|
|
@@ -341,14 +343,44 @@ function topLevelInit(body: AnyNode[], name: string): AnyNode | null {
|
|
|
341
343
|
return null
|
|
342
344
|
}
|
|
343
345
|
|
|
344
|
-
/** `Symbol.for('
|
|
345
|
-
function
|
|
346
|
+
/** `Symbol.for('<key>')` ? */
|
|
347
|
+
function isSymbolFor(n: AnyNode | undefined, key: string): boolean {
|
|
346
348
|
if (!n || n.type !== 'CallExpression') return false
|
|
347
349
|
const callee = n.callee as AnyNode | undefined
|
|
348
350
|
const obj = (callee?.object as { name?: string } | undefined)?.name
|
|
349
351
|
const prop = (callee?.property as { name?: string } | undefined)?.name
|
|
350
352
|
const arg0 = (n.arguments as AnyNode[] | undefined)?.[0] as { value?: unknown } | undefined
|
|
351
|
-
return obj === 'Symbol' && prop === 'for' && arg0?.value ===
|
|
353
|
+
return obj === 'Symbol' && prop === 'for' && arg0?.value === key
|
|
354
|
+
}
|
|
355
|
+
const isComposedPiecesSymbol = (n: AnyNode | undefined): boolean => isSymbolFor(n, 'parseman.composedPieces')
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The key a compiled terminal carries its combinator IR under — see `withCombinatorIR`.
|
|
359
|
+
* EMITTED SOURCE TEXT, like the composed-pieces key: the runtime never reads it, only a
|
|
360
|
+
* downstream macro build does, by finding it in the artifact's AST.
|
|
361
|
+
*/
|
|
362
|
+
const COMBINATOR_IR_KEY = 'parseman.combinatorIR'
|
|
363
|
+
|
|
364
|
+
/** A compiled top-level const initializer's carried combinator IR, or null.
|
|
365
|
+
* The shape is exactly what `withCombinatorIR` emits:
|
|
366
|
+
* `Object.defineProperty(<parser>, Symbol.for('parseman.combinatorIR'), { value: '<ir>', … })`. */
|
|
367
|
+
function carriedCombinatorIR(init: AnyNode): string | null {
|
|
368
|
+
let n = init
|
|
369
|
+
while (n.type === 'ParenthesizedExpression') n = n.expression as AnyNode
|
|
370
|
+
if (n.type !== 'CallExpression') return null
|
|
371
|
+
const callee = n.callee as AnyNode
|
|
372
|
+
const args = n.arguments as AnyNode[]
|
|
373
|
+
if ((callee.object as { name?: string } | undefined)?.name !== 'Object'
|
|
374
|
+
|| (callee.property as { name?: string } | undefined)?.name !== 'defineProperty'
|
|
375
|
+
|| !isSymbolFor(args[1], COMBINATOR_IR_KEY)) return null
|
|
376
|
+
for (const p of (args[2]?.properties as AnyNode[] | undefined) ?? []) {
|
|
377
|
+
const v = p.value as { type?: string; value?: unknown; expressions?: unknown[]; quasis?: Array<{ value: { cooked?: string | null } }> } | undefined
|
|
378
|
+
if (propName(p as never) !== 'value') continue
|
|
379
|
+
if (v?.type === 'Literal' && typeof v.value === 'string') return v.value
|
|
380
|
+
// A bundler may requote it as a template (esbuild does when it holds both quote kinds).
|
|
381
|
+
if (v?.type === 'TemplateLiteral' && v.expressions?.length === 0) return v.quasis?.[0]?.value.cooked ?? null
|
|
382
|
+
}
|
|
383
|
+
return null
|
|
352
384
|
}
|
|
353
385
|
|
|
354
386
|
/** Walk an initializer subtree for the `Object.defineProperty(_,
|
|
@@ -367,9 +399,10 @@ function findCarriedPiecesLiteral(root: AnyNode): { start: number; end: number }
|
|
|
367
399
|
const args = node.arguments as AnyNode[] | undefined
|
|
368
400
|
// Table lowering: `tableRules(program, { [Symbol.for('…composedPieces')]:
|
|
369
401
|
// <literal>, … })`. The metadata object becomes the returned map's
|
|
370
|
-
// prototype, so the symbol remains readable without becoming an own key.
|
|
402
|
+
// prototype, so the symbol remains readable without becoming an own key. The
|
|
403
|
+
// callee's NAME is not checked: a bundler renames a clashing import
|
|
404
|
+
// (`tableRules2`), and the computed key below is what identifies the literal.
|
|
371
405
|
if ((callee as { type?: string; name?: string } | undefined)?.type === 'Identifier'
|
|
372
|
-
&& (callee as { name?: string }).name === 'tableRules'
|
|
373
406
|
&& (args?.[1] as AnyNode | undefined)?.type === 'ObjectExpression') {
|
|
374
407
|
for (const p of ((args![1] as AnyNode).properties as AnyNode[] | undefined) ?? []) {
|
|
375
408
|
if ((p as { computed?: boolean }).computed && isComposedPiecesSymbol(p.key as AnyNode) && p.value) {
|
|
@@ -555,16 +588,17 @@ function transformMacroImpl(
|
|
|
555
588
|
specifiers: Array<{ type: string; local: string; start: number; end: number }>
|
|
556
589
|
}> = []
|
|
557
590
|
|
|
591
|
+
// A macro import is a parseman-alias import carrying `with { type: 'macro' }`
|
|
592
|
+
// (oxc exposes this as ImportDeclaration.attributes).
|
|
593
|
+
const isMacroImport = (s: ImportDeclaration): boolean => s.attributes.some(a => {
|
|
594
|
+
const key = a.key.type === 'Identifier' ? a.key.name : String(a.key.value)
|
|
595
|
+
return key === 'type' && a.value.value === 'macro'
|
|
596
|
+
})
|
|
558
597
|
for (const stmt of body) {
|
|
559
598
|
if (stmt.type !== 'ImportDeclaration') continue
|
|
560
599
|
const s = stmt as ImportDeclaration
|
|
561
600
|
|
|
562
|
-
|
|
563
|
-
// (oxc exposes this as ImportDeclaration.attributes).
|
|
564
|
-
const isMacro = moduleAliases.has(s.source.value) && s.attributes.some(a => {
|
|
565
|
-
const key = a.key.type === 'Identifier' ? a.key.name : String(a.key.value)
|
|
566
|
-
return key === 'type' && a.value.value === 'macro'
|
|
567
|
-
})
|
|
601
|
+
const isMacro = moduleAliases.has(s.source.value) && isMacroImport(s)
|
|
568
602
|
|
|
569
603
|
if (!isMacro) {
|
|
570
604
|
const specifiers: Array<{ type: string; local: string; start: number; end: number }> = []
|
|
@@ -670,8 +704,265 @@ function transformMacroImpl(
|
|
|
670
704
|
setReducerResolver(reducerResolver, code)
|
|
671
705
|
/* A direct builder's free name is rescuable when THIS module imported it — the
|
|
672
706
|
* artifact carries `{ source, imported }` and a downstream compose() re-emits the
|
|
673
|
-
* import (see the re-lower pass below). Module-private consts stay refusals.
|
|
674
|
-
|
|
707
|
+
* import (see the re-lower pass below). Module-private consts stay refusals.
|
|
708
|
+
* While a terminal of ANOTHER module is evaluated, its own imports answer instead
|
|
709
|
+
* (`moduleConstValue`), so its builder's helpers are re-emitted here. */
|
|
710
|
+
let builderImports = (name: string): { source: string; imported: string } | null => importBindings.get(name) ?? null
|
|
711
|
+
setBuilderImportResolver(name => builderImports(name))
|
|
712
|
+
const ownRoot = nearestPackageRoot(id)
|
|
713
|
+
let ownDir = path.dirname(id)
|
|
714
|
+
try { ownDir = fs.realpathSync(ownDir) } catch { /* a virtual module: keep its path */ }
|
|
715
|
+
/** `spec`, imported by `from`, as THIS module must write it; null when it can't: a
|
|
716
|
+
* relative import of another package names a file this one has no stable path to. */
|
|
717
|
+
const rebaseSpecifier = (from: string, spec: string): string | null => {
|
|
718
|
+
if (!spec.startsWith('.')) return spec
|
|
719
|
+
const target = path.resolve(path.dirname(from), spec)
|
|
720
|
+
if (ownRoot === null || nearestPackageRoot(target) !== ownRoot) return null
|
|
721
|
+
const rel = path.relative(ownDir, target).split(path.sep).join('/')
|
|
722
|
+
return rel.startsWith('../') ? rel : `./${rel}`
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/*
|
|
726
|
+
* IMPORTED VALUES — a terminal, a skip set, an options object or a boundary string
|
|
727
|
+
* declared in ANOTHER module and used where the macro evaluates a grammar: a rule
|
|
728
|
+
* body, a combinator's arguments, a `rules({ trivia, scanSkip })` option.
|
|
729
|
+
*
|
|
730
|
+
* Before this, an imported name evaluated to `null`. In a rule body that made the
|
|
731
|
+
* factory "not statically evaluable"; in an option it was worse — the option became
|
|
732
|
+
* `undefined` and the build succeeded without it. So every grammar family re-declared
|
|
733
|
+
* its own copy of each shared terminal, because only a whole `rules()` map could
|
|
734
|
+
* cross a module boundary (as `compose()` pieces).
|
|
735
|
+
*
|
|
736
|
+
* Resolution reads the exporting module, and never runs it:
|
|
737
|
+
* - a relative import resolves to its SOURCE (`resolvePrivateSourceModule`, the
|
|
738
|
+
* same rule imported factories use), whose declaration is evaluated like a local;
|
|
739
|
+
* - a package import resolves to its PUBLISHED ENTRY, a compiled artifact, where a
|
|
740
|
+
* terminal survives only as a compiled parser. The macro therefore carries each
|
|
741
|
+
* such terminal's combinator IR on the exported value (`withCombinatorIR`), the
|
|
742
|
+
* way a grammar carries its composed pieces, and this reads it back. A plain
|
|
743
|
+
* value (a string, an options object, an array of terminals) is left verbatim by
|
|
744
|
+
* the macro and is evaluated as written.
|
|
745
|
+
*
|
|
746
|
+
* Values are rebuilt per transform, and within one transform two grammars returning
|
|
747
|
+
* the same value is safe: `rules()` stamps its options onto a rule's value, so it
|
|
748
|
+
* gives each grammar after the first an unstamped copy (`ownRule`, parser.ts).
|
|
749
|
+
*
|
|
750
|
+
* A binding that exists but cannot be resolved is recorded in `unresolvedImports`,
|
|
751
|
+
* and every place that fails for it fails the BUILD naming it (`failOnUnresolved`).
|
|
752
|
+
*/
|
|
753
|
+
const unresolvedImports = new Set<string>()
|
|
754
|
+
type ValueModule = {
|
|
755
|
+
file: string
|
|
756
|
+
src: string
|
|
757
|
+
body: AnyNode[]
|
|
758
|
+
consts: Map<string, AnyNode>
|
|
759
|
+
imports: Map<string, { source: string; imported: string }>
|
|
760
|
+
opaqueImports: Set<string>
|
|
761
|
+
scope?: ModuleScope
|
|
762
|
+
}
|
|
763
|
+
const valueModules = new Map<string, ValueModule | null>()
|
|
764
|
+
const OPAQUE_IMPORT = 'is a default or namespace import, which the macro does not resolve — import the binding by name'
|
|
765
|
+
const opaqueImportsOf = (moduleBody: AnyNode[]): Set<string> => {
|
|
766
|
+
const out = new Set<string>()
|
|
767
|
+
for (const st of moduleBody) {
|
|
768
|
+
if (st.type !== 'ImportDeclaration') continue
|
|
769
|
+
for (const sp of (st as unknown as ImportDeclaration).specifiers) {
|
|
770
|
+
if (sp.type !== 'ImportSpecifier') out.add(sp.local.name)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return out
|
|
774
|
+
}
|
|
775
|
+
const valueModule = (file: string): ValueModule | null => {
|
|
776
|
+
if (valueModules.has(file)) return valueModules.get(file)!
|
|
777
|
+
const mod = parseModuleCached(file)
|
|
778
|
+
let out: ValueModule | null = null
|
|
779
|
+
if (mod) {
|
|
780
|
+
const consts = new Map<string, AnyNode>()
|
|
781
|
+
// A `let`/`var` counts when nothing reassigns it: a bundled entry declares its
|
|
782
|
+
// exports `var dq = …; export { dq }`.
|
|
783
|
+
let stable: ((name: string) => boolean) | undefined
|
|
784
|
+
for (const st of mod.body as Statement[]) {
|
|
785
|
+
const vd = unwrapVd(st)
|
|
786
|
+
if (!vd) continue
|
|
787
|
+
const isConst = (vd as unknown as { kind?: string }).kind === 'const'
|
|
788
|
+
for (const d of vd.declarations) {
|
|
789
|
+
const idn = d.id as unknown as { type?: string; name?: string }
|
|
790
|
+
if (idn.type !== 'Identifier' || !idn.name || !d.init) continue
|
|
791
|
+
if (isConst || (stable ??= topLevelIsStable(mod.body, mod.src.length))(idn.name)) consts.set(idn.name, d.init as unknown as AnyNode)
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
out = {
|
|
795
|
+
file,
|
|
796
|
+
src: mod.src,
|
|
797
|
+
body: mod.body as AnyNode[],
|
|
798
|
+
consts,
|
|
799
|
+
imports: extractImportBindings(mod.body as AnyNode[]),
|
|
800
|
+
opaqueImports: opaqueImportsOf(mod.body as AnyNode[]),
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
valueModules.set(file, out)
|
|
804
|
+
return out
|
|
805
|
+
}
|
|
806
|
+
const resolveValueFile = (from: string, specifier: string): string | null => {
|
|
807
|
+
const source = resolvePrivateSourceModule(from, specifier)
|
|
808
|
+
if (source) return source
|
|
809
|
+
try { return getCompiledResolver().resolveFileSync(from, specifier).path ?? null } catch { return null }
|
|
810
|
+
}
|
|
811
|
+
const evaluatingConsts = new Set<string>()
|
|
812
|
+
/** Rewrite each carried builder import in `combi`'s graph (written in `from`) to THIS
|
|
813
|
+
* module's spelling; the first that can't be is returned, for the refusal. */
|
|
814
|
+
const rebaseCarriedBuilderImports = (combi: unknown, from: string): { local: string; source: string } | null => {
|
|
815
|
+
const seen = new Set<unknown>()
|
|
816
|
+
const walk = (v: unknown): { local: string; source: string } | null => {
|
|
817
|
+
if (v === null || typeof v !== 'object' || seen.has(v)) return null
|
|
818
|
+
seen.add(v)
|
|
819
|
+
const def = (v as { _def?: { buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }> } })._def
|
|
820
|
+
if (def?.buildImports) {
|
|
821
|
+
const out = []
|
|
822
|
+
for (const bi of def.buildImports) {
|
|
823
|
+
const source = rebaseSpecifier(from, bi.source)
|
|
824
|
+
if (source === null) return bi
|
|
825
|
+
out.push({ ...bi, source })
|
|
826
|
+
}
|
|
827
|
+
def.buildImports = out
|
|
828
|
+
}
|
|
829
|
+
for (const child of Object.values(v as Record<string, unknown>)) {
|
|
830
|
+
for (const c of Array.isArray(child) ? child : [child]) {
|
|
831
|
+
const stuck = walk(c)
|
|
832
|
+
if (stuck) return stuck
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return null
|
|
836
|
+
}
|
|
837
|
+
return walk(combi)
|
|
838
|
+
}
|
|
839
|
+
/** Evaluate one top-level `const` of a foreign module, as the macro would locally. */
|
|
840
|
+
const moduleConstValue = (m: ValueModule, name: string): FreeNameResolution => {
|
|
841
|
+
const key = `${m.file}\0${name}`
|
|
842
|
+
if (evaluatingConsts.has(key)) return { unresolved: `is part of a declaration cycle in ${m.file}` }
|
|
843
|
+
evaluatingConsts.add(key)
|
|
844
|
+
const outerBuilderImports = builderImports
|
|
845
|
+
try {
|
|
846
|
+
const init = m.consts.get(name)! as unknown as Expression
|
|
847
|
+
const ir = carriedCombinatorIR(init as unknown as AnyNode)
|
|
848
|
+
if (ir !== null) {
|
|
849
|
+
let combi: Combinator<unknown> | null = null
|
|
850
|
+
let why = ''
|
|
851
|
+
try { combi = evalCombinatorIR(ir) } catch (e) { why = `: ${(e as Error).message}` }
|
|
852
|
+
if (!combi) return { unresolved: `carries combinator IR that does not evaluate (${m.file})${why}` }
|
|
853
|
+
const stuck = rebaseCarriedBuilderImports(combi, m.file)
|
|
854
|
+
return stuck
|
|
855
|
+
? { unresolved: `has a node() builder reading \`${stuck.local}\` from '${stuck.source}' inside ${m.file}, which this module cannot import — import it there by package name` }
|
|
856
|
+
: { value: { combi, mfSrcs: [] } satisfies ScopeEntry }
|
|
857
|
+
}
|
|
858
|
+
const scope = moduleValueScope(m)
|
|
859
|
+
// Offsets inside this module's reducers must be answered against ITS scope tree.
|
|
860
|
+
reducerResolver.register(m.file)
|
|
861
|
+
// A builder's free names are THIS module's imports, re-spelled for the consumer.
|
|
862
|
+
builderImports = name => {
|
|
863
|
+
const b = m.imports.get(name)
|
|
864
|
+
const source = b ? rebaseSpecifier(m.file, b.source) : null
|
|
865
|
+
return b && source !== null ? { source, imported: b.imported } : null
|
|
866
|
+
}
|
|
867
|
+
const mfs: string[] = []
|
|
868
|
+
const combi = evaluateExpr(init, scope as unknown as Scope, m.src, mfs)
|
|
869
|
+
if (combi) return { value: { combi, mfSrcs: mfs } satisfies ScopeEntry }
|
|
870
|
+
const factory = evaluateWordFactory(init, scope as unknown as Scope, m.src) ?? evaluateWhenFactory(init, scope as unknown as Scope, m.src)
|
|
871
|
+
if (factory) return { value: factory }
|
|
872
|
+
const value = evaluateStaticValue(init, scope as unknown as Scope, m.src)
|
|
873
|
+
if (value !== null && value !== undefined || isStaticNullishExpression(init)) return { value }
|
|
874
|
+
return {
|
|
875
|
+
unresolved: /\btableRules\b/.test(m.src.slice(init.start, init.end))
|
|
876
|
+
? `is a compiled parser in ${m.file} that carries no combinator IR — rebuild that module with parseman v${PARSEMAN_VERSION}`
|
|
877
|
+
: `is not a statically-known combinator or value (${m.file})`,
|
|
878
|
+
}
|
|
879
|
+
} finally {
|
|
880
|
+
evaluatingConsts.delete(key)
|
|
881
|
+
builderImports = outerBuilderImports
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const moduleValueScope = (m: ValueModule): ModuleScope =>
|
|
885
|
+
m.scope ??= new ModuleScope(name => {
|
|
886
|
+
if (m.consts.has(name)) return moduleConstValue(m, name)
|
|
887
|
+
const binding = m.imports.get(name)
|
|
888
|
+
if (binding) return importedValue(m.file, binding)
|
|
889
|
+
return m.opaqueImports.has(name) ? { unresolved: OPAQUE_IMPORT } : null
|
|
890
|
+
}, unresolvedImports)
|
|
891
|
+
/** The value of export `name` of module `m`, following `export { x } from '…'`. */
|
|
892
|
+
const exportedValue = (m: ValueModule, name: string, depth: number): FreeNameResolution => {
|
|
893
|
+
for (const st of m.body) {
|
|
894
|
+
if (st.type !== 'ExportNamedDeclaration' || !(st as { source?: unknown }).source) continue
|
|
895
|
+
for (const sp of (st.specifiers as AnyNode[] | undefined) ?? []) {
|
|
896
|
+
if ((sp.exported as { name?: string }).name !== name) continue
|
|
897
|
+
const source = (st.source as { value: string }).value
|
|
898
|
+
const file = depth < 16 ? resolveValueFile(m.file, source) : null
|
|
899
|
+
const next = file ? valueModule(file) : null
|
|
900
|
+
if (!next) return { unresolved: `is re-exported from '${source}', which does not resolve from ${m.file}` }
|
|
901
|
+
return exportedValue(next, (sp.local as { name: string }).name, depth + 1)
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const local = exportLocalName(m.body, name)
|
|
905
|
+
if (local === null) {
|
|
906
|
+
// A barrel: `export * from '…'`. ponytail: the first star that exports `name`
|
|
907
|
+
// wins; a name two stars both export is ambiguous in ES, which this does not flag.
|
|
908
|
+
for (const st of m.body) {
|
|
909
|
+
if (st.type !== 'ExportAllDeclaration' || (st as { exported?: unknown }).exported) continue
|
|
910
|
+
const file = depth < 16 ? resolveValueFile(m.file, (st.source as { value: string }).value) : null
|
|
911
|
+
const next = file ? valueModule(file) : null
|
|
912
|
+
if (!next) continue
|
|
913
|
+
const r = exportedValue(next, name, depth + 1)
|
|
914
|
+
if (r !== null && 'unresolved' in r && r.unresolved.startsWith('is not exported by ')) continue
|
|
915
|
+
return r
|
|
916
|
+
}
|
|
917
|
+
return { unresolved: `is not exported by ${m.file}` }
|
|
918
|
+
}
|
|
919
|
+
const scope = moduleValueScope(m)
|
|
920
|
+
if (!scope.has(local)) {
|
|
921
|
+
scope.get(local) // records WHY, for the error
|
|
922
|
+
return { unresolved: `could not be resolved in ${m.file}` }
|
|
923
|
+
}
|
|
924
|
+
return { value: scope.get(local) }
|
|
925
|
+
}
|
|
926
|
+
const importedValue = (from: string, binding: { source: string; imported: string }): FreeNameResolution => {
|
|
927
|
+
const file = resolveValueFile(from, binding.source)
|
|
928
|
+
const m = file ? valueModule(file) : null
|
|
929
|
+
if (!m) return { unresolved: `is imported from '${binding.source}', which does not resolve from ${from}` }
|
|
930
|
+
return exportedValue(m, binding.imported, 0)
|
|
931
|
+
}
|
|
932
|
+
/** Declarations of THIS module that no grammar construct references, kept unevaluated
|
|
933
|
+
* until a grammar reads the name. Ordinary code may read any import, and resolving a
|
|
934
|
+
* module for a value nothing compiles is pure build cost — or a build failure. */
|
|
935
|
+
const deferredLocals = new Map<string, Expression>()
|
|
936
|
+
/** Their results, once read: each grammar's factory reads through its own forked scope. */
|
|
937
|
+
const deferredResults = new Map<string, FreeNameResolution>()
|
|
938
|
+
const mainOpaqueImports = opaqueImportsOf(body as unknown as AnyNode[])
|
|
939
|
+
const resolveMainFree = (name: string): FreeNameResolution => {
|
|
940
|
+
const done = deferredResults.get(name)
|
|
941
|
+
if (done !== undefined) return done
|
|
942
|
+
const deferred = deferredLocals.get(name)
|
|
943
|
+
if (deferred !== undefined) {
|
|
944
|
+
deferredLocals.delete(name)
|
|
945
|
+
// Its own misses are reported through the local's name, not beside it.
|
|
946
|
+
const outer = [...unresolvedImports]
|
|
947
|
+
unresolvedImports.clear()
|
|
948
|
+
const value = evaluateStaticValue(deferred, scope, code)
|
|
949
|
+
const why = [...unresolvedImports]
|
|
950
|
+
unresolvedImports.clear()
|
|
951
|
+
for (const n of outer) unresolvedImports.add(n)
|
|
952
|
+
const result: FreeNameResolution = value !== null && value !== undefined || isStaticNullishExpression(deferred)
|
|
953
|
+
? { value }
|
|
954
|
+
: why.length > 0 ? { unresolved: `could not be evaluated at build time, because: ${why.join('; ')}` } : null
|
|
955
|
+
deferredResults.set(name, result)
|
|
956
|
+
return result
|
|
957
|
+
}
|
|
958
|
+
const binding = importBindings.get(name)
|
|
959
|
+
if (!binding) return mainOpaqueImports.has(name) ? { unresolved: OPAQUE_IMPORT } : null
|
|
960
|
+
const resolved = importedValue(id, binding)
|
|
961
|
+
// Its value is now inside the compiled grammar; the import goes too unless
|
|
962
|
+
// something left verbatim still reads it (the same rule as an inlined factory).
|
|
963
|
+
if (resolved !== null && 'value' in resolved) usedImportedFactories.add(name)
|
|
964
|
+
return resolved
|
|
965
|
+
}
|
|
675
966
|
|
|
676
967
|
const topLevelFunction = (moduleBody: AnyNode[], name: string): AnyNode | null => {
|
|
677
968
|
for (const st of moduleBody) {
|
|
@@ -685,19 +976,21 @@ function transformMacroImpl(
|
|
|
685
976
|
}
|
|
686
977
|
return null
|
|
687
978
|
}
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
979
|
+
const sourceScopeUntil = (file: string, moduleBody: AnyNode[], source: string, until: number): Scope | null => {
|
|
980
|
+
// The module's own declarations are still evaluated EAGERLY, in order, up to the
|
|
981
|
+
// factory (a factory module the macro cannot fully account for still falls back).
|
|
982
|
+
// Its imports are what changed: they resolve on first read, like this module's.
|
|
983
|
+
const imports = extractImportBindings(moduleBody)
|
|
984
|
+
const opaque = opaqueImportsOf(moduleBody)
|
|
985
|
+
const out = new ModuleScope(name => {
|
|
986
|
+
const binding = imports.get(name)
|
|
987
|
+
if (binding) return importedValue(file, binding)
|
|
988
|
+
return opaque.has(name) ? { unresolved: OPAQUE_IMPORT } : null
|
|
989
|
+
}, unresolvedImports) as unknown as Scope
|
|
698
990
|
for (const stmt of moduleBody as Statement[]) {
|
|
699
991
|
if (stmt.type === 'ImportDeclaration') {
|
|
700
|
-
|
|
992
|
+
// A module that imports parseman WITHOUT the macro attribute is not macro code.
|
|
993
|
+
if (moduleAliases.has((stmt as unknown as ImportDeclaration).source.value) && !isMacroImport(stmt as unknown as ImportDeclaration)) return null
|
|
701
994
|
continue
|
|
702
995
|
}
|
|
703
996
|
if (stmt.type === 'FunctionDeclaration') continue
|
|
@@ -747,6 +1040,11 @@ function transformMacroImpl(
|
|
|
747
1040
|
}
|
|
748
1041
|
return out
|
|
749
1042
|
}
|
|
1043
|
+
/** Imported factories whose module could not be accounted for because of an
|
|
1044
|
+
* unresolved import — reported when a `rules()` call actually uses one. */
|
|
1045
|
+
const factoryFailures = new Map<string, string[]>()
|
|
1046
|
+
/** Imports the macro inlined — a factory, or an imported terminal/value — and so
|
|
1047
|
+
* may remove once nothing verbatim reads them. */
|
|
750
1048
|
const usedImportedFactories = new Set<string>()
|
|
751
1049
|
const markUsedImportedFactories = (names: readonly string[] | undefined): void => {
|
|
752
1050
|
for (const name of names ?? []) usedImportedFactories.add(name)
|
|
@@ -759,8 +1057,14 @@ function transformMacroImpl(
|
|
|
759
1057
|
const localFor = exportLocalName(mod.body as AnyNode[], binding.imported)
|
|
760
1058
|
const init = localFor ? topLevelInit(mod.body as AnyNode[], localFor) ?? topLevelFunction(mod.body as AnyNode[], localFor) : null
|
|
761
1059
|
if (!init || (init.type !== 'ArrowFunctionExpression' && init.type !== 'FunctionExpression' && init.type !== 'FunctionDeclaration')) continue
|
|
762
|
-
|
|
763
|
-
|
|
1060
|
+
unresolvedImports.clear()
|
|
1061
|
+
const factoryScope = sourceScopeUntil(file, mod.body as AnyNode[], mod.src, init.start)
|
|
1062
|
+
if (!factoryScope) {
|
|
1063
|
+
// Not silent: `rules(<this factory>)` refuses the build naming the binding.
|
|
1064
|
+
if (unresolvedImports.size > 0) factoryFailures.set(localName, [...unresolvedImports])
|
|
1065
|
+
unresolvedImports.clear()
|
|
1066
|
+
continue
|
|
1067
|
+
}
|
|
764
1068
|
factoryDecls.set(localName, {
|
|
765
1069
|
fn: init as unknown as Expression,
|
|
766
1070
|
declaredAt: -1,
|
|
@@ -778,8 +1082,9 @@ function transformMacroImpl(
|
|
|
778
1082
|
// --- Pass 2: evaluate declarations in source order ---
|
|
779
1083
|
// Scope stores enriched ScopeEntry objects so evaluateParserFactory can
|
|
780
1084
|
// replay mfSrcs when outer-scope combinators are referenced inside factories.
|
|
781
|
-
const scope
|
|
782
|
-
|
|
1085
|
+
const scope = new ModuleScope(resolveMainFree, unresolvedImports) as unknown as Scope
|
|
1086
|
+
/** `carry`: a compiled terminal that may need its combinator IR — see `withCombinatorIR`. */
|
|
1087
|
+
const replacements: Array<{ start: number; end: number; replacement: string; carry?: { name: string; combi: Combinator<unknown>; exported: boolean } }> = []
|
|
783
1088
|
const warnings: string[] = []
|
|
784
1089
|
// Collect degradations instead of printing them, so they arrive on the SAME channel
|
|
785
1090
|
// as every other macro warning (the bundler's `this.warn`) with a `file:line` anchor.
|
|
@@ -839,6 +1144,15 @@ function transformMacroImpl(
|
|
|
839
1144
|
for (const bi of def?.buildImports ?? []) {
|
|
840
1145
|
let m = pendingBuilderImports.get(bi.source)
|
|
841
1146
|
if (!m) { m = new Map(); pendingBuilderImports.set(bi.source, m) }
|
|
1147
|
+
const prev = m.get(bi.local)
|
|
1148
|
+
// Two builders reading one local name as different exports cannot share an import.
|
|
1149
|
+
if (prev !== undefined && prev !== bi.imported) {
|
|
1150
|
+
throw new Error(
|
|
1151
|
+
`${id} — parseman will not emit this module: inlined builders read \`${bi.local}\` as both `
|
|
1152
|
+
+ `\`${prev}\` and \`${bi.imported}\` from ${JSON.stringify(bi.source)}. The builder source names `
|
|
1153
|
+
+ `\`${bi.local}\` verbatim, so one import cannot bind both; import them under different names.`,
|
|
1154
|
+
)
|
|
1155
|
+
}
|
|
842
1156
|
m.set(bi.local, bi.imported)
|
|
843
1157
|
}
|
|
844
1158
|
// An ANONYMOUS lazy is inline recursion — follow the thunk. A NAMED lazy is a
|
|
@@ -885,6 +1199,58 @@ function transformMacroImpl(
|
|
|
885
1199
|
warnings.push(`${id}:${lineOf(pos)} — ${msg} (running via the interpreter; add the plugin or simplify the declaration to compile it)`)
|
|
886
1200
|
}
|
|
887
1201
|
|
|
1202
|
+
/**
|
|
1203
|
+
* REFUSE THE BUILD over something the macro could not evaluate, naming each binding
|
|
1204
|
+
* it could not resolve (`unresolvedImports`, filled by the scopes as they miss).
|
|
1205
|
+
*
|
|
1206
|
+
* A warning is not enough here. For a `rules()` option the "fallback" was to build the
|
|
1207
|
+
* grammar WITHOUT the option — `scanSkip` gone, every string and comment inside a
|
|
1208
|
+
* `scanTo`/`balanced` region live again — with the build green and no interpreter
|
|
1209
|
+
* fallback to count. And a terminal imported from a published package is a compiled
|
|
1210
|
+
* parser at runtime, so there is no interpreter fallback for it to take either.
|
|
1211
|
+
*/
|
|
1212
|
+
const unevaluable = (pos: number, what: string): never => {
|
|
1213
|
+
const names = [...unresolvedImports]
|
|
1214
|
+
unresolvedImports.clear()
|
|
1215
|
+
throw new Error(
|
|
1216
|
+
`${id}:${lineOf(pos)} — ${what} can't be evaluated at build time`
|
|
1217
|
+
+ (names.length > 0
|
|
1218
|
+
? `; unresolved binding(s):\n${names.map(n => ` - ${n}`).join('\n')}`
|
|
1219
|
+
: ' (it is not a static combinator/value expression)')
|
|
1220
|
+
+ `\n parseman will not emit this module rather than drop it silently.`,
|
|
1221
|
+
)
|
|
1222
|
+
}
|
|
1223
|
+
/** A `rules()` option: absent/nullish → `undefined`; otherwise evaluated or the build fails. */
|
|
1224
|
+
const requiredOption = <T>(label: string, name: string, value: Expression | undefined, evaluate: (v: Expression) => T | null): T | undefined => {
|
|
1225
|
+
if (value === undefined || isStaticNullishExpression(value)) return undefined
|
|
1226
|
+
unresolvedImports.clear()
|
|
1227
|
+
const out = evaluate(value)
|
|
1228
|
+
if (out === null) {
|
|
1229
|
+
// A const bound to `null`/`undefined` means no option, exactly as the literal does.
|
|
1230
|
+
const id = (value.type === 'Identifier' ? value.name : null)
|
|
1231
|
+
if (id !== null && scope.has(id) && scope.get(id) == null) return undefined
|
|
1232
|
+
return unevaluable(value.start, `${label}: rules({ ${name} })`)
|
|
1233
|
+
}
|
|
1234
|
+
unresolvedImports.clear()
|
|
1235
|
+
return out
|
|
1236
|
+
}
|
|
1237
|
+
/** Options the macro reads by NAME, so a spread — or a computed key it cannot prove
|
|
1238
|
+
* names no option — would hide one from it. */
|
|
1239
|
+
const RULES_OPTIONS = new Set(['trivia', 'scanSkip', 'hostMode', 'trackLines'])
|
|
1240
|
+
const assertPlainOptions = (optExpr: AnyNode | undefined, label: string): void => {
|
|
1241
|
+
for (const p of (optExpr?.type === 'ObjectExpression' ? optExpr.properties as AnyNode[] : [])) {
|
|
1242
|
+
if (propName(p as never) !== null) continue
|
|
1243
|
+
const key = p.type === 'Property' ? evaluateStaticValue(p.key as unknown as Expression, scope, code) : null
|
|
1244
|
+
if (typeof key !== 'string' || RULES_OPTIONS.has(key)) {
|
|
1245
|
+
unevaluable(p.start, `${label}: an options ${p.type === 'Property' ? 'computed key' : 'spread'}`)
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const triviaOption = (optExpr: AnyNode | undefined, label: string): Combinator<unknown> | undefined => {
|
|
1250
|
+
assertPlainOptions(optExpr, label)
|
|
1251
|
+
return requiredOption(label, 'trivia', optionProp(optExpr, 'trivia'), v => evaluateExpr(v, scope, code, []))
|
|
1252
|
+
}
|
|
1253
|
+
|
|
888
1254
|
/**
|
|
889
1255
|
* Compile a `rules(factory)` call into ONE shared replacement expression
|
|
890
1256
|
* for the whole call — see compileRuleMap() in codegen.ts for why this is
|
|
@@ -923,11 +1289,23 @@ function transformMacroImpl(
|
|
|
923
1289
|
const importedFactory = namedFactory?.imported === true && factoryArg === namedFactory.fn
|
|
924
1290
|
? (factoryArgRaw as unknown as { name?: string }).name
|
|
925
1291
|
: undefined
|
|
1292
|
+
const failedFactory = factoryArgRaw.type === 'Identifier'
|
|
1293
|
+
? factoryFailures.get((factoryArgRaw as unknown as { name: string }).name)
|
|
1294
|
+
: undefined
|
|
1295
|
+
if (failedFactory) {
|
|
1296
|
+
for (const n of failedFactory) unresolvedImports.add(n)
|
|
1297
|
+
unevaluable(init.start, `${label}: the imported rules() factory \`${(factoryArgRaw as unknown as { name: string }).name}\``)
|
|
1298
|
+
}
|
|
926
1299
|
|
|
927
1300
|
// Grammar-level options object — evaluate `trivia` / `scanSkip` so the compiled
|
|
928
1301
|
// map seeds them as the ambient defaults (build-time mirror of rules() tagging
|
|
929
1302
|
// grammarTrivia / grammarScanSkip at runtime).
|
|
930
1303
|
const optionsArg = (optionsFirst ? arg0 : arg1) as AnyNode | undefined
|
|
1304
|
+
// `optionProp` reads an object LITERAL: a trailing `rules(factory, OPTS)` would hide
|
|
1305
|
+
// every option from it and ship the grammar without them, as `compose()` would.
|
|
1306
|
+
if (optionsArg !== undefined && optionsArg.type !== 'ObjectExpression' && !isStaticNullishExpression(optionsArg as unknown as Expression)) {
|
|
1307
|
+
unevaluable(optionsArg.start as number, `${label}: rules() options`)
|
|
1308
|
+
}
|
|
931
1309
|
const optionValue = (name: string): Expression | undefined => optionProp(optionsArg, name)
|
|
932
1310
|
|
|
933
1311
|
// Read and VALIDATE hostMode before evaluating the factory, so a mode the macro
|
|
@@ -973,14 +1351,18 @@ function transformMacroImpl(
|
|
|
973
1351
|
*
|
|
974
1352
|
* Evaluated BEFORE the factory because they are now inputs to it.
|
|
975
1353
|
*/
|
|
976
|
-
const
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1354
|
+
const gTrivia = triviaOption(optionsArg, label)
|
|
1355
|
+
const scanSkipArray = requiredOption(label, 'scanSkip', optionValue('scanSkip'), v => {
|
|
1356
|
+
const units = evaluateCombinatorArray(v, scope, code)
|
|
1357
|
+
if (units) return units
|
|
1358
|
+
// An empty array means no skip units, whether written `[]` or named by a const.
|
|
1359
|
+
const value = evaluateStaticValue(v, scope, code)
|
|
1360
|
+
return Array.isArray(value) && value.length === 0 ? [] : null
|
|
1361
|
+
})
|
|
1362
|
+
const gScanSkip = scanSkipArray?.length ? scanSkipArray : undefined
|
|
982
1363
|
|
|
983
1364
|
const why: { reason?: string } = {}
|
|
1365
|
+
unresolvedImports.clear()
|
|
984
1366
|
const ruleMap = evaluateParserFactory(factoryArg, factoryScope, factoryCode, [], why, {
|
|
985
1367
|
...(gTrivia ? { trivia: gTrivia as Combinator<unknown> } : {}),
|
|
986
1368
|
...(gScanSkip ? { scanSkip: gScanSkip } : {}),
|
|
@@ -988,6 +1370,7 @@ function transformMacroImpl(
|
|
|
988
1370
|
...(gTrackLines === true ? { trackLines: true } : {}),
|
|
989
1371
|
})
|
|
990
1372
|
if (!ruleMap) {
|
|
1373
|
+
if (unresolvedImports.size > 0) unevaluable(init.start, `${label}: rules(...) factory`)
|
|
991
1374
|
warn(init.start, why.reason === undefined
|
|
992
1375
|
? `${label}: rules(...) factory isn't statically evaluable`
|
|
993
1376
|
: `${label}: rules(...) factory isn't statically evaluable — ${why.reason}`)
|
|
@@ -1018,6 +1401,8 @@ function transformMacroImpl(
|
|
|
1018
1401
|
// inlined" leaves the author with a ~5x silent perf regression and no lead.
|
|
1019
1402
|
const refusals: string[] = []
|
|
1020
1403
|
const compiled = compileRuleMap([...evaluated.ruleMap], { ...(evaluated.trivia ? { trivia: evaluated.trivia } : {}), ...(evaluated.scanSkip ? { scanSkip: evaluated.scanSkip } : {}), ...(evaluated.trackLines ? { trackLines: true } : {}), recovery, coverage: grammarCoverage, refusals })
|
|
1404
|
+
// An imported terminal's inlined builder reads ITS module's helpers; import them here.
|
|
1405
|
+
if (compiled !== null) collectBuilderImports([...evaluated.ruleMap])
|
|
1021
1406
|
// A table replacement names `tableRules`, which nothing in the consumer's
|
|
1022
1407
|
// module binds. That reference is the whole reason the artifact is small (the
|
|
1023
1408
|
// driver is SHARED, not inlined per grammar), so the import is owned here.
|
|
@@ -1196,34 +1581,58 @@ function transformMacroImpl(
|
|
|
1196
1581
|
// spread by following that module's own import of `binding` to its compiled file
|
|
1197
1582
|
// and recursing, then eval the literal with the resolved ancestors stubbed in so
|
|
1198
1583
|
// the spreads splice their pieces. `seen` guards against an import cycle.
|
|
1199
|
-
|
|
1584
|
+
//
|
|
1585
|
+
// A grammar whose literal IS found is BUILD-COMPILED: a runtime `compose()` cannot
|
|
1586
|
+
// link it, so failing to resolve it is not a reason to fall back to runtime.
|
|
1587
|
+
// `unresolved.why` then says what could not be followed, and the caller fails the
|
|
1588
|
+
// build with it. `local` names a top-level binding of the module rather than an
|
|
1589
|
+
// export — how a bundler leaves an ancestor it inlined.
|
|
1590
|
+
const resolveModulePieces = (
|
|
1591
|
+
module: ResolvedGrammarModule,
|
|
1592
|
+
exportName: string,
|
|
1593
|
+
seen: Set<string>,
|
|
1594
|
+
unresolved: { why?: string },
|
|
1595
|
+
local = false,
|
|
1596
|
+
): RawItem[] | null => {
|
|
1200
1597
|
const { file } = module
|
|
1201
1598
|
const mod = module.source
|
|
1202
1599
|
? lowerPrivateSourceModule(file, moduleAliases, warnUnloweredRegex, recovery)
|
|
1203
1600
|
: parseModuleCached(file)
|
|
1204
1601
|
if (!mod) return null
|
|
1205
|
-
const localFor = exportLocalName(mod.body as AnyNode[], exportName)
|
|
1602
|
+
const localFor = local ? exportName : exportLocalName(mod.body as AnyNode[], exportName)
|
|
1206
1603
|
const initNode = localFor ? topLevelInit(mod.body as AnyNode[], localFor) : null
|
|
1207
1604
|
const literalRange = initNode ? findCarriedPiecesLiteral(initNode) : null
|
|
1208
1605
|
if (!literalRange) return null
|
|
1209
1606
|
const literal = mod.src.slice(literalRange.start, literalRange.end)
|
|
1607
|
+
const fail = (why: string): null => { unresolved.why ??= `${exportName} in ${file}: ${why}`; return null }
|
|
1210
1608
|
|
|
1211
1609
|
const imports = extractImportBindings(mod.body as AnyNode[])
|
|
1212
1610
|
const stubNames: string[] = []
|
|
1213
1611
|
const stubVals: unknown[] = []
|
|
1214
|
-
|
|
1612
|
+
// A bundler may reprint the key with an annotation inside the brackets — esbuild
|
|
1613
|
+
// writes `syn[/* @__PURE__ */ Symbol.for("parseman.composedPieces")]` — so block
|
|
1614
|
+
// comments before `Symbol` are allowed. Missing one leaves the spread unresolved.
|
|
1615
|
+
const spreadRe = /\.\.\.\s*\(?\s*([A-Za-z_$][\w$]*)\s*\[\s*(?:\/\*[^]*?\*\/\s*)*Symbol\s*\.\s*for\s*\(\s*['"]parseman\.composedPieces['"]/g
|
|
1215
1616
|
const done = new Set<string>()
|
|
1216
1617
|
for (let m: RegExpExecArray | null; (m = spreadRe.exec(literal)); ) {
|
|
1217
1618
|
const local = m[1]!
|
|
1218
1619
|
if (done.has(local)) continue
|
|
1219
1620
|
done.add(local)
|
|
1220
1621
|
const b = imports.get(local)
|
|
1221
|
-
let subPieces: RawItem[] = []
|
|
1622
|
+
let subPieces: RawItem[] | null = []
|
|
1222
1623
|
if (b) {
|
|
1223
1624
|
const subModule = resolveGrammarModule(file, b.source)
|
|
1224
|
-
if (subModule
|
|
1225
|
-
|
|
1625
|
+
if (subModule === null) return fail(`its carried pieces spread \`${local}\` from '${b.source}', a module that could not be found`)
|
|
1626
|
+
if (!seen.has(subModule.file)) {
|
|
1627
|
+
subPieces = resolveModulePieces(subModule, b.imported, new Set(seen).add(subModule.file), unresolved)
|
|
1628
|
+
if (subPieces === null) return fail(`its carried pieces spread \`${local}\` from '${b.source}', which could not be resolved`)
|
|
1226
1629
|
}
|
|
1630
|
+
} else if (topLevelInit(mod.body as AnyNode[], local) !== null && !seen.has(`${file}#${local}`)) {
|
|
1631
|
+
// A bundled module: the ancestor was inlined as a top-level binding.
|
|
1632
|
+
subPieces = resolveModulePieces(module, local, new Set(seen).add(`${file}#${local}`), unresolved, true)
|
|
1633
|
+
if (subPieces === null) return fail(`its carried pieces spread the inlined binding \`${local}\`, which carries none`)
|
|
1634
|
+
} else {
|
|
1635
|
+
return fail(`its carried pieces spread \`${local}\`, which is neither an import nor a top-level binding of the module`)
|
|
1227
1636
|
}
|
|
1228
1637
|
stubNames.push(local)
|
|
1229
1638
|
// The KEY comes from the linker, which owns it. Spelling `Symbol.for('…')`
|
|
@@ -1245,49 +1654,41 @@ function transformMacroImpl(
|
|
|
1245
1654
|
// eslint-disable-next-line no-new-func
|
|
1246
1655
|
const list = new Function(...stubNames, `return (${literal})`)(...stubVals) as RawItem[]
|
|
1247
1656
|
return list
|
|
1248
|
-
} catch { return
|
|
1657
|
+
} catch (e) { return fail(`its carried pieces literal did not evaluate (${(e as Error).message})`) }
|
|
1249
1658
|
}
|
|
1250
1659
|
|
|
1660
|
+
// Imported build-compiled grammars whose carried pieces could not be resolved, by
|
|
1661
|
+
// local name, with the reason. `compileComposeCall` fails the build on these.
|
|
1662
|
+
const unresolvedCompiled = new Map<string, string>()
|
|
1663
|
+
|
|
1251
1664
|
const importedPieces = (localName: string): RawItem[] | null => {
|
|
1252
1665
|
if (importedPiecesCache.has(localName)) return importedPiecesCache.get(localName)!
|
|
1253
1666
|
let result: RawItem[] | null = null
|
|
1254
1667
|
const binding = importBindings.get(localName)
|
|
1255
1668
|
if (binding) {
|
|
1256
1669
|
const module = resolveGrammarModule(id, binding.source)
|
|
1257
|
-
|
|
1670
|
+
const unresolved: { why?: string } = {}
|
|
1671
|
+
result = module ? resolveModulePieces(module, binding.imported, new Set([module.file]), unresolved) : null
|
|
1672
|
+
if (result === null && unresolved.why !== undefined) unresolvedCompiled.set(localName, unresolved.why)
|
|
1258
1673
|
}
|
|
1259
1674
|
importedPiecesCache.set(localName, result)
|
|
1260
1675
|
return result
|
|
1261
1676
|
}
|
|
1262
1677
|
|
|
1263
1678
|
/**
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
* condition. A table has no text to splice and no placeholders to resolve, so the
|
|
1271
|
-
* merge moves one level UP, onto the combinators: evaluate each piece's IR back to
|
|
1272
|
-
* a rule map, let a later piece's name override an earlier one — which is exactly
|
|
1273
|
-
* what `compose()` means — and hand the single merged map to
|
|
1274
|
-
* `compileRuleMap`, which encodes ONCE.
|
|
1275
|
-
*
|
|
1276
|
-
* This is what makes table composition the easy kind: no relocation of code
|
|
1277
|
-
* offsets, no merging of const / fn / class / expected / dispatch pools between
|
|
1278
|
-
* two already-encoded programs. `encodeTableProgram` points `enc.winners` at the
|
|
1279
|
-
* merged map (`table/encode.ts:1383`), so a base piece's internal `g.Atom`
|
|
1280
|
-
* resolves BY NAME to the override (`:1036`) rather than through a thunk that
|
|
1281
|
-
* closes over the base. Open recursion — much of the point of compose — therefore
|
|
1282
|
-
* survives the merge, and the result is the table the merged grammar would have
|
|
1283
|
-
* produced had it been written as a single `rules()` call.
|
|
1679
|
+
* Evaluate a carried list back to combinators, one entry per piece: its rule map
|
|
1680
|
+
* and the `rules()` recipe it was built from. `compose()` links the recipes into
|
|
1681
|
+
* one namespace (`mergedCarriedRules`); `composeLeaf()` merges the maps by name.
|
|
1682
|
+
* Either way the merged grammar is encoded ONCE by `compileRuleMap` — no relocation
|
|
1683
|
+
* of code offsets and no merging of const / fn / expected pools between two
|
|
1684
|
+
* already-encoded programs.
|
|
1284
1685
|
*
|
|
1285
1686
|
* `null` means some piece cannot contribute combinators: a FULL BAKED piece (the
|
|
1286
1687
|
* un-serializable fallback at `localCarried`) carries lowered SOURCE and nothing
|
|
1287
1688
|
* else. That shape is codegen-only, so the caller falls back to fused source
|
|
1288
1689
|
* rather than refusing the grammar.
|
|
1289
1690
|
*/
|
|
1290
|
-
type CarriedRuleMap = { ns: string; rules: Array<[string, Combinator<unknown>]
|
|
1691
|
+
type CarriedRuleMap = { ns: string; rules: Array<[string, Combinator<unknown>]>; recipes: readonly RulesRecipe[] }
|
|
1291
1692
|
const carriedRuleMaps = (
|
|
1292
1693
|
items: CarriedItem[],
|
|
1293
1694
|
): { pieces: CarriedRuleMap[]; trackLines: boolean } | null => {
|
|
@@ -1300,7 +1701,8 @@ function transformMacroImpl(
|
|
|
1300
1701
|
// merged encode resolves `trackLines` from `_meta` alone and silently drops line
|
|
1301
1702
|
// tracking for a grammar that asked for it.
|
|
1302
1703
|
if (item.trackLines === true) trackLines = true
|
|
1303
|
-
|
|
1704
|
+
const map = evalRulesIR(item.ir)
|
|
1705
|
+
pieces.push({ ns: item.ns, rules: Object.entries(map), recipes: (map as Record<symbol, readonly RulesRecipe[]>)[RULES_RECIPE]! })
|
|
1304
1706
|
return true
|
|
1305
1707
|
}
|
|
1306
1708
|
for (const item of items) {
|
|
@@ -1345,24 +1747,39 @@ function transformMacroImpl(
|
|
|
1345
1747
|
return [...winners]
|
|
1346
1748
|
}
|
|
1347
1749
|
|
|
1750
|
+
/**
|
|
1751
|
+
* LINK a carried list into the composed map, exactly as runtime `compose()` does:
|
|
1752
|
+
* every piece's carried `rules()` recipe runs again against ONE namespace
|
|
1753
|
+
* (`linkRules`), a later piece's rule winning by name, and the composing trivia and
|
|
1754
|
+
* host mode stamped on every rule. A `g.X` in one piece is then the SAME slot as the
|
|
1755
|
+
* `X` another piece defines, so everything the encoder derives from the graph —
|
|
1756
|
+
* first sets, nullability, expected sets — sees the composed grammar. Merging
|
|
1757
|
+
* per-piece maps by name instead left each piece's cross-piece `g.X` an unresolved
|
|
1758
|
+
* thunk: parsing still bound it by name, but a failure derived as the bare rule name
|
|
1759
|
+
* (or, through a nullable guess, a token the parse never required), so the macro
|
|
1760
|
+
* table reported different `expected` sets than the interpreter and `compile()` of
|
|
1761
|
+
* the same composition.
|
|
1762
|
+
*/
|
|
1348
1763
|
const mergedCarriedRules = (
|
|
1349
1764
|
items: CarriedItem[],
|
|
1765
|
+
link: LinkOptions,
|
|
1350
1766
|
): { rules: Array<[string, Combinator<unknown>]>; trackLines: boolean } | null => {
|
|
1351
1767
|
const carriedMaps = carriedRuleMaps(items)
|
|
1352
1768
|
if (carriedMaps === null) return null
|
|
1353
|
-
|
|
1769
|
+
const linked = linkRules(carriedMaps.pieces.flatMap(p => p.recipes), link)
|
|
1770
|
+
// A name some piece only REFERENCES and none defines stays an undefined slot in
|
|
1771
|
+
// the namespace. It is not a rule, so it is not handed to the encoder as one.
|
|
1772
|
+
const rules = Object.entries(linked).filter(([, rule]) => {
|
|
1773
|
+
if (rule._def.tag !== 'lazy') return true
|
|
1774
|
+
try { rule._def.thunk(); return true } catch { return false }
|
|
1775
|
+
})
|
|
1776
|
+
return { rules, trackLines: carriedMaps.trackLines }
|
|
1354
1777
|
}
|
|
1355
1778
|
|
|
1356
1779
|
/**
|
|
1357
|
-
* COMPOSING-WINS, as an OVERRIDE rather than
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1360
|
-
* its own — correct for `composeLeaf`, whose pieces may legitimately disagree, and
|
|
1361
|
-
* WRONG for `compose`, where the composing grammar's trivia governs every fused rule
|
|
1362
|
-
* INCLUDING the inherited ones. Gap-filling leaves a base rule that declared its own
|
|
1363
|
-
* `rules({ trivia }, …)` still skipping the base's trivia after a delta re-declared
|
|
1364
|
-
* it, so `compose([css, less])` silently parses the inherited rules under css's
|
|
1365
|
-
* whitespace — the multi-level composing-wins contract, inverted.
|
|
1780
|
+
* COMPOSING-WINS for `composeLeaf()`'s carried pieces, as an OVERRIDE rather than
|
|
1781
|
+
* a gap-fill: `compileRuleMap`'s `applyAmbient` only fills a rule that carries no
|
|
1782
|
+
* trivia of its own. (`compose()` gets the same override from `linkRules`.)
|
|
1366
1783
|
*
|
|
1367
1784
|
* Safe to mutate: every rule here came from `evalRuleMapIR`, which constructs FRESH
|
|
1368
1785
|
* combinators per piece per compose, so this cannot leak into another compose of the
|
|
@@ -1456,9 +1873,7 @@ function transformMacroImpl(
|
|
|
1456
1873
|
const a0 = rulesArgs[0] as AnyNode | undefined
|
|
1457
1874
|
const a1 = rulesArgs[1] as AnyNode | undefined
|
|
1458
1875
|
const optExpr = (a0?.type === 'ObjectExpression' ? a0 : a1?.type === 'ObjectExpression' ? a1 : undefined) as AnyNode | undefined
|
|
1459
|
-
|
|
1460
|
-
if (!triviaValue) return undefined
|
|
1461
|
-
return (evaluateExpr(triviaValue, scope, code, []) as Combinator<unknown> | null) ?? undefined
|
|
1876
|
+
return triviaOption(optExpr, 'compose() element')
|
|
1462
1877
|
}
|
|
1463
1878
|
const composingTrivia = (elements: ReadonlyArray<Expression | null>): Combinator<unknown> | undefined => {
|
|
1464
1879
|
for (let i = elements.length - 1; i >= 0; i--) {
|
|
@@ -1497,6 +1912,10 @@ function transformMacroImpl(
|
|
|
1497
1912
|
// because the artifact genuinely WAS 'ast'. Same vacuous-classification shape this
|
|
1498
1913
|
// change exists to remove, one call site over.
|
|
1499
1914
|
const cOptions = (init as unknown as { arguments: Expression[] }).arguments[1] as AnyNode | undefined
|
|
1915
|
+
// `optionProp` reads an object LITERAL: any other options expression, or a spread
|
|
1916
|
+
// inside one, would hide `hostMode` from it and ship the default.
|
|
1917
|
+
if (cOptions !== undefined && cOptions.type !== 'ObjectExpression') unevaluable(cOptions.start, 'compose() options')
|
|
1918
|
+
assertPlainOptions(cOptions, 'compose()')
|
|
1500
1919
|
const cHostModeValue = optionProp(cOptions, 'hostMode')
|
|
1501
1920
|
const cHostMode = cHostModeValue?.type === 'Literal'
|
|
1502
1921
|
? (cHostModeValue as unknown as { value?: unknown }).value
|
|
@@ -1512,7 +1931,25 @@ function transformMacroImpl(
|
|
|
1512
1931
|
const importedFactories: string[] = []
|
|
1513
1932
|
for (let i = 0; i < elements.length; i++) {
|
|
1514
1933
|
const r = argPieces(elements[i]!, `compose${init.start}_${i}`, composing)
|
|
1515
|
-
if (!r) {
|
|
1934
|
+
if (!r) {
|
|
1935
|
+
// A build-compiled argument cannot fall back: runtime `compose()` links live
|
|
1936
|
+
// `rules()` grammars only and would throw at import. Fail here instead.
|
|
1937
|
+
const el = elements[i]!
|
|
1938
|
+
const name = el.type === 'Identifier' ? (el as unknown as { name: string }).name : undefined
|
|
1939
|
+
const why = name === undefined ? undefined : unresolvedCompiled.get(name)
|
|
1940
|
+
if (why !== undefined) {
|
|
1941
|
+
throw new Error(
|
|
1942
|
+
`${id}:${lineOf(init.start)} — compose(): argument ${i} (\`${name}\` from '${importBindings.get(name!)?.source}') `
|
|
1943
|
+
+ `is a build-compiled grammar whose carried pieces could not be resolved: ${why}. `
|
|
1944
|
+
+ 'A runtime compose() cannot link a build-compiled grammar, so this would throw at import. '
|
|
1945
|
+
+ 'The usual cause is a bundler rewriting the upstream module (inlining or renaming an ancestor '
|
|
1946
|
+
+ 'grammar it composes, or reprinting its carried-pieces literal); keep upstream grammar packages '
|
|
1947
|
+
+ 'external to the bundle.',
|
|
1948
|
+
)
|
|
1949
|
+
}
|
|
1950
|
+
warn(init.start, `compose(): argument ${i} isn't a build-resolvable grammar; falling back to runtime`)
|
|
1951
|
+
return null
|
|
1952
|
+
}
|
|
1516
1953
|
carried.push(...r.carried)
|
|
1517
1954
|
importedFactories.push(...(r.importedFactories ?? []))
|
|
1518
1955
|
}
|
|
@@ -1523,9 +1960,11 @@ function transformMacroImpl(
|
|
|
1523
1960
|
// does — one encode, one `tableRules(…)` expression, no linker. `carried` is
|
|
1524
1961
|
// unchanged either way: it is the re-lowerable IR list, not a lowering artifact,
|
|
1525
1962
|
// so a downstream re-compose behaves identically whichever engine emitted here.
|
|
1526
|
-
const merged = mergedCarriedRules(carried
|
|
1963
|
+
const merged = mergedCarriedRules(carried, {
|
|
1964
|
+
trivia: composing,
|
|
1965
|
+
...(cHostMode ? { hostMode: cHostMode as HostMode } : {}),
|
|
1966
|
+
})
|
|
1527
1967
|
if (merged !== null) {
|
|
1528
|
-
if (composing) applyComposingTrivia(merged.rules, composing)
|
|
1529
1968
|
// Harvest the direct-builder import provenance carried on the merged graph so
|
|
1530
1969
|
// the re-lower pass at the end of this transform re-emits those imports.
|
|
1531
1970
|
collectBuilderImports(merged.rules)
|
|
@@ -1809,10 +2248,8 @@ function transformMacroImpl(
|
|
|
1809
2248
|
// ── Simple binding: const name = <expr> ──────────────────────────
|
|
1810
2249
|
const varName = (d.id as unknown as { name: string }).name
|
|
1811
2250
|
if (!referencesAny(init, allNames, scope)) {
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
;(scope as Map<string, unknown>).set(varName, staticValue)
|
|
1815
|
-
}
|
|
2251
|
+
// Evaluated only if a grammar reads it — see `deferredLocals`.
|
|
2252
|
+
deferredLocals.set(varName, init)
|
|
1816
2253
|
continue
|
|
1817
2254
|
}
|
|
1818
2255
|
|
|
@@ -1991,6 +2428,7 @@ function transformMacroImpl(
|
|
|
1991
2428
|
}
|
|
1992
2429
|
|
|
1993
2430
|
const mapFnSources: string[] = []
|
|
2431
|
+
unresolvedImports.clear()
|
|
1994
2432
|
const parser = evaluateExpr(init, scope, code, mapFnSources)
|
|
1995
2433
|
if (parser === null) {
|
|
1996
2434
|
const wordFactory = evaluateWordFactory(init, scope, code)
|
|
@@ -2034,6 +2472,7 @@ function transformMacroImpl(
|
|
|
2034
2472
|
if (exportPrefix) exportedFactories.push({ name: varName, pos: init.start })
|
|
2035
2473
|
continue
|
|
2036
2474
|
}
|
|
2475
|
+
if (unresolvedImports.size > 0) unevaluable(init.start, `"${varName}"`)
|
|
2037
2476
|
warn(init.start, `"${varName}" references a parseman macro import but isn't a statically-evaluable combinator`)
|
|
2038
2477
|
continue
|
|
2039
2478
|
}
|
|
@@ -2055,6 +2494,7 @@ function transformMacroImpl(
|
|
|
2055
2494
|
start: init.start,
|
|
2056
2495
|
end: init.end,
|
|
2057
2496
|
replacement: compiled.inlineExpression,
|
|
2497
|
+
carry: { name: varName, combi: parser, exported: exportPrefix !== '' },
|
|
2058
2498
|
})
|
|
2059
2499
|
|
|
2060
2500
|
// Store enriched scope entry so factories can replay mfSrcs
|
|
@@ -2165,40 +2605,78 @@ function transformMacroImpl(
|
|
|
2165
2605
|
)
|
|
2166
2606
|
}
|
|
2167
2607
|
|
|
2608
|
+
/**
|
|
2609
|
+
* Carry a compiled terminal's combinator IR on its value, when another module can
|
|
2610
|
+
* reach it: it is exported, or something left verbatim (an exported skip array, an
|
|
2611
|
+
* options object) still names it.
|
|
2612
|
+
*
|
|
2613
|
+
* Lowering turns `export const blockComment = regex(…)` into a compiled PARSER, and a
|
|
2614
|
+
* parser is not something the macro can evaluate. So without this, a package could
|
|
2615
|
+
* share a grammar map (whose pieces it carries for `compose()`) but not one terminal,
|
|
2616
|
+
* and every grammar that wanted the same string-or-comment skipper declared its own.
|
|
2617
|
+
* The IR rides on the value exactly like composed pieces do — non-enumerable, under a
|
|
2618
|
+
* `Symbol.for` key — and `carriedCombinatorIR` reads it back out of the artifact.
|
|
2619
|
+
* The parser function itself is unchanged, so no parse runs differently.
|
|
2620
|
+
*/
|
|
2621
|
+
const withCombinatorIR = <R extends (typeof replacements)[number]>(r: R): R => {
|
|
2622
|
+
if (!r.carry || (!r.carry.exported && !referencedNames().live.has(r.carry.name))) return r
|
|
2623
|
+
let ir: string | null = null
|
|
2624
|
+
// An IR the serializer cannot produce leaves the terminal uncarried; a module that
|
|
2625
|
+
// imports it then fails ITS build naming it (see `moduleConstValue`).
|
|
2626
|
+
try { ir = serializeCombinator(r.carry.combi) } catch { /* uncarried */ }
|
|
2627
|
+
return ir === null ? r : {
|
|
2628
|
+
...r,
|
|
2629
|
+
replacement: `/* @__PURE__ */ Object.defineProperty(${r.replacement}, Symbol.for('${COMBINATOR_IR_KEY}'), { value: ${JSON.stringify(ir)}, enumerable: false })`,
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2168
2632
|
const ms = new MagicString(code)
|
|
2169
2633
|
const replacementRanges = runtimeComposeFallback
|
|
2170
2634
|
? []
|
|
2171
2635
|
: replacements.map(({ start, end }) => ({ start, end }))
|
|
2172
2636
|
const isInsideReplacement = (start: number, end: number): boolean =>
|
|
2173
2637
|
replacementRanges.some(r => start >= r.start && end <= r.end)
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2638
|
+
/** Every name the EMITTED module still reads outside the replaced ranges — computed
|
|
2639
|
+
* once (it used to be one whole-module walk per name asked about). A declaration's
|
|
2640
|
+
* own binding name is not a read. `live` leaves out reads inside a local `rules()`
|
|
2641
|
+
* factory, whose text survives lowering only as macro-only dead code, and inside the
|
|
2642
|
+
* `.define()` statements lowering removes. */
|
|
2643
|
+
let referenced: { all: Set<string>; live: Set<string> } | null = null
|
|
2644
|
+
const referencedNames = (): { all: Set<string>; live: Set<string> } => {
|
|
2645
|
+
if (referenced !== null) return referenced
|
|
2646
|
+
const all = new Set<string>()
|
|
2647
|
+
const live = new Set<string>()
|
|
2648
|
+
// Also the `x.define(…)` statements lowering strips.
|
|
2649
|
+
const deadRanges = [...[...factoryDecls.values()].filter(f => !f.imported).map(f => f.fn), ...(anyUnresolved ? [] : defineRemovals)]
|
|
2650
|
+
const walk = (node: unknown, inFactory: boolean, parent?: AnyNode, parentKey?: string): void => {
|
|
2651
|
+
if (!node || typeof node !== 'object') return
|
|
2177
2652
|
const rec = node as AnyNode
|
|
2178
|
-
if (rec.type === 'ImportDeclaration') return
|
|
2179
|
-
if (typeof rec.start === 'number' && typeof rec.end === 'number'
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2653
|
+
if (rec.type === 'ImportDeclaration') return
|
|
2654
|
+
if (typeof rec.start === 'number' && typeof rec.end === 'number') {
|
|
2655
|
+
if (isInsideReplacement(rec.start, rec.end)) return
|
|
2656
|
+
inFactory ||= deadRanges.some(f => rec.start >= f.start && rec.end <= f.end)
|
|
2657
|
+
}
|
|
2658
|
+
if (rec.type === 'Identifier' || rec.type === 'BindingIdentifier') {
|
|
2659
|
+
const isKey = parentKey === 'key'
|
|
2660
|
+
&& (parent?.type === 'ObjectProperty' || parent?.type === 'Property' || parent?.type === 'PropertyDefinition')
|
|
2661
|
+
&& (parent as { computed?: boolean }).computed !== true
|
|
2662
|
+
const isMember = parentKey === 'property'
|
|
2663
|
+
&& (parent?.type === 'StaticMemberExpression' || (parent?.type === 'MemberExpression' && (parent as { computed?: boolean }).computed !== true))
|
|
2664
|
+
const isDeclared = parentKey === 'id' && parent?.type === 'VariableDeclarator'
|
|
2665
|
+
if (!isKey && !isMember && !isDeclared) {
|
|
2666
|
+
all.add(rec.name as string)
|
|
2667
|
+
if (!inFactory) live.add(rec.name as string)
|
|
2668
|
+
}
|
|
2669
|
+
return
|
|
2189
2670
|
}
|
|
2190
2671
|
for (const key of Object.keys(rec)) {
|
|
2191
2672
|
if (key === 'type' || key === 'start' || key === 'end') continue
|
|
2192
2673
|
const value = (rec as Record<string, unknown>)[key]
|
|
2193
|
-
if (Array.isArray(value))
|
|
2194
|
-
|
|
2195
|
-
} else if (walk(value, rec, key)) {
|
|
2196
|
-
return true
|
|
2197
|
-
}
|
|
2674
|
+
if (Array.isArray(value)) for (const child of value) walk(child, inFactory, rec, key)
|
|
2675
|
+
else walk(value, inFactory, rec, key)
|
|
2198
2676
|
}
|
|
2199
|
-
return false
|
|
2200
2677
|
}
|
|
2201
|
-
|
|
2678
|
+
for (const stmt of body as unknown[]) walk(stmt, false)
|
|
2679
|
+
return referenced = { all, live }
|
|
2202
2680
|
}
|
|
2203
2681
|
|
|
2204
2682
|
for (const imp of ordinaryImports) {
|
|
@@ -2208,7 +2686,7 @@ function transformMacroImpl(
|
|
|
2208
2686
|
imp.specifiers.every(spec =>
|
|
2209
2687
|
spec.type === 'ImportSpecifier' &&
|
|
2210
2688
|
usedImportedFactories.has(spec.local) &&
|
|
2211
|
-
!
|
|
2689
|
+
!referencedNames().all.has(spec.local),
|
|
2212
2690
|
)
|
|
2213
2691
|
) {
|
|
2214
2692
|
ms.remove(imp.start, imp.end)
|
|
@@ -2239,7 +2717,7 @@ function transformMacroImpl(
|
|
|
2239
2717
|
// Runtime compose consumes combinator objects. A partially lowered module mixes
|
|
2240
2718
|
// compiled parser functions with those objects (for example `trivia(ws)` after
|
|
2241
2719
|
// `ws` was lowered), so an unresolved compose makes the whole module runtime.
|
|
2242
|
-
const applied = runtimeComposeFallback ? [] : replacements
|
|
2720
|
+
const applied = runtimeComposeFallback ? [] : replacements.map(r => withCombinatorIR(r))
|
|
2243
2721
|
// NO MODULE HOIST. It existed to deduplicate declarations across the fused IIFEs the
|
|
2244
2722
|
// source lowering emitted — `_pfFail` and friends, one copy per variant. A table
|
|
2245
2723
|
// replacement is a `tableRules(...)` call over a data literal: there are no emitted
|