parseman 0.53.0 → 0.55.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 +295 -119
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +295 -119
- 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/repeat.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/combinators/withCtx.d.ts +22 -7
- package/dist/combinators/withCtx.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +2 -0
- package/dist/compiler/fields.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 +319 -130
- package/dist/index.js.map +3 -3
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.js +324 -132
- package/dist/plugin/index.js.map +3 -3
- package/dist/table/assemble.d.ts +9 -0
- package/dist/table/assemble.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 +262 -113
- package/dist/table/index.js.map +3 -3
- package/dist/table/ops.d.ts +3 -1
- package/dist/table/ops.d.ts.map +1 -1
- 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/repeat.ts +6 -1
- package/src/combinators/sequence.ts +9 -1
- package/src/combinators/withCtx.ts +61 -28
- package/src/compiler/fields.ts +5 -2
- package/src/compiler/ir-serialize.ts +7 -1
- package/src/coverage.ts +1 -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 +13 -3
- package/src/table/assemble.ts +137 -23
- package/src/table/emit-assembly.ts +38 -18
- package/src/table/emit.ts +15 -4
- package/src/table/encode.ts +23 -2
- package/src/table/exec.ts +17 -2
- package/src/table/field-spec.ts +17 -0
- package/src/table/ops.ts +3 -1
- package/src/table/scalar-terminal.ts +25 -0
- package/src/types.ts +14 -2
- package/src/version.ts +1 -1
|
@@ -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
|
@@ -96,6 +96,9 @@ function stripTsFromSource(node: Node, code: string): string {
|
|
|
96
96
|
class UnevaluatedExtra {}
|
|
97
97
|
const UNEVALUATED_EXTRA: unknown = new UnevaluatedExtra()
|
|
98
98
|
|
|
99
|
+
/** The stand-in for a derive-form `withCtx(s => next, …)`; see the `withCtx` case. */
|
|
100
|
+
const DERIVE_STAND_IN = (state: unknown): unknown => state
|
|
101
|
+
|
|
99
102
|
// ---------------------------------------------------------------------------
|
|
100
103
|
// Reducer resolution
|
|
101
104
|
//
|
|
@@ -314,7 +317,7 @@ const SUPPORTED: Record<string, (...args: unknown[]) => Combinator<unknown>> = {
|
|
|
314
317
|
a[0] as Readonly<Record<string, Combinator<unknown>>>,
|
|
315
318
|
),
|
|
316
319
|
label: (...a) => parseman.label(a[0] as string, a[1] as Combinator<unknown>),
|
|
317
|
-
field: (...a) => parseman.field(a[0] as string, a[1] as Combinator<unknown
|
|
320
|
+
field: (...a) => parseman.field(a[0] as string, a[1] as Combinator<unknown>, a[2] as parseman.FieldOptions | undefined),
|
|
318
321
|
noTrivia: (...a) => parseman.noTrivia(a[0] as Combinator<unknown>),
|
|
319
322
|
token: (...a) => parseman.token(a[0] as Combinator<unknown>),
|
|
320
323
|
// `routed(fallback)` — the fallback must be forwarded. A zero-arg entry here read
|
|
@@ -1028,10 +1031,17 @@ function exprToCombi(node: Expression, scope: XScope, code?: string, mfs?: strin
|
|
|
1028
1031
|
// survives only when it cannot be evaluated, and then it is `emittableConst`
|
|
1029
1032
|
// that decides — a plain `{}` extras object is indistinguishable from an
|
|
1030
1033
|
// author's `{}`, so an unevaluable one must NOT masquerade as empty state.
|
|
1031
|
-
|
|
1034
|
+
//
|
|
1035
|
+
// A function literal is the DERIVE form, `withCtx(s => next, inner)`. Its
|
|
1036
|
+
// value is never needed here — the encoder prints `extraSrc` into the
|
|
1037
|
+
// artifact — but the def must hold a FUNCTION so the encoder lowers the
|
|
1038
|
+
// derive form at all; the stand-in is that function, like `gate`'s.
|
|
1039
|
+
const extraType = (unwrapStaticExpr(extraArg as Expression) as { type: string }).type
|
|
1040
|
+
const derives = extraType === 'ArrowFunctionExpression' || extraType === 'FunctionExpression'
|
|
1041
|
+
const evaluated = derives ? null : anyValue(extraArg as Expression, scope, code, [])
|
|
1032
1042
|
const usable = typeof evaluated === 'object' && evaluated !== null && !Array.isArray(evaluated)
|
|
1033
1043
|
&& Object.getPrototypeOf(evaluated) === Object.prototype
|
|
1034
|
-
const combi = parseman.withCtx(usable ? evaluated : UNEVALUATED_EXTRA, inner)
|
|
1044
|
+
const combi = parseman.withCtx(derives ? DERIVE_STAND_IN : usable ? evaluated : UNEVALUATED_EXTRA, inner)
|
|
1035
1045
|
if (combi._def.tag !== 'withCtx') return null
|
|
1036
1046
|
combi._def.extraSrc = extraSrc
|
|
1037
1047
|
return combi
|
package/src/table/assemble.ts
CHANGED
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
|
|
67
67
|
import { balanced, scanTo } from '../combinators/scanTo.ts'
|
|
68
68
|
import { buildFieldMap } from '../compiler/fields.ts'
|
|
69
|
+
import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA, LEAD, triviaFieldCapture } from '../combinators/field-trivia.ts'
|
|
69
70
|
import { asciiFoldKey } from '../combinators/dispatch.ts'
|
|
70
71
|
import { projectChild, unwrapChild } from '../combinators/node.ts'
|
|
71
72
|
import { cstOutputHost } from '../compiler/build-arity.ts'
|
|
@@ -129,7 +130,7 @@ import { computeSiteLabels, reachableSites } from './site-labels.ts'
|
|
|
129
130
|
import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
|
|
130
131
|
import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
|
|
131
132
|
import {
|
|
132
|
-
|
|
133
|
+
largeChoiceLiteralPretest, leadingScalarSequence, leadingScalarTerminal, makeScalarRecognizer, scalarTerminalNodeChild,
|
|
133
134
|
scalarTerminalNotChild, type ScalarRecognizer,
|
|
134
135
|
} from './scalar-terminal.ts'
|
|
135
136
|
|
|
@@ -146,6 +147,7 @@ const EMIT_ENABLED = (globalThis as { process?: { env?: Record<string, string |
|
|
|
146
147
|
|
|
147
148
|
/** Failure sentinel — SHARED with the other two engines, see `cell.ts`. */
|
|
148
149
|
import { FAIL, newEndCell } from './cell.ts'
|
|
150
|
+
import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
|
|
149
151
|
|
|
150
152
|
const EMPTY_TL: readonly number[] = Object.freeze([])
|
|
151
153
|
/** `finishCstBuf`'s two empty sentinels, held here so the node piece can inline it. */
|
|
@@ -1053,6 +1055,15 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1053
1055
|
return cur
|
|
1054
1056
|
}
|
|
1055
1057
|
|
|
1058
|
+
/**
|
|
1059
|
+
* A `field(…, { leadingTrivia: true })` term. The pre-scan cursor reaches the
|
|
1060
|
+
* field AFTER `step`'s scan, right before the field runs, so a field inside the
|
|
1061
|
+
* trivia grammar cannot take it first. One closure per call; only such terms pay.
|
|
1062
|
+
*/
|
|
1063
|
+
function leadingFieldTerm(step: typeof nextTerm, child: Piece, input: string, cur: number, ctx: ParseContext): number {
|
|
1064
|
+
return step((i, p, c) => { LEAD.at = cur; return child(i, p, c) }, input, cur, ctx)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1056
1067
|
function committed(c: ParseContext): boolean {
|
|
1057
1068
|
return c._fc === true
|
|
1058
1069
|
}
|
|
@@ -1793,12 +1804,27 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1793
1804
|
}
|
|
1794
1805
|
|
|
1795
1806
|
case OP_FIELD: {
|
|
1796
|
-
const name = k[code[ip + 1]!]
|
|
1807
|
+
const { name, bits } = fieldSpec(k[code[ip + 1]!])
|
|
1797
1808
|
const child = link(code[ip + 2]!)
|
|
1809
|
+
if (bits === 0) {
|
|
1810
|
+
return (input, pos, ctx) => {
|
|
1811
|
+
const v = child(input, pos, ctx)
|
|
1812
|
+
if (v === FAIL) return FAIL
|
|
1813
|
+
ctx._fields?.push({ name, value: v, span: { start: pos, end: EC.e } })
|
|
1814
|
+
return v
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
const leading = (bits & FIELD_LEADING_TRIVIA) !== 0
|
|
1818
|
+
const trailing = (bits & FIELD_TRAILING_TRIVIA) !== 0
|
|
1798
1819
|
return (input, pos, ctx) => {
|
|
1820
|
+
const leadFrom = leading ? LEAD.at : -1
|
|
1821
|
+
LEAD.at = -1
|
|
1799
1822
|
const v = child(input, pos, ctx)
|
|
1800
1823
|
if (v === FAIL) return FAIL
|
|
1801
|
-
|
|
1824
|
+
const end = EC.e
|
|
1825
|
+
const trailEnd = trailing && ctx.trivia !== undefined ? consumeTrivia(input, end, ctx) : end
|
|
1826
|
+
ctx._fields?.push(triviaFieldCapture(name, v, input, pos, end, leadFrom, trailEnd))
|
|
1827
|
+
EC.e = trailEnd
|
|
1802
1828
|
return v
|
|
1803
1829
|
}
|
|
1804
1830
|
}
|
|
@@ -1835,8 +1861,18 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1835
1861
|
|
|
1836
1862
|
case OP_WITHCTX: {
|
|
1837
1863
|
// `extra` and the child are both bound HERE; the piece only swaps a field.
|
|
1838
|
-
const
|
|
1864
|
+
const ki = code[ip + 1]!
|
|
1839
1865
|
const child = link(code[ip + 2]!)
|
|
1866
|
+
if (ki < 0) {
|
|
1867
|
+
const derive = fns[~ki] as (s: unknown) => unknown
|
|
1868
|
+
return (input, pos, ctx) => {
|
|
1869
|
+
const saved = ctx.state
|
|
1870
|
+
ctx.state = derive(saved)
|
|
1871
|
+
try { return child(input, pos, ctx) }
|
|
1872
|
+
finally { ctx.state = saved }
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
const extra = k[ki]
|
|
1840
1876
|
return (input, pos, ctx) => {
|
|
1841
1877
|
const saved = ctx.state
|
|
1842
1878
|
ctx.state = extra
|
|
@@ -2322,6 +2358,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2322
2358
|
const kidIp = code[base + i]!
|
|
2323
2359
|
if (code[kidIp] !== OP_ADJ) {
|
|
2324
2360
|
const kid = link(kidIp)
|
|
2361
|
+
if (leadingTriviaFieldAt(code, k, kidIp)) {
|
|
2362
|
+
return (input, cur, ctx, inherited) => {
|
|
2363
|
+
ctx._sync = sync ?? inherited
|
|
2364
|
+
return leadingFieldTerm(step, kid, input, cur, ctx)
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2325
2367
|
return (input, cur, ctx, inherited) => {
|
|
2326
2368
|
ctx._sync = sync ?? inherited
|
|
2327
2369
|
return step(kid, input, cur, ctx)
|
|
@@ -2389,14 +2431,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2389
2431
|
}
|
|
2390
2432
|
}
|
|
2391
2433
|
|
|
2434
|
+
// A `field(…, { leadingTrivia: true })` term is a boundary fact too: the
|
|
2435
|
+
// runner hands it the cursor from BEFORE the scan (`LEAD`, cell.ts).
|
|
2392
2436
|
let hasAdj = false
|
|
2393
|
-
for (let i = 1; i < n; i++)
|
|
2437
|
+
for (let i = 1; i < n; i++) {
|
|
2438
|
+
const kidIp = code[base + i]!
|
|
2439
|
+
if (code[kidIp] === OP_ADJ || leadingTriviaFieldAt(code, k, kidIp)) { hasAdj = true; break }
|
|
2440
|
+
}
|
|
2394
2441
|
if (hasAdj) {
|
|
2395
2442
|
const first = link(code[base]!)
|
|
2396
2443
|
const runnerAt = (i: number): TermRunner => {
|
|
2397
2444
|
const kidIp = code[base + i]!
|
|
2398
2445
|
if (code[kidIp] !== OP_ADJ) {
|
|
2399
2446
|
const kid = link(kidIp)
|
|
2447
|
+
if (leadingTriviaFieldAt(code, k, kidIp)) {
|
|
2448
|
+
return (input, cur, ctx) => leadingFieldTerm(step, kid, input, cur, ctx)
|
|
2449
|
+
}
|
|
2400
2450
|
return (input, cur, ctx) => step(kid, input, cur, ctx)
|
|
2401
2451
|
}
|
|
2402
2452
|
const negated = code[kidIp + 1] === 1
|
|
@@ -2791,7 +2841,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2791
2841
|
if (n === 2 || n === 3) {
|
|
2792
2842
|
const a0 = link(code[base]!), a1 = link(code[base + 1]!)
|
|
2793
2843
|
const a2 = n === 3 ? link(code[base + 2]!) : undefined
|
|
2794
|
-
const predecide = !
|
|
2844
|
+
const predecide = !REC && !cfg.probe && !cfg.coverage && !cfg.trackLines
|
|
2795
2845
|
const p0ip = predecide ? leadingScalarTerminal(code, code[base]!, 2, true, true) : -1
|
|
2796
2846
|
const p1ip = predecide ? leadingScalarTerminal(code, code[base + 1]!, 2, true, true) : -1
|
|
2797
2847
|
const p2ip = predecide && n === 3 ? leadingScalarTerminal(code, code[base + 2]!, 2, true, true) : -1
|
|
@@ -2958,6 +3008,56 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2958
3008
|
)
|
|
2959
3009
|
}
|
|
2960
3010
|
const masked = bindMasked(0, EMPTY_EXPECTED_PREFIX)
|
|
3011
|
+
// LITERAL PRETESTS. First-char dispatch cannot tell `@{…}` or `@@x` from
|
|
3012
|
+
// `@x`, so every arm sharing the char is entered, and under a CST host
|
|
3013
|
+
// each entered node opens a capture frame before failing on the second
|
|
3014
|
+
// char. An arm that must begin with a literal of two or more chars (or
|
|
3015
|
+
// one of a family of them) is tested on the literal first and, on a
|
|
3016
|
+
// miss, skipped exactly as a char-excluded arm is: its static expected
|
|
3017
|
+
// set is the one reported. The test is pure, so a miss leaves no
|
|
3018
|
+
// capture, trivia or field to roll back.
|
|
3019
|
+
const pBit: number[] = []
|
|
3020
|
+
const pRec: ScalarRecognizer[] = []
|
|
3021
|
+
if (!REC && !cfg.probe && !cfg.coverage && !cfg.trackLines) {
|
|
3022
|
+
for (let i = 0; i < n; i++) {
|
|
3023
|
+
for (const terminal of largeChoiceLiteralPretest(code, k, code[base + i]!) ?? []) {
|
|
3024
|
+
pBit.push(1 << i)
|
|
3025
|
+
pRec.push(scalarFor(terminal)!)
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
const pn = pBit.length
|
|
3030
|
+
if (pn !== 0) {
|
|
3031
|
+
return (input, pos, ctx) => {
|
|
3032
|
+
const c = lead(input, pos)
|
|
3033
|
+
const need = markCst(ctx)
|
|
3034
|
+
const mRaw = MRAW
|
|
3035
|
+
const mTl = MTL
|
|
3036
|
+
const mLv = MLV
|
|
3037
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
3038
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
3039
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
3040
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
3041
|
+
if (c < 128) {
|
|
3042
|
+
const bits = mask[c < 0 ? 128 : c]!
|
|
3043
|
+
let tested = 0
|
|
3044
|
+
let hit = 0
|
|
3045
|
+
for (let i = 0; i < pn; i++) {
|
|
3046
|
+
const b = pBit[i]!
|
|
3047
|
+
if ((bits & b) === 0 || (hit & b) !== 0) continue
|
|
3048
|
+
tested |= b
|
|
3049
|
+
if (pRec[i]!(input, pos) >= 0) hit |= b
|
|
3050
|
+
}
|
|
3051
|
+
return masked(
|
|
3052
|
+
input, pos, ctx, bits & (~tested | hit),
|
|
3053
|
+
need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
|
|
3054
|
+
)
|
|
3055
|
+
}
|
|
3056
|
+
return general(
|
|
3057
|
+
input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
|
|
3058
|
+
)
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
2961
3061
|
return (input, pos, ctx) => {
|
|
2962
3062
|
const c = lead(input, pos)
|
|
2963
3063
|
const need = markCst(ctx)
|
|
@@ -3100,6 +3200,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3100
3200
|
const max = code[ip + 3]!
|
|
3101
3201
|
const sepIp = code[ip + 4]!
|
|
3102
3202
|
const sep = sepIp >= 0 ? link(sepIp) : undefined
|
|
3203
|
+
// A `field(…, { leadingTrivia: true })` separator reads the pre-scan cursor.
|
|
3204
|
+
const sepLeads = sepIp >= 0 && leadingTriviaFieldAt(code, k, sepIp)
|
|
3103
3205
|
const flags = code[ip + 5]!
|
|
3104
3206
|
const keepSeparators = (flags & 2) !== 0
|
|
3105
3207
|
const trailingAllowed = (flags & 1) !== 0
|
|
@@ -3183,6 +3285,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3183
3285
|
let sp = cur
|
|
3184
3286
|
if (hasTrivia) sp = skipTrivia(input, sp, ctx)
|
|
3185
3287
|
ctx._fc = false
|
|
3288
|
+
if (sepLeads) LEAD.at = cur
|
|
3186
3289
|
const sv = sep(input, sp, ctx)
|
|
3187
3290
|
if (sv === FAIL) {
|
|
3188
3291
|
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
@@ -3283,6 +3386,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3283
3386
|
let sp = cur
|
|
3284
3387
|
if (hasTrivia) sp = skipTrivia(input, sp, ctx)
|
|
3285
3388
|
ctx._fc = false
|
|
3389
|
+
if (sepLeads) LEAD.at = cur
|
|
3286
3390
|
const sv = sep(input, sp, ctx)
|
|
3287
3391
|
if (sv === FAIL) {
|
|
3288
3392
|
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
@@ -3706,20 +3810,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3706
3810
|
if (child >= 0) scalarFor(child)
|
|
3707
3811
|
continue
|
|
3708
3812
|
}
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
scalarFor(child)
|
|
3713
|
-
continue
|
|
3714
|
-
}
|
|
3715
|
-
}
|
|
3716
|
-
const family = leadingLiteralFamily(code, k, armIp)
|
|
3717
|
-
if (family !== undefined && family.length >= 2 && family.every(terminal => {
|
|
3718
|
-
const value = k[code[terminal + 1]!]
|
|
3719
|
-
return typeof value === 'string' && value.length >= 2
|
|
3720
|
-
})) {
|
|
3721
|
-
for (const terminal of family) scalarFor(terminal)
|
|
3722
|
-
}
|
|
3813
|
+
const literals = largeChoiceLiteralPretest(code, k, armIp)
|
|
3814
|
+
if (literals !== undefined) for (const terminal of literals) scalarFor(terminal)
|
|
3815
|
+
if (literals !== undefined && literals.length === 1 && literals[0] === child) continue
|
|
3723
3816
|
const sequence = leadingScalarSequence(code, armIp)
|
|
3724
3817
|
if (sequence !== undefined && (sequence.trivia < 0
|
|
3725
3818
|
|| triviaScan[sequence.trivia] != null)) {
|
|
@@ -3767,6 +3860,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3767
3860
|
REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
|
|
3768
3861
|
matchesAt, recoverScan, orSentinel, captureError,
|
|
3769
3862
|
scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
|
|
3863
|
+
LEAD, triviaFieldCapture,
|
|
3770
3864
|
)
|
|
3771
3865
|
emitReached = new Set(pre.reached)
|
|
3772
3866
|
}
|
|
@@ -3848,6 +3942,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3848
3942
|
REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
|
|
3849
3943
|
matchesAt, recoverScan, orSentinel, captureError,
|
|
3850
3944
|
scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
|
|
3945
|
+
LEAD, triviaFieldCapture,
|
|
3851
3946
|
)
|
|
3852
3947
|
emitReached = em.reached
|
|
3853
3948
|
} catch (e) {
|
|
@@ -4067,6 +4162,13 @@ export class AssemblyCache {
|
|
|
4067
4162
|
}
|
|
4068
4163
|
}
|
|
4069
4164
|
|
|
4165
|
+
/**
|
|
4166
|
+
* One rule of the map. A lowered terminal is emitted as this call, not as
|
|
4167
|
+
* `tableRules(…)["Entry"]`: `@__PURE__` covers a call and not a property read
|
|
4168
|
+
* after it, so a bundler had to keep the whole table of every terminal nothing
|
|
4169
|
+
* referenced. With the lookup inside the call the statement is droppable.
|
|
4170
|
+
*/
|
|
4171
|
+
export function tableRules(source: TableProgram | CompactProgram, entry: string): TableRule
|
|
4070
4172
|
/**
|
|
4071
4173
|
* The ASSEMBLED rule map — the same artifact contract as `tableRules`, run
|
|
4072
4174
|
* through linked closures instead of the bytecode interpreter.
|
|
@@ -4076,11 +4178,19 @@ export class AssemblyCache {
|
|
|
4076
4178
|
* assembly built for it. Everything past that point is pieces, and no piece body
|
|
4077
4179
|
* reads an option — `scripts/check-invariants.mjs` INV-6 asserts it. `forCtx`
|
|
4078
4180
|
* carries the argument for why that read is irreducible rather than merely cheap.
|
|
4181
|
+
*
|
|
4182
|
+
* Declared last, so `ReturnType<typeof tableRules>` stays the map.
|
|
4079
4183
|
*/
|
|
4080
4184
|
export function tableRules(
|
|
4081
4185
|
source: TableProgram | CompactProgram,
|
|
4082
|
-
artifactMetadata
|
|
4083
|
-
): Record<string, TableRule>
|
|
4186
|
+
artifactMetadata?: Readonly<Record<symbol, unknown>>,
|
|
4187
|
+
): Record<string, TableRule>
|
|
4188
|
+
export function tableRules(
|
|
4189
|
+
source: TableProgram | CompactProgram,
|
|
4190
|
+
metadataOrEntry: Readonly<Record<symbol, unknown>> | string = {},
|
|
4191
|
+
): Record<string, TableRule> | TableRule {
|
|
4192
|
+
const entry = typeof metadataOrEntry === 'string' ? metadataOrEntry : undefined
|
|
4193
|
+
const artifactMetadata = typeof metadataOrEntry === 'string' ? {} : metadataOrEntry
|
|
4084
4194
|
const prog = expandCompact(source)
|
|
4085
4195
|
const resolved = resolveTable(prog)
|
|
4086
4196
|
const cache = new AssemblyCache(prog, resolved)
|
|
@@ -4105,7 +4215,7 @@ export function tableRules(
|
|
|
4105
4215
|
* `runRule`, after the outer read.
|
|
4106
4216
|
*/
|
|
4107
4217
|
let selected: Assembly | undefined
|
|
4108
|
-
|
|
4218
|
+
const map = stampRuleMap(prog, {
|
|
4109
4219
|
runRule: (ri, input, pos, ctx) => {
|
|
4110
4220
|
const a = selected ?? cache.forCtx(ctx)
|
|
4111
4221
|
selected = undefined
|
|
@@ -4128,4 +4238,8 @@ export function tableRules(
|
|
|
4128
4238
|
return a.scanSkip[skipOf?.[ri] ?? -1]
|
|
4129
4239
|
},
|
|
4130
4240
|
}, artifactMetadata, resolved)
|
|
4241
|
+
if (entry === undefined) return map
|
|
4242
|
+
// Own properties only: the map inherits `toString`, `constructor` and the rest.
|
|
4243
|
+
if (!Object.hasOwn(map, entry)) throw new Error(`tableRules: the program has no rule ${JSON.stringify(entry)}`)
|
|
4244
|
+
return map[entry]!
|
|
4131
4245
|
}
|
|
@@ -63,10 +63,12 @@ import {
|
|
|
63
63
|
computeSiteLabels, reachableSites, type SiteLabel,
|
|
64
64
|
} from './site-labels.ts'
|
|
65
65
|
import {
|
|
66
|
-
|
|
66
|
+
largeChoiceLiteralPretest, leadingScalarSequence, leadingScalarTerminal,
|
|
67
67
|
scalarTerminalNodeChild, scalarTerminalNotChild, type LeadingScalarSequence,
|
|
68
68
|
} from './scalar-terminal.ts'
|
|
69
69
|
import { childSlots } from './child-slots.ts'
|
|
70
|
+
import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
|
|
71
|
+
import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA } from '../combinators/field-trivia.ts'
|
|
70
72
|
|
|
71
73
|
/** What the compiled factory hands back — the emitted twin of `Assembly`. */
|
|
72
74
|
export type EmittedPiece = (input: string, pos: number, ctx: ParseContext) => unknown
|
|
@@ -122,6 +124,9 @@ export const EMITTED_PARAMS = [
|
|
|
122
124
|
'adjacencyHolds',
|
|
123
125
|
// Appended for selected composite lexical programs; old factories ignore it.
|
|
124
126
|
'LEXPROG',
|
|
127
|
+
// Appended for trivia-attributing fields (`field(…, { leadingTrivia })`);
|
|
128
|
+
// old factories ignore them.
|
|
129
|
+
'LEAD', 'triviaFieldCapture',
|
|
125
130
|
] as const
|
|
126
131
|
|
|
127
132
|
/**
|
|
@@ -607,20 +612,12 @@ export function emitAssemblySource(
|
|
|
607
612
|
if (child >= 0) scalarSpecs.add(code[child + 1]!)
|
|
608
613
|
continue
|
|
609
614
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
continue
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
const family = leadingLiteralFamily(code, k, armIp)
|
|
618
|
-
if (family !== undefined && family.length >= 2 && family.every(terminal => {
|
|
619
|
-
const value = k[code[terminal + 1]!]
|
|
620
|
-
return typeof value === 'string' && value.length >= 2
|
|
621
|
-
})) {
|
|
622
|
-
largeChoiceLiteralFamilies.set(armIp, family)
|
|
615
|
+
const literals = largeChoiceLiteralPretest(code, k, armIp)
|
|
616
|
+
if (literals !== undefined && literals.length === 1 && literals[0] === child) {
|
|
617
|
+
largeChoiceScalarSpecs.add(code[child + 1]!)
|
|
618
|
+
continue
|
|
623
619
|
}
|
|
620
|
+
if (literals !== undefined) largeChoiceLiteralFamilies.set(armIp, literals)
|
|
624
621
|
const sequence = leadingScalarSequence(code, armIp)
|
|
625
622
|
// A labelled trivia scanner is still recognition authority here. This
|
|
626
623
|
// pretest is pure: on a miss the ordinary arm's trivia log would have
|
|
@@ -1307,17 +1304,34 @@ return v
|
|
|
1307
1304
|
}
|
|
1308
1305
|
|
|
1309
1306
|
case OP_FIELD: {
|
|
1310
|
-
const
|
|
1307
|
+
const spec = fieldSpec(k[code[ip + 1]!])
|
|
1308
|
+
const name = q(spec.name)
|
|
1311
1309
|
const child = link(code[ip + 2]!)
|
|
1312
1310
|
// `ctx._fields?.push(…)` — conditional on a field-reading node being
|
|
1313
1311
|
// open, exactly as `map.ts` has it, so a field outside one costs a load
|
|
1314
1312
|
// and a branch. The ARRAY is what `OP_NODE` hands to `buildFieldMap`.
|
|
1315
|
-
|
|
1313
|
+
if (spec.bits === 0) {
|
|
1314
|
+
return `${head}
|
|
1316
1315
|
const v=${child}(input,pos,ctx)
|
|
1317
1316
|
if(v===FAIL)return FAIL
|
|
1318
1317
|
const f=ctx._fields
|
|
1319
1318
|
if(f!==undefined)f.push({name:${name},value:v,span:{start:pos,end:EC.e}})
|
|
1320
1319
|
return v
|
|
1320
|
+
}`
|
|
1321
|
+
}
|
|
1322
|
+
const leading = (spec.bits & FIELD_LEADING_TRIVIA) !== 0
|
|
1323
|
+
const trailing = (spec.bits & FIELD_TRAILING_TRIVIA) !== 0
|
|
1324
|
+
return `${head}
|
|
1325
|
+
const lf=${leading ? 'LEAD.at' : '-1'}
|
|
1326
|
+
LEAD.at=-1
|
|
1327
|
+
const v=${child}(input,pos,ctx)
|
|
1328
|
+
if(v===FAIL)return FAIL
|
|
1329
|
+
const e=EC.e
|
|
1330
|
+
const te=${trailing ? 'ctx.trivia!==undefined?consumeTrivia(input,e,ctx):e' : 'e'}
|
|
1331
|
+
const f=ctx._fields
|
|
1332
|
+
if(f!==undefined)f.push(triviaFieldCapture(${name},v,input,pos,e,lf,te))
|
|
1333
|
+
EC.e=te
|
|
1334
|
+
return v
|
|
1321
1335
|
}`
|
|
1322
1336
|
}
|
|
1323
1337
|
|
|
@@ -1687,7 +1701,13 @@ return v
|
|
|
1687
1701
|
`if(!adjacencyHolds(input,cur,ctx,${negated ? 'true' : 'false'},${kinds})){ctx._fe=cur;ctx._fx=${expected};return FAIL}`,
|
|
1688
1702
|
`${vn}=null`,
|
|
1689
1703
|
)
|
|
1690
|
-
} else
|
|
1704
|
+
} else {
|
|
1705
|
+
// A leading-trivia field reads the cursor from before the term's scan.
|
|
1706
|
+
// The callee expression hands it over AFTER the scan statement, right
|
|
1707
|
+
// before the call, so a field inside the trivia grammar cannot take it.
|
|
1708
|
+
const callee = leadingTriviaFieldAt(code, k, childIp) ? `(LEAD.at=cur,${kids[i]!})` : kids[i]!
|
|
1709
|
+
parts.push(emitTerm(callee, vn, tmp(), L, skipFor(L)))
|
|
1710
|
+
}
|
|
1691
1711
|
names.push(vn)
|
|
1692
1712
|
}
|
|
1693
1713
|
parts.push('EC.e=cur')
|
|
@@ -2232,7 +2252,7 @@ ${sep !== undefined ? `if(count>0){
|
|
|
2232
2252
|
const lb=cstLeavesLen(ctx)
|
|
2233
2253
|
let sp=cur
|
|
2234
2254
|
${hasTrivia === 'false' ? '' : hasTrivia === 'true' ? `sp=${skip}(input,sp,ctx)\n` : `if(hasTrivia)sp=${skip}(input,sp,ctx)\n`}ctx._fc=false
|
|
2235
|
-
const sv=${sep}(input,sp,ctx)
|
|
2255
|
+
${sepIp >= 0 && leadingTriviaFieldAt(code, k, sepIp) ? 'LEAD.at=cur\n' : ''}const sv=${sep}(input,sp,ctx)
|
|
2236
2256
|
if(sv===FAIL){
|
|
2237
2257
|
${rb}
|
|
2238
2258
|
if(ctx._fc===true)return FAIL
|