parseman 0.61.5 → 0.61.7
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 +109 -29
- package/dist/analysis/diagnostics.js.map +2 -2
- package/dist/cli/index.js +109 -29
- package/dist/cli/index.js.map +2 -2
- package/dist/combinators/expect.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts +3 -1
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +14 -10
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/index.js +166 -45
- package/dist/index.js.map +2 -2
- package/dist/plugin/entry-supercompiler.d.ts +15 -0
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -1
- package/dist/plugin/index.js +256 -46
- package/dist/plugin/index.js.map +2 -2
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/hold-gap.d.ts +3 -1
- package/dist/table/hold-gap.d.ts.map +1 -1
- package/dist/table/index.js +166 -45
- package/dist/table/index.js.map +2 -2
- package/dist/table/select-hot.d.ts +9 -0
- package/dist/table/select-hot.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/combinators/expect.ts +3 -2
- package/src/combinators/first-set.ts +14 -7
- package/src/combinators/sequence.ts +1 -1
- package/src/plugin/entry-supercompiler.ts +110 -1
- package/src/table/assemble.ts +38 -0
- package/src/table/emit-assembly.ts +42 -5
- package/src/table/encode.ts +1 -1
- package/src/table/hold-gap.ts +22 -13
- package/src/table/select-hot.ts +96 -24
- package/src/version.ts +1 -1
|
@@ -8,6 +8,15 @@ export declare function selectHotSites(code: readonly number[], entries: readonl
|
|
|
8
8
|
/** Measured calls per site (`countSiteVisits` over a corpus). Replaces the
|
|
9
9
|
* static estimate when given. */
|
|
10
10
|
profile?: ArrayLike<number>): Set<number>;
|
|
11
|
+
/**
|
|
12
|
+
* The sites the function for `ip` calls, as `emit-assembly.ts` lowers it. An `OP_CONT`
|
|
13
|
+
* with a body is a late frame, lowered with its wrapper in one function, calling its
|
|
14
|
+
* corner and its tail. With no body, the emitter can't decide there and it's an alias
|
|
15
|
+
* to its wrapper, as `OP_SEED` is to its corner. A gap gate has no body either, so its
|
|
16
|
+
* item counts as the repeat's own child, and adding the row moves no other site's
|
|
17
|
+
* estimate (`gap-gate.ts`).
|
|
18
|
+
*/
|
|
19
|
+
export declare function edgesAsLowered(code: readonly number[], siteBytes: ReadonlyMap<number, number>, ip: number, out: number[]): void;
|
|
11
20
|
/**
|
|
12
21
|
* The largest prefix of the ranking whose real source fits `ceiling`. `siteBytes`
|
|
13
22
|
* leaves out what sites add around their bodies, so the estimate can overshoot:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select-hot.d.ts","sourceRoot":"","sources":["../../src/table/select-hot.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"select-hot.d.ts","sourceRoot":"","sources":["../../src/table/select-hot.ts"],"names":[],"mappings":"AAqCA;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,MAAM,EAAE,MAAM;AACd;iCACiC;AACjC,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,GAC1B,GAAG,CAAC,MAAM,CAAC,CAUb;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAe/H;AAqDD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC;AACtC,2DAA2D;AAC3D,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,MAAM,EACnD,OAAO,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,GAC1B;IAAE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAQ7E"}
|
package/dist/version.d.ts
CHANGED
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
* Kept in sync with `package.json` by `test/unit/version-sync.test.ts`, which fails
|
|
14
14
|
* the build if this constant drifts from the published version.
|
|
15
15
|
*/
|
|
16
|
-
export declare const PARSEMAN_VERSION = "0.61.
|
|
16
|
+
export declare const PARSEMAN_VERSION = "0.61.7";
|
|
17
17
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Combinator, ParseContext, ParseResult, ParserMeta, ParseError } from '../types.ts'
|
|
2
2
|
import { captureError } from '../recovery/scan.ts'
|
|
3
3
|
import { annotateSpanFromLineContext } from '../line-index.ts'
|
|
4
|
-
import { matchesEmpty, isZeroWidthAssertion } from './first-set.ts'
|
|
4
|
+
import { any, matchesEmpty, isZeroWidthAssertion } from './first-set.ts'
|
|
5
5
|
|
|
6
6
|
export type { ParseError }
|
|
7
7
|
export { assertionFailureExpected, directTerminalFailureExpected } from './expected.ts'
|
|
@@ -133,7 +133,8 @@ function derive(c: Combinator<unknown>, seen: Set<Combinator<unknown>>): string[
|
|
|
133
133
|
export function expect<T>(combinator: Combinator<T>, label?: string): Combinator<T | ParseError> {
|
|
134
134
|
const expected = label !== undefined ? [label] : deriveExpected(combinator)
|
|
135
135
|
const meta: ParserMeta = {
|
|
136
|
-
|
|
136
|
+
// It runs on any character: on one its child can't start, it reports the error.
|
|
137
|
+
firstSet: any(),
|
|
137
138
|
canMatchNewline: combinator._meta.canMatchNewline,
|
|
138
139
|
isTrivia: false,
|
|
139
140
|
}
|
|
@@ -202,7 +202,8 @@ function matchesEmptyBody(
|
|
|
202
202
|
case 'many':
|
|
203
203
|
case 'optional':
|
|
204
204
|
case 'not':
|
|
205
|
-
case 'peek':
|
|
205
|
+
case 'peek':
|
|
206
|
+
case 'expect': return true // zero repetitions / absent / lookahead / recovered
|
|
206
207
|
// Default `sepBy` is `(item (sep item)*)?` — it MATCHES THE EMPTY STRING.
|
|
207
208
|
// Any `min >= 1` requires that many ITEMS, so it is nullable only when the
|
|
208
209
|
// item is. (Keying this off `min === 1` reported every `{ min: 2 }` list as
|
|
@@ -220,7 +221,6 @@ function matchesEmptyBody(
|
|
|
220
221
|
case 'trivia':
|
|
221
222
|
case 'token':
|
|
222
223
|
case 'leaf':
|
|
223
|
-
case 'expect':
|
|
224
224
|
case 'withCtx':
|
|
225
225
|
case 'node':
|
|
226
226
|
case 'grammar':
|
|
@@ -382,6 +382,8 @@ export function sequenceFirstSet(parsers: readonly Combinator<unknown>[]): First
|
|
|
382
382
|
* repeat item's, a gap's and a node's gate read a pad through its non-empty
|
|
383
383
|
* matches.
|
|
384
384
|
*
|
|
385
|
+
* `follow: true` asks where a resync sentinel stops, which reads through `expect`.
|
|
386
|
+
*
|
|
385
387
|
* `activeTrivia` is the live trivia, `null` for a scope known to have none (where
|
|
386
388
|
* a reference installs its rule's trivia, `triviaThroughRef`), and `undefined`
|
|
387
389
|
* where it is not known.
|
|
@@ -396,6 +398,7 @@ export function firstSetOf(
|
|
|
396
398
|
resolve?: RefResolver,
|
|
397
399
|
activeTrivia?: Combinator<unknown> | null,
|
|
398
400
|
consuming = true,
|
|
401
|
+
follow = false,
|
|
399
402
|
): FirstSet {
|
|
400
403
|
// `seen` is the current recursion path, not a global visited set. A completed
|
|
401
404
|
// shared child must be analysed again for a sibling; only a back-edge on this
|
|
@@ -403,7 +406,7 @@ export function firstSetOf(
|
|
|
403
406
|
if (seen.has(p)) return any() // cycle → any (safe over-approximation)
|
|
404
407
|
seen.add(p)
|
|
405
408
|
try {
|
|
406
|
-
return firstSetBody(p, seen, resolve, activeTrivia, consuming)
|
|
409
|
+
return firstSetBody(p, seen, resolve, activeTrivia, consuming, follow)
|
|
407
410
|
} finally {
|
|
408
411
|
seen.delete(p)
|
|
409
412
|
}
|
|
@@ -415,8 +418,9 @@ function firstSetBody(
|
|
|
415
418
|
resolve: RefResolver | undefined,
|
|
416
419
|
activeTrivia: Combinator<unknown> | null | undefined,
|
|
417
420
|
consuming: boolean,
|
|
421
|
+
follow: boolean,
|
|
418
422
|
): FirstSet {
|
|
419
|
-
const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve, activeTrivia, consuming)
|
|
423
|
+
const fs = (c: Combinator<unknown>): FirstSet => firstSetOf(c, seen, resolve, activeTrivia, consuming, follow)
|
|
420
424
|
const empties = (c: Combinator<unknown>): boolean => matchesEmpty(c, new Set(), resolve)
|
|
421
425
|
const d = p._def as ParserDef
|
|
422
426
|
switch (d.tag) {
|
|
@@ -437,7 +441,7 @@ function firstSetBody(
|
|
|
437
441
|
// install; the interpreter's own callers (`sequence.ts` follow sentinels)
|
|
438
442
|
// read a reference as 0.58.0 did (#223).
|
|
439
443
|
const through = activeTrivia === null && resolve !== undefined
|
|
440
|
-
return firstSetOf(target, seen, resolve, through ? triviaThroughRef(p, target, null) : activeTrivia, consuming)
|
|
444
|
+
return firstSetOf(target, seen, resolve, through ? triviaThroughRef(p, target, null) : activeTrivia, consuming, follow)
|
|
441
445
|
}
|
|
442
446
|
case 'choice': {
|
|
443
447
|
let out: FirstSet = empty()
|
|
@@ -457,6 +461,9 @@ function firstSetBody(
|
|
|
457
461
|
// original cursor.
|
|
458
462
|
return sequenceSet(d.parsers, fs, empties, activeTrivia, consuming)
|
|
459
463
|
}
|
|
464
|
+
// Runs on any character: on one its child can't start, it reports the error. A
|
|
465
|
+
// resync sentinel still stops where the child could start.
|
|
466
|
+
case 'expect': return follow ? fs(d.parser) : any()
|
|
460
467
|
case 'peek': {
|
|
461
468
|
// Deep-resolve the body: a `ref()` reads `any()` at CONSTRUCTION, so the
|
|
462
469
|
// shallow `_meta.firstSet` baked into the assertion would lose the gate.
|
|
@@ -472,19 +479,19 @@ function firstSetBody(
|
|
|
472
479
|
case 'leaf':
|
|
473
480
|
case 'node':
|
|
474
481
|
case 'sepBy':
|
|
475
|
-
case 'expect':
|
|
476
482
|
// A transaction accepts exactly what its inner accepts. Its own `_meta` is
|
|
477
483
|
// the inner's construction-time set, which reads `any` when the inner opens
|
|
478
484
|
// with a rule reference — resolve through it like any other wrapper.
|
|
479
485
|
case 'attempt': return fs(d.parser)
|
|
480
486
|
// A token runs its body with trivia cleared (`token.ts`).
|
|
481
|
-
case 'token': return firstSetOf(d.parser, seen, resolve, activeTrivia === undefined ? undefined : null, consuming)
|
|
487
|
+
case 'token': return firstSetOf(d.parser, seen, resolve, activeTrivia === undefined ? undefined : null, consuming, follow)
|
|
482
488
|
case 'grammar': return firstSetOf(
|
|
483
489
|
d.parser,
|
|
484
490
|
seen,
|
|
485
491
|
resolve,
|
|
486
492
|
d.clearTrivia ? null : (d.triviaParser ?? activeTrivia),
|
|
487
493
|
consuming,
|
|
494
|
+
follow,
|
|
488
495
|
)
|
|
489
496
|
// A run starts with its first line's prefix; a nullable prefix starts with anything.
|
|
490
497
|
case 'lines': {
|
|
@@ -108,7 +108,7 @@ export function sequence<T extends [Combinator<unknown>, ...Combinator<unknown>[
|
|
|
108
108
|
if (tolerant) {
|
|
109
109
|
followSentinels ??= parsers.map((_, i) => firstSetSentinel(
|
|
110
110
|
parsers.slice(i + 1).reduce<ReturnType<typeof firstSetOf>>(
|
|
111
|
-
(set, parser) => union(set, firstSetOf(parser, new Set(), undefined, undefined, false)), { kind: 'empty' },
|
|
111
|
+
(set, parser) => union(set, firstSetOf(parser, new Set(), undefined, undefined, false, true)), { kind: 'empty' },
|
|
112
112
|
),
|
|
113
113
|
))
|
|
114
114
|
}
|
|
@@ -65,6 +65,8 @@ type Call = {
|
|
|
65
65
|
type Edit = { readonly start: number; readonly end: number; readonly text: string }
|
|
66
66
|
|
|
67
67
|
const PIECE = /^_pf(\d+)$/
|
|
68
|
+
const MERGEABLE = /^_(?:pf|sk)\d+$/
|
|
69
|
+
const TEMP = /^_t\d+_/
|
|
68
70
|
const WRAP = 'function __parseman_supercompile__(){\n'
|
|
69
71
|
const SUFFIX = '\n}'
|
|
70
72
|
|
|
@@ -225,6 +227,113 @@ export function supercompileEntryAssembly(
|
|
|
225
227
|
return supercompileEmittedAssembly(prog, cfg, extraIps, emitted)
|
|
226
228
|
}
|
|
227
229
|
|
|
230
|
+
/** A name in the source: a site, or a temporary (`tmp` in `emit-assembly.ts`). */
|
|
231
|
+
type NameUse = { readonly start: number; readonly end: number; readonly name: string }
|
|
232
|
+
|
|
233
|
+
/** The uses of site and temporary names under `root`, and the names bound there. */
|
|
234
|
+
function namesUnder(root: Ast, uses: NameUse[], bound: Set<string>): boolean {
|
|
235
|
+
let plain = true
|
|
236
|
+
const bind = (id: Ast | undefined): void => {
|
|
237
|
+
const name = nameOf(id)
|
|
238
|
+
if (name !== undefined) bound.add(name)
|
|
239
|
+
}
|
|
240
|
+
const walk = (node: Ast, parent: Ast | undefined): void => {
|
|
241
|
+
if (node.type === 'VariableDeclarator') bind(ast(node.id))
|
|
242
|
+
else if (node.type === 'LabeledStatement') bind(ast(node.label))
|
|
243
|
+
else if (isFunction(node)) {
|
|
244
|
+
if (node !== root) bind(ast(node.id))
|
|
245
|
+
nodes(node.params).forEach(bind)
|
|
246
|
+
}
|
|
247
|
+
const name = nameOf(node)
|
|
248
|
+
if (name !== undefined && (MERGEABLE.test(name) || TEMP.test(name))) {
|
|
249
|
+
const isKey = parent?.type === 'Property' && parent.key === node && parent.computed !== true
|
|
250
|
+
const isProperty = parent?.type === 'MemberExpression' && parent.property === node && parent.computed !== true
|
|
251
|
+
if (parent?.type === 'Property' && parent.shorthand === true) plain = false
|
|
252
|
+
if (!isKey && !isProperty) uses.push({ ...range(node), name })
|
|
253
|
+
}
|
|
254
|
+
for (const child of children(node)) walk(child, node)
|
|
255
|
+
}
|
|
256
|
+
walk(root, undefined)
|
|
257
|
+
return plain
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Functions that read the same are one function: `optional(x)` written in 25 places
|
|
262
|
+
* is 25 rows and one body, and a trivia skip can be pooled under two labels that
|
|
263
|
+
* lower alike. This runs after the join, so a site with one caller has
|
|
264
|
+
* already gone into it, the selection is untouched and every call a parse makes is
|
|
265
|
+
* still made. Each pass can make the callers of a merged function read alike, so it
|
|
266
|
+
* repeats until nothing merges.
|
|
267
|
+
*
|
|
268
|
+
* Only identifiers are compared or renamed: a grammar's own text reaches this source
|
|
269
|
+
* as strings and regexes, and they count as written. A temporary is local by the
|
|
270
|
+
* emitter's contract and is numbered per assembly, so one this function binds is
|
|
271
|
+
* compared in order of use. A site name that's also bound somewhere, or abbreviated
|
|
272
|
+
* in `{_pf1}`, can't be renamed by name, so that source stays as it is.
|
|
273
|
+
*/
|
|
274
|
+
export function mergeSameBodies(source: string): string {
|
|
275
|
+
const parsed = parseSync('parseman-merge-same-bodies.js', WRAP + source + SUFFIX)
|
|
276
|
+
if (parsed.errors.length > 0) return source
|
|
277
|
+
const statements = nodes(ast(nodes((parsed.program as unknown as Ast).body)[0]?.body)?.body)
|
|
278
|
+
type Body = NameUse & { readonly uses: readonly NameUse[]; readonly local: ReadonlySet<string> }
|
|
279
|
+
const bodies: Body[] = []
|
|
280
|
+
const others: NameUse[] = []
|
|
281
|
+
const declared = new Set<string>()
|
|
282
|
+
for (const statement of statements) {
|
|
283
|
+
const name = statement.type === 'FunctionDeclaration' ? nameOf(ast(statement.id)) : undefined
|
|
284
|
+
const uses: NameUse[] = []
|
|
285
|
+
const bound = new Set<string>()
|
|
286
|
+
if (!namesUnder(statement, uses, bound)) return source
|
|
287
|
+
for (const local of bound) declared.add(local)
|
|
288
|
+
if (name === undefined || !MERGEABLE.test(name)) others.push(...uses)
|
|
289
|
+
else bodies.push({ ...range(statement), name, uses: uses.sort((a, b) => a.start - b.start), local: bound })
|
|
290
|
+
}
|
|
291
|
+
for (const local of declared) if (MERGEABLE.test(local)) return source
|
|
292
|
+
|
|
293
|
+
const merged = new Map<string, string>()
|
|
294
|
+
const current = (name: string): string => {
|
|
295
|
+
let at = name
|
|
296
|
+
for (let next = merged.get(at); next !== undefined; next = merged.get(at)) at = next
|
|
297
|
+
return at
|
|
298
|
+
}
|
|
299
|
+
let live = bodies
|
|
300
|
+
for (let more = true; more;) {
|
|
301
|
+
more = false
|
|
302
|
+
const first = new Map<string, string>()
|
|
303
|
+
live = live.filter(body => {
|
|
304
|
+
const temps = new Map<string, number>()
|
|
305
|
+
const edits = body.uses.map((use): Edit => {
|
|
306
|
+
if (MERGEABLE.test(use.name)) {
|
|
307
|
+
const site = current(use.name)
|
|
308
|
+
return { ...use, text: site === body.name ? '#self' : site }
|
|
309
|
+
}
|
|
310
|
+
if (!body.local.has(use.name)) return { ...use, text: use.name }
|
|
311
|
+
const seen = temps.get(use.name) ?? temps.size
|
|
312
|
+
temps.set(use.name, seen)
|
|
313
|
+
return { ...use, text: `#t${seen}` }
|
|
314
|
+
})
|
|
315
|
+
const key = render(source, body.start, body.end, edits)
|
|
316
|
+
const kept = first.get(key)
|
|
317
|
+
if (kept === undefined) { first.set(key, body.name); return true }
|
|
318
|
+
merged.set(body.name, kept)
|
|
319
|
+
more = true
|
|
320
|
+
return false
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
if (merged.size === 0) return source
|
|
324
|
+
|
|
325
|
+
const edits: Edit[] = []
|
|
326
|
+
const rename = (use: NameUse): void => {
|
|
327
|
+
if (MERGEABLE.test(use.name) && merged.has(use.name)) edits.push({ ...use, text: current(use.name) })
|
|
328
|
+
}
|
|
329
|
+
others.forEach(rename)
|
|
330
|
+
for (const body of bodies) {
|
|
331
|
+
if (merged.has(body.name)) edits.push({ start: body.start, end: body.end, text: '' })
|
|
332
|
+
else body.uses.forEach(rename)
|
|
333
|
+
}
|
|
334
|
+
return render(source, 0, source.length, edits)
|
|
335
|
+
}
|
|
336
|
+
|
|
228
337
|
/**
|
|
229
338
|
* Plugin-only final splice for an ordinary macro replacement. The table
|
|
230
339
|
* compiler emits its canonical callback-free public artifact first; this pass
|
|
@@ -274,7 +383,7 @@ export function supercompileRuleMapReplacement(
|
|
|
274
383
|
const start = body.start - prefix.length + 1
|
|
275
384
|
const end = body.end - prefix.length - 1
|
|
276
385
|
if (replacement.slice(start, end) !== FACTORY_ARITY_GUARD + canonical.source + '\n') return replacement
|
|
277
|
-
const transformed = replacement.slice(0, start) + FACTORY_ARITY_GUARD + candidate.source + '\n' + replacement.slice(end)
|
|
386
|
+
const transformed = replacement.slice(0, start) + FACTORY_ARITY_GUARD + mergeSameBodies(candidate.source) + '\n' + replacement.slice(end)
|
|
278
387
|
const checked = parseSync('parseman-entry-supercompile-replacement-result.js', prefix + transformed)
|
|
279
388
|
return checked.errors.length === 0 ? transformed : replacement
|
|
280
389
|
}
|
package/src/table/assemble.ts
CHANGED
|
@@ -2844,8 +2844,46 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2844
2844
|
}
|
|
2845
2845
|
if (hasAdj) {
|
|
2846
2846
|
const first = linkFirst()
|
|
2847
|
+
// What a held gap needs undone if the next term recovers at zero width.
|
|
2848
|
+
// Written after the lookahead returns and read before the next term runs,
|
|
2849
|
+
// so nothing can re-enter this sequence in between.
|
|
2850
|
+
let heldFrom = 0, heldTl = 0, heldScanTl = 0, heldLog = 0, heldScanLog = 0, heldRoot = 0, heldScanRoot = 0
|
|
2847
2851
|
const runnerAt = (i: number): TermRunner => {
|
|
2848
2852
|
const kidIp = code[base + i]!
|
|
2853
|
+
if (i > 1 && holdsGap(code, k, base, n, i - 1, NULLABLE) && rowNullable(code, k, kidIp, NULLABLE)) {
|
|
2854
|
+
const kid = link(kidIp)
|
|
2855
|
+
return (input, cur, ctx) => {
|
|
2856
|
+
const from = heldFrom, tl = heldTl, scanTl = heldScanTl, log = heldLog
|
|
2857
|
+
const scanLog = heldScanLog, root = heldRoot, scanRoot = heldScanRoot
|
|
2858
|
+
const next = step(kid, input, cur, ctx)
|
|
2859
|
+
if (next !== cur || from === cur) return next
|
|
2860
|
+
rollbackScannedTriviaAt(ctx, tl, scanTl, log, scanLog, root, scanRoot)
|
|
2861
|
+
return from
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
if (holdsGap(code, k, base, n, i, NULLABLE) && rowNullable(code, k, code[base + i + 1]!, NULLABLE)) {
|
|
2865
|
+
const kid = link(kidIp)
|
|
2866
|
+
return (input, cur, ctx) => {
|
|
2867
|
+
if (ctx.trivia === undefined) {
|
|
2868
|
+
const v = kid(input, cur, ctx)
|
|
2869
|
+
if (v === FAIL) return -1
|
|
2870
|
+
TERMV = v
|
|
2871
|
+
heldFrom = cur
|
|
2872
|
+
return EC.e
|
|
2873
|
+
}
|
|
2874
|
+
const tl = cstTlLen(ctx), log = ctx._triviaLog?.length ?? 0, root = ctx._rootTriviaLog?.length ?? 0
|
|
2875
|
+
const scanEnd = skipTrivia(input, cur, ctx)
|
|
2876
|
+
const scanTl = cstTlLen(ctx), scanLog = ctx._triviaLog?.length ?? 0, scanRoot = ctx._rootTriviaLog?.length ?? 0
|
|
2877
|
+
const v = kid(input, scanEnd, ctx)
|
|
2878
|
+
if (v === FAIL) return -1
|
|
2879
|
+
TERMV = v
|
|
2880
|
+
heldFrom = cur
|
|
2881
|
+
heldTl = tl; heldScanTl = scanTl
|
|
2882
|
+
heldLog = log; heldScanLog = scanLog
|
|
2883
|
+
heldRoot = root; heldScanRoot = scanRoot
|
|
2884
|
+
return scanEnd
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2849
2887
|
if (holdsGap(code, k, base, n, i, NULLABLE)) {
|
|
2850
2888
|
const kid = link(kidIp)
|
|
2851
2889
|
return (input, cur, ctx) => {
|
|
@@ -467,16 +467,40 @@ ${scanned}
|
|
|
467
467
|
}`
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
const TRIVIA_LOG_LENGTH = 'ctx._cstBuf!==undefined?(ctx._cstBuf.tl!==undefined?ctx._cstBuf.tl.length:0):(ctx._cstTriviaLog!==undefined?ctx._cstTriviaLog.length:0)'
|
|
471
|
+
|
|
470
472
|
/**
|
|
471
473
|
* A lookahead term that HOLDS its gap (`hold-gap.ts`): the gap is scanned and
|
|
472
474
|
* kept, the lookahead runs at its end, and the cursor stays there for the next
|
|
473
|
-
* term, which cannot match empty
|
|
475
|
+
* term, which cannot match empty unless it `recovers`. Then the marks stay in
|
|
476
|
+
* scope for `emitHeldGapUndo`, which follows the next term.
|
|
474
477
|
*/
|
|
475
|
-
function emitHoldTerm(callee: string, dst: string, t: string, l: SiteLabel, skip: string): string {
|
|
478
|
+
function emitHoldTerm(callee: string, dst: string, t: string, l: SiteLabel, skip: string, recovers: boolean): string {
|
|
476
479
|
const fast = `const ${t}v=${callee}(input,cur,ctx)
|
|
477
480
|
if(${t}v===FAIL)return FAIL
|
|
478
481
|
${dst}=${t}v
|
|
479
482
|
cur=EC.e`
|
|
483
|
+
if (recovers) {
|
|
484
|
+
const marked = `let ${t}c=cur,${t}s=cur,${t}tl=0,${t}stl=0,${t}lg=0,${t}slg=0,${t}rt=0,${t}srt=0`
|
|
485
|
+
if (l.tri === TRI_NONE) return `${marked}\n${fast}`
|
|
486
|
+
const held = `${t}tl=${TRIVIA_LOG_LENGTH}
|
|
487
|
+
${t}lg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
|
|
488
|
+
${t}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0
|
|
489
|
+
${t}s=${skip}(input,cur,ctx)
|
|
490
|
+
${t}stl=${TRIVIA_LOG_LENGTH}
|
|
491
|
+
${t}slg=ctx._triviaLog!==undefined?ctx._triviaLog.length:0
|
|
492
|
+
${t}srt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0
|
|
493
|
+
const ${t}v=${callee}(input,${t}s,ctx)
|
|
494
|
+
if(${t}v===FAIL)return FAIL
|
|
495
|
+
${dst}=${t}v
|
|
496
|
+
cur=${t}s`
|
|
497
|
+
return l.tri !== TRI_UNKNOWN ? `${marked}\n${held}` : `${marked}
|
|
498
|
+
if(ctx.trivia===undefined){
|
|
499
|
+
${fast}
|
|
500
|
+
}else{
|
|
501
|
+
${held}
|
|
502
|
+
}`
|
|
503
|
+
}
|
|
480
504
|
if (l.tri === TRI_NONE) return fast
|
|
481
505
|
const held = `const ${t}s=${skip}(input,cur,ctx)
|
|
482
506
|
const ${t}v=${callee}(input,${t}s,ctx)
|
|
@@ -491,6 +515,11 @@ ${held}
|
|
|
491
515
|
}`
|
|
492
516
|
}
|
|
493
517
|
|
|
518
|
+
/** The next term recovered at zero width, so the gap held for it was never consumed. */
|
|
519
|
+
function emitHeldGapUndo(t: string): string {
|
|
520
|
+
return `if(cur===${t}s&&${t}s!==${t}c){rollbackScannedTriviaAt(ctx,${t}tl,${t}stl,${t}lg,${t}slg,${t}rt,${t}srt);cur=${t}c}`
|
|
521
|
+
}
|
|
522
|
+
|
|
494
523
|
/**
|
|
495
524
|
* The repetition's LEAD-TRIVIA skip, as a BLOCK.
|
|
496
525
|
*
|
|
@@ -2019,6 +2048,7 @@ return v
|
|
|
2019
2048
|
}
|
|
2020
2049
|
parts.push('let cur=EC.e')
|
|
2021
2050
|
const names: string[] = ['v0']
|
|
2051
|
+
let heldUndo: string | undefined
|
|
2022
2052
|
for (let i = 1; i < n; i++) {
|
|
2023
2053
|
const vn = `v${i}`
|
|
2024
2054
|
parts.push(`let ${vn}`)
|
|
@@ -2038,9 +2068,16 @@ return v
|
|
|
2038
2068
|
// The callee expression hands it over AFTER the scan statement, right
|
|
2039
2069
|
// before the call, so a field inside the trivia grammar cannot take it.
|
|
2040
2070
|
const callee = leadingTriviaFieldAt(code, k, childIp) ? `(LEAD.at=cur,${kids[i]!})` : kids[i]!
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2071
|
+
if (holdsGap(code, k, base, n, i, nullable)) {
|
|
2072
|
+
const t = tmp()
|
|
2073
|
+
const recovers = rowNullable(code, k, code[base + i + 1]!, nullable)
|
|
2074
|
+
parts.push(emitHoldTerm(callee, vn, t, L, skipFor(L), recovers))
|
|
2075
|
+
heldUndo = recovers ? t : undefined
|
|
2076
|
+
} else {
|
|
2077
|
+
parts.push(emitTerm(callee, vn, tmp(), L, skipFor(L), L.tri >= 0 ? triviaMiss(L.tri) : undefined))
|
|
2078
|
+
if (heldUndo !== undefined) parts.push(emitHeldGapUndo(heldUndo))
|
|
2079
|
+
heldUndo = undefined
|
|
2080
|
+
}
|
|
2044
2081
|
}
|
|
2045
2082
|
names.push(vn)
|
|
2046
2083
|
}
|
package/src/table/encode.ts
CHANGED
|
@@ -751,7 +751,7 @@ class Encoder {
|
|
|
751
751
|
for (let j = i + 1; j < parsers.length; j++) {
|
|
752
752
|
// As the interpreter's `followSentinels` derive them (`sequence.ts`): with no
|
|
753
753
|
// trivia, whatever scope the sequence runs in.
|
|
754
|
-
fs = union(fs, firstSetOf(parsers[j]!, new Set(), rr, undefined, false))
|
|
754
|
+
fs = union(fs, firstSetOf(parsers[j]!, new Set(), rr, undefined, false, true))
|
|
755
755
|
}
|
|
756
756
|
return this.charClass(fs)
|
|
757
757
|
}
|
package/src/table/hold-gap.ts
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Not before an adjacency assertion or a leading-trivia field, which read the
|
|
15
15
|
* cursor from before the gap.
|
|
16
|
+
*
|
|
17
|
+
* A next term that can match empty only by recovering (`expect`) holds the gap
|
|
18
|
+
* too, so valid text still scans it once. If it does recover, the gap is
|
|
19
|
+
* unrecorded and the cursor goes back, where only invalid text pays for it.
|
|
16
20
|
*/
|
|
17
21
|
import {
|
|
18
22
|
OP_ADJ, OP_ATTEMPT, OP_CHOICE, OP_COV, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI,
|
|
@@ -24,16 +28,21 @@ import { leadingTriviaFieldAt } from './field-spec.ts'
|
|
|
24
28
|
import { regexCanMatchEmpty } from '../regex/first-set.ts'
|
|
25
29
|
|
|
26
30
|
const WRAPPERS = new Set([OP_NODE, OP_XFORM, OP_LEAF, OP_SOURCE_LEAF, OP_TOKEN, OP_SCOPE, OP_SCOPE_CAP,
|
|
27
|
-
OP_SCOPE_PLAIN, OP_FIELD, OP_LABEL,
|
|
31
|
+
OP_SCOPE_PLAIN, OP_FIELD, OP_LABEL, OP_WITHCTX, OP_RULE, OP_ATTEMPT, OP_COV, OP_GAPGATE])
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* Can the row at `ip` match empty? `true` wherever it cannot tell. A first-set
|
|
31
35
|
* gate row wraps only a child that cannot (`encode.ts` gates nothing nullable).
|
|
36
|
+
* An `expect` recovers at zero width whatever its child needs; with `recovers`
|
|
37
|
+
* false, this asks what the row can match empty without recovering.
|
|
32
38
|
*/
|
|
33
|
-
export function rowNullable(
|
|
34
|
-
|
|
39
|
+
export function rowNullable(
|
|
40
|
+
code: ArrayLike<number>, k: readonly unknown[], ip: number, memo: Map<number, boolean>, recovers = true,
|
|
41
|
+
): boolean {
|
|
42
|
+
const key = recovers ? ip : ~ip
|
|
43
|
+
const hit = memo.get(key)
|
|
35
44
|
if (hit !== undefined) return hit
|
|
36
|
-
memo.set(
|
|
45
|
+
memo.set(key, true) // a cycle is answered conservatively
|
|
37
46
|
const op = code[ip]!
|
|
38
47
|
const kids: number[] = []
|
|
39
48
|
let out = true
|
|
@@ -42,20 +51,20 @@ export function rowNullable(code: ArrayLike<number>, k: readonly unknown[], ip:
|
|
|
42
51
|
else if (op === OP_RX) { const re = k[code[ip + 1]!]; out = !(re instanceof RegExp) || regexCanMatchEmpty(re.source) }
|
|
43
52
|
else if (op === OP_SEQ || op === OP_SEQV || op === OP_SEQX) {
|
|
44
53
|
childSlots(code, ip, kids)
|
|
45
|
-
out = kids.every(c => rowNullable(code, k, c, memo))
|
|
54
|
+
out = kids.every(c => rowNullable(code, k, c, memo, recovers))
|
|
46
55
|
} else if (op === OP_CHOICE) {
|
|
47
56
|
childSlots(code, ip, kids)
|
|
48
|
-
out = kids.length === 0 || kids.some(c => rowNullable(code, k, c, memo))
|
|
57
|
+
out = kids.length === 0 || kids.some(c => rowNullable(code, k, c, memo, recovers))
|
|
49
58
|
} else if (op === OP_REP || op === OP_REPV) {
|
|
50
|
-
out = code[ip + 2] === 0 || rowNullable(code, k, code[ip + 1]!, memo)
|
|
59
|
+
out = code[ip + 2] === 0 || rowNullable(code, k, code[ip + 1]!, memo, recovers)
|
|
51
60
|
} else if (op === OP_SEED) {
|
|
52
|
-
out = rowNullable(code, k, code[ip + 1]!, memo)
|
|
61
|
+
out = rowNullable(code, k, code[ip + 1]!, memo, recovers)
|
|
53
62
|
} else if (op === OP_CONT) {
|
|
54
|
-
out = rowNullable(code, k, code[ip + 1]!, memo) && rowNullable(code, k, code[ip + 3]!, memo)
|
|
55
|
-
} else if (WRAPPERS.has(op)) {
|
|
56
|
-
if (childSlots(code, ip, kids) && kids.length === 1) out = rowNullable(code, k, kids[0]!, memo)
|
|
63
|
+
out = rowNullable(code, k, code[ip + 1]!, memo, recovers) && rowNullable(code, k, code[ip + 3]!, memo, recovers)
|
|
64
|
+
} else if (WRAPPERS.has(op) || (op === OP_EXPECT && !recovers)) {
|
|
65
|
+
if (childSlots(code, ip, kids) && kids.length === 1) out = rowNullable(code, k, kids[0]!, memo, recovers)
|
|
57
66
|
}
|
|
58
|
-
memo.set(
|
|
67
|
+
memo.set(key, out)
|
|
59
68
|
return out
|
|
60
69
|
}
|
|
61
70
|
|
|
@@ -68,5 +77,5 @@ export function holdsGap(
|
|
|
68
77
|
if (code[term] !== OP_NOT && code[term] !== OP_PEEK) return false
|
|
69
78
|
const next = code[base + i + 1]!
|
|
70
79
|
if (code[next] === OP_ADJ || leadingTriviaFieldAt(code, k, next)) return false
|
|
71
|
-
return !rowNullable(code, k, next, memo)
|
|
80
|
+
return !rowNullable(code, k, next, memo, false)
|
|
72
81
|
}
|