grammatik 0.1.1 → 0.1.2
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/README.md +16 -0
- package/bin/grammatik.js +8 -0
- package/dist/cjs/index.js +1471 -104
- package/dist/cjs/runtime.js +635 -25
- package/dist/esm/index.js +1471 -104
- package/dist/esm/runtime.js +635 -25
- package/dist/types/index.d.ts +36 -0
- package/dist/types/runtime.d.ts +4 -3
- package/package.json +1 -1
- package/src/analyze.js +114 -4
- package/src/codegen.js +147 -11
- package/src/definition.js +5 -0
- package/src/emit.js +29 -7
- package/src/lexer.d.ts +43 -2
- package/src/lexer.js +535 -28
- package/src/lexgen.js +451 -0
- package/src/memoplan.js +220 -0
- package/src/methods.js +1 -1
- package/src/parser.js +31 -3
- package/src/plan.js +88 -26
- package/src/runtime.js +8 -6
- package/src/scan.js +422 -0
- package/src/state.js +33 -1
package/src/scan.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hand-rolled matchers for the handful of regex shapes that dominate real
|
|
6
|
+
* token definitions.
|
|
7
|
+
*
|
|
8
|
+
* A sticky regex call has a fixed cost — reset `lastIndex`, enter the engine,
|
|
9
|
+
* come back — that is larger than the matching itself for the short tokens
|
|
10
|
+
* lexers mostly see (identifiers, whitespace, keywords). Recognising a pattern's
|
|
11
|
+
* *shape* once, when the lexer is built, and running a table-driven loop for it
|
|
12
|
+
* afterwards is measurably faster: ~1.3x on a SQL lexer, ~1.1x on a spreadsheet
|
|
13
|
+
* formula lexer, and ~1.2x on a JSON lexer once its string and number patterns
|
|
14
|
+
* are covered (with `positions: 'offset'`; the default `full` mode spends much of
|
|
15
|
+
* the rest on newline tracking).
|
|
16
|
+
*
|
|
17
|
+
* Recognised shapes, all ASCII-only and case-sensitive unless noted:
|
|
18
|
+
*
|
|
19
|
+
* CLASS `[a-z]+` `[a-z]*` `[A-Za-z_][A-Za-z0-9_]*` `\s+` `\d+`
|
|
20
|
+
* SEQ2 `[A-Za-z]+[0-9]+` (the two classes must be disjoint)
|
|
21
|
+
* KEYWORD `SELECT(?![A-Za-z0-9_])` (also with `ignoreCase`)
|
|
22
|
+
* LINE `--[^\n]*`
|
|
23
|
+
* QUOTED `'(?:''|[^'])*'` (also with `"`)
|
|
24
|
+
* NUMBER `-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?` (JSON), `\d+(?:\.\d+)?`,
|
|
25
|
+
* and the combinations of: a `-?` sign, a JSON or plain integer part,
|
|
26
|
+
* an optional `.digits` fraction, an optional exponent
|
|
27
|
+
* STRING `"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"` (JSON),
|
|
28
|
+
* and `"(?:[^"\\]|\\.)*"` — either quote character
|
|
29
|
+
*
|
|
30
|
+
* Anything else yields `null` from `buildScanner` and stays a regex. That
|
|
31
|
+
* includes every negated class other than the LINE shape, `ignoreCase` on
|
|
32
|
+
* anything but KEYWORD, the `u`/`v` flags, and any class member outside ASCII:
|
|
33
|
+
* declining is always safe, a wrong scanner is a silent lexing bug.
|
|
34
|
+
*
|
|
35
|
+
* Contract: `scanEnd` returns exactly what the regex would, as an end offset or
|
|
36
|
+
* -1, except that it may return `DEFER` where it cannot decide (see below), in
|
|
37
|
+
* which case the caller runs the regex. `test/scan.test.js` fuzzes every
|
|
38
|
+
* scanner against its regex.
|
|
39
|
+
*
|
|
40
|
+
* Every scanner has the same object shape (see `make`). That is deliberate:
|
|
41
|
+
* `scanEnd` is called from the lexer's hottest loop, and with one shape per
|
|
42
|
+
* kind its property loads went megamorphic and the whole gain (1.34x) fell to
|
|
43
|
+
* 1.13x. Add a field to `make`, never to one kind's literal.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** Returned by `scanEnd` when the regex must decide (non-ASCII text under `\s`). */
|
|
47
|
+
export const DEFER = -2
|
|
48
|
+
|
|
49
|
+
const CLASS = 0
|
|
50
|
+
const SEQ2 = 1
|
|
51
|
+
const KEYWORD = 2
|
|
52
|
+
const LINE = 3
|
|
53
|
+
const QUOTED = 4
|
|
54
|
+
const NUMBER = 5
|
|
55
|
+
const STRING = 6
|
|
56
|
+
|
|
57
|
+
/** The kinds of scanner `buildScanner` can return, for code that writes them out as source. */
|
|
58
|
+
export const SCANNER_KINDS = Object.freeze({ CLASS, SEQ2, KEYWORD, LINE, QUOTED, NUMBER, STRING })
|
|
59
|
+
|
|
60
|
+
const NONE = new Uint8Array(128)
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {object} Scanner
|
|
64
|
+
* @property {number} kind
|
|
65
|
+
* @property {boolean} wide Class contains `\s`: non-ASCII input is not ours to judge.
|
|
66
|
+
* @property {Uint8Array} first CLASS/SEQ2: may start the match. LINE: unused.
|
|
67
|
+
* @property {Uint8Array} rest CLASS/SEQ2: may continue the first run.
|
|
68
|
+
* @property {Uint8Array} second SEQ2: the second run's class.
|
|
69
|
+
* @property {Uint8Array} look KEYWORD: characters that must NOT follow. LINE: stop characters.
|
|
70
|
+
* @property {number[]} lower KEYWORD: each character's lower-case code.
|
|
71
|
+
* @property {number[]} upper KEYWORD: each character's upper-case code.
|
|
72
|
+
* @property {string} prefix LINE: the literal prefix.
|
|
73
|
+
* @property {number} quote QUOTED, STRING: the quote's char code.
|
|
74
|
+
* @property {boolean} sign NUMBER: an optional leading `-`.
|
|
75
|
+
* @property {boolean} jsonInt NUMBER: the integer part is `0|[1-9]\d*` rather than `\d+`.
|
|
76
|
+
* @property {boolean} frac NUMBER: an optional `.digits`.
|
|
77
|
+
* @property {boolean} exp NUMBER: an optional `e[+-]digits`.
|
|
78
|
+
* @property {boolean} ctrl STRING: characters below U+0020 are not allowed inside.
|
|
79
|
+
* @property {boolean} uni STRING: `\uXXXX` is an escape.
|
|
80
|
+
* @property {boolean} escAny STRING: a backslash may precede any character but a line terminator.
|
|
81
|
+
* @property {Uint8Array} esc STRING: the characters that may follow a backslash.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/** @returns {Scanner} */
|
|
85
|
+
function make (kind, fields) {
|
|
86
|
+
return {
|
|
87
|
+
kind,
|
|
88
|
+
wide: false,
|
|
89
|
+
first: NONE,
|
|
90
|
+
rest: NONE,
|
|
91
|
+
second: NONE,
|
|
92
|
+
look: NONE,
|
|
93
|
+
lower: [],
|
|
94
|
+
upper: [],
|
|
95
|
+
prefix: '',
|
|
96
|
+
quote: -1,
|
|
97
|
+
sign: false,
|
|
98
|
+
jsonInt: false,
|
|
99
|
+
frac: false,
|
|
100
|
+
exp: false,
|
|
101
|
+
ctrl: false,
|
|
102
|
+
uni: false,
|
|
103
|
+
escAny: false,
|
|
104
|
+
esc: NONE,
|
|
105
|
+
...fields
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const SHORTHAND = {
|
|
110
|
+
d: (t) => { for (let k = 48; k < 58; k++) t[k] = 1 },
|
|
111
|
+
w: (t) => { for (let k = 0; k < 128; k++) if (/\w/.test(String.fromCharCode(k))) t[k] = 1 },
|
|
112
|
+
s: (t) => { for (const k of [9, 10, 11, 12, 13, 32]) t[k] = 1 }
|
|
113
|
+
}
|
|
114
|
+
const SIMPLE_ESCAPES = { t: '\t', r: '\r', n: '\n', f: '\f', v: '\v' }
|
|
115
|
+
const ALNUM = /[a-zA-Z0-9]/
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The membership table of a bracket class body (the text between `[` and `]`),
|
|
119
|
+
* or null when it is anything but plain ASCII members, ranges and simple
|
|
120
|
+
* escapes. `\s` is refused inside brackets — only the bare `\s` shape is
|
|
121
|
+
* handled, because a class mixing it with other members would need its
|
|
122
|
+
* non-ASCII-whitespace behaviour reproduced here.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} body
|
|
125
|
+
* @returns {Uint8Array | null}
|
|
126
|
+
*/
|
|
127
|
+
function parseClass (body) {
|
|
128
|
+
if (body.length === 0 || body[0] === '^') return null
|
|
129
|
+
const table = new Uint8Array(128)
|
|
130
|
+
for (let i = 0; i < body.length; i++) {
|
|
131
|
+
let c = body[i]
|
|
132
|
+
if (c === '\\') {
|
|
133
|
+
const n = body[++i]
|
|
134
|
+
if (n === 'd' || n === 'w') { SHORTHAND[n](table); continue }
|
|
135
|
+
if (n === undefined) return null
|
|
136
|
+
if (ALNUM.test(n) && !(n in SIMPLE_ESCAPES)) return null // \s \b \0 \x41 A \cJ ...
|
|
137
|
+
c = SIMPLE_ESCAPES[n] ?? n
|
|
138
|
+
}
|
|
139
|
+
if (body[i + 1] === '-' && i + 2 < body.length) {
|
|
140
|
+
let hi = body[i + 2]
|
|
141
|
+
let advance = 2
|
|
142
|
+
if (hi === '\\') {
|
|
143
|
+
hi = body[i + 3]
|
|
144
|
+
advance = 3
|
|
145
|
+
if (hi === undefined || ALNUM.test(hi)) return null
|
|
146
|
+
}
|
|
147
|
+
const from = c.charCodeAt(0)
|
|
148
|
+
const to = hi.charCodeAt(0)
|
|
149
|
+
if (from > to || to >= 128) return null
|
|
150
|
+
for (let k = from; k <= to; k++) table[k] = 1
|
|
151
|
+
i += advance
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
const code = c.charCodeAt(0)
|
|
155
|
+
if (code >= 128) return null
|
|
156
|
+
table[code] = 1
|
|
157
|
+
}
|
|
158
|
+
return table
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const BODY = String.raw`((?:[^\]\\]|\\.)*)`
|
|
162
|
+
const RE_CLASS_RUN = new RegExp(String.raw`^\[${BODY}\]([+*])$`)
|
|
163
|
+
const RE_CLASS_PAIR = new RegExp(String.raw`^\[${BODY}\]\[${BODY}\]\*$`)
|
|
164
|
+
const RE_CLASS_SEQ = new RegExp(String.raw`^\[${BODY}\]\+\[${BODY}\]\+$`)
|
|
165
|
+
const RE_SHORTHAND = /^\\([sdw])[+*]$/
|
|
166
|
+
const RE_KEYWORD = new RegExp(String.raw`^([A-Za-z0-9_]+)\(\?!\[${BODY}\]\)$`)
|
|
167
|
+
const RE_LINE = new RegExp(String.raw`^([^\\\[\]()*+?.|^$]+)\[\^${BODY}\]\*$`)
|
|
168
|
+
const RE_QUOTED = /^(['"])\(\?:\1\1\|\[\^\1\]\)\*\1$/
|
|
169
|
+
// -? (?:0|[1-9]\d*) or \d+ (?:\.\d+)? (?:[eE][+-]?\d+)? — `[0-9]` is read as `\d`
|
|
170
|
+
const RE_NUMBER = /^(-\?)?(?:(\(\?:0\|\[1-9\]\\d\*\))|\\d\+)(\(\?:\\\.\\d\+\)\?)?(\(\?:\[eE\]\[\+-\]\?\\d\+\)\?)?$/
|
|
171
|
+
// "(?:[^"\<ctrl>?]|\(?:[<escapes>](|u[0-9a-fA-F]{4})?))*"
|
|
172
|
+
const RE_STRING_ESC = new RegExp(String.raw`^(["'])\(\?:\[\^\1\\\\(\\u0000-\\u001f|\\x00-\\x1f)?\]\|\\\\\(\?:\[${BODY}\](\|u\[0-9a-fA-F\]\{4\})?\)\)\*\1$`)
|
|
173
|
+
// "(?:[^"\]|\.)*"
|
|
174
|
+
const RE_STRING_ANY = new RegExp(String.raw`^(["'])\(\?:\[\^\1\\\\\]\|\\\\\.\)\*\1$`)
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {string} source The token's `pattern`.
|
|
178
|
+
* @param {boolean} ignoreCase
|
|
179
|
+
* @returns {Scanner | null}
|
|
180
|
+
*/
|
|
181
|
+
export function buildScanner (source, ignoreCase) {
|
|
182
|
+
let m = RE_KEYWORD.exec(source)
|
|
183
|
+
if (m) {
|
|
184
|
+
const look = parseClass(m[2])
|
|
185
|
+
if (look === null) return null
|
|
186
|
+
const word = m[1]
|
|
187
|
+
const lower = []
|
|
188
|
+
const upper = []
|
|
189
|
+
for (const ch of word) {
|
|
190
|
+
lower.push((ignoreCase ? ch.toLowerCase() : ch).charCodeAt(0))
|
|
191
|
+
upper.push((ignoreCase ? ch.toUpperCase() : ch).charCodeAt(0))
|
|
192
|
+
}
|
|
193
|
+
return make(KEYWORD, { look, lower, upper })
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Everything below is case-sensitive only: a case-folded class would need its
|
|
197
|
+
// folding reproduced, and no pattern in the wild has asked for it yet.
|
|
198
|
+
if (ignoreCase) return null
|
|
199
|
+
|
|
200
|
+
m = RE_STRING_ESC.exec(source)
|
|
201
|
+
if (m) {
|
|
202
|
+
const esc = parseClass(m[3])
|
|
203
|
+
if (esc === null) return null
|
|
204
|
+
return make(STRING, { quote: m[1].charCodeAt(0), ctrl: m[2] !== undefined, uni: m[4] !== undefined, esc })
|
|
205
|
+
}
|
|
206
|
+
m = RE_STRING_ANY.exec(source)
|
|
207
|
+
if (m) return make(STRING, { quote: m[1].charCodeAt(0), escAny: true })
|
|
208
|
+
|
|
209
|
+
// A number needs at least one feature beyond a bare digit run, which CLASS already does.
|
|
210
|
+
m = RE_NUMBER.exec(source.replace(/\[0-9\]/g, '\\d'))
|
|
211
|
+
if (m && (m[1] || m[2] || m[3] || m[4])) {
|
|
212
|
+
return make(NUMBER, { sign: m[1] !== undefined, jsonInt: m[2] !== undefined, frac: m[3] !== undefined, exp: m[4] !== undefined })
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
m = RE_CLASS_RUN.exec(source)
|
|
216
|
+
if (m) {
|
|
217
|
+
const table = parseClass(m[1])
|
|
218
|
+
return table && make(CLASS, { first: table, rest: table })
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
m = RE_CLASS_PAIR.exec(source)
|
|
222
|
+
if (m) {
|
|
223
|
+
const first = parseClass(m[1])
|
|
224
|
+
const rest = parseClass(m[2])
|
|
225
|
+
return first && rest && make(CLASS, { first, rest })
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
m = RE_CLASS_SEQ.exec(source)
|
|
229
|
+
if (m) {
|
|
230
|
+
const first = parseClass(m[1])
|
|
231
|
+
const second = parseClass(m[2])
|
|
232
|
+
// Disjoint classes mean the first run never has to give anything back to
|
|
233
|
+
// the second, so greedy scanning agrees with the regex's backtracking.
|
|
234
|
+
if (first && second && first.every((v, i) => !(v && second[i]))) {
|
|
235
|
+
return make(SEQ2, { first, rest: first, second })
|
|
236
|
+
}
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
m = RE_SHORTHAND.exec(source)
|
|
241
|
+
if (m) {
|
|
242
|
+
const table = new Uint8Array(128)
|
|
243
|
+
SHORTHAND[m[1]](table)
|
|
244
|
+
return make(CLASS, { first: table, rest: table, wide: m[1] === 's' })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
m = RE_QUOTED.exec(source)
|
|
248
|
+
if (m) return make(QUOTED, { quote: m[1].charCodeAt(0) })
|
|
249
|
+
|
|
250
|
+
m = RE_LINE.exec(source)
|
|
251
|
+
if (m) {
|
|
252
|
+
const stop = parseClass(m[2])
|
|
253
|
+
return stop && make(LINE, { prefix: m[1], look: stop })
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const skipDigits = (text, i, length) => {
|
|
260
|
+
while (i < length) {
|
|
261
|
+
const c = text.charCodeAt(i)
|
|
262
|
+
if (c < 48 || c > 57) break
|
|
263
|
+
i++
|
|
264
|
+
}
|
|
265
|
+
return i
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const isHex = (c) => (c >= 48 && c <= 57) || (c >= 65 && c <= 70) || (c >= 97 && c <= 102)
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Where the pattern's sticky match at `offset` ends, -1 for none, or `DEFER`.
|
|
272
|
+
* A zero-length match is reported as -1, as the lexer treats it that way.
|
|
273
|
+
*
|
|
274
|
+
* @param {Scanner} sc
|
|
275
|
+
* @param {string} text
|
|
276
|
+
* @param {number} offset Always below `length`.
|
|
277
|
+
* @param {number} length
|
|
278
|
+
* @returns {number}
|
|
279
|
+
*/
|
|
280
|
+
export function scanEnd (sc, text, offset, length) {
|
|
281
|
+
switch (sc.kind) {
|
|
282
|
+
case CLASS: {
|
|
283
|
+
const code = text.charCodeAt(offset)
|
|
284
|
+
if (code >= 128) return sc.wide ? DEFER : -1
|
|
285
|
+
if (sc.first[code] !== 1) return -1
|
|
286
|
+
const rest = sc.rest
|
|
287
|
+
let j = offset + 1
|
|
288
|
+
while (j < length) {
|
|
289
|
+
const c = text.charCodeAt(j)
|
|
290
|
+
if (c >= 128) return sc.wide ? DEFER : j
|
|
291
|
+
if (rest[c] !== 1) break
|
|
292
|
+
j++
|
|
293
|
+
}
|
|
294
|
+
return j
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
case SEQ2: {
|
|
298
|
+
const c0 = text.charCodeAt(offset)
|
|
299
|
+
if (c0 >= 128 || sc.first[c0] !== 1) return -1
|
|
300
|
+
const { first, second } = sc
|
|
301
|
+
let j = offset + 1
|
|
302
|
+
while (j < length) {
|
|
303
|
+
const c = text.charCodeAt(j)
|
|
304
|
+
if (c < 128 && first[c] === 1) j++
|
|
305
|
+
else break
|
|
306
|
+
}
|
|
307
|
+
const mid = j
|
|
308
|
+
while (j < length) {
|
|
309
|
+
const c = text.charCodeAt(j)
|
|
310
|
+
if (c < 128 && second[c] === 1) j++
|
|
311
|
+
else break
|
|
312
|
+
}
|
|
313
|
+
return j > mid ? j : -1
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case KEYWORD: {
|
|
317
|
+
const { lower, upper } = sc
|
|
318
|
+
const n = lower.length
|
|
319
|
+
if (offset + n > length) return -1
|
|
320
|
+
for (let i = 0; i < n; i++) {
|
|
321
|
+
const c = text.charCodeAt(offset + i)
|
|
322
|
+
if (c !== lower[i] && c !== upper[i]) return -1
|
|
323
|
+
}
|
|
324
|
+
const end = offset + n
|
|
325
|
+
if (end < length) {
|
|
326
|
+
const c = text.charCodeAt(end)
|
|
327
|
+
if (c < 128 && sc.look[c] === 1) return -1
|
|
328
|
+
}
|
|
329
|
+
return end
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
case LINE: {
|
|
333
|
+
if (!text.startsWith(sc.prefix, offset)) return -1
|
|
334
|
+
let j = offset + sc.prefix.length
|
|
335
|
+
const stop = sc.look
|
|
336
|
+
while (j < length) {
|
|
337
|
+
const c = text.charCodeAt(j)
|
|
338
|
+
if (c < 128 && stop[c] === 1) break
|
|
339
|
+
j++
|
|
340
|
+
}
|
|
341
|
+
return j
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
case NUMBER: {
|
|
345
|
+
let j = offset
|
|
346
|
+
if (sc.sign && text.charCodeAt(j) === 45) j++
|
|
347
|
+
const c = text.charCodeAt(j)
|
|
348
|
+
if (sc.jsonInt) {
|
|
349
|
+
// `0` stands alone: "01" is the number 0 followed by 1.
|
|
350
|
+
if (c === 48) j++
|
|
351
|
+
else if (c >= 49 && c <= 57) j = skipDigits(text, j + 1, length)
|
|
352
|
+
else return -1
|
|
353
|
+
} else {
|
|
354
|
+
if (!(c >= 48 && c <= 57)) return -1
|
|
355
|
+
j = skipDigits(text, j + 1, length)
|
|
356
|
+
}
|
|
357
|
+
// A fraction or exponent counts only if it is complete: "1." is 1 then ".".
|
|
358
|
+
if (sc.frac && text.charCodeAt(j) === 46) {
|
|
359
|
+
const k = skipDigits(text, j + 1, length)
|
|
360
|
+
if (k > j + 1) j = k
|
|
361
|
+
}
|
|
362
|
+
if (sc.exp) {
|
|
363
|
+
const e = text.charCodeAt(j)
|
|
364
|
+
if (e === 101 || e === 69) {
|
|
365
|
+
let k = j + 1
|
|
366
|
+
const s = text.charCodeAt(k)
|
|
367
|
+
if (s === 43 || s === 45) k++
|
|
368
|
+
const end = skipDigits(text, k, length)
|
|
369
|
+
if (end > k) j = end
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return j
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
case STRING: {
|
|
376
|
+
const q = sc.quote
|
|
377
|
+
if (text.charCodeAt(offset) !== q) return -1
|
|
378
|
+
let j = offset + 1
|
|
379
|
+
for (;;) {
|
|
380
|
+
if (j >= length) return -1
|
|
381
|
+
const c = text.charCodeAt(j)
|
|
382
|
+
if (c === q) return j + 1
|
|
383
|
+
if (c === 92) {
|
|
384
|
+
const d = text.charCodeAt(j + 1)
|
|
385
|
+
if (sc.escAny) {
|
|
386
|
+
// `\\.`: any character but a line terminator (or the end of the text).
|
|
387
|
+
if (!(d >= 0) || d === 10 || d === 13 || d === 0x2028 || d === 0x2029) return -1
|
|
388
|
+
j += 2
|
|
389
|
+
} else if (d < 128 && sc.esc[d] === 1) {
|
|
390
|
+
j += 2
|
|
391
|
+
} else if (sc.uni && d === 117 && isHex(text.charCodeAt(j + 2)) && isHex(text.charCodeAt(j + 3)) &&
|
|
392
|
+
isHex(text.charCodeAt(j + 4)) && isHex(text.charCodeAt(j + 5))) {
|
|
393
|
+
j += 6
|
|
394
|
+
} else {
|
|
395
|
+
return -1
|
|
396
|
+
}
|
|
397
|
+
continue
|
|
398
|
+
}
|
|
399
|
+
if (sc.ctrl && c < 32) return -1
|
|
400
|
+
j++
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
default: { // QUOTED
|
|
405
|
+
const q = sc.quote
|
|
406
|
+
if (text.charCodeAt(offset) !== q) return -1
|
|
407
|
+
const quote = String.fromCharCode(q)
|
|
408
|
+
// The regex prefers to read `''` as an escaped quote, and only when that
|
|
409
|
+
// leaves no closing quote does it backtrack to the *latest* pair and
|
|
410
|
+
// close on its first quote instead.
|
|
411
|
+
let fallback = -1
|
|
412
|
+
let j = offset + 1
|
|
413
|
+
for (;;) {
|
|
414
|
+
j = text.indexOf(quote, j)
|
|
415
|
+
if (j === -1) return fallback
|
|
416
|
+
if (text.charCodeAt(j + 1) !== q) return j + 1
|
|
417
|
+
fallback = j + 1
|
|
418
|
+
j += 2
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
package/src/state.js
CHANGED
|
@@ -27,14 +27,25 @@ export const FAIL = Symbol('grammatik.fail')
|
|
|
27
27
|
* @property {boolean} track Whether to record error-reporting detail.
|
|
28
28
|
* @property {number} steps Combinator invocations, for the runaway guard.
|
|
29
29
|
* @property {number} maxSteps 0 disables the guard.
|
|
30
|
+
* @property {{ ends: Int32Array, vals: any[] }[]|null} rm Per-rule memo tables for the
|
|
31
|
+
* rules chosen by `selectMemoRules`, indexed by slot. `ends[i]` is the cursor
|
|
32
|
+
* after the rule ran at token `i`, plus one (0 means "not seen yet"); `vals[i]`
|
|
33
|
+
* is what it returned.
|
|
30
34
|
*/
|
|
31
35
|
|
|
32
36
|
/**
|
|
33
37
|
* @param {import('./lexer.js').Token[]} tokens
|
|
34
38
|
* @param {Record<string, any>} grammar
|
|
35
|
-
* @param {{ memo?: boolean, maxSteps?: number, track?: boolean }} [options]
|
|
39
|
+
* @param {{ memo?: boolean, maxSteps?: number, track?: boolean, memoSlots?: number }} [options]
|
|
36
40
|
* @returns {ParserState}
|
|
37
41
|
*/
|
|
42
|
+
const memoTables = (slots, length) => {
|
|
43
|
+
if (!slots) return null
|
|
44
|
+
const tables = new Array(slots)
|
|
45
|
+
for (let i = 0; i < slots; i++) tables[i] = { ends: new Int32Array(length + 1), vals: new Array(length + 1) }
|
|
46
|
+
return tables
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
export const createState = (tokens, grammar, options = {}) => ({
|
|
39
50
|
tokens,
|
|
40
51
|
idx: 0,
|
|
@@ -42,6 +53,7 @@ export const createState = (tokens, grammar, options = {}) => ({
|
|
|
42
53
|
expected: [],
|
|
43
54
|
grammar,
|
|
44
55
|
memo: options.memo ? new Map() : null,
|
|
56
|
+
rm: memoTables(options.memoSlots, tokens.length),
|
|
45
57
|
// Entering a rule is one array store rather than an object allocation, and
|
|
46
58
|
// the stack is snapshotted only on a furthest failure — by the time an error
|
|
47
59
|
// is built it would otherwise have unwound to just the start rule.
|
|
@@ -79,6 +91,26 @@ export function expect (state, expectation) {
|
|
|
79
91
|
if (!state.expected.includes(expectation)) state.expected.push(expectation)
|
|
80
92
|
}
|
|
81
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Thrown when no token matches. It lives here, beside `ParseError`, rather than in
|
|
96
|
+
* the lexer: a parser whose lexer was generated as source needs this class and
|
|
97
|
+
* nothing else from the lexer module, and importing it from there would pull the
|
|
98
|
+
* whole lexer engine back into the bundle.
|
|
99
|
+
*/
|
|
100
|
+
export class LexError extends Error {
|
|
101
|
+
/**
|
|
102
|
+
* @param {string} message
|
|
103
|
+
* @param {{ offset: number, line: number, col: number }} position
|
|
104
|
+
*/
|
|
105
|
+
constructor (message, position) {
|
|
106
|
+
super(message)
|
|
107
|
+
this.name = 'LexError'
|
|
108
|
+
this.offset = position.offset
|
|
109
|
+
this.line = position.line
|
|
110
|
+
this.col = position.col
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
82
114
|
export class ParseError extends Error {
|
|
83
115
|
/**
|
|
84
116
|
* @param {string} message
|