parseman 0.53.0 → 0.54.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 +278 -118
- package/dist/analysis/diagnostics.js.map +2 -2
- package/dist/cli/index.js +278 -118
- package/dist/cli/index.js.map +2 -2
- package/dist/combinators/field-trivia.d.ts +40 -0
- package/dist/combinators/field-trivia.d.ts.map +1 -0
- package/dist/combinators/map.d.ts +19 -2
- package/dist/combinators/map.d.ts.map +1 -1
- package/dist/combinators/repeat.d.ts.map +1 -1
- package/dist/combinators/sequence.d.ts.map +1 -1
- package/dist/compiler/fields.d.ts +2 -0
- package/dist/compiler/fields.d.ts.map +1 -1
- package/dist/cst/trivia-charscan.d.ts +11 -3
- package/dist/cst/trivia-charscan.d.ts.map +1 -1
- package/dist/cst/trivia-kinds.d.ts +11 -0
- package/dist/cst/trivia-kinds.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +284 -120
- package/dist/index.js.map +2 -2
- package/dist/plugin/index.js +284 -120
- package/dist/plugin/index.js.map +2 -2
- package/dist/table/assemble.d.ts +9 -0
- package/dist/table/assemble.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 +6 -2
- 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/field-spec.d.ts +13 -0
- package/dist/table/field-spec.d.ts.map +1 -0
- package/dist/table/index.js +245 -112
- package/dist/table/index.js.map +2 -2
- package/dist/table/scalar-terminal.d.ts +11 -0
- package/dist/table/scalar-terminal.d.ts.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/analysis/duplication.ts +1 -1
- package/src/analysis/rebuild.ts +1 -1
- package/src/combinators/field-trivia.ts +44 -0
- package/src/combinators/map.ts +48 -6
- package/src/combinators/repeat.ts +6 -1
- package/src/combinators/sequence.ts +9 -1
- package/src/compiler/fields.ts +5 -2
- package/src/compiler/ir-serialize.ts +7 -1
- package/src/coverage.ts +1 -1
- package/src/cst/trivia-charscan.ts +17 -56
- package/src/cst/trivia-kinds.ts +24 -8
- package/src/index.ts +1 -0
- package/src/plugin/evaluator.ts +1 -1
- package/src/table/assemble.ts +126 -22
- package/src/table/emit-assembly.ts +38 -18
- package/src/table/emit.ts +15 -4
- package/src/table/encode.ts +18 -2
- package/src/table/exec.ts +15 -1
- package/src/table/field-spec.ts +17 -0
- package/src/table/scalar-terminal.ts +25 -0
- package/src/types.ts +12 -2
- package/src/version.ts +1 -1
package/src/table/assemble.ts
CHANGED
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
import type { Combinator, FieldMap, FirstSet, ParseContext, ParseResult, ParserDef } from '../types.ts'
|
|
67
67
|
import { balanced, scanTo } from '../combinators/scanTo.ts'
|
|
68
68
|
import { buildFieldMap } from '../compiler/fields.ts'
|
|
69
|
+
import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA, LEAD, triviaFieldCapture } from '../combinators/field-trivia.ts'
|
|
69
70
|
import { asciiFoldKey } from '../combinators/dispatch.ts'
|
|
70
71
|
import { projectChild, unwrapChild } from '../combinators/node.ts'
|
|
71
72
|
import { cstOutputHost } from '../compiler/build-arity.ts'
|
|
@@ -129,7 +130,7 @@ import { computeSiteLabels, reachableSites } from './site-labels.ts'
|
|
|
129
130
|
import { refuseUnclassifiedRootScope } from '../cst/root-trivia-scope.ts'
|
|
130
131
|
import { captureError, firstSetSentinel, matchesAt, orSentinel, recoverScan } from '../recovery/scan.ts'
|
|
131
132
|
import {
|
|
132
|
-
|
|
133
|
+
largeChoiceLiteralPretest, leadingScalarSequence, leadingScalarTerminal, makeScalarRecognizer, scalarTerminalNodeChild,
|
|
133
134
|
scalarTerminalNotChild, type ScalarRecognizer,
|
|
134
135
|
} from './scalar-terminal.ts'
|
|
135
136
|
|
|
@@ -146,6 +147,7 @@ const EMIT_ENABLED = (globalThis as { process?: { env?: Record<string, string |
|
|
|
146
147
|
|
|
147
148
|
/** Failure sentinel — SHARED with the other two engines, see `cell.ts`. */
|
|
148
149
|
import { FAIL, newEndCell } from './cell.ts'
|
|
150
|
+
import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
|
|
149
151
|
|
|
150
152
|
const EMPTY_TL: readonly number[] = Object.freeze([])
|
|
151
153
|
/** `finishCstBuf`'s two empty sentinels, held here so the node piece can inline it. */
|
|
@@ -1053,6 +1055,15 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1053
1055
|
return cur
|
|
1054
1056
|
}
|
|
1055
1057
|
|
|
1058
|
+
/**
|
|
1059
|
+
* A `field(…, { leadingTrivia: true })` term. The pre-scan cursor reaches the
|
|
1060
|
+
* field AFTER `step`'s scan, right before the field runs, so a field inside the
|
|
1061
|
+
* trivia grammar cannot take it first. One closure per call; only such terms pay.
|
|
1062
|
+
*/
|
|
1063
|
+
function leadingFieldTerm(step: typeof nextTerm, child: Piece, input: string, cur: number, ctx: ParseContext): number {
|
|
1064
|
+
return step((i, p, c) => { LEAD.at = cur; return child(i, p, c) }, input, cur, ctx)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1056
1067
|
function committed(c: ParseContext): boolean {
|
|
1057
1068
|
return c._fc === true
|
|
1058
1069
|
}
|
|
@@ -1793,12 +1804,27 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
1793
1804
|
}
|
|
1794
1805
|
|
|
1795
1806
|
case OP_FIELD: {
|
|
1796
|
-
const name = k[code[ip + 1]!]
|
|
1807
|
+
const { name, bits } = fieldSpec(k[code[ip + 1]!])
|
|
1797
1808
|
const child = link(code[ip + 2]!)
|
|
1809
|
+
if (bits === 0) {
|
|
1810
|
+
return (input, pos, ctx) => {
|
|
1811
|
+
const v = child(input, pos, ctx)
|
|
1812
|
+
if (v === FAIL) return FAIL
|
|
1813
|
+
ctx._fields?.push({ name, value: v, span: { start: pos, end: EC.e } })
|
|
1814
|
+
return v
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
const leading = (bits & FIELD_LEADING_TRIVIA) !== 0
|
|
1818
|
+
const trailing = (bits & FIELD_TRAILING_TRIVIA) !== 0
|
|
1798
1819
|
return (input, pos, ctx) => {
|
|
1820
|
+
const leadFrom = leading ? LEAD.at : -1
|
|
1821
|
+
LEAD.at = -1
|
|
1799
1822
|
const v = child(input, pos, ctx)
|
|
1800
1823
|
if (v === FAIL) return FAIL
|
|
1801
|
-
|
|
1824
|
+
const end = EC.e
|
|
1825
|
+
const trailEnd = trailing && ctx.trivia !== undefined ? consumeTrivia(input, end, ctx) : end
|
|
1826
|
+
ctx._fields?.push(triviaFieldCapture(name, v, input, pos, end, leadFrom, trailEnd))
|
|
1827
|
+
EC.e = trailEnd
|
|
1802
1828
|
return v
|
|
1803
1829
|
}
|
|
1804
1830
|
}
|
|
@@ -2322,6 +2348,12 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2322
2348
|
const kidIp = code[base + i]!
|
|
2323
2349
|
if (code[kidIp] !== OP_ADJ) {
|
|
2324
2350
|
const kid = link(kidIp)
|
|
2351
|
+
if (leadingTriviaFieldAt(code, k, kidIp)) {
|
|
2352
|
+
return (input, cur, ctx, inherited) => {
|
|
2353
|
+
ctx._sync = sync ?? inherited
|
|
2354
|
+
return leadingFieldTerm(step, kid, input, cur, ctx)
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2325
2357
|
return (input, cur, ctx, inherited) => {
|
|
2326
2358
|
ctx._sync = sync ?? inherited
|
|
2327
2359
|
return step(kid, input, cur, ctx)
|
|
@@ -2389,14 +2421,22 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2389
2421
|
}
|
|
2390
2422
|
}
|
|
2391
2423
|
|
|
2424
|
+
// A `field(…, { leadingTrivia: true })` term is a boundary fact too: the
|
|
2425
|
+
// runner hands it the cursor from BEFORE the scan (`LEAD`, cell.ts).
|
|
2392
2426
|
let hasAdj = false
|
|
2393
|
-
for (let i = 1; i < n; i++)
|
|
2427
|
+
for (let i = 1; i < n; i++) {
|
|
2428
|
+
const kidIp = code[base + i]!
|
|
2429
|
+
if (code[kidIp] === OP_ADJ || leadingTriviaFieldAt(code, k, kidIp)) { hasAdj = true; break }
|
|
2430
|
+
}
|
|
2394
2431
|
if (hasAdj) {
|
|
2395
2432
|
const first = link(code[base]!)
|
|
2396
2433
|
const runnerAt = (i: number): TermRunner => {
|
|
2397
2434
|
const kidIp = code[base + i]!
|
|
2398
2435
|
if (code[kidIp] !== OP_ADJ) {
|
|
2399
2436
|
const kid = link(kidIp)
|
|
2437
|
+
if (leadingTriviaFieldAt(code, k, kidIp)) {
|
|
2438
|
+
return (input, cur, ctx) => leadingFieldTerm(step, kid, input, cur, ctx)
|
|
2439
|
+
}
|
|
2400
2440
|
return (input, cur, ctx) => step(kid, input, cur, ctx)
|
|
2401
2441
|
}
|
|
2402
2442
|
const negated = code[kidIp + 1] === 1
|
|
@@ -2791,7 +2831,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2791
2831
|
if (n === 2 || n === 3) {
|
|
2792
2832
|
const a0 = link(code[base]!), a1 = link(code[base + 1]!)
|
|
2793
2833
|
const a2 = n === 3 ? link(code[base + 2]!) : undefined
|
|
2794
|
-
const predecide = !
|
|
2834
|
+
const predecide = !REC && !cfg.probe && !cfg.coverage && !cfg.trackLines
|
|
2795
2835
|
const p0ip = predecide ? leadingScalarTerminal(code, code[base]!, 2, true, true) : -1
|
|
2796
2836
|
const p1ip = predecide ? leadingScalarTerminal(code, code[base + 1]!, 2, true, true) : -1
|
|
2797
2837
|
const p2ip = predecide && n === 3 ? leadingScalarTerminal(code, code[base + 2]!, 2, true, true) : -1
|
|
@@ -2958,6 +2998,56 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
2958
2998
|
)
|
|
2959
2999
|
}
|
|
2960
3000
|
const masked = bindMasked(0, EMPTY_EXPECTED_PREFIX)
|
|
3001
|
+
// LITERAL PRETESTS. First-char dispatch cannot tell `@{…}` or `@@x` from
|
|
3002
|
+
// `@x`, so every arm sharing the char is entered, and under a CST host
|
|
3003
|
+
// each entered node opens a capture frame before failing on the second
|
|
3004
|
+
// char. An arm that must begin with a literal of two or more chars (or
|
|
3005
|
+
// one of a family of them) is tested on the literal first and, on a
|
|
3006
|
+
// miss, skipped exactly as a char-excluded arm is: its static expected
|
|
3007
|
+
// set is the one reported. The test is pure, so a miss leaves no
|
|
3008
|
+
// capture, trivia or field to roll back.
|
|
3009
|
+
const pBit: number[] = []
|
|
3010
|
+
const pRec: ScalarRecognizer[] = []
|
|
3011
|
+
if (!REC && !cfg.probe && !cfg.coverage && !cfg.trackLines) {
|
|
3012
|
+
for (let i = 0; i < n; i++) {
|
|
3013
|
+
for (const terminal of largeChoiceLiteralPretest(code, k, code[base + i]!) ?? []) {
|
|
3014
|
+
pBit.push(1 << i)
|
|
3015
|
+
pRec.push(scalarFor(terminal)!)
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
const pn = pBit.length
|
|
3020
|
+
if (pn !== 0) {
|
|
3021
|
+
return (input, pos, ctx) => {
|
|
3022
|
+
const c = lead(input, pos)
|
|
3023
|
+
const need = markCst(ctx)
|
|
3024
|
+
const mRaw = MRAW
|
|
3025
|
+
const mTl = MTL
|
|
3026
|
+
const mLv = MLV
|
|
3027
|
+
const mFl = need ? ctx._fields?.length ?? 0 : 0
|
|
3028
|
+
const mEr = need ? ctx._errors?.length ?? 0 : 0
|
|
3029
|
+
const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
3030
|
+
const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
3031
|
+
if (c < 128) {
|
|
3032
|
+
const bits = mask[c < 0 ? 128 : c]!
|
|
3033
|
+
let tested = 0
|
|
3034
|
+
let hit = 0
|
|
3035
|
+
for (let i = 0; i < pn; i++) {
|
|
3036
|
+
const b = pBit[i]!
|
|
3037
|
+
if ((bits & b) === 0 || (hit & b) !== 0) continue
|
|
3038
|
+
tested |= b
|
|
3039
|
+
if (pRec[i]!(input, pos) >= 0) hit |= b
|
|
3040
|
+
}
|
|
3041
|
+
return masked(
|
|
3042
|
+
input, pos, ctx, bits & (~tested | hit),
|
|
3043
|
+
need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
|
|
3044
|
+
)
|
|
3045
|
+
}
|
|
3046
|
+
return general(
|
|
3047
|
+
input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
|
|
3048
|
+
)
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
2961
3051
|
return (input, pos, ctx) => {
|
|
2962
3052
|
const c = lead(input, pos)
|
|
2963
3053
|
const need = markCst(ctx)
|
|
@@ -3100,6 +3190,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3100
3190
|
const max = code[ip + 3]!
|
|
3101
3191
|
const sepIp = code[ip + 4]!
|
|
3102
3192
|
const sep = sepIp >= 0 ? link(sepIp) : undefined
|
|
3193
|
+
// A `field(…, { leadingTrivia: true })` separator reads the pre-scan cursor.
|
|
3194
|
+
const sepLeads = sepIp >= 0 && leadingTriviaFieldAt(code, k, sepIp)
|
|
3103
3195
|
const flags = code[ip + 5]!
|
|
3104
3196
|
const keepSeparators = (flags & 2) !== 0
|
|
3105
3197
|
const trailingAllowed = (flags & 1) !== 0
|
|
@@ -3183,6 +3275,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3183
3275
|
let sp = cur
|
|
3184
3276
|
if (hasTrivia) sp = skipTrivia(input, sp, ctx)
|
|
3185
3277
|
ctx._fc = false
|
|
3278
|
+
if (sepLeads) LEAD.at = cur
|
|
3186
3279
|
const sv = sep(input, sp, ctx)
|
|
3187
3280
|
if (sv === FAIL) {
|
|
3188
3281
|
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
@@ -3283,6 +3376,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3283
3376
|
let sp = cur
|
|
3284
3377
|
if (hasTrivia) sp = skipTrivia(input, sp, ctx)
|
|
3285
3378
|
ctx._fc = false
|
|
3379
|
+
if (sepLeads) LEAD.at = cur
|
|
3286
3380
|
const sv = sep(input, sp, ctx)
|
|
3287
3381
|
if (sv === FAIL) {
|
|
3288
3382
|
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
@@ -3706,20 +3800,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3706
3800
|
if (child >= 0) scalarFor(child)
|
|
3707
3801
|
continue
|
|
3708
3802
|
}
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
scalarFor(child)
|
|
3713
|
-
continue
|
|
3714
|
-
}
|
|
3715
|
-
}
|
|
3716
|
-
const family = leadingLiteralFamily(code, k, armIp)
|
|
3717
|
-
if (family !== undefined && family.length >= 2 && family.every(terminal => {
|
|
3718
|
-
const value = k[code[terminal + 1]!]
|
|
3719
|
-
return typeof value === 'string' && value.length >= 2
|
|
3720
|
-
})) {
|
|
3721
|
-
for (const terminal of family) scalarFor(terminal)
|
|
3722
|
-
}
|
|
3803
|
+
const literals = largeChoiceLiteralPretest(code, k, armIp)
|
|
3804
|
+
if (literals !== undefined) for (const terminal of literals) scalarFor(terminal)
|
|
3805
|
+
if (literals !== undefined && literals.length === 1 && literals[0] === child) continue
|
|
3723
3806
|
const sequence = leadingScalarSequence(code, armIp)
|
|
3724
3807
|
if (sequence !== undefined && (sequence.trivia < 0
|
|
3725
3808
|
|| triviaScan[sequence.trivia] != null)) {
|
|
@@ -3767,6 +3850,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3767
3850
|
REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
|
|
3768
3851
|
matchesAt, recoverScan, orSentinel, captureError,
|
|
3769
3852
|
scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
|
|
3853
|
+
LEAD, triviaFieldCapture,
|
|
3770
3854
|
)
|
|
3771
3855
|
emitReached = new Set(pre.reached)
|
|
3772
3856
|
}
|
|
@@ -3848,6 +3932,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
|
|
|
3848
3932
|
REC ? prog.cc.map((_, i) => sentinelFor(i)) : EMPTY_SENTS,
|
|
3849
3933
|
matchesAt, recoverScan, orSentinel, captureError,
|
|
3850
3934
|
scalarRecognizers, commitTriviaScan, scanTriviaCompact, t.lex, adjacencyHolds, t.lexPrograms,
|
|
3935
|
+
LEAD, triviaFieldCapture,
|
|
3851
3936
|
)
|
|
3852
3937
|
emitReached = em.reached
|
|
3853
3938
|
} catch (e) {
|
|
@@ -4067,6 +4152,13 @@ export class AssemblyCache {
|
|
|
4067
4152
|
}
|
|
4068
4153
|
}
|
|
4069
4154
|
|
|
4155
|
+
/**
|
|
4156
|
+
* One rule of the map. A lowered terminal is emitted as this call, not as
|
|
4157
|
+
* `tableRules(…)["Entry"]`: `@__PURE__` covers a call and not a property read
|
|
4158
|
+
* after it, so a bundler had to keep the whole table of every terminal nothing
|
|
4159
|
+
* referenced. With the lookup inside the call the statement is droppable.
|
|
4160
|
+
*/
|
|
4161
|
+
export function tableRules(source: TableProgram | CompactProgram, entry: string): TableRule
|
|
4070
4162
|
/**
|
|
4071
4163
|
* The ASSEMBLED rule map — the same artifact contract as `tableRules`, run
|
|
4072
4164
|
* through linked closures instead of the bytecode interpreter.
|
|
@@ -4076,11 +4168,19 @@ export class AssemblyCache {
|
|
|
4076
4168
|
* assembly built for it. Everything past that point is pieces, and no piece body
|
|
4077
4169
|
* reads an option — `scripts/check-invariants.mjs` INV-6 asserts it. `forCtx`
|
|
4078
4170
|
* carries the argument for why that read is irreducible rather than merely cheap.
|
|
4171
|
+
*
|
|
4172
|
+
* Declared last, so `ReturnType<typeof tableRules>` stays the map.
|
|
4079
4173
|
*/
|
|
4080
4174
|
export function tableRules(
|
|
4081
4175
|
source: TableProgram | CompactProgram,
|
|
4082
|
-
artifactMetadata
|
|
4083
|
-
): Record<string, TableRule>
|
|
4176
|
+
artifactMetadata?: Readonly<Record<symbol, unknown>>,
|
|
4177
|
+
): Record<string, TableRule>
|
|
4178
|
+
export function tableRules(
|
|
4179
|
+
source: TableProgram | CompactProgram,
|
|
4180
|
+
metadataOrEntry: Readonly<Record<symbol, unknown>> | string = {},
|
|
4181
|
+
): Record<string, TableRule> | TableRule {
|
|
4182
|
+
const entry = typeof metadataOrEntry === 'string' ? metadataOrEntry : undefined
|
|
4183
|
+
const artifactMetadata = typeof metadataOrEntry === 'string' ? {} : metadataOrEntry
|
|
4084
4184
|
const prog = expandCompact(source)
|
|
4085
4185
|
const resolved = resolveTable(prog)
|
|
4086
4186
|
const cache = new AssemblyCache(prog, resolved)
|
|
@@ -4105,7 +4205,7 @@ export function tableRules(
|
|
|
4105
4205
|
* `runRule`, after the outer read.
|
|
4106
4206
|
*/
|
|
4107
4207
|
let selected: Assembly | undefined
|
|
4108
|
-
|
|
4208
|
+
const map = stampRuleMap(prog, {
|
|
4109
4209
|
runRule: (ri, input, pos, ctx) => {
|
|
4110
4210
|
const a = selected ?? cache.forCtx(ctx)
|
|
4111
4211
|
selected = undefined
|
|
@@ -4128,4 +4228,8 @@ export function tableRules(
|
|
|
4128
4228
|
return a.scanSkip[skipOf?.[ri] ?? -1]
|
|
4129
4229
|
},
|
|
4130
4230
|
}, artifactMetadata, resolved)
|
|
4231
|
+
if (entry === undefined) return map
|
|
4232
|
+
// Own properties only: the map inherits `toString`, `constructor` and the rest.
|
|
4233
|
+
if (!Object.hasOwn(map, entry)) throw new Error(`tableRules: the program has no rule ${JSON.stringify(entry)}`)
|
|
4234
|
+
return map[entry]!
|
|
4131
4235
|
}
|
|
@@ -63,10 +63,12 @@ import {
|
|
|
63
63
|
computeSiteLabels, reachableSites, type SiteLabel,
|
|
64
64
|
} from './site-labels.ts'
|
|
65
65
|
import {
|
|
66
|
-
|
|
66
|
+
largeChoiceLiteralPretest, leadingScalarSequence, leadingScalarTerminal,
|
|
67
67
|
scalarTerminalNodeChild, scalarTerminalNotChild, type LeadingScalarSequence,
|
|
68
68
|
} from './scalar-terminal.ts'
|
|
69
69
|
import { childSlots } from './child-slots.ts'
|
|
70
|
+
import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
|
|
71
|
+
import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA } from '../combinators/field-trivia.ts'
|
|
70
72
|
|
|
71
73
|
/** What the compiled factory hands back — the emitted twin of `Assembly`. */
|
|
72
74
|
export type EmittedPiece = (input: string, pos: number, ctx: ParseContext) => unknown
|
|
@@ -122,6 +124,9 @@ export const EMITTED_PARAMS = [
|
|
|
122
124
|
'adjacencyHolds',
|
|
123
125
|
// Appended for selected composite lexical programs; old factories ignore it.
|
|
124
126
|
'LEXPROG',
|
|
127
|
+
// Appended for trivia-attributing fields (`field(…, { leadingTrivia })`);
|
|
128
|
+
// old factories ignore them.
|
|
129
|
+
'LEAD', 'triviaFieldCapture',
|
|
125
130
|
] as const
|
|
126
131
|
|
|
127
132
|
/**
|
|
@@ -607,20 +612,12 @@ export function emitAssemblySource(
|
|
|
607
612
|
if (child >= 0) scalarSpecs.add(code[child + 1]!)
|
|
608
613
|
continue
|
|
609
614
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
continue
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
const family = leadingLiteralFamily(code, k, armIp)
|
|
618
|
-
if (family !== undefined && family.length >= 2 && family.every(terminal => {
|
|
619
|
-
const value = k[code[terminal + 1]!]
|
|
620
|
-
return typeof value === 'string' && value.length >= 2
|
|
621
|
-
})) {
|
|
622
|
-
largeChoiceLiteralFamilies.set(armIp, family)
|
|
615
|
+
const literals = largeChoiceLiteralPretest(code, k, armIp)
|
|
616
|
+
if (literals !== undefined && literals.length === 1 && literals[0] === child) {
|
|
617
|
+
largeChoiceScalarSpecs.add(code[child + 1]!)
|
|
618
|
+
continue
|
|
623
619
|
}
|
|
620
|
+
if (literals !== undefined) largeChoiceLiteralFamilies.set(armIp, literals)
|
|
624
621
|
const sequence = leadingScalarSequence(code, armIp)
|
|
625
622
|
// A labelled trivia scanner is still recognition authority here. This
|
|
626
623
|
// pretest is pure: on a miss the ordinary arm's trivia log would have
|
|
@@ -1307,17 +1304,34 @@ return v
|
|
|
1307
1304
|
}
|
|
1308
1305
|
|
|
1309
1306
|
case OP_FIELD: {
|
|
1310
|
-
const
|
|
1307
|
+
const spec = fieldSpec(k[code[ip + 1]!])
|
|
1308
|
+
const name = q(spec.name)
|
|
1311
1309
|
const child = link(code[ip + 2]!)
|
|
1312
1310
|
// `ctx._fields?.push(…)` — conditional on a field-reading node being
|
|
1313
1311
|
// open, exactly as `map.ts` has it, so a field outside one costs a load
|
|
1314
1312
|
// and a branch. The ARRAY is what `OP_NODE` hands to `buildFieldMap`.
|
|
1315
|
-
|
|
1313
|
+
if (spec.bits === 0) {
|
|
1314
|
+
return `${head}
|
|
1316
1315
|
const v=${child}(input,pos,ctx)
|
|
1317
1316
|
if(v===FAIL)return FAIL
|
|
1318
1317
|
const f=ctx._fields
|
|
1319
1318
|
if(f!==undefined)f.push({name:${name},value:v,span:{start:pos,end:EC.e}})
|
|
1320
1319
|
return v
|
|
1320
|
+
}`
|
|
1321
|
+
}
|
|
1322
|
+
const leading = (spec.bits & FIELD_LEADING_TRIVIA) !== 0
|
|
1323
|
+
const trailing = (spec.bits & FIELD_TRAILING_TRIVIA) !== 0
|
|
1324
|
+
return `${head}
|
|
1325
|
+
const lf=${leading ? 'LEAD.at' : '-1'}
|
|
1326
|
+
LEAD.at=-1
|
|
1327
|
+
const v=${child}(input,pos,ctx)
|
|
1328
|
+
if(v===FAIL)return FAIL
|
|
1329
|
+
const e=EC.e
|
|
1330
|
+
const te=${trailing ? 'ctx.trivia!==undefined?consumeTrivia(input,e,ctx):e' : 'e'}
|
|
1331
|
+
const f=ctx._fields
|
|
1332
|
+
if(f!==undefined)f.push(triviaFieldCapture(${name},v,input,pos,e,lf,te))
|
|
1333
|
+
EC.e=te
|
|
1334
|
+
return v
|
|
1321
1335
|
}`
|
|
1322
1336
|
}
|
|
1323
1337
|
|
|
@@ -1687,7 +1701,13 @@ return v
|
|
|
1687
1701
|
`if(!adjacencyHolds(input,cur,ctx,${negated ? 'true' : 'false'},${kinds})){ctx._fe=cur;ctx._fx=${expected};return FAIL}`,
|
|
1688
1702
|
`${vn}=null`,
|
|
1689
1703
|
)
|
|
1690
|
-
} else
|
|
1704
|
+
} else {
|
|
1705
|
+
// A leading-trivia field reads the cursor from before the term's scan.
|
|
1706
|
+
// The callee expression hands it over AFTER the scan statement, right
|
|
1707
|
+
// before the call, so a field inside the trivia grammar cannot take it.
|
|
1708
|
+
const callee = leadingTriviaFieldAt(code, k, childIp) ? `(LEAD.at=cur,${kids[i]!})` : kids[i]!
|
|
1709
|
+
parts.push(emitTerm(callee, vn, tmp(), L, skipFor(L)))
|
|
1710
|
+
}
|
|
1691
1711
|
names.push(vn)
|
|
1692
1712
|
}
|
|
1693
1713
|
parts.push('EC.e=cur')
|
|
@@ -2232,7 +2252,7 @@ ${sep !== undefined ? `if(count>0){
|
|
|
2232
2252
|
const lb=cstLeavesLen(ctx)
|
|
2233
2253
|
let sp=cur
|
|
2234
2254
|
${hasTrivia === 'false' ? '' : hasTrivia === 'true' ? `sp=${skip}(input,sp,ctx)\n` : `if(hasTrivia)sp=${skip}(input,sp,ctx)\n`}ctx._fc=false
|
|
2235
|
-
const sv=${sep}(input,sp,ctx)
|
|
2255
|
+
${sepIp >= 0 && leadingTriviaFieldAt(code, k, sepIp) ? 'LEAD.at=cur\n' : ''}const sv=${sep}(input,sp,ctx)
|
|
2236
2256
|
if(sv===FAIL){
|
|
2237
2257
|
${rb}
|
|
2238
2258
|
if(ctx._fc===true)return FAIL
|
package/src/table/emit.ts
CHANGED
|
@@ -352,12 +352,16 @@ export type ExpressionEmitOptions = EmitOptions & {
|
|
|
352
352
|
*
|
|
353
353
|
* `null` is what a `rules()` call site needs: its initialiser evaluates to the
|
|
354
354
|
* `{ name: fn, … }` object, exactly what `tableRules(…)` already returns, so
|
|
355
|
-
* the map form is the expression WITHOUT the
|
|
355
|
+
* the map form is the expression WITHOUT the entry argument — not a second
|
|
356
356
|
* emitter. `compileRuleMap`'s replacement has the same shape and the same
|
|
357
357
|
* splice point, which is what makes the two lowerings interchangeable there.
|
|
358
358
|
*/
|
|
359
359
|
readonly entry?: string | null
|
|
360
|
-
/**
|
|
360
|
+
/**
|
|
361
|
+
* Static metadata object passed to `tableRules` as its second argument. Only
|
|
362
|
+
* the rule-map form (`entry: null`) takes it; with an entry, that argument is
|
|
363
|
+
* the entry's name.
|
|
364
|
+
*/
|
|
361
365
|
readonly metadataSource?: string
|
|
362
366
|
}
|
|
363
367
|
|
|
@@ -379,11 +383,18 @@ export function emitTableExpression(prog: TableProgram, opts: ExpressionEmitOpti
|
|
|
379
383
|
const entry = opts.entry === undefined ? 'grammar' : opts.entry
|
|
380
384
|
const ref = opts.runtimeRef ?? 'tableRules'
|
|
381
385
|
const fns = opts.fnSources ?? prog.fns.map(() => '() => {}')
|
|
382
|
-
|
|
386
|
+
// The entry is an ARGUMENT, never a trailing `[entry]`: `@__PURE__` covers the
|
|
387
|
+
// call and not a property read after it, so `tableRules({…})["Entry"]` left an
|
|
388
|
+
// unreferenced terminal's whole table in every bundle.
|
|
389
|
+
const meta = opts.metadataSource
|
|
390
|
+
if (entry !== null && meta !== undefined) {
|
|
391
|
+
throw new Error('emitTableExpression: metadataSource is for the rule-map form (entry: null); a single entry carries none')
|
|
392
|
+
}
|
|
393
|
+
const tail = entry !== null ? `,${jsString(entry)}` : meta === undefined ? '' : `,${meta}`
|
|
383
394
|
return [
|
|
384
395
|
`/* @__PURE__ */ ${ref}({`,
|
|
385
396
|
...programFields(prog, fns, opts),
|
|
386
|
-
|
|
397
|
+
`}${tail})`,
|
|
387
398
|
].join('\n')
|
|
388
399
|
}
|
|
389
400
|
|
package/src/table/encode.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { deriveExpected } from '../combinators/expect.ts'
|
|
|
7
7
|
import { assertionFailureExpected, directTerminalFailureExpected } from '../combinators/expected.ts'
|
|
8
8
|
import { buildReadsRaw, buildReadsState, buildReadsTrivia } from '../compiler/build-arity.ts'
|
|
9
9
|
import { buildReadsFields, parserHasOwnFields } from '../compiler/fields.ts'
|
|
10
|
+
import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA } from '../combinators/field-trivia.ts'
|
|
10
11
|
import { asciiFoldKey, branchUsesRouted, parserUsesRouted } from '../combinators/dispatch.ts'
|
|
11
12
|
import {
|
|
12
13
|
OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
|
|
@@ -555,6 +556,17 @@ class Encoder {
|
|
|
555
556
|
return i
|
|
556
557
|
}
|
|
557
558
|
|
|
559
|
+
private fieldSpecIndex = new Map<string, number>()
|
|
560
|
+
private fieldSpecSlot(name: string, bits: number): number {
|
|
561
|
+
const key = JSON.stringify([name, bits])
|
|
562
|
+
const hit = this.fieldSpecIndex.get(key)
|
|
563
|
+
if (hit !== undefined) return hit
|
|
564
|
+
const i = this.k.length
|
|
565
|
+
this.k.push([name, bits])
|
|
566
|
+
this.fieldSpecIndex.set(key, i)
|
|
567
|
+
return i
|
|
568
|
+
}
|
|
569
|
+
|
|
558
570
|
/**
|
|
559
571
|
* A `notAdjacent({ kinds })` filter, pooled by its CONTENTS.
|
|
560
572
|
*
|
|
@@ -1518,8 +1530,12 @@ class Encoder {
|
|
|
1518
1530
|
for (let i = 0; i < arms.length; i++) this.code[head + 6 + i] = arms[i]!
|
|
1519
1531
|
return head
|
|
1520
1532
|
}
|
|
1521
|
-
case 'field':
|
|
1522
|
-
|
|
1533
|
+
case 'field': {
|
|
1534
|
+
// A trivia-attributing field carries `[name, bits]` in the name slot
|
|
1535
|
+
// (`FIELD_LEADING_TRIVIA`/`FIELD_TRAILING_TRIVIA`); a plain one the name.
|
|
1536
|
+
const bits = (d.leadingTrivia === true ? FIELD_LEADING_TRIVIA : 0) | (d.trailingTrivia === true ? FIELD_TRAILING_TRIVIA : 0)
|
|
1537
|
+
return this.emit(OP_FIELD, bits === 0 ? this.constant(d.name) : this.fieldSpecSlot(d.name, bits), this.node(d.parser).ip)
|
|
1538
|
+
}
|
|
1523
1539
|
case 'lazy': {
|
|
1524
1540
|
// A named reference is not a hop: it resolves to the target's row.
|
|
1525
1541
|
// Emitting a trampoline here cost one dispatch per reference for nothing.
|
package/src/table/exec.ts
CHANGED
|
@@ -72,6 +72,8 @@ import { scalarTerminalNotChild } from './scalar-terminal.ts'
|
|
|
72
72
|
* a successful match to the other engine.
|
|
73
73
|
*/
|
|
74
74
|
import { FAIL, newEndCell, type EndCell } from './cell.ts'
|
|
75
|
+
import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
|
|
76
|
+
import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA, LEAD, triviaFieldCapture } from '../combinators/field-trivia.ts'
|
|
75
77
|
|
|
76
78
|
/**
|
|
77
79
|
* DIAGNOSTIC ROW COUNTER — off unless `PM_TABLE_COUNT=1` at process start.
|
|
@@ -645,11 +647,21 @@ function makeDriver(
|
|
|
645
647
|
}
|
|
646
648
|
|
|
647
649
|
case OP_FIELD: {
|
|
650
|
+
const spec = fieldSpec(k[code[ip + 1]!])
|
|
651
|
+
const leadFrom = LEAD.at
|
|
652
|
+
if (spec.bits !== 0) LEAD.at = -1
|
|
648
653
|
const v = exec(code[ip + 2]!, input, pos, ctx)
|
|
649
654
|
if (v === FAIL) return FAIL
|
|
650
655
|
// Conditional on a live sink, exactly as src/combinators/map.ts has it:
|
|
651
656
|
// a `field()` outside any field-reading node costs nothing.
|
|
652
|
-
|
|
657
|
+
if (spec.bits === 0) {
|
|
658
|
+
ctx._fields?.push({ name: spec.name, value: v, span: { start: pos, end: EC.e } })
|
|
659
|
+
return v
|
|
660
|
+
}
|
|
661
|
+
const end = EC.e
|
|
662
|
+
const trailEnd = (spec.bits & FIELD_TRAILING_TRIVIA) !== 0 && ctx.trivia !== undefined ? consumeTrivia(input, end, ctx) : end
|
|
663
|
+
ctx._fields?.push(triviaFieldCapture(spec.name, v, input, pos, end, (spec.bits & FIELD_LEADING_TRIVIA) !== 0 ? leadFrom : -1, trailEnd))
|
|
664
|
+
EC.e = trailEnd
|
|
653
665
|
return v
|
|
654
666
|
}
|
|
655
667
|
|
|
@@ -885,6 +897,7 @@ function makeDriver(
|
|
|
885
897
|
const scanTl = need ? cstTlLen(ctx) : 0
|
|
886
898
|
const scanLog = need ? ctx._triviaLog?.length ?? 0 : 0
|
|
887
899
|
const scanRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
|
|
900
|
+
if (leadingTriviaFieldAt(code, k, child)) LEAD.at = cur
|
|
888
901
|
const v = exec(child, input, scanEnd, ctx)
|
|
889
902
|
if (v === FAIL) { if (REC) ctx._sync = inheritedSync; return FAIL }
|
|
890
903
|
if (EC.e > scanEnd) cur = EC.e
|
|
@@ -1256,6 +1269,7 @@ function makeDriver(
|
|
|
1256
1269
|
let sp = cur
|
|
1257
1270
|
if (hasTrivia) sp = skipTrivia(input, sp, ctx)
|
|
1258
1271
|
ctx._fc = false
|
|
1272
|
+
if (leadingTriviaFieldAt(code, k, sep)) LEAD.at = cur
|
|
1259
1273
|
const sv = exec(sep, input, sp, ctx)
|
|
1260
1274
|
if (sv === FAIL) {
|
|
1261
1275
|
if (needMark) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { OP_FIELD } from './ops.ts'
|
|
2
|
+
import { FIELD_LEADING_TRIVIA } from '../combinators/field-trivia.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An `OP_FIELD` name slot: the name, or `[name, bits]` for a field that
|
|
6
|
+
* attributes trivia (`FIELD_LEADING_TRIVIA` / `FIELD_TRAILING_TRIVIA`). A
|
|
7
|
+
* program printed before those options existed carries only names, and reads
|
|
8
|
+
* as bits 0.
|
|
9
|
+
*/
|
|
10
|
+
export function fieldSpec(slot: unknown): { readonly name: string; readonly bits: number } {
|
|
11
|
+
return Array.isArray(slot) ? { name: slot[0] as string, bits: slot[1] as number } : { name: slot as string, bits: 0 }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Is the instruction at `ip` a `field(…, { leadingTrivia: true })`? */
|
|
15
|
+
export function leadingTriviaFieldAt(code: ArrayLike<number>, k: readonly unknown[], ip: number): boolean {
|
|
16
|
+
return code[ip] === OP_FIELD && (fieldSpec(k[code[ip + 1]!]).bits & FIELD_LEADING_TRIVIA) !== 0
|
|
17
|
+
}
|
|
@@ -261,3 +261,28 @@ export function leadingLiteralFamily(
|
|
|
261
261
|
|
|
262
262
|
return walk(ip, 0)
|
|
263
263
|
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The literal terminals an arm of a LARGE ordered choice (more than three arms)
|
|
267
|
+
* may be pretested on before it is entered, or `undefined` when it has none.
|
|
268
|
+
*
|
|
269
|
+
* A pretest runs again when the arm then matches, so a large choice only takes
|
|
270
|
+
* literals of two or more characters: one `startsWith` is the price of skipping an
|
|
271
|
+
* arm that first-char dispatch could not exclude (`@{` and `@@` beside `@name`).
|
|
272
|
+
* A one-character literal is exactly what the dispatch mask already decided.
|
|
273
|
+
* The emitted and the closure engines both select through this function.
|
|
274
|
+
*/
|
|
275
|
+
export function largeChoiceLiteralPretest(
|
|
276
|
+
code: ArrayLike<number>, constants: readonly unknown[], armIp: number,
|
|
277
|
+
): readonly number[] | undefined {
|
|
278
|
+
const child = leadingScalarTerminal(code, armIp, 2, true, true)
|
|
279
|
+
if (child >= 0) {
|
|
280
|
+
const spec = constants[code[child + 1]!]
|
|
281
|
+
if (code[child] === OP_LIT && typeof spec === 'string' && spec.length >= 2) return [child]
|
|
282
|
+
}
|
|
283
|
+
const family = leadingLiteralFamily(code, constants, armIp)
|
|
284
|
+
return family !== undefined && family.length >= 2 && family.every(terminal => {
|
|
285
|
+
const value = constants[code[terminal + 1]!]
|
|
286
|
+
return typeof value === 'string' && value.length >= 2
|
|
287
|
+
}) ? family : undefined
|
|
288
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -83,7 +83,7 @@ export type ParserDef =
|
|
|
83
83
|
| { tag: 'routed'; fallback?: Combinator<unknown> }
|
|
84
84
|
| { tag: 'leaf'; parser: Combinator<unknown>; fn: (v: unknown, span: { start: number; end: number }) => unknown; fnSrc?: string; cstValue?: 'source' }
|
|
85
85
|
| { tag: 'label'; label: string; parser: Combinator<unknown> }
|
|
86
|
-
| { tag: 'field'; name: string; parser: Combinator<unknown
|
|
86
|
+
| { tag: 'field'; name: string; parser: Combinator<unknown>; leadingTrivia?: true; trailingTrivia?: true }
|
|
87
87
|
| { tag: 'grammar'; parser: Combinator<unknown>; triviaParser: Combinator<unknown> | undefined; clearTrivia?: boolean; captureTrivia?: boolean; rootCapture?: 'opaque'; trackLines: boolean; constructionTrackLines?: 'on' | 'off' | 'inherit'; constructionCaptureTriviaKinds?: readonly string[] }
|
|
88
88
|
| { tag: 'lazy'; thunk: () => Combinator<unknown> }
|
|
89
89
|
| { tag: 'not'; parser: Combinator<unknown> }
|
|
@@ -203,6 +203,16 @@ export type BuildHost = ((
|
|
|
203
203
|
export type FieldCapture<T = unknown> = {
|
|
204
204
|
value: T
|
|
205
205
|
span: Span
|
|
206
|
+
/**
|
|
207
|
+
* `field(…, { leadingTrivia: true })` only: the source text of the trivia the
|
|
208
|
+
* enclosing sequence or separated list skipped right before this field, or `''`.
|
|
209
|
+
*/
|
|
210
|
+
leading?: string
|
|
211
|
+
/**
|
|
212
|
+
* `field(…, { trailingTrivia: true })` only: the source text of the trivia the
|
|
213
|
+
* field consumed after its match, or `''`.
|
|
214
|
+
*/
|
|
215
|
+
trailing?: string
|
|
206
216
|
}
|
|
207
217
|
|
|
208
218
|
export type FieldMap = Record<string, FieldCapture | FieldCapture[]>
|
|
@@ -382,7 +392,7 @@ export type ParseContext = {
|
|
|
382
392
|
*/
|
|
383
393
|
_cstTriviaLog?: number[] | undefined
|
|
384
394
|
/** Framework-internal: active node() field captures, enabled only when needed. */
|
|
385
|
-
_fields?: Array<{ name: string; value: unknown; span: Span }> | undefined
|
|
395
|
+
_fields?: Array<{ name: string; value: unknown; span: Span; leading?: string; trailing?: string }> | undefined
|
|
386
396
|
/** Framework-internal: lazy capture buffer for active node() parse. */
|
|
387
397
|
_cstBuf?: CstCaptureBuf | undefined
|
|
388
398
|
/** Framework-internal: value/span already consumed by an enclosing dispatch(). */
|
package/src/version.ts
CHANGED