parseman 0.57.0 → 0.58.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/diagnostics.js +1800 -962
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +1929 -1091
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +11 -1
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +69 -16
- package/dist/compiler/token-capability.js.map +2 -2
- package/dist/cst/capture-buffer.d.ts +6 -0
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/index.js +1017 -177
- package/dist/index.js.map +3 -3
- package/dist/plugin/index.d.ts +25 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1210 -236
- package/dist/plugin/index.js.map +3 -3
- package/dist/run/index.d.ts +2 -0
- package/dist/run/index.d.ts.map +1 -1
- package/dist/run/index.js +9 -0
- package/dist/run/index.js.map +1 -1
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +1 -1
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/factor-choice.d.ts +42 -0
- package/dist/table/factor-choice.d.ts.map +1 -0
- package/dist/table/fail-clean.d.ts +3 -0
- package/dist/table/fail-clean.d.ts.map +1 -0
- package/dist/table/hold-gap.d.ts +8 -0
- package/dist/table/hold-gap.d.ts.map +1 -0
- package/dist/table/index.js +1370 -259
- package/dist/table/index.js.map +3 -3
- package/dist/table/lex-program.d.ts +7 -3
- package/dist/table/lex-program.d.ts.map +1 -1
- package/dist/table/ops.d.ts +8 -0
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/run-support.d.ts +19 -0
- package/dist/table/run-support.d.ts.map +1 -1
- package/dist/table/scalar-terminal.d.ts +2 -0
- package/dist/table/scalar-terminal.d.ts.map +1 -1
- package/dist/table/site-labels.d.ts +66 -0
- package/dist/table/site-labels.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/combinators/dispatch.ts +12 -3
- package/src/compiler/token-alphabet.ts +57 -17
- package/src/cst/capture-buffer.ts +25 -0
- package/src/plugin/index.ts +230 -35
- package/src/run/index.ts +5 -0
- package/src/table/assemble.ts +252 -62
- package/src/table/child-slots.ts +2 -2
- package/src/table/compile-rule-map.ts +14 -5
- package/src/table/emit-assembly.ts +112 -23
- package/src/table/emit.ts +3 -1
- package/src/table/encode.ts +83 -14
- package/src/table/exec.ts +27 -8
- package/src/table/factor-choice.ts +173 -0
- package/src/table/fail-clean.ts +130 -0
- package/src/table/hold-gap.ts +68 -0
- package/src/table/lex-program.ts +197 -43
- package/src/table/ops.ts +9 -1
- package/src/table/run-support.ts +35 -0
- package/src/table/scalar-terminal.ts +8 -0
- package/src/table/site-labels.ts +135 -4
- package/src/version.ts +1 -1
package/src/table/lex-program.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Combinator, ParseContext } from '../types.ts'
|
|
2
2
|
import { failAt } from '../combinators/probe.ts'
|
|
3
|
+
import { lead } from './run-support.ts'
|
|
3
4
|
import type { LexBodyRecognizer } from './lex-body.ts'
|
|
4
5
|
import type { LexProgramSpec, ResolvedClass } from './program.ts'
|
|
5
6
|
|
|
@@ -16,11 +17,15 @@ export const LEX_NODE_SEQUENCE2 = 1
|
|
|
16
17
|
export const LEX_NODE_SEQUENCE3 = 2
|
|
17
18
|
export const LEX_NODE_SEQUENCE4 = 3
|
|
18
19
|
export const LEX_NODE_SEQUENCE5 = 4
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* `[op, armCount, exclusive, choiceFx, (armClass, armFx) × armCount]`: any arity,
|
|
22
|
+
* no padding. `exclusive` is the character choice's one-arm dispatch decision.
|
|
23
|
+
*/
|
|
24
|
+
export const LEX_NODE_CHOICE = 5
|
|
22
25
|
export const LEX_NODE_NOT = 8
|
|
23
26
|
export const LEX_NODE_OPTIONAL = 9
|
|
27
|
+
/** `[op, min, max]` after the item: a separator-less repeat, `max` −1 unbounded. */
|
|
28
|
+
export const LEX_NODE_REPEAT = 10
|
|
24
29
|
|
|
25
30
|
export function lexProgramDigest(words: readonly number[]): number {
|
|
26
31
|
let hash = 0x811C9DC5
|
|
@@ -60,73 +65,88 @@ function terminalNode(
|
|
|
60
65
|
failure: readonly string[],
|
|
61
66
|
tracksLines: boolean,
|
|
62
67
|
): BuiltLexNode {
|
|
63
|
-
const match: PureLexRunner = (input, pos) => {
|
|
64
|
-
const result = matcher(input, pos)
|
|
65
|
-
return result < 0 ? -1 : result / 2
|
|
66
|
-
}
|
|
67
68
|
return {
|
|
68
|
-
match,
|
|
69
|
+
match: (input, pos) => {
|
|
70
|
+
const result = matcher(input, pos)
|
|
71
|
+
return result < 0 ? -1 : result / 2
|
|
72
|
+
},
|
|
69
73
|
run: (input, pos, ctx) => {
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
74
|
+
const result = matcher(input, pos)
|
|
75
|
+
if (result < 0) {
|
|
72
76
|
ctx._fe = pos
|
|
73
77
|
ctx._fx = failure as string[]
|
|
74
78
|
if (ctx._probe !== undefined) failAt(ctx, ctx._fx, pos)
|
|
75
79
|
return -1
|
|
76
80
|
}
|
|
81
|
+
const end = result / 2
|
|
77
82
|
if (tracksLines) trackLexLines(ctx, input, end)
|
|
78
83
|
return end
|
|
79
84
|
},
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
// Each sequence captures its parts' runners as scalars: a part is reached by a
|
|
89
|
+
// direct call, not a property read, and no arity nests another sequence.
|
|
83
90
|
function sequence2(a: BuiltLexNode, b: BuiltLexNode): BuiltLexNode {
|
|
91
|
+
const am = a.match, bm = b.match, ar = a.run, br = b.run
|
|
84
92
|
return {
|
|
85
93
|
match: (input, pos) => {
|
|
86
|
-
const e0 =
|
|
87
|
-
return e0 < 0 ? -1 :
|
|
94
|
+
const e0 = am(input, pos)
|
|
95
|
+
return e0 < 0 ? -1 : bm(input, e0)
|
|
88
96
|
},
|
|
89
97
|
run: (input, pos, ctx) => {
|
|
90
|
-
const e0 =
|
|
98
|
+
const e0 = ar(input, pos, ctx)
|
|
91
99
|
if (e0 < 0) return -1
|
|
92
100
|
ctx._fc = false
|
|
93
|
-
return
|
|
101
|
+
return br(input, e0, ctx)
|
|
94
102
|
},
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
function sequence3(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode): BuiltLexNode {
|
|
107
|
+
const am = a.match, bm = b.match, cm = c.match, ar = a.run, br = b.run, cr = c.run
|
|
99
108
|
return {
|
|
100
109
|
match: (input, pos) => {
|
|
101
|
-
const e0 =
|
|
110
|
+
const e0 = am(input, pos)
|
|
102
111
|
if (e0 < 0) return -1
|
|
103
|
-
const e1 =
|
|
104
|
-
return e1 < 0 ? -1 :
|
|
112
|
+
const e1 = bm(input, e0)
|
|
113
|
+
return e1 < 0 ? -1 : cm(input, e1)
|
|
105
114
|
},
|
|
106
115
|
run: (input, pos, ctx) => {
|
|
107
|
-
const e0 =
|
|
116
|
+
const e0 = ar(input, pos, ctx)
|
|
108
117
|
if (e0 < 0) return -1
|
|
109
118
|
ctx._fc = false
|
|
110
|
-
const e1 =
|
|
119
|
+
const e1 = br(input, e0, ctx)
|
|
111
120
|
if (e1 < 0) return -1
|
|
112
121
|
ctx._fc = false
|
|
113
|
-
return
|
|
122
|
+
return cr(input, e1, ctx)
|
|
114
123
|
},
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
function sequence4(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLexNode): BuiltLexNode {
|
|
119
|
-
const
|
|
128
|
+
const am = a.match, bm = b.match, cm = c.match, dm = d.match
|
|
129
|
+
const ar = a.run, br = b.run, cr = c.run, dr = d.run
|
|
120
130
|
return {
|
|
121
131
|
match: (input, pos) => {
|
|
122
|
-
const
|
|
123
|
-
|
|
132
|
+
const e0 = am(input, pos)
|
|
133
|
+
if (e0 < 0) return -1
|
|
134
|
+
const e1 = bm(input, e0)
|
|
135
|
+
if (e1 < 0) return -1
|
|
136
|
+
const e2 = cm(input, e1)
|
|
137
|
+
return e2 < 0 ? -1 : dm(input, e2)
|
|
124
138
|
},
|
|
125
139
|
run: (input, pos, ctx) => {
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
140
|
+
const e0 = ar(input, pos, ctx)
|
|
141
|
+
if (e0 < 0) return -1
|
|
142
|
+
ctx._fc = false
|
|
143
|
+
const e1 = br(input, e0, ctx)
|
|
144
|
+
if (e1 < 0) return -1
|
|
145
|
+
ctx._fc = false
|
|
146
|
+
const e2 = cr(input, e1, ctx)
|
|
147
|
+
if (e2 < 0) return -1
|
|
128
148
|
ctx._fc = false
|
|
129
|
-
return
|
|
149
|
+
return dr(input, e2, ctx)
|
|
130
150
|
},
|
|
131
151
|
}
|
|
132
152
|
}
|
|
@@ -134,17 +154,33 @@ function sequence4(a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLe
|
|
|
134
154
|
function sequence5(
|
|
135
155
|
a: BuiltLexNode, b: BuiltLexNode, c: BuiltLexNode, d: BuiltLexNode, e: BuiltLexNode,
|
|
136
156
|
): BuiltLexNode {
|
|
137
|
-
const
|
|
157
|
+
const am = a.match, bm = b.match, cm = c.match, dm = d.match, em = e.match
|
|
158
|
+
const ar = a.run, br = b.run, cr = c.run, dr = d.run, er = e.run
|
|
138
159
|
return {
|
|
139
160
|
match: (input, pos) => {
|
|
140
|
-
const
|
|
141
|
-
|
|
161
|
+
const e0 = am(input, pos)
|
|
162
|
+
if (e0 < 0) return -1
|
|
163
|
+
const e1 = bm(input, e0)
|
|
164
|
+
if (e1 < 0) return -1
|
|
165
|
+
const e2 = cm(input, e1)
|
|
166
|
+
if (e2 < 0) return -1
|
|
167
|
+
const e3 = dm(input, e2)
|
|
168
|
+
return e3 < 0 ? -1 : em(input, e3)
|
|
142
169
|
},
|
|
143
170
|
run: (input, pos, ctx) => {
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
171
|
+
const e0 = ar(input, pos, ctx)
|
|
172
|
+
if (e0 < 0) return -1
|
|
146
173
|
ctx._fc = false
|
|
147
|
-
|
|
174
|
+
const e1 = br(input, e0, ctx)
|
|
175
|
+
if (e1 < 0) return -1
|
|
176
|
+
ctx._fc = false
|
|
177
|
+
const e2 = cr(input, e1, ctx)
|
|
178
|
+
if (e2 < 0) return -1
|
|
179
|
+
ctx._fc = false
|
|
180
|
+
const e3 = dr(input, e2, ctx)
|
|
181
|
+
if (e3 < 0) return -1
|
|
182
|
+
ctx._fc = false
|
|
183
|
+
return er(input, e3, ctx)
|
|
148
184
|
},
|
|
149
185
|
}
|
|
150
186
|
}
|
|
@@ -169,9 +205,9 @@ function choiceNode(
|
|
|
169
205
|
): BuiltLexNode {
|
|
170
206
|
let matchTail: PureLexRunner = () => -1
|
|
171
207
|
for (let i = arms.length - 1; i >= 0; i--) {
|
|
172
|
-
const
|
|
208
|
+
const armMatch = arms[i]!.match, next = matchTail
|
|
173
209
|
matchTail = (input, pos) => {
|
|
174
|
-
const end =
|
|
210
|
+
const end = armMatch(input, pos)
|
|
175
211
|
return end < 0 ? next(input, pos) : end
|
|
176
212
|
}
|
|
177
213
|
}
|
|
@@ -181,7 +217,7 @@ function choiceNode(
|
|
|
181
217
|
return -1
|
|
182
218
|
}
|
|
183
219
|
for (let i = arms.length - 1; i >= 0; i--) {
|
|
184
|
-
const
|
|
220
|
+
const armRun = arms[i]!.run, cls = classes[i], staticExpected = armExpected[i]!, next = tail
|
|
185
221
|
tail = (input, pos, ctx, lead, acc, best) => {
|
|
186
222
|
if (cls !== undefined && !classHas(cls, lead)) {
|
|
187
223
|
return next(
|
|
@@ -191,7 +227,7 @@ function choiceNode(
|
|
|
191
227
|
)
|
|
192
228
|
}
|
|
193
229
|
ctx._fc = false
|
|
194
|
-
const end =
|
|
230
|
+
const end = armRun(input, pos, ctx)
|
|
195
231
|
if (end >= 0) return end
|
|
196
232
|
const at = ctx._fe ?? pos
|
|
197
233
|
if (at > best) { best = at; acc = undefined }
|
|
@@ -211,11 +247,68 @@ function choiceNode(
|
|
|
211
247
|
}
|
|
212
248
|
}
|
|
213
249
|
|
|
250
|
+
/**
|
|
251
|
+
* A choice whose arms' classes are disjoint, as the character choice's exclusive
|
|
252
|
+
* dispatch: the arm the lead selects runs alone, its failure is the choice's at
|
|
253
|
+
* the choice's position, and a lead no arm claims reports the arms' openers. The
|
|
254
|
+
* selected index reaches its arm through a fixed chain, never an array.
|
|
255
|
+
*/
|
|
256
|
+
function exclusiveChoiceNode(
|
|
257
|
+
arms: readonly BuiltLexNode[],
|
|
258
|
+
classes: readonly ResolvedClass[],
|
|
259
|
+
choiceFx: readonly string[],
|
|
260
|
+
): BuiltLexNode {
|
|
261
|
+
// A signed byte holds every arm index up to 127, which is every real grammar's
|
|
262
|
+
// choice; a wider choice takes a wider table so its 129th arm is not read as -128.
|
|
263
|
+
const ascii = (classes.length <= 128 ? new Int8Array(128) : new Int32Array(128)).fill(-1)
|
|
264
|
+
const hi: number[] = []
|
|
265
|
+
for (let i = 0; i < classes.length; i++) {
|
|
266
|
+
const cls = classes[i]!
|
|
267
|
+
for (let c = 0; c < 128; c++) if (cls.ascii[c] === 1) ascii[c] = i
|
|
268
|
+
for (let r = 0; r < cls.hi.length; r += 2) hi.push(cls.hi[r]!, cls.hi[r + 1]!, i)
|
|
269
|
+
}
|
|
270
|
+
const select = (code: number): number => {
|
|
271
|
+
if (code < 0) return -1
|
|
272
|
+
if (code < 128) return ascii[code]!
|
|
273
|
+
for (let r = 0; r < hi.length; r += 3) if (code >= hi[r]! && code <= hi[r + 1]!) return hi[r + 2]!
|
|
274
|
+
return -1
|
|
275
|
+
}
|
|
276
|
+
let matchArm: (arm: number, input: string, pos: number) => number = () => -1
|
|
277
|
+
let selectRun: (arm: number, input: string, pos: number, ctx: ParseContext) => number = () => -1
|
|
278
|
+
for (let i = arms.length - 1; i >= 0; i--) {
|
|
279
|
+
const armMatch = arms[i]!.match, armRun = arms[i]!.run, nextMatch = matchArm, nextRun = selectRun
|
|
280
|
+
matchArm = (selected, input, pos) => selected === i ? armMatch(input, pos) : nextMatch(selected, input, pos)
|
|
281
|
+
selectRun = (selected, input, pos, ctx) => selected === i ? armRun(input, pos, ctx) : nextRun(selected, input, pos, ctx)
|
|
282
|
+
}
|
|
283
|
+
const runArm: LexProgramRunner = (input, pos, ctx) => {
|
|
284
|
+
const arm = select(lead(input, pos))
|
|
285
|
+
if (arm >= 0) {
|
|
286
|
+
ctx._fc = false
|
|
287
|
+
const end = selectRun(arm, input, pos, ctx)
|
|
288
|
+
if (end >= 0) return end
|
|
289
|
+
if (committed(ctx)) return -1
|
|
290
|
+
const failed = ctx._fx
|
|
291
|
+
if (failed !== undefined && failed.length > 0) { ctx._fe = pos; return -1 }
|
|
292
|
+
}
|
|
293
|
+
ctx._fe = pos
|
|
294
|
+
ctx._fx = choiceFx as string[]
|
|
295
|
+
return -1
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
match: (input, pos) => {
|
|
299
|
+
const arm = select(lead(input, pos))
|
|
300
|
+
return arm < 0 ? -1 : matchArm(arm, input, pos)
|
|
301
|
+
},
|
|
302
|
+
run: runArm,
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
214
306
|
function notNode(body: BuiltLexNode, failure: readonly string[]): BuiltLexNode {
|
|
307
|
+
const bodyMatch = body.match
|
|
215
308
|
return {
|
|
216
|
-
match: (input, pos) =>
|
|
309
|
+
match: (input, pos) => bodyMatch(input, pos) < 0 ? pos : -1,
|
|
217
310
|
run: (input, pos, ctx) => {
|
|
218
|
-
if (
|
|
311
|
+
if (bodyMatch(input, pos) < 0) return pos
|
|
219
312
|
ctx._fe = pos
|
|
220
313
|
ctx._fx = failure as string[]
|
|
221
314
|
return -1
|
|
@@ -224,19 +317,65 @@ function notNode(body: BuiltLexNode, failure: readonly string[]): BuiltLexNode {
|
|
|
224
317
|
}
|
|
225
318
|
|
|
226
319
|
function optionalNode(body: BuiltLexNode): BuiltLexNode {
|
|
320
|
+
const bodyMatch = body.match, bodyRun = body.run
|
|
227
321
|
return {
|
|
228
322
|
match: (input, pos) => {
|
|
229
|
-
const end =
|
|
323
|
+
const end = bodyMatch(input, pos)
|
|
230
324
|
return end < 0 ? pos : end
|
|
231
325
|
},
|
|
232
326
|
run: (input, pos, ctx) => {
|
|
233
327
|
ctx._fc = false
|
|
234
|
-
const end =
|
|
328
|
+
const end = bodyRun(input, pos, ctx)
|
|
235
329
|
return end < 0 && !committed(ctx) ? pos : end
|
|
236
330
|
},
|
|
237
331
|
}
|
|
238
332
|
}
|
|
239
333
|
|
|
334
|
+
/**
|
|
335
|
+
* The table's separator-less `OP_REP` under `token()`: no trivia, no separator,
|
|
336
|
+
* nothing to roll back. Only an optional item stops at end of input or on a
|
|
337
|
+
* zero-width match (`repItem`); a failed item ends the loop with its own
|
|
338
|
+
* failure published, and a committed one fails the repeat.
|
|
339
|
+
*/
|
|
340
|
+
function repeatNode(item: BuiltLexNode, min: number, max: number): BuiltLexNode {
|
|
341
|
+
const itemMatch = item.match, itemRun = item.run
|
|
342
|
+
return {
|
|
343
|
+
match: (input, pos) => {
|
|
344
|
+
let cur = pos
|
|
345
|
+
let count = 0
|
|
346
|
+
for (;;) {
|
|
347
|
+
if (max >= 0 && count >= max) break
|
|
348
|
+
const optional = count >= min && (count > 0 || min === 0)
|
|
349
|
+
if (optional && cur >= input.length) break
|
|
350
|
+
const end = itemMatch(input, cur)
|
|
351
|
+
if (end < 0 || (optional && end === cur)) break
|
|
352
|
+
cur = end
|
|
353
|
+
count++
|
|
354
|
+
}
|
|
355
|
+
return count < min ? -1 : cur
|
|
356
|
+
},
|
|
357
|
+
run: (input, pos, ctx) => {
|
|
358
|
+
let cur = pos
|
|
359
|
+
let count = 0
|
|
360
|
+
for (;;) {
|
|
361
|
+
if (max >= 0 && count >= max) break
|
|
362
|
+
const optional = count >= min && (count > 0 || min === 0)
|
|
363
|
+
if (optional && cur >= input.length) break
|
|
364
|
+
ctx._fc = false
|
|
365
|
+
const end = itemRun(input, cur, ctx)
|
|
366
|
+
if (end < 0) {
|
|
367
|
+
if (committed(ctx)) return -1
|
|
368
|
+
break
|
|
369
|
+
}
|
|
370
|
+
if (optional && end === cur) break
|
|
371
|
+
cur = end
|
|
372
|
+
count++
|
|
373
|
+
}
|
|
374
|
+
return count < min ? -1 : cur
|
|
375
|
+
},
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
240
379
|
function fixedTreeRunner(
|
|
241
380
|
words: readonly number[],
|
|
242
381
|
matchers: readonly LexBodyRecognizer[],
|
|
@@ -269,9 +408,14 @@ function fixedTreeRunner(
|
|
|
269
408
|
else stack.push(sequence5(nodes[0]!, nodes[1]!, nodes[2]!, nodes[3]!, nodes[4]!))
|
|
270
409
|
continue
|
|
271
410
|
}
|
|
272
|
-
if (op
|
|
273
|
-
const count =
|
|
411
|
+
if (op === LEX_NODE_CHOICE) {
|
|
412
|
+
const count = words[at++]!
|
|
413
|
+
const exclusive = words[at++]!
|
|
414
|
+
if (exclusive !== 0 && exclusive !== 1) throw new TypeError('table lexical tree choice has an invalid dispatch mode')
|
|
274
415
|
const choiceFx = expected[words[at++]!]
|
|
416
|
+
if (!Number.isInteger(count) || count < 2 || count > stack.length) {
|
|
417
|
+
throw new TypeError('table lexical tree choice has an invalid arm count')
|
|
418
|
+
}
|
|
275
419
|
if (choiceFx === undefined) throw new TypeError('table lexical tree choice has an invalid expected set')
|
|
276
420
|
const armClasses: Array<ResolvedClass | undefined> = []
|
|
277
421
|
const armFx: Array<readonly string[]> = []
|
|
@@ -284,7 +428,9 @@ function fixedTreeRunner(
|
|
|
284
428
|
armFx.push(fx)
|
|
285
429
|
}
|
|
286
430
|
const arms = Array.from({ length: count }, pop).reverse()
|
|
287
|
-
stack.push(
|
|
431
|
+
stack.push(exclusive === 1
|
|
432
|
+
? exclusiveChoiceNode(arms, armClasses as ResolvedClass[], choiceFx)
|
|
433
|
+
: choiceNode(arms, armClasses, armFx, choiceFx))
|
|
288
434
|
continue
|
|
289
435
|
}
|
|
290
436
|
if (op === LEX_NODE_NOT) {
|
|
@@ -294,6 +440,14 @@ function fixedTreeRunner(
|
|
|
294
440
|
continue
|
|
295
441
|
}
|
|
296
442
|
if (op === LEX_NODE_OPTIONAL) { stack.push(optionalNode(pop())); continue }
|
|
443
|
+
if (op === LEX_NODE_REPEAT) {
|
|
444
|
+
const min = words[at++]!, max = words[at++]!
|
|
445
|
+
if (!Number.isInteger(min) || min < 0 || !Number.isInteger(max) || (max >= 0 && max < min)) {
|
|
446
|
+
throw new TypeError('table lexical tree repeat has invalid bounds')
|
|
447
|
+
}
|
|
448
|
+
stack.push(repeatNode(pop(), min, max))
|
|
449
|
+
continue
|
|
450
|
+
}
|
|
297
451
|
throw new TypeError('table lexical tree has an unknown fixed body')
|
|
298
452
|
}
|
|
299
453
|
if (stack.length !== 1) throw new TypeError('table lexical tree does not resolve to one root body')
|
package/src/table/ops.ts
CHANGED
|
@@ -478,6 +478,14 @@ export const OP_LEX_BODY = 42
|
|
|
478
478
|
export const OP_LEX_PROGRAM = 43
|
|
479
479
|
/** `SOURCE_LEAF f c` — `sourceLeaf()`: reduce semantically, capture source text. */
|
|
480
480
|
export const OP_SOURCE_LEAF = 44
|
|
481
|
+
/**
|
|
482
|
+
* `RGATE cls child fx` — a choice arm that opens with the token its `dispatch()`
|
|
483
|
+
* already routed. `cls` is what may follow that token (the arm's next term,
|
|
484
|
+
* with the sequence's trivia), tested at the routed token's END; a miss fails
|
|
485
|
+
* there with the next term's set `fx`, as running the arm would. No routed token
|
|
486
|
+
* at this position, a completions probe, or tolerant recovery enters the arm.
|
|
487
|
+
*/
|
|
488
|
+
export const OP_RGATE = 45
|
|
481
489
|
/**
|
|
482
490
|
* `DISPATCH sel d other otherRouted n a1 … an` — `dispatch()`.
|
|
483
491
|
*
|
|
@@ -501,7 +509,7 @@ export const OP_NAMES: Record<number, string> = {
|
|
|
501
509
|
[OP_LIT]: 'LIT', [OP_RX]: 'RX', [OP_SEQ]: 'SEQ', [OP_SEQV]: 'SEQV',
|
|
502
510
|
[OP_CHOICE]: 'CHOICE', [OP_REP]: 'REP', [OP_REPV]: 'REPV', [OP_OPT]: 'OPT',
|
|
503
511
|
[OP_XFORM]: 'XFORM', [OP_NODE]: 'NODE', [OP_RULE]: 'RULE', [OP_GATE]: 'GATE',
|
|
504
|
-
[OP_NOT]: 'NOT', [OP_PEEK]: 'PEEK', [OP_LEAF]: 'LEAF', [OP_SOURCE_LEAF]: 'SOURCE_LEAF', [OP_EMPTY]: 'EMPTY',
|
|
512
|
+
[OP_NOT]: 'NOT', [OP_PEEK]: 'PEEK', [OP_LEAF]: 'LEAF', [OP_SOURCE_LEAF]: 'SOURCE_LEAF', [OP_RGATE]: 'RGATE', [OP_EMPTY]: 'EMPTY',
|
|
505
513
|
[OP_LIT_TRACK]: 'LIT_TRACK', [OP_RX_TRACK]: 'RX_TRACK', [OP_NODE_TRACK]: 'NODE_TRACK',
|
|
506
514
|
[OP_SCOPE]: 'SCOPE', [OP_EXPECT]: 'EXPECT', [OP_SEQX]: 'SEQX', [OP_SCAN]: 'SCAN',
|
|
507
515
|
[OP_FIELD]: 'FIELD', [OP_LIT_CI]: 'LIT_CI', [OP_LIT_CI_TRACK]: 'LIT_CI_TRACK', [OP_DISPATCH]: 'DISPATCH', [OP_ROUTED]: 'ROUTED',
|
package/src/table/run-support.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* reads only its arguments.
|
|
14
14
|
*/
|
|
15
15
|
import type { ParseContext } from '../types.ts'
|
|
16
|
+
import { popCstChild, pushCstChild } from '../cst/capture-buffer.ts'
|
|
16
17
|
|
|
17
18
|
export type Span = { start: number; end: number }
|
|
18
19
|
export type LineSpan = Span & {
|
|
@@ -64,3 +65,37 @@ export function lead(input: string, pos: number): number {
|
|
|
64
65
|
if (c < 0xd800 || c > 0xdbff) return c
|
|
65
66
|
return input.codePointAt(pos) ?? c
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
const EMPTY_KIDS: readonly unknown[] = Object.freeze([])
|
|
70
|
+
const EMPTY_TRIVIA: readonly number[] = Object.freeze([])
|
|
71
|
+
|
|
72
|
+
type CollapseBuild = (
|
|
73
|
+
children: readonly unknown[], fields: undefined, span: { start: number; end: number },
|
|
74
|
+
rawChildren: readonly unknown[], triviaLog: readonly number[], state: undefined,
|
|
75
|
+
) => unknown
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A PASS-THROUGH COLLAPSE whose one child built `undefined` (`site-labels.ts`
|
|
79
|
+
* `passCollapse`).
|
|
80
|
+
*
|
|
81
|
+
* A collapse over a single node is that node: the node publishes its value into
|
|
82
|
+
* the collector the collapse's parent opened, exactly where the collapse would
|
|
83
|
+
* have published it, over the same span. So both engines run the body with no
|
|
84
|
+
* frame of the collapse's own and hand its value up. The one value a frame would
|
|
85
|
+
* have treated differently is `undefined`, which a frame does not count as a
|
|
86
|
+
* child: the collapse then builds from no children. This is that case — take the
|
|
87
|
+
* child's publication back and publish the build instead, as the frame would
|
|
88
|
+
* have. The caller has already set `EC.e`.
|
|
89
|
+
*/
|
|
90
|
+
export function collapseEmpty(
|
|
91
|
+
ctx: ParseContext, build: CollapseBuild, input: string, pos: number, end: number, readsRaw: boolean,
|
|
92
|
+
): unknown {
|
|
93
|
+
const collected = ctx._cstBuf !== undefined || ctx._cstChildren !== undefined
|
|
94
|
+
if (collected) popCstChild(ctx)
|
|
95
|
+
// A frame keeps the raw entry of the child it does not count, for a reducer
|
|
96
|
+
// that reads `rawChildren`.
|
|
97
|
+
const raw = readsRaw ? [rawEntry(undefined, input, pos, end)] : EMPTY_KIDS
|
|
98
|
+
const nd = build(EMPTY_KIDS, undefined, { start: pos, end }, raw, EMPTY_TRIVIA, undefined)
|
|
99
|
+
if (collected) pushCstChild(ctx, nd, ctx._cstBuf?.noRaw === true ? undefined : rawEntry(nd, input, pos, end))
|
|
100
|
+
return nd
|
|
101
|
+
}
|
|
@@ -54,6 +54,14 @@ export function scalarTerminalNotChild(code: ArrayLike<number>, ip: number): num
|
|
|
54
54
|
return op === OP_RX || op === OP_LIT ? child : -1
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** The same terminal under `peek()`: its match decides the lookahead, and nothing it captured survives it. */
|
|
58
|
+
export function scalarTerminalPeekChild(code: ArrayLike<number>, ip: number): number {
|
|
59
|
+
if (code[ip] !== OP_PEEK) return -1
|
|
60
|
+
const child = code[ip + 1]!
|
|
61
|
+
const op = code[child]
|
|
62
|
+
return op === OP_RX || op === OP_LIT ? child : -1
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
/**
|
|
58
66
|
* A direct scalar terminal that every successful execution of `ip` must match
|
|
59
67
|
* first, through wrappers that cannot consume or select before their child.
|
package/src/table/site-labels.ts
CHANGED
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
* label licenses at a leaf is dropping the TEST — never the capture.
|
|
61
61
|
*/
|
|
62
62
|
import {
|
|
63
|
-
OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_SOURCE_LEAF,
|
|
63
|
+
OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_SOURCE_LEAF,
|
|
64
64
|
OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE,
|
|
65
|
-
OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
|
|
65
|
+
OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_XFORM,
|
|
66
66
|
} from './ops.ts'
|
|
67
67
|
import { childSlots } from './child-slots.ts'
|
|
68
68
|
|
|
@@ -142,6 +142,61 @@ export function reachableSites(code: Int32Array, roots: Iterable<number>): Set<n
|
|
|
142
142
|
return seen
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* A node both engines lower as a PASS-THROUGH COLLAPSE (`run-support.ts`
|
|
147
|
+
* `collapseEmpty`): `collapse: true` over a direct reducer, no projection, no CST
|
|
148
|
+
* host, no line tracking, whose body is one node behind wrappers that consume and
|
|
149
|
+
* capture nothing (a first-set gate, a trivia scope, a rule reference). One
|
|
150
|
+
* predicate, so the two engines and the labels cannot disagree on a site.
|
|
151
|
+
*/
|
|
152
|
+
export function passCollapse(code: Int32Array, ip: number, hostCst: boolean): boolean {
|
|
153
|
+
const flags = code[ip + 3]!
|
|
154
|
+
if (hostCst || code[ip] !== OP_NODE || code[ip + 1]! < 0 || code[ip + 4]! >= 0) return false
|
|
155
|
+
if (flags !== 32 && flags !== 34) return false
|
|
156
|
+
let at = code[ip + 2]!
|
|
157
|
+
for (let n = 0; n < 8; n++) {
|
|
158
|
+
const op = code[at]
|
|
159
|
+
if (op === OP_GATE || op === OP_SCOPE_PLAIN || (op === OP_SCOPE && code[at + 3] === 0)) at = code[at + 2]!
|
|
160
|
+
else if (op === OP_RULE) at = code[at + 1]!
|
|
161
|
+
else break
|
|
162
|
+
}
|
|
163
|
+
return code[at] === OP_NODE
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* A collapse that a FIRST CHARACTER can reduce to a pass-through: its body is a
|
|
168
|
+
* sequence of `optional`/`not` terms, each behind a first-character gate, then
|
|
169
|
+
* one node. Where the character at the collapse's start opens none of those
|
|
170
|
+
* terms (nor its sequence's trivia — the engines test that against the site's
|
|
171
|
+
* label), every one of them matches empty there and the body is that node alone.
|
|
172
|
+
* `leads` are the gate rows, `lits` the constant indices of direct literal terms,
|
|
173
|
+
* `last` the node term. Not a `passCollapse`.
|
|
174
|
+
*/
|
|
175
|
+
export function gatedPassCollapse(code: Int32Array, ip: number, hostCst: boolean): { leads: number[]; lits: number[]; last: number } | undefined {
|
|
176
|
+
const flags = code[ip + 3]!
|
|
177
|
+
if (hostCst || code[ip] !== OP_NODE || code[ip + 1]! < 0 || code[ip + 4]! >= 0) return undefined
|
|
178
|
+
if (flags !== 32 && flags !== 34) return undefined
|
|
179
|
+
const seq = code[ip + 2]!
|
|
180
|
+
if (code[seq] !== OP_SEQ && code[seq] !== OP_SEQV) return undefined
|
|
181
|
+
const n = code[seq + 1]!
|
|
182
|
+
if (n < 2) return undefined
|
|
183
|
+
const leads: number[] = []
|
|
184
|
+
const lits: number[] = []
|
|
185
|
+
for (let i = 0; i < n - 1; i++) {
|
|
186
|
+
const t = code[seq + 2 + i]!
|
|
187
|
+
if (code[t] !== OP_OPT && code[t] !== OP_NOT) return undefined
|
|
188
|
+
const g = code[t + 1]!
|
|
189
|
+
// A gate row carries the term's first characters; a direct literal is its own.
|
|
190
|
+
if (code[g] === OP_GATE) leads.push(g)
|
|
191
|
+
else if (code[g] === OP_LIT) lits.push(code[g + 1]!)
|
|
192
|
+
else return undefined
|
|
193
|
+
}
|
|
194
|
+
const last = code[seq + 2 + n - 1]!
|
|
195
|
+
let at = last
|
|
196
|
+
for (let k = 0; k < 8 && (code[at] === OP_GATE || code[at] === OP_RULE); k++) at = code[at] === OP_RULE ? code[at + 1]! : code[at + 2]!
|
|
197
|
+
return code[at] === OP_NODE ? { leads, lits, last } : undefined
|
|
198
|
+
}
|
|
199
|
+
|
|
145
200
|
/**
|
|
146
201
|
* What a site hands DOWN to its children. Every op but the two that write the
|
|
147
202
|
* context passes its own label through unchanged.
|
|
@@ -159,6 +214,10 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
|
|
|
159
214
|
}
|
|
160
215
|
if (op === OP_NODE || op === OP_NODE_TRACK) {
|
|
161
216
|
const flags = code[ip + 3]!
|
|
217
|
+
// A pass-through collapse opens nothing: its body runs in its parent's
|
|
218
|
+
// collector and capture setting.
|
|
219
|
+
if (passCollapse(code, ip, hostCst)) return at
|
|
220
|
+
const gated = gatedPassCollapse(code, ip, hostCst) !== undefined
|
|
162
221
|
// The same selection `emit-assembly.ts` makes: the trailing-trivia bit (128)
|
|
163
222
|
// keeps the children-only shape.
|
|
164
223
|
const directChildren = !hostCst && op === OP_NODE && code[ip + 1]! >= 0 && code[ip + 4]! < 0
|
|
@@ -168,7 +227,9 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
|
|
|
168
227
|
// arrays instead of a CstCaptureBuf. Descendants still capture, but they
|
|
169
228
|
// must take the generic collector branch rather than dereference `_cstBuf`.
|
|
170
229
|
// False means UNKNOWN in this lattice, which is exactly the safe answer.
|
|
171
|
-
|
|
230
|
+
const framed = { tri: at.tri, buf: false, raw: RAW_UNKNOWN, cap: CAP_OFF }
|
|
231
|
+
// A gated pass collapse runs its node term in its parent's collector too.
|
|
232
|
+
return gated ? meet(at, framed) : framed
|
|
172
233
|
}
|
|
173
234
|
// `ctx._cstBuf = buf` and `ctx.captureTrivia = <literal>`, both unconditional
|
|
174
235
|
// — `emit-assembly.ts`'s `OP_NODE` body opens the buffer before it descends
|
|
@@ -178,7 +239,8 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
|
|
|
178
239
|
? RAW_OMIT
|
|
179
240
|
: RAW_CAPTURE
|
|
180
241
|
if (at.buf && raw === at.raw && cap === at.cap) return at
|
|
181
|
-
|
|
242
|
+
const framed = { tri: at.tri, buf: true, raw, cap }
|
|
243
|
+
return gated ? meet(at, framed) : framed
|
|
182
244
|
}
|
|
183
245
|
// THE TWO BOUNDARIES clear every capture sink for their child and contribute
|
|
184
246
|
// ONE leaf for the whole match; `token()` clears `ctx.trivia` as well. So a
|
|
@@ -251,3 +313,72 @@ export function computeSiteLabels(
|
|
|
251
313
|
|
|
252
314
|
return { at: (ip: number): SiteLabel => labels.get(ip) ?? TOP }
|
|
253
315
|
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Does a piece entered at `ip` need the caller's INSTALLED trivia scanner handed
|
|
319
|
+
* across an engine boundary?
|
|
320
|
+
*
|
|
321
|
+
* Each table engine keeps its own installed-scanner slot, and the two hand theirs
|
|
322
|
+
* across at every call between them. A site whose label names its trivia (`tri`)
|
|
323
|
+
* reads no slot: both engines lower its skips onto that trivia's own scanner, and
|
|
324
|
+
* a call out of it can hand that scanner across instead of the slot. Only a site
|
|
325
|
+
* whose trivia the labels cannot name (`TRI_UNKNOWN`) reads the slot. So the
|
|
326
|
+
* handoff is needed exactly when such a site is reachable from `ip` before a
|
|
327
|
+
* scope installs a scanner of its own (an aliased scope, which installs what is
|
|
328
|
+
* already installed, does not). `false` is also the answer for a body with no
|
|
329
|
+
* trivia at all.
|
|
330
|
+
*/
|
|
331
|
+
/**
|
|
332
|
+
* A trivia scope that writes only what its label proves is already in force, so
|
|
333
|
+
* it runs as its child. A slot scope under that same slot is one; so is a scope
|
|
334
|
+
* with no trivia where the label proves none (inside `token()` or another such
|
|
335
|
+
* scope), since every way to `TRI_NONE` leaves no trivia, no kind labels and no
|
|
336
|
+
* scanner — what this scope would write. `OP_SCOPE_PLAIN` has no root policy; a
|
|
337
|
+
* policy-bearing `OP_SCOPE` is one only with policy zero, and `OP_SCOPE_CAP`
|
|
338
|
+
* never is, since it raises `captureTrivia`. `TRI_UNKNOWN` matches nothing.
|
|
339
|
+
*/
|
|
340
|
+
export function scopeIsIdentity(code: Int32Array, ip: number, at: SiteLabel): boolean {
|
|
341
|
+
const op = code[ip]
|
|
342
|
+
if (op !== OP_SCOPE_PLAIN && !(op === OP_SCOPE && code[ip + 3]! === 0)) return false
|
|
343
|
+
const ki = code[ip + 1]!
|
|
344
|
+
return at.tri === (ki < 0 ? TRI_NONE : ki)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function scanHandoffNeeded(code: Int32Array, ip: number, labels: SiteLabels): boolean {
|
|
348
|
+
const seen = new Set<number>()
|
|
349
|
+
const stack = [ip]
|
|
350
|
+
const kids: number[] = []
|
|
351
|
+
while (stack.length > 0) {
|
|
352
|
+
const at = stack.pop()!
|
|
353
|
+
if (seen.has(at)) continue
|
|
354
|
+
seen.add(at)
|
|
355
|
+
const l = labels.at(at)
|
|
356
|
+
if (l.tri === TRI_UNKNOWN) return true
|
|
357
|
+
const op = code[at]
|
|
358
|
+
// A scan runs interpreted combinators that reach pieces through `prog.scans`.
|
|
359
|
+
if (op === OP_SCAN) return true
|
|
360
|
+
if (op === OP_TOKEN || op === OP_SCOPE_CAP) continue
|
|
361
|
+
if ((op === OP_SCOPE_PLAIN || op === OP_SCOPE) && !(code[at + 1]! >= 0 && l.tri === code[at + 1]!
|
|
362
|
+
&& (op === OP_SCOPE_PLAIN || code[at + 3]! === 0))) continue
|
|
363
|
+
kids.length = 0
|
|
364
|
+
if (!childSlots(code, at, kids)) return true
|
|
365
|
+
for (const c of kids) stack.push(c)
|
|
366
|
+
}
|
|
367
|
+
return false
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The first characters that can start trivia at a site labelled `tri`, as code
|
|
372
|
+
* point ranges: none for `TRI_NONE`, `undefined` where the labels cannot name the
|
|
373
|
+
* trivia or its first set is unbounded.
|
|
374
|
+
*/
|
|
375
|
+
export function triviaLeadRanges(
|
|
376
|
+
tri: number, trivia: readonly ({ _meta: { firstSet: { kind: string; ranges?: readonly { lo: number; hi: number }[] } } } | undefined)[],
|
|
377
|
+
): readonly { lo: number; hi: number }[] | undefined {
|
|
378
|
+
if (tri === TRI_NONE) return []
|
|
379
|
+
if (tri < 0) return undefined
|
|
380
|
+
const fs = trivia[tri]?._meta.firstSet
|
|
381
|
+
if (fs === undefined) return undefined
|
|
382
|
+
if (fs.kind === 'empty') return []
|
|
383
|
+
return fs.kind === 'ranges' && fs.ranges !== undefined ? fs.ranges : undefined
|
|
384
|
+
}
|
package/src/version.ts
CHANGED