parseman 0.55.4 → 0.57.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 (125) 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 +3745 -3180
  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 +2182 -1617
  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/node.d.ts +1 -1
  17. package/dist/combinators/trivia-skip.d.ts +1 -0
  18. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  19. package/dist/compiler/build-arity.d.ts +1 -1
  20. package/dist/compiler/build-arity.d.ts.map +1 -1
  21. package/dist/compiler/fields.d.ts.map +1 -1
  22. package/dist/compiler/grammar-coverage-ids.d.ts.map +1 -1
  23. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  24. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  25. package/dist/compiler/token-capability.js +25 -0
  26. package/dist/compiler/token-capability.js.map +1 -1
  27. package/dist/coverage.d.ts.map +1 -1
  28. package/dist/cst/reflection.d.ts.map +1 -1
  29. package/dist/cst/trivia-kinds.d.ts +9 -1
  30. package/dist/cst/trivia-kinds.d.ts.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +2348 -1697
  34. package/dist/index.js.map +3 -3
  35. package/dist/language-service/index.js +3 -0
  36. package/dist/language-service/index.js.map +1 -1
  37. package/dist/parse-context.d.ts.map +1 -1
  38. package/dist/plugin/entry-supercompiler.d.ts +1 -1
  39. package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
  40. package/dist/plugin/evaluator.d.ts.map +1 -1
  41. package/dist/plugin/index.d.ts +27 -1
  42. package/dist/plugin/index.d.ts.map +1 -1
  43. package/dist/plugin/index.js +2360 -1314
  44. package/dist/plugin/index.js.map +3 -3
  45. package/dist/plugin/ir-eval.d.ts +2 -4
  46. package/dist/plugin/ir-eval.d.ts.map +1 -1
  47. package/dist/regex/classes.d.ts.map +1 -1
  48. package/dist/run/index.js +3 -0
  49. package/dist/run/index.js.map +1 -1
  50. package/dist/spec/index.js +15 -0
  51. package/dist/spec/index.js.map +1 -1
  52. package/dist/spec/model.d.ts.map +1 -1
  53. package/dist/table/assemble.d.ts +9 -65
  54. package/dist/table/assemble.d.ts.map +1 -1
  55. package/dist/table/child-slots.d.ts +4 -6
  56. package/dist/table/child-slots.d.ts.map +1 -1
  57. package/dist/table/compile-rule-map.d.ts +14 -2
  58. package/dist/table/compile-rule-map.d.ts.map +1 -1
  59. package/dist/table/emit-assembly.d.ts +17 -2
  60. package/dist/table/emit-assembly.d.ts.map +1 -1
  61. package/dist/table/emit.d.ts +5 -1
  62. package/dist/table/emit.d.ts.map +1 -1
  63. package/dist/table/encode.d.ts.map +1 -1
  64. package/dist/table/exec-baseline.d.ts.map +1 -1
  65. package/dist/table/exec.d.ts.map +1 -1
  66. package/dist/table/index.js +3884 -3262
  67. package/dist/table/index.js.map +3 -3
  68. package/dist/table/line-scan.d.ts +9 -0
  69. package/dist/table/line-scan.d.ts.map +1 -0
  70. package/dist/table/ops.d.ts +9 -6
  71. package/dist/table/ops.d.ts.map +1 -1
  72. package/dist/table/program.d.ts +24 -2
  73. package/dist/table/program.d.ts.map +1 -1
  74. package/dist/table/select-hot.d.ts +6 -0
  75. package/dist/table/select-hot.d.ts.map +1 -0
  76. package/dist/table/trivia-gap.d.ts +31 -0
  77. package/dist/table/trivia-gap.d.ts.map +1 -0
  78. package/dist/types.d.ts +38 -0
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/version.d.ts +1 -1
  81. package/package.json +4 -1
  82. package/src/analysis/choice-cost.ts +1 -0
  83. package/src/analysis/commitment.ts +13 -0
  84. package/src/analysis/duplication.ts +3 -0
  85. package/src/analysis/gating.ts +5 -0
  86. package/src/analysis/rebuild.ts +2 -2
  87. package/src/combinators/attempt.ts +2 -0
  88. package/src/combinators/choice.ts +3 -0
  89. package/src/combinators/dispatch.ts +3 -0
  90. package/src/combinators/expect.ts +5 -0
  91. package/src/combinators/first-set.ts +9 -1
  92. package/src/combinators/lines.ts +244 -0
  93. package/src/combinators/node.ts +6 -13
  94. package/src/combinators/trivia-skip.ts +48 -3
  95. package/src/compiler/build-arity.ts +3 -3
  96. package/src/compiler/fields.ts +3 -0
  97. package/src/compiler/grammar-coverage-ids.ts +1 -0
  98. package/src/compiler/ir-serialize.ts +10 -0
  99. package/src/compiler/token-alphabet.ts +3 -1
  100. package/src/coverage.ts +6 -0
  101. package/src/cst/reflection.ts +3 -0
  102. package/src/cst/trivia-kinds.ts +12 -1
  103. package/src/index.ts +2 -0
  104. package/src/parse-context.ts +3 -0
  105. package/src/plugin/entry-supercompiler.ts +8 -9
  106. package/src/plugin/evaluator.ts +4 -0
  107. package/src/plugin/index.ts +138 -13
  108. package/src/plugin/ir-eval.ts +10 -1
  109. package/src/regex/classes.ts +3 -0
  110. package/src/spec/model.ts +15 -0
  111. package/src/table/assemble.ts +125 -33
  112. package/src/table/child-slots.ts +4 -6
  113. package/src/table/compile-rule-map.ts +55 -15
  114. package/src/table/emit-assembly.ts +139 -26
  115. package/src/table/emit.ts +13 -11
  116. package/src/table/encode.ts +87 -27
  117. package/src/table/exec-baseline.ts +1 -3
  118. package/src/table/exec.ts +10 -6
  119. package/src/table/line-scan.ts +19 -0
  120. package/src/table/ops.ts +9 -6
  121. package/src/table/program.ts +41 -2
  122. package/src/table/select-hot.ts +70 -0
  123. package/src/table/trivia-gap.ts +109 -0
  124. package/src/types.ts +32 -0
  125. 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
+ }
@@ -83,7 +83,7 @@ export type NodeOptions<Tags extends readonly string[] = readonly never[]> = {
83
83
  * cannot read.
84
84
  *
85
85
  * Arity gates five capture tiers (`>= 1` children, `>= 2` fields, `>= 4` rawChildren,
86
- * `>= 5` triviaLog, `>= 6` a clone of `_ctx.state`), and parseman normally derives it
86
+ * `>= 5` triviaLog, `>= 6` `_ctx.state`), and parseman normally derives it
87
87
  * from the reducer's declaration — including across module boundaries. A few shapes
88
88
  * are genuinely undecidable: a rest parameter (`...args`), a body that reads
89
89
  * `arguments`, a reassigned or dynamically constructed reducer. Those fail OPEN, which
@@ -208,7 +208,7 @@ export function node<N>(
208
208
  : baseDef
209
209
  // Arity-gated elision — decided once, identically to the compiler (build-arity.ts).
210
210
  // When the build never reads the trivia (4th) arg, disable per-node CST-trivia
211
- // capture for the inner scope; when it never reads state (5th), skip the state clone.
211
+ // capture for the inner scope; when it never reads state (6th), it is handed none.
212
212
  // A STRUCTURAL node (no own build) defers to `ctx.build` / a default CST, which
213
213
  // may read either, so capture both.
214
214
  // These are the DIRECT builder's own needs, and they are not the whole story. A node
@@ -228,7 +228,7 @@ export function node<N>(
228
228
  // or without a node log. Forcing capture for it handed a three-argument reducer
229
229
  // a log it never reads, at the price of the full capture frame on every match.
230
230
  const capturesTrivia = captureTrivia || (build ? buildReadsTrivia(def) : project === undefined)
231
- const clonesState = build ? buildReadsState(def) : project === undefined
231
+ const readsState = build ? buildReadsState(def) : project === undefined
232
232
  const hasOwnFields = parserHasOwnFields(combinator)
233
233
  const capturesFields = hasOwnFields && (build ? buildReadsFields(def) : project === undefined)
234
234
  // First-set fail-fast (mirrors emitNode's codegen guard): a node whose body can't
@@ -298,17 +298,10 @@ export function node<N>(
298
298
  if (!r.ok) return r
299
299
 
300
300
  // unwrap/collapse: a single captured child IS the value — skip build.
301
- const st = clonesState && ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined
301
+ const st = readsState ? ctx.state : undefined
302
302
  const nodeType = def.type ?? missingInferredType()
303
303
  const cstOutput = cstOutputHost(ctx.build)
304
304
  const span = ctx.trackLines ? annotateSpanFromLineContext(r.span, ctx) : r.span
305
- // A direct builder that never declared `state` still owes the host its snapshot.
306
- // The clone happens AFTER the body, so unlike trivia it needs no gate up front —
307
- // build it here, on a branch the eval-AST path never takes. Matches what
308
- // `hostMode: 'cst'` does in the compiled engine.
309
- const hostState = clonesState
310
- ? st
311
- : ctx.state !== undefined ? Object.assign({}, ctx.state as Record<string, unknown>) : undefined
312
305
  const built: unknown = unwrap && children.length === 1
313
306
  ? unwrapChild(children[0])
314
307
  : collapse && children.length === 1
@@ -326,14 +319,14 @@ export function node<N>(
326
319
  ? children[0]
327
320
  : project !== undefined
328
321
  ? cstOutput && ctx.build
329
- ? ctx.build(nodeType, children, fields, span, rawChildren, triviaLog, hostState, tags)
322
+ ? ctx.build(nodeType, children, fields, span, rawChildren, triviaLog, ctx.state, tags)
330
323
  : projectChild(children, project, nodeType)
331
324
  : build
332
325
  // A direct builder normally owns its result. The positioned-CST host is
333
326
  // the one exception: it must never receive an arbitrary AST object as a
334
327
  // child of a CST node, so build this grammar node through that host.
335
328
  ? cstOutput && ctx.build
336
- ? ctx.build(nodeType, children, fields, span, rawChildren, triviaLog, hostState, tags)
329
+ ? ctx.build(nodeType, children, fields, span, rawChildren, triviaLog, ctx.state, tags)
337
330
  : build(children, fields, span, rawChildren, triviaLog, st)
338
331
  // Structural node: a `ctx.build` host if present, else a default CST.
339
332
  : ctx.build
@@ -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 {
@@ -568,11 +570,54 @@ function regexTriviaScanner(parser: Combinator<unknown>): FastTriviaScanner | nu
568
570
  // and every trivia gap in every dialect went through the labelled char scanner.
569
571
  const d = parser._def.tag === 'label' ? parser._def.parser._def : parser._def
570
572
  if (d.tag !== 'regex' || d.flags) return null
571
- const source = d.source
573
+ return sourceScanner(d.source)
574
+ }
575
+
576
+ function sourceScanner(source: string): FastTriviaScanner | null {
572
577
  return classRunSource(source)
573
578
  ?? altStarSource(source)
574
579
  ?? prefixRunSource(source)
575
580
  ?? delimitedSource(source)
581
+ ?? sharedPrefixAltSource(source)
582
+ }
583
+
584
+ /**
585
+ * `<literal>(?:<alt>|<alt>…)`: a literal every alternative shares, as in jess's
586
+ * one comment arm `\/(?:\*(?:[^*]|\*(?!\/))*\*\/|\/[^\n\r]*)` for both `/*…*\/`
587
+ * and `//…`. Each `<literal><alt>` is lowered on its own, and the scanner takes
588
+ * the first that matches, as the regex does. Without it that arm had no
589
+ * scanner, so neither did the trivia it sits in.
590
+ */
591
+ function sharedPrefixAltSource(source: string): FastTriviaScanner | null {
592
+ const m = /^((?:\\.|[^\\[\]()*+?|^$.{}])+)\(\?:(.*)\)$/.exec(source)
593
+ if (!m) return null
594
+ const alts: string[] = []
595
+ const body = m[2]!
596
+ let depth = 0
597
+ let start = 0
598
+ for (let i = 0; i < body.length; i++) {
599
+ const c = body[i]
600
+ if (c === '\\') { i++; continue }
601
+ if (c === '[') { while (i < body.length && body[i] !== ']') i += body[i] === '\\' ? 2 : 1; continue }
602
+ if (c === '(') depth++
603
+ else if (c === ')' && --depth < 0) return null
604
+ else if (c === '|' && depth === 0) { alts.push(body.slice(start, i)); start = i + 1 }
605
+ }
606
+ if (depth !== 0 || alts.length === 0) return null
607
+ alts.push(body.slice(start))
608
+ const scanners: FastTriviaScanner[] = []
609
+ for (const alt of alts) {
610
+ const scanner = sourceScanner(m[1]! + alt)
611
+ if (scanner === null) return null
612
+ scanners.push(scanner)
613
+ }
614
+ return (input, cur) => {
615
+ for (const scanner of scanners) {
616
+ const end = scanner(input, cur)
617
+ if (end > cur) return end
618
+ }
619
+ return cur
620
+ }
576
621
  }
577
622
 
578
623
  /**
@@ -338,7 +338,7 @@ export function confirmedArityForDef(def: NodeDef): number | null {
338
338
  where: `node("${def.type ?? '<inferred>'}")`,
339
339
  subject,
340
340
  fellBackTo: `could not confirm its declared arity — ${unconfirmableReason(def, src)} — `
341
- + 'so this node captures children, fields and raw children, logs trivia, and clones `_ctx.state` on every match',
341
+ + 'so this node captures children, fields and raw children, logs trivia, and hands it `_ctx.state` on every match',
342
342
  otherwise: 'only the tiers the reducer actually declares would be captured '
343
343
  + '(arity >= 1 children, >= 2 fields, >= 4 raw, >= 5 trivia, >= 6 state). '
344
344
  + 'Declare it with `node(..., { buildArity: n })` if it cannot be derived',
@@ -371,9 +371,9 @@ export function buildReadsTrivia(def: NodeDef): boolean {
371
371
  return arity >= 5
372
372
  }
373
373
 
374
- /** Build reads the 6th (state) arg? Unknown/unparseable → true (keep state clone). */
374
+ /** Build reads the 6th (state) arg? Unknown/unparseable → true (hand it the state). */
375
375
  export function buildReadsState(def: NodeDef): boolean {
376
- if (!def.build) return true // structural node: host may read state — keep clone
376
+ if (!def.build) return true // structural node: host may read state
377
377
  const arity = confirmedArityForDef(def)
378
378
  if (arity === null) return true
379
379
  return arity >= 6
@@ -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':
@@ -1,4 +1,4 @@
1
- import type { Combinator, ParseContext } from '../types.ts'
1
+ import type { CharRange, 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'
@@ -20,6 +20,14 @@ export type LabeledTriviaSpec = {
20
20
  */
21
21
  readonly visitor: CssTriviaVisitor | null
22
22
  readonly charArms: readonly TriviaArmMatcher[] | null
23
+ /**
24
+ * `table/trivia-gap.ts`'s root-selection fast path, for the selection it was
25
+ * last primed with: the selected arms' first characters, and the unlabelled
26
+ * scanner that answers a run free of them (`null`: no fast path).
27
+ */
28
+ rootKinds: Readonly<Record<string, number>> | undefined
29
+ rootLeaders: readonly CharRange[]
30
+ rootScan: ((input: string, cur: number) => number) | null
23
31
  }
24
32
 
25
33
  /** See `trivia-skip.ts`: bounded scalar memoisation avoids a WeakMap in the
@@ -114,6 +122,9 @@ export function labeledTriviaSpec(arms: LabeledTriviaSpec['arms'], minRepeats: n
114
122
  minRepeats,
115
123
  visitor: commonLabeledTriviaVisitor({ arms, minRepeats }),
116
124
  charArms: charArmsFor({ arms, minRepeats }),
125
+ rootKinds: undefined,
126
+ rootLeaders: [],
127
+ rootScan: null,
117
128
  }
118
129
  }
119
130
 
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,
@@ -66,6 +68,7 @@ export function createParseContext(): ParseContext {
66
68
  _rootTriviaKindIndex: undefined,
67
69
  _rootTriviaStrictScopes: undefined,
68
70
  _rootTriviaCapture: undefined,
71
+ _gap: undefined,
69
72
  _cstTriviaLog: undefined,
70
73
  _fields: undefined,
71
74
  _cstBuf: undefined,
@@ -11,7 +11,7 @@ import { parseSync } from 'oxc-parser'
11
11
  import { OP_ATTEMPT, OP_NODE, OP_NODE_TRACK, OP_REPV, OP_SCAN } from '../table/ops.ts'
12
12
  import { emitAssemblySource, type EmitResult } from '../table/emit-assembly.ts'
13
13
  import { defaultAssemblyCfgs } from '../table/emit.ts'
14
- import { resolveTable, type TableProgram } from '../table/program.ts'
14
+ import { assemblyExtraIps, resolveTable, type TableProgram } from '../table/program.ts'
15
15
 
16
16
  type Ast = { type: string; start?: number; end?: number; [key: string]: unknown }
17
17
 
@@ -229,20 +229,19 @@ export function supercompileEntryAssembly(
229
229
  * then finds the precompiled assembly through the table object's AST shape and
230
230
  * replaces only that factory body. Any mismatch returns the canonical source.
231
231
  */
232
- export function supercompileRuleMapReplacement(prog: TableProgram, replacement: string): string {
232
+ export function supercompileRuleMapReplacement(
233
+ prog: TableProgram,
234
+ replacement: string,
235
+ select?: ReadonlySet<number>,
236
+ ): string {
233
237
  const cfg = defaultAssemblyCfgs(prog)[0]
234
238
  if (cfg === undefined) return replacement
235
- const extraIps: number[] = []
236
- for (const scan of prog.scans ?? []) {
237
- for (const ref of scan.skip) extraIps.push(ref[0])
238
- if (scan.sentinel !== undefined) extraIps.push(scan.sentinel[0])
239
- }
240
- for (const set of prog.scanSkip ?? []) for (const ref of set) extraIps.push(ref[0])
239
+ const extraIps = assemblyExtraIps(prog)
241
240
 
242
241
  let canonical: EmitResult
243
242
  let candidate: EntrySupercompileResult
244
243
  try {
245
- canonical = emitAssemblySource(resolveTable(prog), prog, cfg, extraIps, true)
244
+ canonical = emitAssemblySource(resolveTable(prog), prog, cfg, extraIps, true, select)
246
245
  candidate = supercompileEmittedAssembly(prog, cfg, extraIps, canonical)
247
246
  } catch {
248
247
  return replacement
@@ -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