parseman 0.60.0 → 0.60.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/diagnostics.js +3 -3
- package/dist/analysis/diagnostics.js.map +1 -1
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/plugin/index.d.ts +18 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +115 -47
- package/dist/plugin/index.js.map +1 -1
- package/dist/table/assemble.d.ts +1 -1
- package/dist/table/compile-rule-map.d.ts +1 -1
- package/dist/table/compile.d.ts +1 -1
- package/dist/table/exec.d.ts +0 -35
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/index.js +3 -3
- package/dist/table/index.js.map +1 -1
- package/dist/table/ops.d.ts +1 -10
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -4
- package/src/plugin/index.ts +189 -63
- package/src/table/assemble.ts +5 -5
- package/src/table/compile-rule-map.ts +1 -1
- package/src/table/compile.ts +1 -1
- package/src/table/emit-assembly.ts +2 -2
- package/src/table/encode.ts +1 -1
- package/src/table/exec.ts +0 -69
- package/src/table/ops.ts +1 -10
- package/src/version.ts +1 -1
- package/dist/table/encode-baseline.d.ts +0 -16
- package/dist/table/encode-baseline.d.ts.map +0 -1
- package/dist/table/exec-baseline.d.ts +0 -9
- package/dist/table/exec-baseline.d.ts.map +0 -1
- package/src/table/encode-baseline.ts +0 -346
- package/src/table/exec-baseline.ts +0 -557
package/src/table/exec.ts
CHANGED
|
@@ -75,60 +75,6 @@ import { FAIL, newEndCell, type EndCell } from './cell.ts'
|
|
|
75
75
|
import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
|
|
76
76
|
import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA, LEAD, triviaFieldCapture } from '../combinators/field-trivia.ts'
|
|
77
77
|
|
|
78
|
-
/**
|
|
79
|
-
* DIAGNOSTIC ROW COUNTER — off unless `PM_TABLE_COUNT=1` at process start.
|
|
80
|
-
*
|
|
81
|
-
* Read ONCE, at module load, into a module const, so this is not the per-parse
|
|
82
|
-
* option branch G5 forbids: with the variable false the whole thing folds away
|
|
83
|
-
* after tier-up. It exists because "too many rows" and "each row too slow" are
|
|
84
|
-
* different defects with the same symptom, and only a count separates them.
|
|
85
|
-
* No timing run in this repo may set it — see `bench/jess/table-rows.ts`.
|
|
86
|
-
*/
|
|
87
|
-
export const tableCounters: {
|
|
88
|
-
rows: number
|
|
89
|
-
byOp: Int32Array
|
|
90
|
-
/** Distinct reducer/host functions actually reaching each shared call site. */
|
|
91
|
-
sites: Map<string, Set<unknown>>
|
|
92
|
-
/**
|
|
93
|
-
* SPECULATION AT UNGATED CHOICES. `encode.ts:368-376` gives a choice its
|
|
94
|
-
* first-char dispatch only when NO arm is nullable, ALL arms are pairwise
|
|
95
|
-
* disjoint and ALL map to a char class; any one failure ungates every arm of
|
|
96
|
-
* the site, which then runs the linear loop below. Codegen has no such
|
|
97
|
-
* all-or-nothing rule — `emitFirstMatch` guards each arm on its OWN first set
|
|
98
|
-
* — so these counts are work the table does and the compiled engine does not.
|
|
99
|
-
*
|
|
100
|
-
* `ungatedEntries` is arms entered there; `ungatedFails` is those that then
|
|
101
|
-
* failed, which is pure waste by construction: a failing arm consumed nothing
|
|
102
|
-
* the winning arm will not scan again.
|
|
103
|
-
*/
|
|
104
|
-
ungatedEntries: number
|
|
105
|
-
ungatedFails: number
|
|
106
|
-
/** Rows executed INSIDE failed ungated arms — the subtree each wasted entry drags. */
|
|
107
|
-
ungatedFailRows: number
|
|
108
|
-
gatedEntries: number
|
|
109
|
-
/** Arm entries DECLINED by the per-arm class gate — the work this change removes. */
|
|
110
|
-
armGateSkips: number
|
|
111
|
-
} = { rows: 0, byOp: new Int32Array(64), sites: new Map(), ungatedEntries: 0, ungatedFails: 0, ungatedFailRows: 0, gatedEntries: 0, armGateSkips: 0 }
|
|
112
|
-
|
|
113
|
-
const COUNT = process.env.PM_TABLE_COUNT === '1'
|
|
114
|
-
|
|
115
|
-
function siteFn(site: string, fn: unknown): void {
|
|
116
|
-
let s = tableCounters.sites.get(site)
|
|
117
|
-
if (s === undefined) { s = new Set(); tableCounters.sites.set(site, s) }
|
|
118
|
-
s.add(fn)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function resetTableCounters(): void {
|
|
122
|
-
tableCounters.rows = 0
|
|
123
|
-
tableCounters.byOp = new Int32Array(64)
|
|
124
|
-
tableCounters.sites = new Map()
|
|
125
|
-
tableCounters.ungatedEntries = 0
|
|
126
|
-
tableCounters.ungatedFails = 0
|
|
127
|
-
tableCounters.ungatedFailRows = 0
|
|
128
|
-
tableCounters.gatedEntries = 0
|
|
129
|
-
tableCounters.armGateSkips = 0
|
|
130
|
-
}
|
|
131
|
-
|
|
132
78
|
const EMPTY_TL: readonly number[] = Object.freeze([])
|
|
133
79
|
const EMPTY_FX: string[] = []
|
|
134
80
|
const ROUTED_FX: string[] = ['routed()']
|
|
@@ -380,7 +326,6 @@ function makeDriver(
|
|
|
380
326
|
// recursing, so the reference driver's stack grows with the grammar's
|
|
381
327
|
// nesting, not with its gate rows.
|
|
382
328
|
rows: for (;;) {
|
|
383
|
-
if (COUNT) { tableCounters.rows++; tableCounters.byOp[code[ip]!]!++ }
|
|
384
329
|
switch (code[ip]) {
|
|
385
330
|
case OP_LIT: {
|
|
386
331
|
const s = k[code[ip + 1]!] as string
|
|
@@ -891,7 +836,6 @@ function makeDriver(
|
|
|
891
836
|
// Running it through `exec` at the POST-scan position would find the gap
|
|
892
837
|
// already consumed and answer "adjacent" unconditionally.
|
|
893
838
|
if (code[child] === OP_ADJ) {
|
|
894
|
-
if (COUNT) { tableCounters.rows++; tableCounters.byOp[OP_ADJ]!++ }
|
|
895
839
|
const ki = code[child + 2]!
|
|
896
840
|
if (!adjacencyHolds(input, cur, ctx, code[child + 1] === 1, ki < 0 ? undefined : k[ki] as readonly string[])) {
|
|
897
841
|
ctx._fe = cur; ctx._fx = fx[code[child + 3]!] as string[]
|
|
@@ -927,10 +871,6 @@ function makeDriver(
|
|
|
927
871
|
// LIT/RX in place removes both. The duplication is in the DRIVER,
|
|
928
872
|
// which ships once for every grammar — the cost this design trades on.
|
|
929
873
|
const cop = code[child]
|
|
930
|
-
// The two inline terminals below execute a ROW without going through
|
|
931
|
-
// `exec`, so the counter has to see them or the row count understates
|
|
932
|
-
// exactly where the driver already won.
|
|
933
|
-
if (COUNT && (cop === OP_LIT || cop === OP_RX)) { tableCounters.rows++; tableCounters.byOp[cop]!++ }
|
|
934
874
|
if (cop === OP_LIT) {
|
|
935
875
|
const lit = k[code[child + 1]!] as string
|
|
936
876
|
if (!input.startsWith(lit, cur)) {
|
|
@@ -971,7 +911,6 @@ function makeDriver(
|
|
|
971
911
|
const reducer = code[ip + 1]!
|
|
972
912
|
if (reducer < 0) return values![~reducer]
|
|
973
913
|
const fn = fns[reducer] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
974
|
-
if (COUNT) siteFn('SEQX fn()', fn)
|
|
975
914
|
return fn(values, { start: pos, end: cur })
|
|
976
915
|
}
|
|
977
916
|
return values
|
|
@@ -1003,7 +942,6 @@ function makeDriver(
|
|
|
1003
942
|
}
|
|
1004
943
|
if (arm >= 0) {
|
|
1005
944
|
ctx._fc = false
|
|
1006
|
-
if (COUNT) tableCounters.gatedEntries++
|
|
1007
945
|
const v = exec(code[base + arm]!, input, pos, ctx)
|
|
1008
946
|
if (v !== FAIL) return v
|
|
1009
947
|
// THE CUT. `dispatch()` is the library's one true cut: once its
|
|
@@ -1085,7 +1023,6 @@ function makeDriver(
|
|
|
1085
1023
|
// Under a completion probe every arm is entered, as the interpreter's
|
|
1086
1024
|
// `firstMatch` enters it: its opener then reaches the probe (`failAt`).
|
|
1087
1025
|
if (ctx._probe === undefined && cls !== undefined && cls !== null && !classHas(cls, c)) {
|
|
1088
|
-
if (COUNT) tableCounters.armGateSkips++
|
|
1089
1026
|
// THE GATED ARM STILL REPORTS. The interpreter has no char gate: it
|
|
1090
1027
|
// enters the arm, the arm's opener fails, and its static set is what
|
|
1091
1028
|
// lands in the accumulation. Declining to enter must not also decline
|
|
@@ -1094,11 +1031,8 @@ function makeDriver(
|
|
|
1094
1031
|
continue
|
|
1095
1032
|
}
|
|
1096
1033
|
ctx._fc = false
|
|
1097
|
-
if (COUNT) tableCounters.ungatedEntries++
|
|
1098
|
-
const rows0 = COUNT ? tableCounters.rows : 0
|
|
1099
1034
|
const v = exec(code[base + i]!, input, pos, ctx)
|
|
1100
1035
|
if (v !== FAIL) return v
|
|
1101
|
-
if (COUNT) { tableCounters.ungatedFails++; tableCounters.ungatedFailRows += tableCounters.rows - rows0 }
|
|
1102
1036
|
const at = ctx._fe ?? pos
|
|
1103
1037
|
if (at > best) { best = at; acc = undefined }
|
|
1104
1038
|
if (at === best) push(ctx._fx)
|
|
@@ -1421,7 +1355,6 @@ function makeDriver(
|
|
|
1421
1355
|
const v = exec(code[ip + 2]!, input, pos, ctx)
|
|
1422
1356
|
if (v === FAIL) return FAIL
|
|
1423
1357
|
const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
1424
|
-
if (COUNT) siteFn('XFORM fn()', fn)
|
|
1425
1358
|
return fn(v, { start: pos, end: EC.e })
|
|
1426
1359
|
}
|
|
1427
1360
|
|
|
@@ -1463,7 +1396,6 @@ function makeDriver(
|
|
|
1463
1396
|
if (v === FAIL) return FAIL
|
|
1464
1397
|
const end = EC.e
|
|
1465
1398
|
const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
1466
|
-
if (COUNT) siteFn('LEAF fn()', fn)
|
|
1467
1399
|
const out = fn(v, { start: pos, end })
|
|
1468
1400
|
if (wasCapturing) pushCstLeaf(ctx, {
|
|
1469
1401
|
_tag: 'leaf',
|
|
@@ -1566,7 +1498,6 @@ function makeDriver(
|
|
|
1566
1498
|
children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
|
|
1567
1499
|
rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
|
|
1568
1500
|
) => unknown
|
|
1569
|
-
if (COUNT) siteFn('NODE build()', build)
|
|
1570
1501
|
nd = build(kids, fieldMap, span, cap.rawChildren, (flags & 4) !== 0 || hostCst ? cap.triviaLog : EMPTY_TL, st)
|
|
1571
1502
|
}
|
|
1572
1503
|
} else if (host !== undefined) {
|
package/src/table/ops.ts
CHANGED
|
@@ -74,13 +74,7 @@ export const OP_XFORM = 9
|
|
|
74
74
|
* from the reducer's declared arity by the same analysis codegen runs, and
|
|
75
75
|
* forced on under `hostMode: 'cst'`. The driver reads the bits and re-derives
|
|
76
76
|
* nothing.
|
|
77
|
-
|
|
78
|
-
* TWO ENCODERS SHARE THIS OPCODE and they do NOT agree on its length. The layout
|
|
79
|
-
* above is `encode.ts` / `exec.ts`. `encode-baseline.ts` / `exec-baseline.ts` emit
|
|
80
|
-
* the FOUR-word form `NODE b c flags` with bits 2 and 3 only, and refuse
|
|
81
|
-
* `collapse` / `unwrap` / `project` outright. Neither driver walks the stream by
|
|
82
|
-
* instruction length, so the divergence is contained — but read the pair you are
|
|
83
|
-
* in, not this comment alone. */
|
|
77
|
+
*/
|
|
84
78
|
export const OP_NODE = 10
|
|
85
79
|
/** `RULE r` — `r` indexes `prog.rules`. */
|
|
86
80
|
export const OP_RULE = 11
|
|
@@ -93,9 +87,6 @@ export const OP_NOT = 13
|
|
|
93
87
|
* `['peek(<child tag>)']`, because `peek.ts:60` DISCARDS the body's expectation:
|
|
94
88
|
* a lookahead's failure is "the guard did not hold", not a request for whatever
|
|
95
89
|
* token the body happened to stop on.
|
|
96
|
-
*
|
|
97
|
-
* `encode-baseline.ts` / `exec-baseline.ts` emit and read the two-word form and
|
|
98
|
-
* are internally consistent; read the pair you are in.
|
|
99
90
|
*/
|
|
100
91
|
export const OP_PEEK = 14
|
|
101
92
|
/** `LEAF f c` — `leaf()`: reduce, then record as a spanned leaf. */
|
package/src/version.ts
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { Combinator } from '../types.ts';
|
|
2
|
-
import type { TableProgram } from './program.ts';
|
|
3
|
-
/** Settings that select the TABLE'S CONTENTS. They never reach the driver. */
|
|
4
|
-
export type TableSettings = {
|
|
5
|
-
readonly hostMode?: 'ast' | 'cst';
|
|
6
|
-
readonly trackLines?: boolean;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Encode a rule map into ONE table for ONE settings pair.
|
|
10
|
-
*
|
|
11
|
-
* Two settings pairs give two programs and therefore two cached reference
|
|
12
|
-
* tables; the driver that reads them is the same function in both cases and
|
|
13
|
-
* never sees `settings`.
|
|
14
|
-
*/
|
|
15
|
-
export declare function encodeTableBaseline(ruleMap: Record<string, Combinator<unknown>>, settings?: TableSettings): TableProgram;
|
|
16
|
-
//# sourceMappingURL=encode-baseline.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"encode-baseline.d.ts","sourceRoot":"","sources":["../../src/table/encode-baseline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAuB,MAAM,aAAa,CAAA;AAUlE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAQhD,8EAA8E;AAC9E,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;IACjC,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAC9B,CAAA;AAqTD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC,EAC5C,QAAQ,GAAE,aAAkB,GAC3B,YAAY,CAId"}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type CompactProgram, type TableProgram, type TableRule } from './program.ts';
|
|
2
|
-
/**
|
|
3
|
-
* Turn a program into the rule map a compiled artifact exports.
|
|
4
|
-
*
|
|
5
|
-
* The entries have the SAME signature as codegen rule functions, so `run()`,
|
|
6
|
-
* the linker's public wrappers and every consumer are unchanged.
|
|
7
|
-
*/
|
|
8
|
-
export declare function execRulesBaseline(source: TableProgram | CompactProgram): Record<string, TableRule>;
|
|
9
|
-
//# sourceMappingURL=exec-baseline.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"exec-baseline.d.ts","sourceRoot":"","sources":["../../src/table/exec-baseline.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,cAAc,EAA6C,KAAK,YAAY,EAAE,KAAK,SAAS,EAClG,MAAM,cAAc,CAAA;AAggBrB;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAwBlG"}
|
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import type { Combinator, FirstSet, ParserDef } from '../types.ts'
|
|
2
|
-
import { firstSetOf, matchesEmpty } from '../combinators/first-set.ts'
|
|
3
|
-
import { leadRanges } from '../lead-ranges.ts'
|
|
4
|
-
import { deriveExpected } from '../combinators/expect.ts'
|
|
5
|
-
import { buildReadsState, buildReadsTrivia } from '../compiler/build-arity.ts'
|
|
6
|
-
import {
|
|
7
|
-
OP_CHOICE, OP_EMPTY, OP_GATE, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_NODE, OP_NOT, OP_OPT,
|
|
8
|
-
OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SEQ, OP_SEQV, OP_XFORM,
|
|
9
|
-
OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_EXPECT,
|
|
10
|
-
} from './ops.ts'
|
|
11
|
-
import type { TableProgram } from './program.ts'
|
|
12
|
-
|
|
13
|
-
/** Raised when a construct has no opcode yet. Prototype scope is explicit. */
|
|
14
|
-
class UnsupportedConstructBaseline extends Error {
|
|
15
|
-
readonly tag: string
|
|
16
|
-
constructor(tag: string) { super(`table lowering: no opcode for '${tag}'`); this.tag = tag }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Settings that select the TABLE'S CONTENTS. They never reach the driver. */
|
|
20
|
-
export type TableSettings = {
|
|
21
|
-
readonly hostMode?: 'ast' | 'cst'
|
|
22
|
-
readonly trackLines?: boolean
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type Emitted = { readonly ip: number }
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Do these first sets share no character?
|
|
29
|
-
*
|
|
30
|
-
* `any` and `empty` are never disjoint from anything — an over-approximating arm
|
|
31
|
-
* has to fall back to ordered first-match or the choice silently picks the wrong
|
|
32
|
-
* one. See the `Keyword`-ahead-of-`Num` case in the choice encoder.
|
|
33
|
-
*/
|
|
34
|
-
function disjointSets(sets: readonly FirstSet[]): boolean {
|
|
35
|
-
const seen: Array<{ lo: number; hi: number }> = []
|
|
36
|
-
for (const fs of sets) {
|
|
37
|
-
if (fs.kind !== 'ranges' || fs.ranges.length === 0) return false
|
|
38
|
-
for (const r of leadRanges(fs.ranges)) {
|
|
39
|
-
for (const p of seen) if (r.lo <= p.hi && p.lo <= r.hi) return false
|
|
40
|
-
seen.push(r)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return true
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
class Encoder {
|
|
47
|
-
code: number[] = []
|
|
48
|
-
k: unknown[] = []
|
|
49
|
-
fns: unknown[] = []
|
|
50
|
-
cc: string[] = []
|
|
51
|
-
fx: (readonly string[])[] = []
|
|
52
|
-
disp: (readonly number[])[] = []
|
|
53
|
-
rules: Record<string, number> = {}
|
|
54
|
-
|
|
55
|
-
private kIndex = new Map<unknown, number>()
|
|
56
|
-
private ccIndex = new Map<string, number>()
|
|
57
|
-
private fxIndex = new Map<string, number>()
|
|
58
|
-
/** Memoized by combinator identity: a shared sub-combinator is ONE row. */
|
|
59
|
-
private memo = new Map<Combinator<unknown>, number>()
|
|
60
|
-
private pending = new Map<Combinator<unknown>, number[]>()
|
|
61
|
-
|
|
62
|
-
readonly settings: TableSettings
|
|
63
|
-
/** Resolved once, HERE, at table-build time — never consulted at run time. */
|
|
64
|
-
readonly track: boolean
|
|
65
|
-
constructor(settings: TableSettings) {
|
|
66
|
-
this.settings = settings
|
|
67
|
-
this.track = settings.trackLines === true
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private constant(v: unknown): number {
|
|
71
|
-
const key = v instanceof RegExp ? `re:${JSON.stringify([v.source, v.flags])}` : v
|
|
72
|
-
const hit = this.kIndex.get(key)
|
|
73
|
-
if (hit !== undefined) return hit
|
|
74
|
-
const i = this.k.length
|
|
75
|
-
this.k.push(v)
|
|
76
|
-
this.kIndex.set(key, i)
|
|
77
|
-
return i
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private fn(v: unknown): number {
|
|
81
|
-
const i = this.fns.length
|
|
82
|
-
this.fns.push(v)
|
|
83
|
-
return i
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private expected(list: readonly string[]): number {
|
|
87
|
-
const key = JSON.stringify(list)
|
|
88
|
-
const hit = this.fxIndex.get(key)
|
|
89
|
-
if (hit !== undefined) return hit
|
|
90
|
-
const i = this.fx.length
|
|
91
|
-
this.fx.push(list)
|
|
92
|
-
this.fxIndex.set(key, i)
|
|
93
|
-
return i
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** A first set becomes a char class STRING of `[lo, hi]` pairs, or −1 for `any`. */
|
|
97
|
-
private charClass(fs: FirstSet): number {
|
|
98
|
-
if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
|
|
99
|
-
let spec = ''
|
|
100
|
-
for (const r of fs.ranges) spec += String.fromCharCode(r.lo, r.hi)
|
|
101
|
-
const hit = this.ccIndex.get(spec)
|
|
102
|
-
if (hit !== undefined) return hit
|
|
103
|
-
const i = this.cc.length
|
|
104
|
-
this.cc.push(spec)
|
|
105
|
-
this.ccIndex.set(spec, i)
|
|
106
|
-
return i
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private emit(...words: number[]): number {
|
|
110
|
-
const ip = this.code.length
|
|
111
|
-
for (const w of words) this.code.push(w)
|
|
112
|
-
return ip
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Reserve `n` operand slots to patch once children are laid out. */
|
|
116
|
-
private emitHead(op: number, n: number): number {
|
|
117
|
-
const ip = this.code.length
|
|
118
|
-
this.code.push(op)
|
|
119
|
-
for (let i = 0; i < n; i++) this.code.push(0)
|
|
120
|
-
return ip
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
encodeRule(name: string, p: Combinator<unknown>): void {
|
|
124
|
-
this.rules[name] = this.node(p).ip
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
node(p: Combinator<unknown>): Emitted {
|
|
128
|
-
const hit = this.memo.get(p)
|
|
129
|
-
if (hit !== undefined) return { ip: hit }
|
|
130
|
-
// Recursion: reserve a trampoline row now, patch its target when the real
|
|
131
|
-
// body lands. One extra indirection per recursive rule, zero per call site.
|
|
132
|
-
const inFlight = this.pending.get(p)
|
|
133
|
-
if (inFlight !== undefined) {
|
|
134
|
-
const ip = this.emitHead(OP_RULE, 1)
|
|
135
|
-
inFlight.push(ip + 1)
|
|
136
|
-
return { ip }
|
|
137
|
-
}
|
|
138
|
-
const patches: number[] = []
|
|
139
|
-
this.pending.set(p, patches)
|
|
140
|
-
const ip = this.encodeDef(p)
|
|
141
|
-
this.pending.delete(p)
|
|
142
|
-
this.memo.set(p, ip)
|
|
143
|
-
for (const slot of patches) this.code[slot] = ip
|
|
144
|
-
return { ip }
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private encodeDef(p: Combinator<unknown>): number {
|
|
148
|
-
const d = p._def as ParserDef
|
|
149
|
-
// REFUSE what this frozen copy cannot lower, rather than lowering it wrong.
|
|
150
|
-
//
|
|
151
|
-
// `scanTo` and `token` have no `case` below, so they reach `default:` and throw
|
|
152
|
-
// — a refusal, which is safe. `balanced()` does NOT: it overrides `.parse` and
|
|
153
|
-
// leaves `_def` as the EAGER INTERIOR with no distinguishing tag, so the switch
|
|
154
|
-
// would encode the interior, report nothing, and produce a table that parses and
|
|
155
|
-
// yields a different tree. An ablation measurement taken on a grammar using
|
|
156
|
-
// `balanced()` would then be comparing against a parser that is not the one
|
|
157
|
-
// under test. Refused explicitly, since this copy has no OP_CALL to fall back on.
|
|
158
|
-
if ((p as { _balancedAmbient?: unknown })._balancedAmbient !== undefined) {
|
|
159
|
-
throw new UnsupportedConstructBaseline('balanced() — the frozen ablation encoder cannot lower it')
|
|
160
|
-
}
|
|
161
|
-
switch (d.tag) {
|
|
162
|
-
case 'literal': {
|
|
163
|
-
if (d.caseInsensitive) throw new UnsupportedConstructBaseline('literal(caseInsensitive)')
|
|
164
|
-
return this.emit(this.track ? OP_LIT_TRACK : OP_LIT, this.constant(d.value), this.expected(deriveExpected(p)))
|
|
165
|
-
}
|
|
166
|
-
case 'regex': {
|
|
167
|
-
const flags = d.flags.includes('y') ? d.flags : `${d.flags}y`
|
|
168
|
-
return this.emit(this.track ? OP_RX_TRACK : OP_RX, this.constant(new RegExp(d.source, flags)), this.expected(deriveExpected(p)))
|
|
169
|
-
}
|
|
170
|
-
case 'keywords': {
|
|
171
|
-
// `keywords()` compiles to ONE sticky regex and pushes a leaf — which is
|
|
172
|
-
// exactly what OP_RX does. Rebuilding the same regex here needs no new
|
|
173
|
-
// opcode. Construction mirrors src/combinators/keywords.ts:87-106; the
|
|
174
|
-
// words arrive already sorted longest-first on the def.
|
|
175
|
-
const alt = d.words.map(w => w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
|
|
176
|
-
const boundary = d.boundary ? `(?![${d.boundary}])` : ''
|
|
177
|
-
const flags = d.caseInsensitive ? 'iy' : 'uy'
|
|
178
|
-
return this.emit(
|
|
179
|
-
this.track ? OP_RX_TRACK : OP_RX,
|
|
180
|
-
this.constant(new RegExp(`(?:${alt})${boundary}`, flags)),
|
|
181
|
-
this.expected(deriveExpected(p)),
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
case 'expect': {
|
|
185
|
-
// `expect()` NEVER fails: on a failed child it yields a ParseError value
|
|
186
|
-
// at zero width. That is a distinct outcome, so it gets its own row.
|
|
187
|
-
return this.emit(OP_EXPECT, this.node(d.parser).ip, this.expected([...d.expected]))
|
|
188
|
-
}
|
|
189
|
-
case 'sequence': {
|
|
190
|
-
const kids = d.parsers.map(c => this.node(c).ip)
|
|
191
|
-
const head = this.emitHead(d.valueUnused ? OP_SEQV : OP_SEQ, 1 + kids.length)
|
|
192
|
-
this.code[head + 1] = kids.length
|
|
193
|
-
for (let i = 0; i < kids.length; i++) this.code[head + 2 + i] = kids[i]!
|
|
194
|
-
return head
|
|
195
|
-
}
|
|
196
|
-
case 'choice': {
|
|
197
|
-
if (d.gates.some(g => g !== null)) throw new UnsupportedConstructBaseline('choice(gate:)')
|
|
198
|
-
// Arm ORDER is semantics. `literalsLongestFirst` / `sharedPrefix` /
|
|
199
|
-
// `greedyClassify` reorder or restructure the arms, so encoding them as
|
|
200
|
-
// a plain ordered choice would pick a different arm and build a
|
|
201
|
-
// different tree while still parsing. Refuse them rather than lower them
|
|
202
|
-
// wrong.
|
|
203
|
-
// `greedyClassify` runs ONE arm and then re-attributes the match to a
|
|
204
|
-
// DIFFERENT arm by string equality, re-applying that arm's transforms.
|
|
205
|
-
// That is a different execution, not a different order, so it is refused
|
|
206
|
-
// rather than approximated.
|
|
207
|
-
if (d.strategy.tag === 'greedyClassify') throw new UnsupportedConstructBaseline('choice(strategy=greedyClassify)')
|
|
208
|
-
// `autoNot` rejects an arm that matched but is followed by a char in a
|
|
209
|
-
// sibling's first set, so a later arm can win. Ignoring it would pick a
|
|
210
|
-
// different arm and build a different tree behind a successful parse.
|
|
211
|
-
if (d.autoNot.some(a => a !== null)) throw new UnsupportedConstructBaseline('choice(autoNot)')
|
|
212
|
-
// ORDER is the only thing the remaining strategies change, and order is
|
|
213
|
-
// table data. `literalsLongestFirst` carries its order explicitly;
|
|
214
|
-
// `sharedPrefix` is documented in choice.ts:52 as a firstMatch
|
|
215
|
-
// specialization, so declared order is already correct for it.
|
|
216
|
-
const order = d.strategy.tag === 'literalsLongestFirst'
|
|
217
|
-
? d.strategy.sortedIndices
|
|
218
|
-
: d.parsers.map((_, i) => i)
|
|
219
|
-
const arms = order.map(i => d.parsers[i]!)
|
|
220
|
-
const kids = arms.map(c => this.node(c).ip)
|
|
221
|
-
// O(1) first-char dispatch is only SOUND when the arms are disjoint.
|
|
222
|
-
// With overlapping first sets, "the first arm whose class contains this
|
|
223
|
-
// char" is not "the first arm that matches": a `Keyword` arm whose first
|
|
224
|
-
// set over-approximates to include digits was selected ahead of `Num`
|
|
225
|
-
// for the input `0` — the parse still succeeded, and only the TREE moved.
|
|
226
|
-
// Disjointness is computed HERE, not read off `d.disjoint`. That flag is
|
|
227
|
-
// set when `choice()` is CONSTRUCTED, at which point `g.X` arms are
|
|
228
|
-
// unresolved refs whose first set is `any` — so every recursive grammar
|
|
229
|
-
// reports non-disjoint and loses its dispatch. (Codegen has the same
|
|
230
|
-
// problem and solves it the same way: a placeholder resolved at fuse.)
|
|
231
|
-
let dispIdx = -1
|
|
232
|
-
if (arms.every(a => !matchesEmpty(a))) {
|
|
233
|
-
const sets = arms.map(a => firstSetOf(a))
|
|
234
|
-
if (disjointSets(sets)) {
|
|
235
|
-
const classes = sets.map(fs => this.charClass(fs))
|
|
236
|
-
if (classes.every(c => c >= 0)) {
|
|
237
|
-
dispIdx = this.disp.length
|
|
238
|
-
this.disp.push(classes)
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const head = this.emitHead(OP_CHOICE, 2 + kids.length)
|
|
243
|
-
this.code[head + 1] = dispIdx
|
|
244
|
-
this.code[head + 2] = kids.length
|
|
245
|
-
for (let i = 0; i < kids.length; i++) this.code[head + 3 + i] = kids[i]!
|
|
246
|
-
return head
|
|
247
|
-
}
|
|
248
|
-
case 'many':
|
|
249
|
-
case 'oneOrMore': {
|
|
250
|
-
const child = this.node(d.parser).ip
|
|
251
|
-
return this.emit(
|
|
252
|
-
d.valueUnused ? OP_REPV : OP_REP,
|
|
253
|
-
child, d.tag === 'many' ? 0 : d.min, d.max ?? -1, -1, 0,
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
case 'sepBy': {
|
|
257
|
-
const child = this.node(d.parser).ip
|
|
258
|
-
const sep = this.node(d.separator).ip
|
|
259
|
-
return this.emit(OP_REP, child, d.min, d.max ?? -1, sep, d.trailing === 'allow' ? 1 : 0)
|
|
260
|
-
}
|
|
261
|
-
case 'optional':
|
|
262
|
-
return this.emit(OP_OPT, this.node(d.parser).ip)
|
|
263
|
-
case 'transform': {
|
|
264
|
-
const child = this.node(d.parser).ip
|
|
265
|
-
return this.emit(OP_XFORM, this.fn(d.fn), child)
|
|
266
|
-
}
|
|
267
|
-
case 'leaf': {
|
|
268
|
-
const child = this.node(d.parser).ip
|
|
269
|
-
return this.emit(d.cstValue === 'source' ? OP_SOURCE_LEAF : OP_LEAF, this.fn(d.fn), child)
|
|
270
|
-
}
|
|
271
|
-
case 'node': {
|
|
272
|
-
if (d.unwrap || d.collapse || d.project !== undefined) throw new UnsupportedConstructBaseline('node(unwrap|collapse|project)')
|
|
273
|
-
if (d.build === undefined) throw new UnsupportedConstructBaseline('node(no build)')
|
|
274
|
-
const child = this.node(d.parser).ip
|
|
275
|
-
// Capture flags, resolved HERE from the reducer's declared arity using the
|
|
276
|
-
// same analysis codegen runs (`src/compiler/build-arity.ts`). `hostMode:
|
|
277
|
-
// 'cst'` forces them on, exactly as the emitted `cstOut` path does. The
|
|
278
|
-
// driver reads a bit; it re-derives nothing and sees no setting.
|
|
279
|
-
const cstOut = this.settings.hostMode === 'cst'
|
|
280
|
-
const flags = (cstOut || buildReadsTrivia(d) ? 4 : 0) | (cstOut || buildReadsState(d) ? 8 : 0)
|
|
281
|
-
const body = this.emit(this.track ? OP_NODE_TRACK : OP_NODE, this.fn(d.build), child, flags)
|
|
282
|
-
// The rule's own first-set gate — the emitted code's `_ngc` test, as data.
|
|
283
|
-
// A NULLABLE rule has no gate: it succeeds on input its first set does
|
|
284
|
-
// not contain (including EOF), so gating it would reject a legal empty
|
|
285
|
-
// match. Caught by the ladder's `empty` and `garbage` cases.
|
|
286
|
-
if (matchesEmpty(p)) return body
|
|
287
|
-
const cls = this.charClass(firstSetOf(p))
|
|
288
|
-
if (cls < 0) return body
|
|
289
|
-
return this.emit(OP_GATE, cls, body, this.expected(deriveExpected(p)))
|
|
290
|
-
}
|
|
291
|
-
case 'lazy':
|
|
292
|
-
// A named reference is not a hop: it resolves to the target's row.
|
|
293
|
-
// Emitting a trampoline here cost one dispatch per reference for nothing.
|
|
294
|
-
return this.node(d.thunk()).ip
|
|
295
|
-
case 'not':
|
|
296
|
-
return this.emit(OP_NOT, this.node(d.parser).ip)
|
|
297
|
-
case 'peek':
|
|
298
|
-
return this.emit(OP_PEEK, this.node(d.parser).ip)
|
|
299
|
-
// A contained transaction changes what an enclosing choice does next, so
|
|
300
|
-
// it cannot be transparent.
|
|
301
|
-
case 'attempt':
|
|
302
|
-
if (d.contain === true) throw new UnsupportedConstructBaseline('attempt(contain)')
|
|
303
|
-
return this.node(d.parser).ip
|
|
304
|
-
// Transparent wrappers: no row of their own, no dispatch at run time.
|
|
305
|
-
case 'token':
|
|
306
|
-
case 'label':
|
|
307
|
-
case 'trivia':
|
|
308
|
-
return this.node(d.parser).ip
|
|
309
|
-
case 'grammar': {
|
|
310
|
-
// A trivia scope is a ROW, not a lowering decision: the scope's trivia
|
|
311
|
-
// combinator goes in the const pool and the driver installs it.
|
|
312
|
-
if (d.clearTrivia === true) return this.emit(OP_SCOPE, -1, this.node(d.parser).ip)
|
|
313
|
-
if (d.triviaParser === undefined) return this.node(d.parser).ip
|
|
314
|
-
return this.emit(OP_SCOPE, this.constant(d.triviaParser), this.node(d.parser).ip)
|
|
315
|
-
}
|
|
316
|
-
default:
|
|
317
|
-
throw new UnsupportedConstructBaseline(d.tag)
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
finish(): TableProgram {
|
|
322
|
-
if (this.code.length === 0) this.emit(OP_EMPTY)
|
|
323
|
-
return {
|
|
324
|
-
code: this.code, k: this.k, fns: this.fns, cc: this.cc,
|
|
325
|
-
fx: this.fx, disp: this.disp, dsp: [], rules: this.rules,
|
|
326
|
-
lines: this.track ? 1 : 0,
|
|
327
|
-
...(this.settings.hostMode === undefined ? {} : { hostMode: this.settings.hostMode }),
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Encode a rule map into ONE table for ONE settings pair.
|
|
334
|
-
*
|
|
335
|
-
* Two settings pairs give two programs and therefore two cached reference
|
|
336
|
-
* tables; the driver that reads them is the same function in both cases and
|
|
337
|
-
* never sees `settings`.
|
|
338
|
-
*/
|
|
339
|
-
export function encodeTableBaseline(
|
|
340
|
-
ruleMap: Record<string, Combinator<unknown>>,
|
|
341
|
-
settings: TableSettings = {},
|
|
342
|
-
): TableProgram {
|
|
343
|
-
const enc = new Encoder(settings)
|
|
344
|
-
for (const name of Object.keys(ruleMap)) enc.encodeRule(name, ruleMap[name]!)
|
|
345
|
-
return enc.finish()
|
|
346
|
-
}
|