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/coverage.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { scanTo } from './combinators/scanTo.ts'
|
|
|
18
18
|
import { sequence } from './combinators/sequence.ts'
|
|
19
19
|
import { token, leaf, sourceLeaf } from './combinators/token.ts'
|
|
20
20
|
import { withCtx } from './combinators/withCtx.ts'
|
|
21
|
-
import { composedCoverageRules } from './compiler/linker.ts'
|
|
21
|
+
import { composedCoverageRules, linkedWinnersOf } from './compiler/linker.ts'
|
|
22
22
|
import { buildGrammarPlan, type GrammarCoverageDefinition, type GrammarCoveragePlan } from './compiler/grammar-coverage-ids.ts'
|
|
23
23
|
|
|
24
24
|
export type { GrammarCoverageDefinition } from './compiler/grammar-coverage-ids.ts'
|
|
@@ -156,11 +156,11 @@ export function grammarCoverageDefinitions(entry: Combinator<unknown>, winners?:
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/** Definitions for a runtime `compose()` result, normalized through its final
|
|
159
|
-
*
|
|
159
|
+
* winner map. Anything else — a compiled table, a plain map — fails rather than
|
|
160
160
|
* reporting source-piece identities as though they were final grammar IDs. */
|
|
161
161
|
export function composedGrammarCoverageDefinitions(grammar: Record<string, unknown>, startRule: string): readonly GrammarCoverageDefinition[] {
|
|
162
162
|
const winners = composedCoverageRules(grammar)
|
|
163
|
-
if (!winners) throw new TypeError('semantic coverage needs
|
|
163
|
+
if (!winners) throw new TypeError('semantic coverage needs a runtime compose() result (a linked interpreter grammar); this is not one')
|
|
164
164
|
const entry = winners[startRule]
|
|
165
165
|
if (!entry) throw new TypeError(`semantic coverage start rule ${JSON.stringify(startRule)} is not a final winner`)
|
|
166
166
|
return grammarCoverageDefinitions(entry, winners)
|
|
@@ -343,7 +343,7 @@ function coverageEntry(entry: Combinator<unknown>, collector: GrammarCoverageCol
|
|
|
343
343
|
case 'trivia': return trivia(build(def.parser))
|
|
344
344
|
case 'token': return token(build(def.parser))
|
|
345
345
|
case 'leaf': return def.cstValue === 'source' ? sourceLeaf(build(def.parser), def.fn) : leaf(build(def.parser), def.fn)
|
|
346
|
-
case 'field': return field(def.name, build(def.parser))
|
|
346
|
+
case 'field': return field(def.name, build(def.parser), { leadingTrivia: def.leadingTrivia === true, trailingTrivia: def.trailingTrivia === true })
|
|
347
347
|
case 'grammar': return grammarParser({
|
|
348
348
|
...(def.triviaParser === undefined ? (def.clearTrivia ? { trivia: null } : {}) : { trivia: build(def.triviaParser) }),
|
|
349
349
|
...(def.captureTrivia ? { captureTrivia: true } : {}),
|
|
@@ -380,7 +380,9 @@ function coverageEntry(entry: Combinator<unknown>, collector: GrammarCoverageCol
|
|
|
380
380
|
|
|
381
381
|
export function runWithGrammarCoverage(entry: Runnable, input: string, options: RunOptions & { collector?: GrammarCoverageCollector; trace?: GrammarTraceSink } = {}): { result: RunResult; coverage: GrammarCoverageSnapshot } {
|
|
382
382
|
if (typeof entry === 'function') throw new TypeError('runWithGrammarCoverage currently requires an interpreter combinator entry')
|
|
383
|
-
|
|
383
|
+
// A runtime composition's rule is planned through its winner map, so its IDs are
|
|
384
|
+
// the ones `composedGrammarCoverageDefinitions` reports.
|
|
385
|
+
const plan = buildGrammarPlan(entry, linkedWinnersOf(entry))
|
|
384
386
|
const definitions = plan.definitions
|
|
385
387
|
const collector = options.collector ?? createGrammarCoverageCollector(definitions)
|
|
386
388
|
const { collector: _collector, trace, ...runOptions } = options
|
package/src/cst/reflection.ts
CHANGED
|
@@ -115,6 +115,11 @@ export function collectGrammarReflection(
|
|
|
115
115
|
): GrammarReflection {
|
|
116
116
|
const byType = new Map<string, string[]>()
|
|
117
117
|
const followLazy = opts?.followLazy !== false
|
|
118
|
+
// ONE walk over the whole map. A node already visited from an earlier rule had
|
|
119
|
+
// its whole subgraph visited then, and `addNode` is idempotent, so a fresh set
|
|
120
|
+
// per rule only re-walked shared structure — O(rules × graph), and every unbound
|
|
121
|
+
// cross-piece hole threw (a stack-capturing Error) once per rule reaching it.
|
|
122
|
+
const seen = new Set<Combinator<unknown>>()
|
|
118
123
|
for (const [, rule] of ruleMap) {
|
|
119
124
|
if (!followLazy && rule._def.tag === 'lazy') {
|
|
120
125
|
try {
|
|
@@ -123,7 +128,7 @@ export function collectGrammarReflection(
|
|
|
123
128
|
} catch {}
|
|
124
129
|
continue
|
|
125
130
|
}
|
|
126
|
-
collectFromCombinator(rule, byType,
|
|
131
|
+
collectFromCombinator(rule, byType, seen, followLazy)
|
|
127
132
|
}
|
|
128
133
|
return { nodes: [...byType].map(([type, tags]) => ({ type, tags })) }
|
|
129
134
|
}
|
|
@@ -226,74 +226,35 @@ function plainRegexSource(p: Combinator<unknown>): string | null {
|
|
|
226
226
|
return d.tag === 'regex' && d.flags === '' ? d.source : null
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
let visitSpec2: LabeledTriviaSpec | undefined
|
|
232
|
-
let visitSpec3: LabeledTriviaSpec | undefined
|
|
233
|
-
let visitValue0: CssTriviaVisitor | null | undefined
|
|
234
|
-
let visitValue1: CssTriviaVisitor | null | undefined
|
|
235
|
-
let visitValue2: CssTriviaVisitor | null | undefined
|
|
236
|
-
let visitValue3: CssTriviaVisitor | null | undefined
|
|
229
|
+
/** The fields of a spec the char-level lowerings below are computed from. */
|
|
230
|
+
type TriviaArmsSpec = Pick<LabeledTriviaSpec, 'arms' | 'minRepeats'>
|
|
237
231
|
|
|
238
|
-
/**
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const visitor = commonCssTriviaVisitor(sources)
|
|
246
|
-
visitSpec3 = visitSpec2
|
|
247
|
-
visitValue3 = visitValue2
|
|
248
|
-
visitSpec2 = visitSpec1
|
|
249
|
-
visitValue2 = visitValue1
|
|
250
|
-
visitSpec1 = visitSpec0
|
|
251
|
-
visitValue1 = visitValue0
|
|
252
|
-
visitSpec0 = spec
|
|
253
|
-
visitValue0 = visitor
|
|
254
|
-
return visitor
|
|
232
|
+
/**
|
|
233
|
+
* A direct classified scanner for the four canonical CSS/Less trivia tuples.
|
|
234
|
+
* Computed once per spec, by `analyzeLabeledTrivia`, and read off it as
|
|
235
|
+
* `spec.visitor`: a scan never asks again.
|
|
236
|
+
*/
|
|
237
|
+
export function commonLabeledTriviaVisitor(spec: TriviaArmsSpec): CssTriviaVisitor | null {
|
|
238
|
+
return commonCssTriviaVisitor(spec.minRepeats <= 1 ? spec.arms.map(arm => plainRegexSource(arm.parser)) : [])
|
|
255
239
|
}
|
|
256
240
|
|
|
257
|
-
let armSpec0: LabeledTriviaSpec | undefined
|
|
258
|
-
let armSpec1: LabeledTriviaSpec | undefined
|
|
259
|
-
let armSpec2: LabeledTriviaSpec | undefined
|
|
260
|
-
let armSpec3: LabeledTriviaSpec | undefined
|
|
261
|
-
let armValue0: readonly TriviaArmMatcher[] | null | undefined
|
|
262
|
-
let armValue1: readonly TriviaArmMatcher[] | null | undefined
|
|
263
|
-
let armValue2: readonly TriviaArmMatcher[] | null | undefined
|
|
264
|
-
let armValue3: readonly TriviaArmMatcher[] | null | undefined
|
|
265
|
-
|
|
266
241
|
/**
|
|
267
242
|
* The character-level matchers for a spec's arms — index-parallel to `spec.arms`,
|
|
268
243
|
* so a matcher's index IS its kind index — or `null` when any arm has no lowering.
|
|
244
|
+
* Computed once per spec, as `spec.charArms`.
|
|
269
245
|
*
|
|
270
246
|
* `spec.minRepeats > 1` is excluded: the loops below report only an END, and a run
|
|
271
247
|
* that fails a minimum of two must report NOTHING. `classifiedTrivia()` never
|
|
272
248
|
* produces one, and the combinator path still handles it if a host writes one.
|
|
273
249
|
*/
|
|
274
|
-
export function charArmsFor(spec:
|
|
275
|
-
if (spec
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
arms = null
|
|
282
|
-
} else {
|
|
283
|
-
for (const arm of spec.arms) {
|
|
284
|
-
const c = classifyParser(arm.parser)
|
|
285
|
-
if (!c) { arms = null; break }
|
|
286
|
-
arms.push(c)
|
|
287
|
-
}
|
|
250
|
+
export function charArmsFor(spec: TriviaArmsSpec): readonly TriviaArmMatcher[] | null {
|
|
251
|
+
if (spec.minRepeats > 1) return null
|
|
252
|
+
const arms: TriviaArmMatcher[] = []
|
|
253
|
+
for (const arm of spec.arms) {
|
|
254
|
+
const c = classifyParser(arm.parser)
|
|
255
|
+
if (!c) return null
|
|
256
|
+
arms.push(c)
|
|
288
257
|
}
|
|
289
|
-
armSpec3 = armSpec2
|
|
290
|
-
armValue3 = armValue2
|
|
291
|
-
armSpec2 = armSpec1
|
|
292
|
-
armValue2 = armValue1
|
|
293
|
-
armSpec1 = armSpec0
|
|
294
|
-
armValue1 = armValue0
|
|
295
|
-
armSpec0 = spec
|
|
296
|
-
armValue0 = arms
|
|
297
258
|
return arms
|
|
298
259
|
}
|
|
299
260
|
|
package/src/cst/trivia-kinds.ts
CHANGED
|
@@ -2,7 +2,10 @@ import type { Combinator, ParseContext } from '../types.ts'
|
|
|
2
2
|
import { pushCstTriviaEntry, pushTriviaLogEntry } from './capture-buffer.ts'
|
|
3
3
|
import { startsFirstSet } from '../combinators/first-set.ts'
|
|
4
4
|
import { createDetachedParseContext } from '../parse-context.ts'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
charArmsFor, charTriviaEnd, charTriviaKindMask, charTriviaVisit, commonLabeledTriviaVisitor, type TriviaArmMatcher,
|
|
7
|
+
} from './trivia-charscan.ts'
|
|
8
|
+
import type { CssTriviaVisitor } from './trivia-css-scanner.ts'
|
|
6
9
|
|
|
7
10
|
export type TriviaChunk = { start: number; end: number; kindIndex: number }
|
|
8
11
|
|
|
@@ -10,6 +13,13 @@ export type LabeledTriviaSpec = {
|
|
|
10
13
|
readonly labels: readonly string[]
|
|
11
14
|
readonly arms: ReadonlyArray<{ label: string; kindIndex: number; parser: Combinator<unknown> }>
|
|
12
15
|
readonly minRepeats: number
|
|
16
|
+
/**
|
|
17
|
+
* The spec's char-level scanners, built with it. A scan reads them here: the
|
|
18
|
+
* four-slot memo they used to sit behind thrashed on a grammar with more trivia
|
|
19
|
+
* tables than slots, and rebuilt one on every miss.
|
|
20
|
+
*/
|
|
21
|
+
readonly visitor: CssTriviaVisitor | null
|
|
22
|
+
readonly charArms: readonly TriviaArmMatcher[] | null
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
/** See `trivia-skip.ts`: bounded scalar memoisation avoids a WeakMap in the
|
|
@@ -93,12 +103,18 @@ export function analyzeLabeledTrivia(trivia: Combinator<unknown>): LabeledTrivia
|
|
|
93
103
|
arms.push({ label: peeled.label, kindIndex: 0, parser: peeled.parser })
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
|
|
106
|
+
return cacheLabeledTrivia(trivia, labeledTriviaSpec(arms, minRepeats))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A spec, with the char-level scanners every scan of it reads. */
|
|
110
|
+
export function labeledTriviaSpec(arms: LabeledTriviaSpec['arms'], minRepeats: number): LabeledTriviaSpec {
|
|
111
|
+
return {
|
|
97
112
|
labels: arms.map(a => a.label),
|
|
98
113
|
arms,
|
|
99
114
|
minRepeats,
|
|
115
|
+
visitor: commonLabeledTriviaVisitor({ arms, minRepeats }),
|
|
116
|
+
charArms: charArmsFor({ arms, minRepeats }),
|
|
100
117
|
}
|
|
101
|
-
return cacheLabeledTrivia(trivia, spec)
|
|
102
118
|
}
|
|
103
119
|
|
|
104
120
|
/** Label table on a `trivia()` combinator, if all arms are labeled. */
|
|
@@ -169,9 +185,9 @@ export function scanLabeledTriviaEnd(
|
|
|
169
185
|
spec: LabeledTriviaSpec,
|
|
170
186
|
state?: unknown,
|
|
171
187
|
): number {
|
|
172
|
-
// `
|
|
188
|
+
// `charArms` only lowers `minRepeats <= 1`, where "no chunk matched" and
|
|
173
189
|
// "the minimum was not met" are the same position — so an end IS the answer.
|
|
174
|
-
const chars =
|
|
190
|
+
const chars = spec.charArms
|
|
175
191
|
if (chars !== null) return charTriviaEnd(input, cur, chars)
|
|
176
192
|
|
|
177
193
|
let pos = cur
|
|
@@ -205,9 +221,9 @@ export function visitLabeledTrivia(
|
|
|
205
221
|
visit: (start: number, end: number, kindIndex: number) => void,
|
|
206
222
|
): number | undefined {
|
|
207
223
|
if (spec.minRepeats > 1) return undefined
|
|
208
|
-
const common =
|
|
224
|
+
const common = spec.visitor
|
|
209
225
|
if (common !== null) return common(input, cur, visit)
|
|
210
|
-
const chars =
|
|
226
|
+
const chars = spec.charArms
|
|
211
227
|
if (chars !== null) return charTriviaVisit(input, cur, chars, visit)
|
|
212
228
|
|
|
213
229
|
let pos = cur
|
|
@@ -331,7 +347,7 @@ export function triviaKindMaskAt(
|
|
|
331
347
|
spec: LabeledTriviaSpec,
|
|
332
348
|
state?: unknown,
|
|
333
349
|
): number {
|
|
334
|
-
const chars =
|
|
350
|
+
const chars = spec.charArms
|
|
335
351
|
if (chars !== null) return charTriviaKindMask(input, pos, chars)
|
|
336
352
|
|
|
337
353
|
let cur = pos
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ export { node } from './combinators/node.ts'
|
|
|
30
30
|
export type { BuildNode, NodeCombinator, NodeOptions, NodeProjectOptions } from './combinators/node.ts'
|
|
31
31
|
// lazy() is intentionally NOT exported.
|
|
32
32
|
export { transform, trivia, classifiedTrivia, label, field } from './combinators/map.ts'
|
|
33
|
+
export type { FieldOptions } from './combinators/map.ts'
|
|
33
34
|
export { parse, parser, noTrivia } from './combinators/grammar.ts'
|
|
34
35
|
export type { ParseOptions, ParserOptions, ParsemanParser } from './combinators/grammar.ts'
|
|
35
36
|
export { token, leaf, sourceLeaf } from './combinators/token.ts'
|
package/src/plugin/evaluator.ts
CHANGED
|
@@ -154,6 +154,69 @@ export type ScopeEntry = {
|
|
|
154
154
|
}
|
|
155
155
|
export type Scope = Map<string, ScopeEntry>
|
|
156
156
|
|
|
157
|
+
/** How a {@link ModuleScope} answers a name it does not hold: a value, the reason a
|
|
158
|
+
* binding the module DOES have cannot be known at build time, or `null` — not a
|
|
159
|
+
* binding this scope can speak for (an ordinary miss). */
|
|
160
|
+
export type FreeNameResolution = { value: unknown } | { unresolved: string } | null
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A module's scope whose IMPORTED (and, for a foreign module, top-level) bindings
|
|
164
|
+
* resolve on first read instead of being absent.
|
|
165
|
+
*
|
|
166
|
+
* Absent was the defect. `anyValue` answered an imported name with `null`, so
|
|
167
|
+
* `rules({ scanSkip: [importedUnit] })` evaluated to nothing, the option quietly
|
|
168
|
+
* became `undefined`, and the build succeeded with no ambient skip — and a terminal
|
|
169
|
+
* imported into a rule body could not be lowered at all. Lazy, because a grammar
|
|
170
|
+
* module imports far more than combinators (AST classes, helpers) and resolving each
|
|
171
|
+
* one eagerly would read every one of those modules for nothing.
|
|
172
|
+
*
|
|
173
|
+
* A binding that exists but cannot be resolved is RECORDED in `unresolved` (shared by
|
|
174
|
+
* every scope of one transform) as "`name` <reason>", so the plugin can fail the build
|
|
175
|
+
* naming it rather than warn about an anonymous "isn't statically evaluable". Only a
|
|
176
|
+
* VALUE read records: `has()` is how the evaluator probes a name it may legitimately
|
|
177
|
+
* leave to runtime (a named reducer), so a probe that misses is not a failure.
|
|
178
|
+
*/
|
|
179
|
+
export class ModuleScope extends Map<string, unknown> {
|
|
180
|
+
private readonly tried = new Map<string, string | undefined>()
|
|
181
|
+
private readonly resolveFree: (name: string) => FreeNameResolution
|
|
182
|
+
readonly unresolved: Set<string>
|
|
183
|
+
constructor(resolveFree: (name: string) => FreeNameResolution, unresolved: Set<string>) {
|
|
184
|
+
super()
|
|
185
|
+
this.resolveFree = resolveFree
|
|
186
|
+
this.unresolved = unresolved
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
override has(name: string): boolean {
|
|
190
|
+
return super.has(name) || this.resolve(name)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
override get(name: string): unknown {
|
|
194
|
+
if (super.has(name) || this.resolve(name)) return super.get(name)
|
|
195
|
+
const why = this.tried.get(name)
|
|
196
|
+
if (why !== undefined) this.unresolved.add(`\`${name}\` ${why}`)
|
|
197
|
+
return undefined
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A child scope (a factory body) that keeps resolving this module's free names. */
|
|
201
|
+
fork(): ModuleScope {
|
|
202
|
+
const child = new ModuleScope(this.resolveFree, this.unresolved)
|
|
203
|
+
for (const [k, v] of super.entries()) child.set(k, v)
|
|
204
|
+
return child
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private resolve(name: string): boolean {
|
|
208
|
+
if (this.tried.has(name)) return false
|
|
209
|
+
const r = this.resolveFree(name)
|
|
210
|
+
if (r !== null && 'value' in r) {
|
|
211
|
+
super.set(name, r.value)
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
// A name nothing binds YET is not remembered: a later declaration may bind it.
|
|
215
|
+
if (r !== null) this.tried.set(name, r.unresolved)
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
157
220
|
// Internal XScope also holds non-Combinator values (g proxy objects etc.)
|
|
158
221
|
type XScopeVal = ScopeEntry | unknown
|
|
159
222
|
type XScope = Map<string, XScopeVal>
|
|
@@ -251,7 +314,7 @@ const SUPPORTED: Record<string, (...args: unknown[]) => Combinator<unknown>> = {
|
|
|
251
314
|
a[0] as Readonly<Record<string, Combinator<unknown>>>,
|
|
252
315
|
),
|
|
253
316
|
label: (...a) => parseman.label(a[0] as string, a[1] as Combinator<unknown>),
|
|
254
|
-
field: (...a) => parseman.field(a[0] as string, a[1] as Combinator<unknown
|
|
317
|
+
field: (...a) => parseman.field(a[0] as string, a[1] as Combinator<unknown>, a[2] as parseman.FieldOptions | undefined),
|
|
255
318
|
noTrivia: (...a) => parseman.noTrivia(a[0] as Combinator<unknown>),
|
|
256
319
|
token: (...a) => parseman.token(a[0] as Combinator<unknown>),
|
|
257
320
|
// `routed(fallback)` — the fallback must be forwarded. A zero-arg entry here read
|
|
@@ -556,6 +619,20 @@ function staticNodeOptions(expr: Expression, scope: XScope): StaticNodeOptions {
|
|
|
556
619
|
return opts.unwrap || opts.collapse || opts.project !== undefined || opts.captureTrivia || opts.trailingTrivia || opts.tags !== undefined || opts.buildArity !== undefined ? opts : undefined
|
|
557
620
|
}
|
|
558
621
|
|
|
622
|
+
/**
|
|
623
|
+
* `scanTo`/`balanced` options: `undefined` when absent, `UNKNOWN_OPTIONS` when present
|
|
624
|
+
* but not statically known. Passing the unknown value on as `null` is what built a
|
|
625
|
+
* region with NO skip units out of `balanced('(', ')', importedOpts)` — the options
|
|
626
|
+
* silently vanished.
|
|
627
|
+
*/
|
|
628
|
+
const UNKNOWN_OPTIONS = Symbol('parseman.unknownOptions')
|
|
629
|
+
function scanOptions(optsArg: Expression | { type: 'SpreadElement' } | undefined, scope: XScope, code?: string): unknown {
|
|
630
|
+
if (optsArg === undefined) return undefined
|
|
631
|
+
if (optsArg.type === 'SpreadElement') return UNKNOWN_OPTIONS
|
|
632
|
+
const opts = anyValue(optsArg as Expression, scope, code, [])
|
|
633
|
+
return isHole(optsArg as Expression, opts, scope) ? UNKNOWN_OPTIONS : opts
|
|
634
|
+
}
|
|
635
|
+
|
|
559
636
|
/**
|
|
560
637
|
* Evaluate a call expression to a Combinator.
|
|
561
638
|
* `mfs` accumulates mapFn source texts in depth-first order — must match
|
|
@@ -893,9 +970,8 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
|
|
|
893
970
|
const open = anyValue(openArg as Expression, scope, code, [])
|
|
894
971
|
const close = anyValue(closeArg as Expression, scope, code, [])
|
|
895
972
|
if (typeof open !== 'string' || typeof close !== 'string') return null
|
|
896
|
-
const opts = optsArg
|
|
897
|
-
|
|
898
|
-
: undefined
|
|
973
|
+
const opts = scanOptions(optsArg, scope, code)
|
|
974
|
+
if (opts === UNKNOWN_OPTIONS) return null
|
|
899
975
|
try { return parseman.balanced(open, close, opts as parseman.ScanToOptions | undefined) } catch { return null }
|
|
900
976
|
}
|
|
901
977
|
|
|
@@ -906,9 +982,8 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
|
|
|
906
982
|
if (!sentinelArg || sentinelArg.type === 'SpreadElement') return null
|
|
907
983
|
const sentinel = anyValue(sentinelArg as Expression, scope, code, [])
|
|
908
984
|
if (!isCombinator(sentinel)) return null
|
|
909
|
-
const opts = optsArg
|
|
910
|
-
|
|
911
|
-
: undefined
|
|
985
|
+
const opts = scanOptions(optsArg, scope, code)
|
|
986
|
+
if (opts === UNKNOWN_OPTIONS) return null
|
|
912
987
|
try { return parseman.scanTo(sentinel, opts as parseman.ScanToOptions | undefined) } catch { return null }
|
|
913
988
|
}
|
|
914
989
|
|
|
@@ -1039,6 +1114,22 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
|
|
|
1039
1114
|
} catch { return null }
|
|
1040
1115
|
}
|
|
1041
1116
|
|
|
1117
|
+
/**
|
|
1118
|
+
* An element that evaluated to `null` without BEING `null` — an unresolved name, a
|
|
1119
|
+
* call the evaluator could not build. Kept as a `null` slot, it made the container
|
|
1120
|
+
* look evaluated: `{ skip: [unknownUnit] }` became `{ skip: [null] }`, and an options
|
|
1121
|
+
* object went missing piece by piece with no warning. So a hole fails the whole
|
|
1122
|
+
* container. A function literal is exempt: it evaluates to `null` by design, its
|
|
1123
|
+
* source being captured by the caller that needs it.
|
|
1124
|
+
*/
|
|
1125
|
+
function isHole(node: Expression, value: unknown, scope: XScope): boolean {
|
|
1126
|
+
if (value !== null) return false
|
|
1127
|
+
const n = unwrapStaticExpr(node) as { type: string; value?: unknown; name?: string }
|
|
1128
|
+
if (n.type === 'Identifier') return !scope.has(n.name!) // a bound nullish const is a value
|
|
1129
|
+
return !(n.type === 'Literal' && n.value === null)
|
|
1130
|
+
&& n.type !== 'ArrowFunctionExpression' && n.type !== 'FunctionExpression'
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1042
1133
|
/** Evaluate any expression to its JS value (not necessarily a Combinator). */
|
|
1043
1134
|
function anyValue(node: Expression, scope: XScope, code?: string, mfs?: string[]): unknown {
|
|
1044
1135
|
if (node.type === 'TSAsExpression'
|
|
@@ -1064,7 +1155,9 @@ function anyValue(node: Expression, scope: XScope, code?: string, mfs?: string[]
|
|
|
1064
1155
|
for (const el of arr.elements) {
|
|
1065
1156
|
if (el === null) { out.push(null); continue }
|
|
1066
1157
|
if ((el as { type: string }).type === 'SpreadElement') return null
|
|
1067
|
-
|
|
1158
|
+
const v = anyValue(el as Expression, scope, code, mfs)
|
|
1159
|
+
if (isHole(el as Expression, v, scope)) return null
|
|
1160
|
+
out.push(v)
|
|
1068
1161
|
}
|
|
1069
1162
|
return out
|
|
1070
1163
|
}
|
|
@@ -1074,7 +1167,10 @@ function anyValue(node: Expression, scope: XScope, code?: string, mfs?: string[]
|
|
|
1074
1167
|
for (const prop of node.properties) {
|
|
1075
1168
|
const key = propName(prop as never)
|
|
1076
1169
|
if (key === null) return null
|
|
1077
|
-
|
|
1170
|
+
const valueNode = (prop as unknown as ObjectProperty).value as Expression
|
|
1171
|
+
const v = anyValue(valueNode, scope, code, mfs)
|
|
1172
|
+
if (isHole(valueNode, v, scope)) return null
|
|
1173
|
+
obj[key] = v
|
|
1078
1174
|
}
|
|
1079
1175
|
return obj
|
|
1080
1176
|
}
|
|
@@ -1184,15 +1280,16 @@ export function evaluateExpr(
|
|
|
1184
1280
|
* Evaluate a `const X = [combinator, …]` array literal into an array of
|
|
1185
1281
|
* Combinators. Lets a shared option array (e.g. a `skip` set reused across
|
|
1186
1282
|
* `scanTo`/`balanced` calls) be referenced by name — `{ skip: X }` — instead of
|
|
1187
|
-
* inlining the array at every call site. Returns null when `node`
|
|
1188
|
-
*
|
|
1283
|
+
* inlining the array at every call site. Returns null when `node` doesn't evaluate
|
|
1284
|
+
* to an array of statically-resolvable combinators.
|
|
1189
1285
|
*/
|
|
1190
1286
|
export function evaluateCombinatorArray(
|
|
1191
1287
|
node: Expression,
|
|
1192
1288
|
scope: Scope,
|
|
1193
1289
|
code?: string,
|
|
1194
1290
|
): Combinator<unknown>[] | null {
|
|
1195
|
-
|
|
1291
|
+
// Any expression, not just an array literal: `rules({ scanSkip: sharedSkip })`
|
|
1292
|
+
// names the array, and requiring a literal here is what dropped it.
|
|
1196
1293
|
const val = anyValue(node, scope as XScope, code, [])
|
|
1197
1294
|
if (!Array.isArray(val) || val.length === 0) return null
|
|
1198
1295
|
if (!val.every(isCombinator)) return null
|
|
@@ -1453,7 +1550,7 @@ export function evaluateParserFactory(
|
|
|
1453
1550
|
built = rules(options ?? {}, (g: Record<string, Combinator<unknown>>) => {
|
|
1454
1551
|
// Outer ScopeEntry values carry their mfSrcs and are replayed by scopeGet()
|
|
1455
1552
|
// when body statements or return expressions reference them.
|
|
1456
|
-
const localScope: XScope = new Map(scope as XScope)
|
|
1553
|
+
const localScope: XScope = scope instanceof ModuleScope ? scope.fork() : new Map(scope as XScope)
|
|
1457
1554
|
localScope.set(proxyName, g)
|
|
1458
1555
|
|
|
1459
1556
|
// ── Phase 1: the factory's own body statements ────────────────────────
|
|
@@ -1564,7 +1661,10 @@ export function applyDefineStatement(
|
|
|
1564
1661
|
/** Check if an AST node references any name from the given scope or names set. */
|
|
1565
1662
|
export function referencesAny(node: Node, names: Set<string>, scope: Scope): boolean {
|
|
1566
1663
|
if (node.type === 'Identifier') {
|
|
1567
|
-
|
|
1664
|
+
// The names the module has BOUND so far, not every import it could resolve: this
|
|
1665
|
+
// decides whether a declaration is macro code at all, and an ordinary
|
|
1666
|
+
// `const x = importedThing` must not start reading modules to find out.
|
|
1667
|
+
return names.has(node.name) || Map.prototype.has.call(scope, node.name)
|
|
1568
1668
|
}
|
|
1569
1669
|
for (const key of Object.keys(node) as (keyof typeof node)[]) {
|
|
1570
1670
|
const child = node[key]
|