parseman 0.55.4 → 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.
Files changed (88) hide show
  1. package/dist/analysis/choice-cost.d.ts.map +1 -1
  2. package/dist/analysis/commitment.d.ts.map +1 -1
  3. package/dist/analysis/diagnostics.js +3353 -3095
  4. package/dist/analysis/diagnostics.js.map +3 -3
  5. package/dist/analysis/duplication.d.ts.map +1 -1
  6. package/dist/analysis/gating.d.ts.map +1 -1
  7. package/dist/cli/index.js +1813 -1555
  8. package/dist/cli/index.js.map +3 -3
  9. package/dist/combinators/attempt.d.ts.map +1 -1
  10. package/dist/combinators/choice.d.ts.map +1 -1
  11. package/dist/combinators/dispatch.d.ts.map +1 -1
  12. package/dist/combinators/expect.d.ts.map +1 -1
  13. package/dist/combinators/first-set.d.ts.map +1 -1
  14. package/dist/combinators/lines.d.ts +79 -0
  15. package/dist/combinators/lines.d.ts.map +1 -0
  16. package/dist/combinators/trivia-skip.d.ts +1 -0
  17. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  18. package/dist/compiler/fields.d.ts.map +1 -1
  19. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  20. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  21. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  22. package/dist/compiler/token-capability.js +25 -0
  23. package/dist/compiler/token-capability.js.map +1 -1
  24. package/dist/coverage.d.ts.map +1 -1
  25. package/dist/cst/reflection.d.ts.map +1 -1
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1905 -1625
  29. package/dist/index.js.map +3 -3
  30. package/dist/language-service/index.js +2 -0
  31. package/dist/language-service/index.js.map +1 -1
  32. package/dist/parse-context.d.ts.map +1 -1
  33. package/dist/plugin/evaluator.d.ts.map +1 -1
  34. package/dist/plugin/index.js +1318 -1045
  35. package/dist/plugin/index.js.map +3 -3
  36. package/dist/plugin/ir-eval.d.ts +0 -4
  37. package/dist/plugin/ir-eval.d.ts.map +1 -1
  38. package/dist/run/index.js +2 -0
  39. package/dist/run/index.js.map +1 -1
  40. package/dist/spec/index.js +15 -0
  41. package/dist/spec/index.js.map +1 -1
  42. package/dist/spec/model.d.ts.map +1 -1
  43. package/dist/table/assemble.d.ts +0 -65
  44. package/dist/table/assemble.d.ts.map +1 -1
  45. package/dist/table/emit.d.ts.map +1 -1
  46. package/dist/table/encode.d.ts.map +1 -1
  47. package/dist/table/exec.d.ts.map +1 -1
  48. package/dist/table/index.js +3442 -3192
  49. package/dist/table/index.js.map +3 -3
  50. package/dist/table/line-scan.d.ts +9 -0
  51. package/dist/table/line-scan.d.ts.map +1 -0
  52. package/dist/table/program.d.ts +11 -2
  53. package/dist/table/program.d.ts.map +1 -1
  54. package/dist/types.d.ts +21 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/version.d.ts +1 -1
  57. package/package.json +1 -1
  58. package/src/analysis/choice-cost.ts +1 -0
  59. package/src/analysis/commitment.ts +13 -0
  60. package/src/analysis/duplication.ts +3 -0
  61. package/src/analysis/gating.ts +5 -0
  62. package/src/analysis/rebuild.ts +2 -2
  63. package/src/combinators/attempt.ts +2 -0
  64. package/src/combinators/choice.ts +3 -0
  65. package/src/combinators/dispatch.ts +3 -0
  66. package/src/combinators/expect.ts +5 -0
  67. package/src/combinators/first-set.ts +9 -1
  68. package/src/combinators/lines.ts +244 -0
  69. package/src/combinators/trivia-skip.ts +4 -2
  70. package/src/compiler/fields.ts +3 -0
  71. package/src/compiler/grammar-coverage-ids.ts +1 -0
  72. package/src/compiler/ir-serialize.ts +10 -0
  73. package/src/compiler/token-alphabet.ts +3 -1
  74. package/src/coverage.ts +6 -0
  75. package/src/cst/reflection.ts +3 -0
  76. package/src/index.ts +2 -0
  77. package/src/parse-context.ts +2 -0
  78. package/src/plugin/evaluator.ts +4 -0
  79. package/src/plugin/ir-eval.ts +3 -0
  80. package/src/spec/model.ts +15 -0
  81. package/src/table/assemble.ts +6 -12
  82. package/src/table/emit.ts +7 -6
  83. package/src/table/encode.ts +18 -6
  84. package/src/table/exec.ts +3 -0
  85. package/src/table/line-scan.ts +19 -0
  86. package/src/table/program.ts +17 -2
  87. package/src/types.ts +14 -0
  88. 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 {
@@ -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
@@ -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'
@@ -48,6 +48,8 @@ export function createParseContext(): ParseContext {
48
48
  _triviaCaptureMask: undefined,
49
49
  trackLines: false,
50
50
  state: undefined,
51
+ lineView: undefined,
52
+ _columnsSplit: undefined,
51
53
  build: undefined,
52
54
  _errors: undefined,
53
55
  _tolerant: undefined,
@@ -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
@@ -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':
@@ -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) {
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[] = []
@@ -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) {
@@ -0,0 +1,19 @@
1
+ import type { Combinator } from '../types.ts'
2
+ import type { ScanSpec, SubtreeRef } from './program.ts'
3
+ import { columns, lines } from '../combinators/lines.ts'
4
+
5
+ /**
6
+ * Rebuild a `lines()` (kind 2) or `columns()` (kind 3) scan spec as the
7
+ * combinator it was constructed as, its subtrees bound to the table's pieces.
8
+ * Both drivers run the result through its own `parse`, as they do `scanTo`.
9
+ */
10
+ export function rebuildLineScan(s: ScanSpec, subtree: (r: SubtreeRef) => Combinator<unknown>): Combinator<unknown> | undefined {
11
+ if (s.kind === 2) {
12
+ return lines(subtree(s.prefix!), subtree(s.body!), {
13
+ ...(s.first === undefined ? {} : { first: subtree(s.first) }),
14
+ ...(s.lazy === undefined ? {} : { lazy: subtree(s.lazy) }),
15
+ }) as Combinator<unknown>
16
+ }
17
+ if (s.kind === 3) return columns(s.n!) as Combinator<unknown>
18
+ return undefined
19
+ }