parseman 0.56.0 → 0.58.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/diagnostics.js +2232 -1088
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +2247 -1103
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/node.d.ts +1 -1
- package/dist/compiler/build-arity.d.ts +1 -1
- package/dist/compiler/build-arity.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/cst/trivia-kinds.d.ts +9 -1
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/index.js +1446 -236
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +1 -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/index.d.ts +51 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2126 -380
- package/dist/plugin/index.js.map +3 -3
- package/dist/plugin/ir-eval.d.ts +2 -0
- package/dist/plugin/ir-eval.d.ts.map +1 -1
- package/dist/regex/classes.d.ts.map +1 -1
- package/dist/run/index.js +1 -0
- package/dist/run/index.js.map +1 -1
- package/dist/table/assemble.d.ts +9 -0
- 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/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 +1798 -316
- 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 +17 -6
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +13 -0
- package/dist/table/program.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/select-hot.d.ts +6 -0
- package/dist/table/select-hot.d.ts.map +1 -0
- package/dist/table/site-labels.d.ts +66 -0
- package/dist/table/site-labels.d.ts.map +1 -1
- package/dist/table/trivia-gap.d.ts +31 -0
- package/dist/table/trivia-gap.d.ts.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +4 -1
- package/src/combinators/dispatch.ts +12 -3
- package/src/combinators/node.ts +6 -13
- package/src/combinators/trivia-skip.ts +44 -1
- package/src/compiler/build-arity.ts +3 -3
- package/src/compiler/token-alphabet.ts +57 -17
- package/src/cst/capture-buffer.ts +25 -0
- package/src/cst/trivia-kinds.ts +12 -1
- package/src/parse-context.ts +1 -0
- package/src/plugin/entry-supercompiler.ts +8 -9
- package/src/plugin/index.ts +362 -42
- package/src/plugin/ir-eval.ts +7 -1
- package/src/regex/classes.ts +3 -0
- package/src/table/assemble.ts +371 -83
- package/src/table/child-slots.ts +6 -8
- package/src/table/compile-rule-map.ts +64 -15
- package/src/table/emit-assembly.ts +244 -42
- package/src/table/emit.ts +11 -8
- package/src/table/encode.ts +149 -35
- package/src/table/exec-baseline.ts +1 -3
- package/src/table/exec.ts +34 -14
- 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 +18 -7
- package/src/table/program.ts +24 -0
- package/src/table/run-support.ts +35 -0
- package/src/table/scalar-terminal.ts +8 -0
- package/src/table/select-hot.ts +70 -0
- package/src/table/site-labels.ts +135 -4
- package/src/table/trivia-gap.ts +109 -0
- package/src/types.ts +18 -0
- package/src/version.ts +1 -1
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.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH SITES A SELECTIVE BUILD-TIME ASSEMBLY EMITS.
|
|
3
|
+
*
|
|
4
|
+
* The emitted engine beats the closure engine per site, but its bytes are paid by
|
|
5
|
+
* every process that loads the module. Emitting every site made jess's Less AST
|
|
6
|
+
* artifact 20.8x its grammar source. So a build emits only the sites with the
|
|
7
|
+
* highest ESTIMATED hotness per byte, up to a byte budget, and the closure engine
|
|
8
|
+
* runs the rest (`assemble.ts`, the `HYBRID` path).
|
|
9
|
+
*
|
|
10
|
+
* Hotness is estimated statically, since a macro build has no workload to
|
|
11
|
+
* profile: every rule entry starts at 1, a repetition multiplies its body by
|
|
12
|
+
* `LOOP`, and a choice or dispatch splits its weight evenly across its arms. The
|
|
13
|
+
* weights are propagated `ROUNDS` times, so a recursive cycle saturates at `CAP`
|
|
14
|
+
* instead of diverging. Sites are then taken greedily by weight per emitted byte.
|
|
15
|
+
* A build given a corpus (`assemblyProfile`) ranks by measured calls instead.
|
|
16
|
+
*/
|
|
17
|
+
import { childSlots } from './child-slots.ts'
|
|
18
|
+
import { OP_CHOICE, OP_DISPATCH, OP_REP, OP_REPV } from './ops.ts'
|
|
19
|
+
|
|
20
|
+
const LOOP = 8
|
|
21
|
+
const ROUNDS = 60
|
|
22
|
+
const CAP = 1e12
|
|
23
|
+
|
|
24
|
+
/** The sites to emit: highest weight per byte first, while they fit in `budget` bytes. */
|
|
25
|
+
export function selectHotSites(
|
|
26
|
+
code: readonly number[],
|
|
27
|
+
entries: readonly number[],
|
|
28
|
+
siteBytes: ReadonlyMap<number, number>,
|
|
29
|
+
budget: number,
|
|
30
|
+
/** Measured calls per site (`countSiteVisits` over a corpus). Replaces the
|
|
31
|
+
* static estimate when given. */
|
|
32
|
+
profile?: ArrayLike<number>,
|
|
33
|
+
): Set<number> {
|
|
34
|
+
const selected = new Set<number>()
|
|
35
|
+
if (budget <= 0) return selected
|
|
36
|
+
const weight = profile === undefined ? staticWeights(code, entries) : undefined
|
|
37
|
+
const w = (ip: number): number => (weight !== undefined ? weight.get(ip) : profile![ip]) ?? 0
|
|
38
|
+
// Ties (saturated cycles) break by site offset, so a build is deterministic.
|
|
39
|
+
const ranked = [...siteBytes.keys()].sort((a, b) =>
|
|
40
|
+
w(b) / siteBytes.get(b)! - w(a) / siteBytes.get(a)! || a - b)
|
|
41
|
+
let spent = 0
|
|
42
|
+
for (const ip of ranked) {
|
|
43
|
+
if (w(ip) === 0) break
|
|
44
|
+
const bytes = siteBytes.get(ip)!
|
|
45
|
+
if (spent + bytes > budget) continue
|
|
46
|
+
spent += bytes
|
|
47
|
+
selected.add(ip)
|
|
48
|
+
}
|
|
49
|
+
return selected
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function staticWeights(code: readonly number[], entries: readonly number[]): Map<number, number> {
|
|
53
|
+
let weight = new Map<number, number>()
|
|
54
|
+
const kids: number[] = []
|
|
55
|
+
for (let round = 0; round < ROUNDS; round++) {
|
|
56
|
+
const next = new Map<number, number>()
|
|
57
|
+
for (const ip of entries) next.set(ip, 1)
|
|
58
|
+
for (const [ip, w] of weight) {
|
|
59
|
+
kids.length = 0
|
|
60
|
+
childSlots(code, ip, kids)
|
|
61
|
+
const op = code[ip]
|
|
62
|
+
const scale = op === OP_REP || op === OP_REPV ? LOOP
|
|
63
|
+
: op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
|
|
64
|
+
: 1
|
|
65
|
+
for (const kid of kids) next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
|
|
66
|
+
}
|
|
67
|
+
weight = next
|
|
68
|
+
}
|
|
69
|
+
return weight
|
|
70
|
+
}
|
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
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { CharRange, Combinator, ParseContext } from '../types.ts'
|
|
2
|
+
import type { LabeledTriviaSpec } from '../cst/trivia-kinds.ts'
|
|
3
|
+
import { commitTriviaScan, fastTriviaScanner, scanTriviaCompact } from '../combinators/trivia-skip.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* THE TABLE ENGINES' DEFERRED-COMMIT TRIVIA SKIP: `commitTriviaScan(scanTriviaCompact(…))`,
|
|
7
|
+
* the path every labelled trivia scan takes once a log is live, answered without
|
|
8
|
+
* the scan where the answer is already known. Both engines call it: the closure
|
|
9
|
+
* engine's `skipTrivia` and the emitted assembly's skips.
|
|
10
|
+
*
|
|
11
|
+
* ONLY THE ROOT LOG IS LIVE, so only a root-selected chunk can be recorded,
|
|
12
|
+
* and a run in which no character can start a selected arm holds none: its end
|
|
13
|
+
* is the whole answer, and the unlabelled scanner gives it without the
|
|
14
|
+
* chunk-by-chunk visit. Every chunk starts inside the run, so a run free of
|
|
15
|
+
* selected leaders is free of selected chunks. jess parses with
|
|
16
|
+
* `rootTrivia: { select: [comment labels] }`, which is this case for every run
|
|
17
|
+
* of plain whitespace.
|
|
18
|
+
*
|
|
19
|
+
* A SEQUENCE TERM THAT MATCHES NOTHING hands back the cursor from BEFORE its
|
|
20
|
+
* trivia, so the next term asks for the same gap again: on `benchmark.less`,
|
|
21
|
+
* 21,741 of the closure engine's 53,607 trivia scans per parse started where
|
|
22
|
+
* the previous one did. A scan that recorded nothing records nothing again from
|
|
23
|
+
* the same position, in the same parse, under the same trivia, root selection,
|
|
24
|
+
* root-capture policy, trivia log and state, and with no CST capture, and it
|
|
25
|
+
* ends at the same place, so the last such scan answers the repeat (`ctx._gap`).
|
|
26
|
+
* The state is part of the key because a trivia arm can read it (`gate`), and
|
|
27
|
+
* the log because a `leaf` sets it aside while its child runs. The memo lives
|
|
28
|
+
* on the context so it is released with the parse.
|
|
29
|
+
*
|
|
30
|
+
* Neither applies with `trackLines` (a scan records line ranges) or under CST
|
|
31
|
+
* capture (a scan records into the open buffer).
|
|
32
|
+
*/
|
|
33
|
+
export function skipDeferredTrivia(input: string, cur: number, ctx: ParseContext): number {
|
|
34
|
+
const triviaP = ctx.trivia
|
|
35
|
+
const plain = ctx.trackLines !== true && ctx.captureTrivia !== true
|
|
36
|
+
const gap = ctx._gap
|
|
37
|
+
if (plain && gap !== undefined && cur === gap.at && input === gap.input && triviaP === gap.of
|
|
38
|
+
&& ctx._rootTriviaCapture === gap.root && ctx.state === gap.state
|
|
39
|
+
&& ctx._rootTriviaKindIndex === gap.rootKinds && ctx._triviaLog === gap.log) return gap.end
|
|
40
|
+
const end = clearRunEnd(input, cur, ctx, triviaP, plain)
|
|
41
|
+
if (end >= 0) {
|
|
42
|
+
remember(cur, end, ctx, input, triviaP)
|
|
43
|
+
return end
|
|
44
|
+
}
|
|
45
|
+
const scan = scanTriviaCompact(input, cur, ctx)
|
|
46
|
+
if (typeof scan !== 'number') return commitTriviaScan(scan)
|
|
47
|
+
if (plain) remember(cur, scan, ctx, input, triviaP)
|
|
48
|
+
return scan
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The end of the run at `cur` when nothing root-selected lies in it, else −1. */
|
|
52
|
+
function clearRunEnd(input: string, cur: number, ctx: ParseContext, triviaP: Combinator<unknown> | undefined, plain: boolean): number {
|
|
53
|
+
const rootKinds = ctx._rootTriviaKindIndex
|
|
54
|
+
const spec = triviaP?._meta.labeledTriviaSpec
|
|
55
|
+
if (!plain || triviaP === undefined || spec == null || rootKinds === undefined || ctx._triviaLog !== undefined) return -1
|
|
56
|
+
if (spec.rootKinds !== rootKinds) prime(spec, triviaP, rootKinds)
|
|
57
|
+
const fast = spec.rootScan
|
|
58
|
+
if (fast === null) return -1
|
|
59
|
+
const end = fast(input, cur)
|
|
60
|
+
return ctx._rootTriviaCapture === false || clearOf(spec.rootLeaders, input, cur, end) ? end : -1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function remember(cur: number, end: number, ctx: ParseContext, input: string, triviaP: Combinator<unknown> | undefined): void {
|
|
64
|
+
const gap = ctx._gap
|
|
65
|
+
if (gap === undefined) {
|
|
66
|
+
ctx._gap = { at: cur, end, input, of: triviaP, root: ctx._rootTriviaCapture, rootKinds: ctx._rootTriviaKindIndex, log: ctx._triviaLog, state: ctx.state }
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
gap.at = cur
|
|
70
|
+
gap.end = end
|
|
71
|
+
gap.input = input
|
|
72
|
+
gap.of = triviaP
|
|
73
|
+
gap.root = ctx._rootTriviaCapture
|
|
74
|
+
gap.rootKinds = ctx._rootTriviaKindIndex
|
|
75
|
+
gap.log = ctx._triviaLog
|
|
76
|
+
gap.state = ctx.state
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Prime `spec` for the selection `rootKinds`: the first-character ranges of the
|
|
81
|
+
* arms it selects, and the scanner, which is `null` where one of those arms is
|
|
82
|
+
* unbounded or the trivia has no unlabelled scanner. `run()` builds one
|
|
83
|
+
* selection per parse, so this runs once per parse per trivia.
|
|
84
|
+
*/
|
|
85
|
+
function prime(spec: LabeledTriviaSpec, triviaP: Combinator<unknown>, rootKinds: Readonly<Record<string, number>>): void {
|
|
86
|
+
const ranges: CharRange[] = []
|
|
87
|
+
let bounded = true
|
|
88
|
+
for (const arm of spec.arms) {
|
|
89
|
+
if (rootKinds[arm.label] === undefined) continue
|
|
90
|
+
const fs = arm.parser._meta.firstSet
|
|
91
|
+
if (fs.kind === 'any') bounded = false
|
|
92
|
+
else if (fs.kind === 'ranges') ranges.push(...fs.ranges)
|
|
93
|
+
}
|
|
94
|
+
const classified = triviaP._meta.triviaScanner
|
|
95
|
+
spec.rootKinds = rootKinds
|
|
96
|
+
spec.rootLeaders = ranges
|
|
97
|
+
spec.rootScan = !bounded ? null : classified === undefined ? fastTriviaScanner(triviaP) : classified
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** No character in `[from, to)` falls in `ranges`. A surrogate is never clear:
|
|
101
|
+
* the ranges are code points, and a code unit cannot be checked against them. */
|
|
102
|
+
function clearOf(ranges: readonly CharRange[], input: string, from: number, to: number): boolean {
|
|
103
|
+
for (let i = from; i < to; i++) {
|
|
104
|
+
const c = input.charCodeAt(i)
|
|
105
|
+
if (c >= 0xD800 && c <= 0xDFFF) return false
|
|
106
|
+
for (const r of ranges) if (c >= r.lo && c <= r.hi) return false
|
|
107
|
+
}
|
|
108
|
+
return true
|
|
109
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -398,6 +398,12 @@ export type ParseContext = {
|
|
|
398
398
|
* selected markers into the document-root capture.
|
|
399
399
|
*/
|
|
400
400
|
_rootTriviaCapture?: boolean | undefined
|
|
401
|
+
/**
|
|
402
|
+
* Framework-internal: the last deferred-commit trivia scan in this parse that
|
|
403
|
+
* recorded nothing (`table/trivia-gap.ts`). Held on the context so it is freed
|
|
404
|
+
* with the parse.
|
|
405
|
+
*/
|
|
406
|
+
_gap?: TriviaGapMemo | undefined
|
|
401
407
|
/**
|
|
402
408
|
* Framework-internal: flat per-node trivia log for CST capture mode.
|
|
403
409
|
* When set alongside _cstRawChildren, each trivia entry is recorded as three
|
|
@@ -606,3 +612,15 @@ export type CompiledParser<T> = {
|
|
|
606
612
|
/** Present only when compiled with `{ coverage: true }`. */
|
|
607
613
|
coverageDefinitions?: readonly import('./compiler/grammar-coverage-ids.ts').GrammarCoverageDefinition[]
|
|
608
614
|
}
|
|
615
|
+
|
|
616
|
+
/** A trivia gap scanned from `at` to `end` that recorded nothing, and the key it holds for. */
|
|
617
|
+
export interface TriviaGapMemo {
|
|
618
|
+
at: number
|
|
619
|
+
end: number
|
|
620
|
+
input: string
|
|
621
|
+
of: Combinator<unknown> | undefined
|
|
622
|
+
root: boolean | undefined
|
|
623
|
+
rootKinds: Readonly<Record<string, number>> | undefined
|
|
624
|
+
log: number[] | undefined
|
|
625
|
+
state: unknown
|
|
626
|
+
}
|
package/src/version.ts
CHANGED