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.
- package/dist/analysis/choice-cost.d.ts.map +1 -1
- package/dist/analysis/commitment.d.ts.map +1 -1
- package/dist/analysis/diagnostics.js +3745 -3180
- 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 +2182 -1617
- 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/node.d.ts +1 -1
- package/dist/combinators/trivia-skip.d.ts +1 -0
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts +1 -1
- package/dist/compiler/build-arity.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/cst/trivia-kinds.d.ts +9 -1
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2348 -1697
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +3 -0
- package/dist/language-service/index.js.map +1 -1
- package/dist/parse-context.d.ts.map +1 -1
- package/dist/plugin/entry-supercompiler.d.ts +1 -1
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts +27 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2360 -1314
- package/dist/plugin/index.js.map +3 -3
- package/dist/plugin/ir-eval.d.ts +2 -4
- package/dist/plugin/ir-eval.d.ts.map +1 -1
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/run/index.js +3 -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 +9 -65
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/child-slots.d.ts +4 -6
- package/dist/table/child-slots.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +14 -2
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +17 -2
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts +5 -1
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec-baseline.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/index.js +3884 -3262
- 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/ops.d.ts +9 -6
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +24 -2
- package/dist/table/program.d.ts.map +1 -1
- package/dist/table/select-hot.d.ts +6 -0
- package/dist/table/select-hot.d.ts.map +1 -0
- package/dist/table/trivia-gap.d.ts +31 -0
- package/dist/table/trivia-gap.d.ts.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +4 -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/node.ts +6 -13
- package/src/combinators/trivia-skip.ts +48 -3
- package/src/compiler/build-arity.ts +3 -3
- 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/cst/trivia-kinds.ts +12 -1
- package/src/index.ts +2 -0
- package/src/parse-context.ts +3 -0
- package/src/plugin/entry-supercompiler.ts +8 -9
- package/src/plugin/evaluator.ts +4 -0
- package/src/plugin/index.ts +138 -13
- package/src/plugin/ir-eval.ts +10 -1
- package/src/regex/classes.ts +3 -0
- package/src/spec/model.ts +15 -0
- package/src/table/assemble.ts +125 -33
- package/src/table/child-slots.ts +4 -6
- package/src/table/compile-rule-map.ts +55 -15
- package/src/table/emit-assembly.ts +139 -26
- package/src/table/emit.ts +13 -11
- package/src/table/encode.ts +87 -27
- package/src/table/exec-baseline.ts +1 -3
- package/src/table/exec.ts +10 -6
- package/src/table/line-scan.ts +19 -0
- package/src/table/ops.ts +9 -6
- package/src/table/program.ts +41 -2
- package/src/table/select-hot.ts +70 -0
- package/src/table/trivia-gap.ts +109 -0
- package/src/types.ts +32 -0
- 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
|
+
}
|
package/src/combinators/node.ts
CHANGED
|
@@ -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`
|
|
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 (
|
|
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
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
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/cst/trivia-kinds.ts
CHANGED
|
@@ -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'
|
package/src/parse-context.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
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
|