parseman 0.55.3 → 0.56.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/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts.map +1 -1
- package/dist/analysis/diagnostics.js +3360 -3098
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/analysis/duplication.d.ts.map +1 -1
- package/dist/analysis/gating.d.ts.map +1 -1
- package/dist/cli/index.js +1820 -1558
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/attempt.d.ts.map +1 -1
- package/dist/combinators/choice.d.ts.map +1 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/lines.d.ts +79 -0
- package/dist/combinators/lines.d.ts.map +1 -0
- package/dist/combinators/trivia-skip.d.ts +1 -0
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +25 -0
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/coverage.d.ts.map +1 -1
- package/dist/cst/reflection.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1912 -1628
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +2 -0
- package/dist/language-service/index.js.map +1 -1
- package/dist/parse-context.d.ts.map +1 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.js +1325 -1048
- package/dist/plugin/index.js.map +3 -3
- package/dist/plugin/ir-eval.d.ts +0 -4
- package/dist/plugin/ir-eval.d.ts.map +1 -1
- package/dist/run/index.js +2 -0
- package/dist/run/index.js.map +1 -1
- package/dist/spec/index.js +15 -0
- package/dist/spec/index.js.map +1 -1
- package/dist/spec/model.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +0 -65
- package/dist/table/assemble.d.ts.map +1 -1
- 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/index.js +3449 -3195
- package/dist/table/index.js.map +3 -3
- package/dist/table/line-scan.d.ts +9 -0
- package/dist/table/line-scan.d.ts.map +1 -0
- package/dist/table/program.d.ts +11 -2
- package/dist/table/program.d.ts.map +1 -1
- package/dist/types.d.ts +26 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/analysis/choice-cost.ts +1 -0
- package/src/analysis/commitment.ts +13 -0
- package/src/analysis/duplication.ts +3 -0
- package/src/analysis/gating.ts +5 -0
- package/src/analysis/rebuild.ts +2 -2
- package/src/combinators/attempt.ts +2 -0
- package/src/combinators/choice.ts +3 -0
- package/src/combinators/dispatch.ts +3 -0
- package/src/combinators/expect.ts +5 -0
- package/src/combinators/first-set.ts +9 -1
- package/src/combinators/lines.ts +244 -0
- package/src/combinators/trivia-skip.ts +4 -2
- package/src/compiler/fields.ts +3 -0
- package/src/compiler/grammar-coverage-ids.ts +1 -0
- package/src/compiler/ir-serialize.ts +10 -0
- package/src/compiler/token-alphabet.ts +3 -1
- package/src/coverage.ts +6 -0
- package/src/cst/reflection.ts +3 -0
- package/src/index.ts +2 -0
- package/src/parse-context.ts +2 -0
- package/src/plugin/evaluator.ts +4 -0
- package/src/plugin/ir-eval.ts +3 -0
- package/src/spec/model.ts +15 -0
- package/src/table/assemble.ts +10 -13
- package/src/table/compile.ts +5 -2
- package/src/table/emit.ts +7 -6
- package/src/table/encode.ts +18 -6
- package/src/table/exec.ts +7 -1
- package/src/table/line-scan.ts +19 -0
- package/src/table/program.ts +17 -2
- package/src/types.ts +19 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { Combinator, ParseContext, ParseResult, ParserMeta } from '../types.ts'
|
|
2
|
+
import { any, matchesEmpty } from './first-set.ts'
|
|
3
|
+
import { createDetachedParseContext } from '../parse-context.ts'
|
|
4
|
+
import { recordLineRangeFromContext } from '../line-index.ts'
|
|
5
|
+
import { failAt } from './probe.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One line of a run that `lines()` hands its body.
|
|
9
|
+
*
|
|
10
|
+
* The body parses the run's lines with their prefixes removed and joined by
|
|
11
|
+
* `\n`. Each entry says where a line's content sits in that text (`offset`) and
|
|
12
|
+
* in the run's own input (`source`), where its column count starts, and whether
|
|
13
|
+
* it joined the run through `lazy`.
|
|
14
|
+
*/
|
|
15
|
+
export type RunLine = {
|
|
16
|
+
/** Where the line's content starts in the body's input. */
|
|
17
|
+
readonly offset: number
|
|
18
|
+
/** Where the same content starts in the run's input. */
|
|
19
|
+
readonly source: number
|
|
20
|
+
/** Where the line ends in the run's input, before its newline. */
|
|
21
|
+
readonly end: number
|
|
22
|
+
/**
|
|
23
|
+
* The column at `source`: tab stops every four columns, counted from the
|
|
24
|
+
* start of the outermost input's line, through every enclosing run.
|
|
25
|
+
*/
|
|
26
|
+
readonly column: number
|
|
27
|
+
/**
|
|
28
|
+
* Columns of the character at `source` that a prefix already took. A
|
|
29
|
+
* `columns(n)` prefix that ends inside a tab leaves the tab in the content and
|
|
30
|
+
* records the columns it took here, so the content keeps the tab's remainder.
|
|
31
|
+
*/
|
|
32
|
+
readonly split: number
|
|
33
|
+
/** The line joined through `lazy`, with nothing removed. */
|
|
34
|
+
readonly lazy: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The lines of the run whose body is parsing, and the run enclosing it. */
|
|
38
|
+
export type LineView = {
|
|
39
|
+
readonly lines: readonly RunLine[]
|
|
40
|
+
readonly parent: LineView | undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type LinesOptions = {
|
|
44
|
+
/** The prefix of the run's first line, when it differs from the others' (a list item's marker). */
|
|
45
|
+
first?: Combinator<unknown>
|
|
46
|
+
/**
|
|
47
|
+
* A line the prefix does not match continues the run, whole, when `lazy`
|
|
48
|
+
* matches at its start: Markdown's lazy continuation lines. Only the match
|
|
49
|
+
* counts; what `lazy` consumes is not removed.
|
|
50
|
+
*/
|
|
51
|
+
lazy?: Combinator<unknown>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type LinesContext = ParseContext
|
|
55
|
+
|
|
56
|
+
/** The index of the run line holding `offset` of the body's input, by binary search. */
|
|
57
|
+
function lineIndexAt(view: LineView, offset: number): number {
|
|
58
|
+
const lines = view.lines
|
|
59
|
+
let lo = 0, hi = lines.length - 1
|
|
60
|
+
while (lo < hi) {
|
|
61
|
+
const mid = (lo + hi + 1) >> 1
|
|
62
|
+
if (lines[mid]!.offset <= offset) lo = mid
|
|
63
|
+
else hi = mid - 1
|
|
64
|
+
}
|
|
65
|
+
return lo
|
|
66
|
+
}
|
|
67
|
+
const lineAt = (view: LineView, offset: number): RunLine | undefined => view.lines[lineIndexAt(view, offset)]
|
|
68
|
+
|
|
69
|
+
/** Map an offset in a run body's input to the offset in the run's own input. */
|
|
70
|
+
export function sourceOffset(view: LineView, offset: number): number {
|
|
71
|
+
const line = lineAt(view, offset)
|
|
72
|
+
return line === undefined ? offset : line.source + (offset - line.offset)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The column at `pos` and the columns of the character there already taken,
|
|
77
|
+
* counting from the line start of the current input, or, inside a run's body,
|
|
78
|
+
* from that run's line, whose column and split the enclosing runs supply.
|
|
79
|
+
*/
|
|
80
|
+
function columnAt(input: string, pos: number, ctx: LinesContext): { column: number; split: number } {
|
|
81
|
+
const view = ctx.lineView
|
|
82
|
+
let start: number, column: number, split: number
|
|
83
|
+
const line = view === undefined ? undefined : lineAt(view, pos)
|
|
84
|
+
if (line !== undefined && pos >= line.offset) { start = line.offset; column = line.column; split = line.split }
|
|
85
|
+
else { start = input.lastIndexOf('\n', pos - 1) + 1; column = 0; split = 0 }
|
|
86
|
+
for (let i = start; i < pos; i++) {
|
|
87
|
+
if (input.charCodeAt(i) === 0x09) {
|
|
88
|
+
// A tab reaches the next stop; one the prefix split has fewer columns left.
|
|
89
|
+
const width = 4 - (column - (i === start ? split : 0)) % 4 - (i === start ? split : 0)
|
|
90
|
+
column += width
|
|
91
|
+
} else column++
|
|
92
|
+
split = 0
|
|
93
|
+
}
|
|
94
|
+
return { column, split: pos === start ? split : 0 }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `n` columns of indentation: spaces and tabs, a tab reaching the next stop of
|
|
99
|
+
* four columns. When the `n`th column falls inside a tab, the match stops before
|
|
100
|
+
* that tab, and a `lines()` prefix ending there leaves the tab's remaining
|
|
101
|
+
* columns in the content (`RunLine.split`). Fails when the indentation is
|
|
102
|
+
* narrower than `n`.
|
|
103
|
+
*/
|
|
104
|
+
export function columns(n: number): Combinator<string> {
|
|
105
|
+
if (!Number.isInteger(n) || n < 0) throw new TypeError(`columns(n) needs a non-negative integer, got ${n}`)
|
|
106
|
+
const expected = [`columns(${n})`]
|
|
107
|
+
const meta: ParserMeta = {
|
|
108
|
+
firstSet: n === 0 ? any() : { kind: 'ranges', ranges: [{ lo: 0x09, hi: 0x09 }, { lo: 0x20, hi: 0x20 }] },
|
|
109
|
+
canMatchNewline: false,
|
|
110
|
+
isTrivia: false,
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
_tag: 'columns',
|
|
114
|
+
_meta: meta,
|
|
115
|
+
_def: { tag: 'columns', n },
|
|
116
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<string> {
|
|
117
|
+
const c = ctx as LinesContext
|
|
118
|
+
let { column, split } = columnAt(input, pos, c)
|
|
119
|
+
const goal = column + n
|
|
120
|
+
let at = pos
|
|
121
|
+
while (column < goal) {
|
|
122
|
+
const code = input.charCodeAt(at)
|
|
123
|
+
if (code === 0x20) { column++; split = 0; at++; continue }
|
|
124
|
+
if (code !== 0x09) return { ok: false, expected, span: { start: at, end: at } }
|
|
125
|
+
const width = 4 - (column - split) % 4 - split
|
|
126
|
+
if (column + width > goal) {
|
|
127
|
+
// The goal falls inside this tab: stop before it, and say how much of it is taken.
|
|
128
|
+
c._columnsSplit = { at, split: split + (goal - column) }
|
|
129
|
+
return { ok: true, value: input.slice(pos, at), span: { start: pos, end: at } }
|
|
130
|
+
}
|
|
131
|
+
column += width; split = 0; at++
|
|
132
|
+
}
|
|
133
|
+
return { ok: true, value: input.slice(pos, at), span: { start: pos, end: at } }
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parse `body` over a run of lines with a prefix removed from each.
|
|
140
|
+
*
|
|
141
|
+
* The run starts at `pos`, where `first` (default `prefix`) must match. Each
|
|
142
|
+
* following line joins while `prefix` matches at its start, or, failing that,
|
|
143
|
+
* while `lazy` does; the run ends at the first line that joins neither way or
|
|
144
|
+
* at the end of the input. A prefix never reaches past its own line.
|
|
145
|
+
*
|
|
146
|
+
* `body` parses the lines' contents joined by `\n`, starting at 0. While it
|
|
147
|
+
* runs, `ctx.lineView` describes the lines (`RunLine`), so nested `columns()`
|
|
148
|
+
* and `lines()` count tab stops from the right column. The result spans from
|
|
149
|
+
* `pos` to where the body stopped, mapped back to this input: a body that
|
|
150
|
+
* consumes a whole line ends at the next line's start, so lines it leaves, and
|
|
151
|
+
* the rest of a line where it stops partway, are the enclosing grammar's. Spans inside the body's value and its CST are offsets in the
|
|
152
|
+
* body's input; `sourceOffset(view, offset)` maps them back. Recovered errors
|
|
153
|
+
* the body records are mapped back to this input, line tracking records the
|
|
154
|
+
* run's lines in this input, and a completion probe at a cursor inside a
|
|
155
|
+
* line's content sees what the body expects there.
|
|
156
|
+
*
|
|
157
|
+
* The run is read before the body runs, so a body that leaves most of its run
|
|
158
|
+
* makes a repeated run reread the lines it left: a body reads its run to the end.
|
|
159
|
+
*/
|
|
160
|
+
export function lines<T>(prefix: Combinator<unknown>, body: Combinator<T>, options: LinesOptions = {}): Combinator<T> {
|
|
161
|
+
const { first, lazy } = options
|
|
162
|
+
const opening = first ?? prefix
|
|
163
|
+
const meta: ParserMeta = {
|
|
164
|
+
// An opening that matches empty starts a run at any character.
|
|
165
|
+
firstSet: matchesEmpty(opening) ? any() : opening._meta.firstSet,
|
|
166
|
+
canMatchNewline: true,
|
|
167
|
+
isTrivia: false,
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
_tag: 'lines',
|
|
171
|
+
_meta: meta,
|
|
172
|
+
_def: { tag: 'lines', prefix, body: body as Combinator<unknown>, ...(first === undefined ? {} : { first }), ...(lazy === undefined ? {} : { lazy }) },
|
|
173
|
+
parse(input: string, pos: number, ctx: ParseContext): ParseResult<T> {
|
|
174
|
+
const c = ctx as LinesContext
|
|
175
|
+
// Prefixes and lazy checks are probes: they run detached, so they leave
|
|
176
|
+
// no leaves, trivia or errors in the parse that encloses the run.
|
|
177
|
+
const probe = createDetachedParseContext(false, ctx.state) as LinesContext
|
|
178
|
+
probe.lineView = c.lineView
|
|
179
|
+
const run: RunLine[] = []
|
|
180
|
+
const parts: string[] = []
|
|
181
|
+
let offset = 0
|
|
182
|
+
for (let start = pos; start < input.length || start === pos;) {
|
|
183
|
+
let end = input.indexOf('\n', start)
|
|
184
|
+
if (end < 0) end = input.length
|
|
185
|
+
probe._columnsSplit = undefined
|
|
186
|
+
const head = (run.length === 0 ? opening : prefix).parse(input, start, probe)
|
|
187
|
+
let content: number
|
|
188
|
+
let joined: 'prefix' | 'lazy' | undefined
|
|
189
|
+
if (head.ok && head.span.end <= end) { content = head.span.end; joined = 'prefix' }
|
|
190
|
+
else if (run.length === 0) {
|
|
191
|
+
// A prefix is a probe: its failure is the run's, never a commitment.
|
|
192
|
+
return { ok: false, expected: head.ok ? ['line prefix'] : head.expected, span: { start, end: start } }
|
|
193
|
+
} else if (lazy !== undefined && lazy.parse(input, start, probe).ok) { content = start; joined = 'lazy' }
|
|
194
|
+
else break
|
|
195
|
+
const mark = probe._columnsSplit as { at: number; split: number } | undefined
|
|
196
|
+
const here = columnAt(input, content, c)
|
|
197
|
+
const split = joined === 'prefix' && mark !== undefined && mark.at === content ? mark.split : here.split
|
|
198
|
+
run.push({ offset, source: content, end, column: here.column + (split - here.split), split, lazy: joined === 'lazy' })
|
|
199
|
+
const text = input.slice(content, end)
|
|
200
|
+
const newline = end < input.length
|
|
201
|
+
parts.push(newline ? `${text}\n` : text)
|
|
202
|
+
offset += text.length + (newline ? 1 : 0)
|
|
203
|
+
start = end + 1
|
|
204
|
+
if (!newline) break
|
|
205
|
+
}
|
|
206
|
+
const view: LineView = { lines: run, parent: c.lineView }
|
|
207
|
+
// The body parses another string: what records positions in this input
|
|
208
|
+
// (the line index, the completion probe) stays out of its way. A probe
|
|
209
|
+
// whose cursor lies in a line's content gets its own, at the body's offset.
|
|
210
|
+
const saved = c.lineView, starts = c._lineStarts, lineIndex = c._lineIndex, scanned = c._lineScannedTo, probing = c._probe
|
|
211
|
+
const errors = c._errors?.length ?? 0
|
|
212
|
+
let cursor = -1
|
|
213
|
+
if (probing !== undefined) for (const line of run) {
|
|
214
|
+
if (probing.offset >= line.source && probing.offset <= line.end) { cursor = line.offset + probing.offset - line.source; break }
|
|
215
|
+
}
|
|
216
|
+
const inner: ParseContext['_probe'] = cursor < 0 ? undefined : { offset: cursor, best: null }
|
|
217
|
+
c.lineView = view; c._lineStarts = undefined; c._lineIndex = undefined; c._lineScannedTo = undefined; c._probe = inner
|
|
218
|
+
let result: ParseResult<T>
|
|
219
|
+
try { result = body.parse(parts.join(''), 0, ctx) }
|
|
220
|
+
finally { c.lineView = saved; c._lineStarts = starts; c._lineIndex = lineIndex; c._lineScannedTo = scanned; c._probe = probing }
|
|
221
|
+
// The body's deepest failure at the cursor counts where it is in this input.
|
|
222
|
+
if (inner?.best) failAt(c, inner.best.expected, sourceOffset(view, inner.best.span.start))
|
|
223
|
+
// Errors the body recovered from move to where they are in this input.
|
|
224
|
+
if (c._errors !== undefined) for (let i = errors; i < c._errors.length; i++) {
|
|
225
|
+
const error = c._errors[i] as { span: { start: number; end: number } }
|
|
226
|
+
error.span = { start: sourceOffset(view, error.span.start), end: sourceOffset(view, error.span.end) }
|
|
227
|
+
}
|
|
228
|
+
if (!result.ok) {
|
|
229
|
+
const at = sourceOffset(view, result.span.start)
|
|
230
|
+
return { ...result, span: { start: at, end: at } }
|
|
231
|
+
}
|
|
232
|
+
// The body's end maps to the end of its line: a body that consumed a
|
|
233
|
+
// whole line, newline included, ends at the next line's start.
|
|
234
|
+
const stop = result.span.end
|
|
235
|
+
const index = lineIndexAt(view, stop)
|
|
236
|
+
const line = run[index]!
|
|
237
|
+
const end = stop === line.offset && index > 0 ? run[index - 1]!.end + 1
|
|
238
|
+
: stop - line.offset > line.end - line.source ? line.end + 1
|
|
239
|
+
: line.source + (stop - line.offset)
|
|
240
|
+
recordLineRangeFromContext(c, input, pos, end)
|
|
241
|
+
return { ok: true, value: result.value, span: { start: pos, end } }
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -188,15 +188,17 @@ function parseTriviaNoCapture(
|
|
|
188
188
|
* `peek(sequence(literal('a'), literal('zzz')))` over `"ab"` offered `"zzz"` as
|
|
189
189
|
* the ONLY completion, though no input can ever reach it there.
|
|
190
190
|
*/
|
|
191
|
-
export type LookaheadMark = TriviaRollbackMark & { probeBest: ParseFail | null }
|
|
191
|
+
export type LookaheadMark = TriviaRollbackMark & { probeBest: ParseFail | null; split: ParseContext['_columnsSplit'] }
|
|
192
192
|
|
|
193
193
|
export function saveLookaheadMark(ctx: ParseContext): LookaheadMark {
|
|
194
|
-
return { ...saveTriviaMark(ctx), probeBest: ctx._probe ? ctx._probe.best : null }
|
|
194
|
+
return { ...saveTriviaMark(ctx), probeBest: ctx._probe ? ctx._probe.best : null, split: ctx._columnsSplit }
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
export function rollbackLookahead(ctx: ParseContext, mark: LookaheadMark): void {
|
|
198
198
|
rollbackTrivia(ctx, mark)
|
|
199
199
|
if (ctx._probe) ctx._probe.best = mark.probeBest
|
|
200
|
+
// A lookahead takes no columns of a tab: `lines()` reads `columns()`'s split mark.
|
|
201
|
+
ctx._columnsSplit = mark.split
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
function scanWithLabels(input: string, cur: number, ctx: ParseContext): CompactTriviaScan {
|
package/src/compiler/fields.ts
CHANGED
|
@@ -30,6 +30,7 @@ export function parserHasOwnFields(p: Combinator<unknown>, seen: Set<Combinator<
|
|
|
30
30
|
|| (d.recoverAt !== undefined && parserHasOwnFields(d.recoverAt, seen))
|
|
31
31
|
|| (d.stopAt !== undefined && parserHasOwnFields(d.stopAt, seen))
|
|
32
32
|
case 'recover': return parserHasOwnFields(d.parser, seen) || parserHasOwnFields(d.sentinel, seen)
|
|
33
|
+
case 'lines': return parserHasOwnFields(d.body, seen)
|
|
33
34
|
case 'routed': return d.fallback ? parserHasOwnFields(d.fallback, seen) : false
|
|
34
35
|
case 'many':
|
|
35
36
|
case 'oneOrMore':
|
|
@@ -98,6 +99,7 @@ function hasTriviaSite(p: Combinator<unknown>, seen: Set<Combinator<unknown>>, t
|
|
|
98
99
|
case 'sepBy':
|
|
99
100
|
case 'scanTo':
|
|
100
101
|
case 'recover':
|
|
102
|
+
case 'lines':
|
|
101
103
|
return true
|
|
102
104
|
// Nested node manages its own trivia frame; none logs into THIS frame. The
|
|
103
105
|
// root log has no frames, so it must look inside.
|
|
@@ -159,6 +161,7 @@ export function parserEnablesTriviaCapture(p: Combinator<unknown>, seen: Set<Com
|
|
|
159
161
|
|| (d.recoverAt !== undefined && parserEnablesTriviaCapture(d.recoverAt, seen))
|
|
160
162
|
|| (d.stopAt !== undefined && parserEnablesTriviaCapture(d.stopAt, seen))
|
|
161
163
|
case 'recover': return parserEnablesTriviaCapture(d.parser, seen) || parserEnablesTriviaCapture(d.sentinel, seen)
|
|
164
|
+
case 'lines': return parserEnablesTriviaCapture(d.body, seen)
|
|
162
165
|
case 'routed': return d.fallback ? parserEnablesTriviaCapture(d.fallback, seen) : false
|
|
163
166
|
case 'many':
|
|
164
167
|
case 'oneOrMore':
|
|
@@ -39,6 +39,7 @@ function children(def: ParserDef, winners?: Record<string, Combinator<unknown>>)
|
|
|
39
39
|
...(def.recoverAt === undefined ? [] : [def.recoverAt]),
|
|
40
40
|
...(def.stopAt === undefined ? [] : [def.stopAt]),
|
|
41
41
|
]
|
|
42
|
+
case 'lines': return [def.prefix, def.body, ...(def.first === undefined ? [] : [def.first]), ...(def.lazy === undefined ? [] : [def.lazy])]
|
|
42
43
|
case 'lazy': {
|
|
43
44
|
let resolved: Combinator<unknown>
|
|
44
45
|
try { resolved = def.thunk() } catch (e) {
|
|
@@ -72,6 +72,8 @@ function childrenOf(def: ParserDef): Comb[] {
|
|
|
72
72
|
...(def.stopAt === undefined ? [] : [def.stopAt]),
|
|
73
73
|
]
|
|
74
74
|
case 'routed': return def.fallback ? [def.fallback] : []
|
|
75
|
+
case 'lines': return [def.prefix, def.body, ...(def.first === undefined ? [] : [def.first]), ...(def.lazy === undefined ? [] : [def.lazy])]
|
|
76
|
+
case 'columns':
|
|
75
77
|
case 'lazy':
|
|
76
78
|
case 'literal':
|
|
77
79
|
case 'regex':
|
|
@@ -488,6 +490,14 @@ class Serializer {
|
|
|
488
490
|
case 'expect': return `expect(${kid(def.parser)}${def.label !== undefined ? `, ${JSON.stringify(def.label)}` : ''})`
|
|
489
491
|
case 'scanTo':
|
|
490
492
|
return `scanTo(${kid(def.sentinel)}, { skip: [${def.skip.map(kid).join(', ')}]${def.raw ? ', raw: true' : ''}, orEOF: ${def.orEOF}${def.recoverAt === undefined ? '' : `, recoverAt: ${kid(def.recoverAt)}`}${def.stopAt === undefined ? '' : `, stopAt: ${kid(def.stopAt)}`} })`
|
|
493
|
+
case 'lines': {
|
|
494
|
+
const options = [
|
|
495
|
+
...(def.first === undefined ? [] : [`first: ${kid(def.first)}`]),
|
|
496
|
+
...(def.lazy === undefined ? [] : [`lazy: ${kid(def.lazy)}`]),
|
|
497
|
+
]
|
|
498
|
+
return `lines(${kid(def.prefix)}, ${kid(def.body)}${options.length === 0 ? '' : `, { ${options.join(', ')} }`})`
|
|
499
|
+
}
|
|
500
|
+
case 'columns': return `columns(${def.n})`
|
|
491
501
|
case 'transform': {
|
|
492
502
|
if (def.fnSrc === undefined) throw new Unserializable('transform without fnSrc')
|
|
493
503
|
// `_tf` sets `_def.fnSrc` so re-lowering INLINES the callback (a plain
|
|
@@ -1698,6 +1698,8 @@ function normalizeLexicalBody(
|
|
|
1698
1698
|
case 'adjacency': return done({ refusal: 'adjacency depends on parser state' })
|
|
1699
1699
|
case 'recover': return done({ refusal: 'recover is effectful' })
|
|
1700
1700
|
case 'scanTo': return done({ refusal: 'scanTo is not a lexical run' })
|
|
1701
|
+
case 'lines': return done({ refusal: 'lines removes prefixes from its input' })
|
|
1702
|
+
case 'columns': return done({ refusal: 'columns counts tab stops' })
|
|
1701
1703
|
case 'sepBy': return done({ refusal: 'separated repetition is not a lexical run' })
|
|
1702
1704
|
case 'dispatch': return done({ refusal: 'nested dispatch has semantic outcomes' })
|
|
1703
1705
|
case 'routed': return done({ refusal: 'routed depends on an outer dispatch' })
|
|
@@ -2026,7 +2028,7 @@ function decisionLead(
|
|
|
2026
2028
|
return finish({ gap: 'repeated or nullable leading language needs a range-partition proof' })
|
|
2027
2029
|
case 'peek': return finish({ gap: 'positive lookahead needs an exact effect-free compatible view' })
|
|
2028
2030
|
case 'not': return finish({ gap: 'negative lookahead needs a decidable complement' })
|
|
2029
|
-
case 'withCtx': case 'guard': case 'adjacency': case 'recover': case 'scanTo': case 'routed': case 'unknown':
|
|
2031
|
+
case 'withCtx': case 'guard': case 'adjacency': case 'recover': case 'scanTo': case 'routed': case 'unknown': case 'lines': case 'columns':
|
|
2030
2032
|
return finish({ gap: `leading ${def.tag} language is not represented` })
|
|
2031
2033
|
case 'literal': case 'keywords': case 'regex':
|
|
2032
2034
|
throw new Error('parseman: decision lead terminal escaped its direct case')
|
package/src/coverage.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lines } from './combinators/lines.ts'
|
|
1
2
|
import { run, type RunOptions, type RunResult, type Runnable } from './functional/run.ts'
|
|
2
3
|
import { GRAMMAR_COVERAGE_DEFINITIONS } from './grammar-metadata.ts'
|
|
3
4
|
export { GRAMMAR_COVERAGE_DEFINITIONS } from './grammar-metadata.ts'
|
|
@@ -365,6 +366,11 @@ function coverageEntry(entry: Combinator<unknown>, collector: GrammarCoverageCol
|
|
|
365
366
|
...(def.recoverAt === undefined ? {} : { recoverAt: build(def.recoverAt) }),
|
|
366
367
|
...(def.stopAt === undefined ? {} : { stopAt: build(def.stopAt) }),
|
|
367
368
|
})
|
|
369
|
+
case 'lines': return lines(build(def.prefix), build(def.body), {
|
|
370
|
+
...(def.first === undefined ? {} : { first: build(def.first) }),
|
|
371
|
+
...(def.lazy === undefined ? {} : { lazy: build(def.lazy) }),
|
|
372
|
+
})
|
|
373
|
+
case 'columns': return parser
|
|
368
374
|
// Zero-width assertion with no coverage-bearing child; reuse it verbatim so
|
|
369
375
|
// the rebuilt sequence still recognises the boundary marker.
|
|
370
376
|
case 'adjacency': return parser
|
package/src/cst/reflection.ts
CHANGED
|
@@ -66,6 +66,9 @@ function childrenOf(def: ParserDef): readonly Combinator<unknown>[] {
|
|
|
66
66
|
]
|
|
67
67
|
case 'routed':
|
|
68
68
|
return def.fallback ? [def.fallback] : []
|
|
69
|
+
case 'lines':
|
|
70
|
+
return [def.prefix, def.body, ...(def.first === undefined ? [] : [def.first]), ...(def.lazy === undefined ? [] : [def.lazy])]
|
|
71
|
+
case 'columns':
|
|
69
72
|
case 'literal':
|
|
70
73
|
case 'regex':
|
|
71
74
|
case 'keywords':
|
package/src/index.ts
CHANGED
|
@@ -152,6 +152,8 @@ export { isParseError } from './combinators/expect.ts'
|
|
|
152
152
|
export { expect } from './combinators/expect.ts'
|
|
153
153
|
export { completionsAt } from './combinators/completions.ts'
|
|
154
154
|
export { scanTo, balanced } from './combinators/scanTo.ts'
|
|
155
|
+
export { lines, columns, sourceOffset } from './combinators/lines.ts'
|
|
156
|
+
export type { LinesOptions, LineView, RunLine } from './combinators/lines.ts'
|
|
155
157
|
export type { ScanToOptions } from './combinators/scanTo.ts'
|
|
156
158
|
|
|
157
159
|
export type { CSTNode, CSTLeaf, CSTError, CSTTrivia, CSTChild, CSTRawChild, NodeLike } from './cst/types.ts'
|
package/src/parse-context.ts
CHANGED
package/src/plugin/evaluator.ts
CHANGED
|
@@ -320,6 +320,10 @@ const SUPPORTED: Record<string, (...args: unknown[]) => Combinator<unknown>> = {
|
|
|
320
320
|
field: (...a) => parseman.field(a[0] as string, a[1] as Combinator<unknown>, a[2] as parseman.FieldOptions | undefined),
|
|
321
321
|
noTrivia: (...a) => parseman.noTrivia(a[0] as Combinator<unknown>),
|
|
322
322
|
token: (...a) => parseman.token(a[0] as Combinator<unknown>),
|
|
323
|
+
// `lines(prefix, body, { first, lazy })`: its options are combinators, which the
|
|
324
|
+
// generic argument evaluation already resolves; a missing option stays absent.
|
|
325
|
+
lines: (...a) => parseman.lines(a[0] as Combinator<unknown>, a[1] as Combinator<unknown>, a[2] as parseman.LinesOptions | undefined),
|
|
326
|
+
columns: (...a) => parseman.columns(a[0] as number),
|
|
323
327
|
// `routed(fallback)` — the fallback must be forwarded. A zero-arg entry here read
|
|
324
328
|
// as "routed takes no arguments" and SILENTLY built a bare `routed()`, so a
|
|
325
329
|
// production written to work both inside and outside a dispatch branch lost its
|
package/src/plugin/ir-eval.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* The macro plugin's IR evaluator: turns a composed grammar's carried IR back
|
|
3
3
|
* into combinators so the build can re-lower it. Build time only — see below.
|
|
4
4
|
*/
|
|
5
|
+
import { columns, lines } from '../combinators/lines.ts'
|
|
5
6
|
import type { Combinator } from '../types.ts'
|
|
6
7
|
import { rules } from '../combinators/parser.ts'
|
|
7
8
|
import { ref } from '../combinators/ref.ts'
|
|
@@ -155,11 +156,13 @@ function evalIR(ir: string, rulesImpl: unknown): unknown {
|
|
|
155
156
|
'rules', 'ref', 'regex', 'literal', 'keywords', 'sequence', 'choice', 'dispatch', 'when', 'startsWith', 'endsWith', 'matches', 'otherwise', 'routed', 'attempt',
|
|
156
157
|
'many', 'oneOrMore', 'optional', 'sepBy', 'keepSeparator', 'not', 'peek', 'node', 'parser',
|
|
157
158
|
'scanTo', 'balanced', 'token', 'leaf', 'transform', 'trivia', 'classifiedTrivia', 'label', 'field', 'expect', 'adjacent', 'notAdjacent', '_tf', '_lf', '_nd', '_gch', '_wc',
|
|
159
|
+
'lines', 'columns',
|
|
158
160
|
`return (${ir})`,
|
|
159
161
|
)
|
|
160
162
|
return fn(
|
|
161
163
|
rulesImpl, ref, regex, literal, keywords, sequence, choice, dispatch, when, startsWith, endsWith, matches, otherwise, routed, attempt,
|
|
162
164
|
many, oneOrMore, optional, sepBy, keepSeparator, not, peek, node, parser,
|
|
163
165
|
scanTo, balanced, token, leaf, transform, trivia, classifiedTrivia, label, field, expectC, adjacent, notAdjacent, _tf, _lf, _nd, _gch, _wc,
|
|
166
|
+
lines, columns,
|
|
164
167
|
)
|
|
165
168
|
}
|
package/src/spec/model.ts
CHANGED
|
@@ -477,6 +477,21 @@ class Builder {
|
|
|
477
477
|
return this.walk(def.parser)
|
|
478
478
|
case 'scanTo':
|
|
479
479
|
return { kind: 'annotation', text: '…' }
|
|
480
|
+
// A run of lines: each line opens with the prefix, the first with `first`
|
|
481
|
+
// when given, and the body reads the lines' contents.
|
|
482
|
+
case 'lines':
|
|
483
|
+
return {
|
|
484
|
+
kind: 'seq',
|
|
485
|
+
items: [
|
|
486
|
+
{ kind: 'annotation', text: 'each line begins with' },
|
|
487
|
+
this.walk(def.prefix),
|
|
488
|
+
...(def.first === undefined ? [] : [{ kind: 'annotation', text: 'the first with' } as SpecNode, this.walk(def.first)]),
|
|
489
|
+
...(def.lazy === undefined ? [] : [{ kind: 'annotation', text: 'or continues where' } as SpecNode, this.walk(def.lazy)]),
|
|
490
|
+
this.walk(def.body),
|
|
491
|
+
],
|
|
492
|
+
}
|
|
493
|
+
case 'columns':
|
|
494
|
+
return { kind: 'annotation', text: `${def.n} columns` }
|
|
480
495
|
|
|
481
496
|
// Trivia — elided by default; included as an annotation when asked.
|
|
482
497
|
case 'trivia':
|
package/src/table/assemble.ts
CHANGED
|
@@ -63,6 +63,8 @@
|
|
|
63
63
|
* Semantics are `exec.ts`'s, case for case. That file remains the reference and
|
|
64
64
|
* the three-way identity sweep gates this against it.
|
|
65
65
|
*/
|
|
66
|
+
import { rebuildLineScan } from './line-scan.ts'
|
|
67
|
+
import { scanRefs } from './program.ts'
|
|
66
68
|
import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
|
|
67
69
|
import { balanced, scanTo } from '../combinators/scanTo.ts'
|
|
68
70
|
import { buildFieldMap } from '../compiler/fields.ts'
|
|
@@ -486,12 +488,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
486
488
|
}
|
|
487
489
|
|
|
488
490
|
const labelExtraIps: number[] = []
|
|
489
|
-
for (const s of prog.scans ?? [])
|
|
490
|
-
for (const r of s.skip) labelExtraIps.push(r[0])
|
|
491
|
-
if (s.sentinel !== undefined) labelExtraIps.push(s.sentinel[0])
|
|
492
|
-
if (s.recoverAt !== undefined) labelExtraIps.push(s.recoverAt[0])
|
|
493
|
-
if (s.stopAt !== undefined) labelExtraIps.push(s.stopAt[0])
|
|
494
|
-
}
|
|
491
|
+
for (const s of prog.scans ?? []) for (const r of scanRefs(s)) labelExtraIps.push(r[0])
|
|
495
492
|
for (const set of prog.scanSkip ?? []) for (const r of set) labelExtraIps.push(r[0])
|
|
496
493
|
const closureLabels = computeSiteLabels(
|
|
497
494
|
code, [...Object.values(prog.rules), ...labelExtraIps], hostCst,
|
|
@@ -3781,12 +3778,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3781
3778
|
// sites need emitted names too or `subtreeComb` would have to fall back to
|
|
3782
3779
|
// a closure for them and the two engines would both be live in one parse.
|
|
3783
3780
|
const extraIps: number[] = []
|
|
3784
|
-
for (const s of prog.scans ?? [])
|
|
3785
|
-
for (const r of s.skip) extraIps.push(r[0])
|
|
3786
|
-
if (s.sentinel !== undefined) extraIps.push(s.sentinel[0])
|
|
3787
|
-
if (s.recoverAt !== undefined) extraIps.push(s.recoverAt[0])
|
|
3788
|
-
if (s.stopAt !== undefined) extraIps.push(s.stopAt[0])
|
|
3789
|
-
}
|
|
3781
|
+
for (const s of prog.scans ?? []) for (const r of scanRefs(s)) extraIps.push(r[0])
|
|
3790
3782
|
for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
|
|
3791
3783
|
const roots = [...Object.values(prog.rules), ...extraIps]
|
|
3792
3784
|
for (const ip of reachableSites(code, roots)) {
|
|
@@ -3964,6 +3956,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3964
3956
|
}
|
|
3965
3957
|
|
|
3966
3958
|
for (const s of prog.scans ?? []) {
|
|
3959
|
+
const rebuilt = rebuildLineScan(s, subtreeComb)
|
|
3960
|
+
if (rebuilt !== undefined) { scansArr.push(rebuilt); continue }
|
|
3967
3961
|
const skip = s.skip.map(r => subtreeComb(r))
|
|
3968
3962
|
const raw = (s.flags & 1) !== 0
|
|
3969
3963
|
if (s.kind === 1) {
|
|
@@ -4235,7 +4229,10 @@ export function tableRules(
|
|
|
4235
4229
|
scanSkipFor: (ri, ctx) => {
|
|
4236
4230
|
const a = cache.forCtx(ctx)
|
|
4237
4231
|
selected = a
|
|
4238
|
-
|
|
4232
|
+
// An array read at -1 is a named-property miss that walks the prototype
|
|
4233
|
+
// chain on every call; a rule without a scan-skip set never indexes.
|
|
4234
|
+
const slot = skipOf?.[ri]
|
|
4235
|
+
return slot === undefined || slot < 0 ? undefined : a.scanSkip[slot]
|
|
4239
4236
|
},
|
|
4240
4237
|
}, artifactMetadata, resolved)
|
|
4241
4238
|
if (entry === undefined) return map
|
package/src/table/compile.ts
CHANGED
|
@@ -277,8 +277,11 @@ function compileImpl<T>(
|
|
|
277
277
|
// falls back to regex-scanning generated hooks; a table has no hooks to scan,
|
|
278
278
|
// so this is the channel.
|
|
279
279
|
...(plan === undefined ? {} : { coverageDefinitions: plan.definitions }),
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
// The same three arguments as a combinator's own `parse`, so a compiled
|
|
281
|
+
// parser runs where a combinator does. A caller probing many positions
|
|
282
|
+
// passes one context and pays for none per call.
|
|
283
|
+
parse(input: string, pos = 0, ctx?: ParseContext): ParseResult<T> {
|
|
284
|
+
return runOnce(input, pos, ctx ?? createParseContext())
|
|
282
285
|
},
|
|
283
286
|
parseWithContext(input: string, parseCtx: ParseContext, pos = 0): ParseResult<T> {
|
|
284
287
|
return runOnce(input, pos, parseCtx)
|
package/src/table/emit.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanRefs } from './program.ts'
|
|
1
2
|
import type { FoldedProgram, TableProgram } from './program.ts'
|
|
2
3
|
import { resolveTable } from './program.ts'
|
|
3
4
|
import { EMITTED_PARAMS, Unemittable, emitAssemblySource } from './emit-assembly.ts'
|
|
@@ -109,6 +110,11 @@ function emitScanSpec(s: import('./program.ts').ScanSpec): string {
|
|
|
109
110
|
if (s.sent !== undefined) parts.push(`sent:${s.sent === null ? 'null' : jsString(s.sent)}`)
|
|
110
111
|
if (s.open !== undefined) parts.push(`open:${jsString(s.open)}`)
|
|
111
112
|
if (s.close !== undefined) parts.push(`close:${jsString(s.close)}`)
|
|
113
|
+
for (const key of ['prefix', 'body', 'first', 'lazy'] as const) {
|
|
114
|
+
const r = s[key]
|
|
115
|
+
if (r !== undefined) parts.push(`${key}:${emitRef(r)}`)
|
|
116
|
+
}
|
|
117
|
+
if (s.n !== undefined) parts.push(`n:${s.n}`)
|
|
112
118
|
return `{${parts.join(',')}}`
|
|
113
119
|
}
|
|
114
120
|
|
|
@@ -196,12 +202,7 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
|
|
|
196
202
|
// The scan pool and the scan-skip sets are linked from subtrees, so their
|
|
197
203
|
// sites need emitted names too — same list `assemble.ts` builds.
|
|
198
204
|
const extraIps: number[] = []
|
|
199
|
-
for (const s of prog.scans ?? [])
|
|
200
|
-
for (const r of s.skip) extraIps.push(r[0])
|
|
201
|
-
if (s.sentinel !== undefined) extraIps.push(s.sentinel[0])
|
|
202
|
-
if (s.recoverAt !== undefined) extraIps.push(s.recoverAt[0])
|
|
203
|
-
if (s.stopAt !== undefined) extraIps.push(s.stopAt[0])
|
|
204
|
-
}
|
|
205
|
+
for (const s of prog.scans ?? []) for (const r of scanRefs(s)) extraIps.push(r[0])
|
|
205
206
|
for (const set of prog.scanSkip ?? []) for (const r of set) extraIps.push(r[0])
|
|
206
207
|
|
|
207
208
|
const out: string[] = []
|
package/src/table/encode.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { scanRefs } from './program.ts'
|
|
1
2
|
import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
|
|
2
3
|
import { classifyFinalChoice, firstSetOf, intersects, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
|
|
3
4
|
import { childrenOf } from '../analysis/gating.ts'
|
|
@@ -683,6 +684,7 @@ class Encoder {
|
|
|
683
684
|
case 'expect':
|
|
684
685
|
case 'recover':
|
|
685
686
|
case 'scanTo':
|
|
687
|
+
case 'lines':
|
|
686
688
|
case 'unknown':
|
|
687
689
|
return true
|
|
688
690
|
case 'lazy': {
|
|
@@ -987,6 +989,17 @@ class Encoder {
|
|
|
987
989
|
open: bal.open,
|
|
988
990
|
close: bal.close,
|
|
989
991
|
}))
|
|
992
|
+
// `lines()` and `columns()` lower as their constructor arguments too: the
|
|
993
|
+
// table rebuilds each and runs it, the body and prefixes being table subtrees.
|
|
994
|
+
if (d.tag === 'lines') {
|
|
995
|
+
return this.emit(OP_SCAN, this.scanSlot({
|
|
996
|
+
kind: 2, flags: 0, skip: [],
|
|
997
|
+
prefix: this.subtree(d.prefix), body: this.subtree(d.body),
|
|
998
|
+
...(d.first === undefined ? {} : { first: this.subtree(d.first) }),
|
|
999
|
+
...(d.lazy === undefined ? {} : { lazy: this.subtree(d.lazy) }),
|
|
1000
|
+
}))
|
|
1001
|
+
}
|
|
1002
|
+
if (d.tag === 'columns') return this.emit(OP_SCAN, this.scanSlot({ kind: 3, flags: 0, skip: [], n: d.n }))
|
|
990
1003
|
if (d.tag === 'scanTo') {
|
|
991
1004
|
const sentDef = d.sentinel._def
|
|
992
1005
|
return this.emit(OP_SCAN, this.scanSlot({
|
|
@@ -1762,12 +1775,7 @@ class Encoder {
|
|
|
1762
1775
|
// interiors uncollapsed and — worse — left the spec's own offset pointing at
|
|
1763
1776
|
// a trampoline whose target had been rewritten around it.
|
|
1764
1777
|
const refs: SubtreeRef[] = []
|
|
1765
|
-
for (const s of this.scans)
|
|
1766
|
-
if (s.sentinel !== undefined) refs.push(s.sentinel)
|
|
1767
|
-
if (s.recoverAt !== undefined) refs.push(s.recoverAt)
|
|
1768
|
-
if (s.stopAt !== undefined) refs.push(s.stopAt)
|
|
1769
|
-
refs.push(...s.skip)
|
|
1770
|
-
}
|
|
1778
|
+
for (const s of this.scans) refs.push(...scanRefs(s))
|
|
1771
1779
|
for (const set of this.scanSkipSets) refs.push(...set)
|
|
1772
1780
|
const stack = [...Object.values(this.rules), ...refs.map(r => r[0])]
|
|
1773
1781
|
while (stack.length > 0) {
|
|
@@ -1788,6 +1796,10 @@ class Encoder {
|
|
|
1788
1796
|
...(s.sentinel === undefined ? {} : { sentinel: res(s.sentinel) }),
|
|
1789
1797
|
...(s.recoverAt === undefined ? {} : { recoverAt: res(s.recoverAt) }),
|
|
1790
1798
|
...(s.stopAt === undefined ? {} : { stopAt: res(s.stopAt) }),
|
|
1799
|
+
...(s.prefix === undefined ? {} : { prefix: res(s.prefix) }),
|
|
1800
|
+
...(s.body === undefined ? {} : { body: res(s.body) }),
|
|
1801
|
+
...(s.first === undefined ? {} : { first: res(s.first) }),
|
|
1802
|
+
...(s.lazy === undefined ? {} : { lazy: res(s.lazy) }),
|
|
1791
1803
|
}))
|
|
1792
1804
|
this.scanSkipSets = this.scanSkipSets.map(set => set.map(res))
|
|
1793
1805
|
}
|
package/src/table/exec.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rebuildLineScan } from './line-scan.ts'
|
|
1
2
|
import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
|
|
2
3
|
import { balanced, scanTo } from '../combinators/scanTo.ts'
|
|
3
4
|
import { buildFieldMap } from '../compiler/fields.ts'
|
|
@@ -1682,6 +1683,8 @@ function makeDriver(
|
|
|
1682
1683
|
* drift.
|
|
1683
1684
|
*/
|
|
1684
1685
|
const scans: readonly Combinator<unknown>[] = (prog.scans ?? []).map(s => {
|
|
1686
|
+
const rebuilt = rebuildLineScan(s, subtreeComb)
|
|
1687
|
+
if (rebuilt !== undefined) return rebuilt
|
|
1685
1688
|
const skip = s.skip.map(r => subtreeComb(r))
|
|
1686
1689
|
const raw = (s.flags & 1) !== 0
|
|
1687
1690
|
if (s.kind === 1) {
|
|
@@ -1814,6 +1817,9 @@ export function execRules(
|
|
|
1814
1817
|
// set instead gave a `composeLeaf` piece's rules a skip list the interpreter
|
|
1815
1818
|
// never gives them — a divergence that is invisible, because the parse
|
|
1816
1819
|
// succeeds having skipped over a delimiter it should have stopped at.
|
|
1817
|
-
scanSkipFor: ri =>
|
|
1820
|
+
scanSkipFor: ri => {
|
|
1821
|
+
const slot = prog.scanSkipOf?.[ri]
|
|
1822
|
+
return slot === undefined || slot < 0 ? undefined : d.scanSkip[slot]
|
|
1823
|
+
},
|
|
1818
1824
|
}, artifactMetadata, t)
|
|
1819
1825
|
}
|