parseman 0.59.3 → 0.60.1
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 +876 -190
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/cli/index.js +876 -190
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/dispatch.d.ts +13 -1
- package/dist/combinators/dispatch.d.ts.map +1 -1
- package/dist/combinators/first-set.d.ts +35 -2
- package/dist/combinators/first-set.d.ts.map +1 -1
- package/dist/combinators/regex.d.ts.map +1 -1
- package/dist/combinators/trivia-boundary.d.ts +11 -0
- package/dist/combinators/trivia-boundary.d.ts.map +1 -1
- package/dist/combinators/trivia-skip.d.ts +1 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +2 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +65 -53
- package/dist/compiler/token-capability.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +896 -199
- package/dist/index.js.map +3 -3
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.js +903 -201
- package/dist/plugin/index.js.map +3 -3
- package/dist/regex/first-set.d.ts +19 -0
- package/dist/regex/first-set.d.ts.map +1 -1
- package/dist/table/assemble.d.ts +15 -1
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/child-slots.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +1 -1
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/compile.d.ts +1 -1
- package/dist/table/emit-assembly.d.ts +8 -2
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/emit.d.ts +4 -3
- package/dist/table/emit.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/exec.d.ts +0 -35
- package/dist/table/exec.d.ts.map +1 -1
- package/dist/table/factor-choice.d.ts +27 -0
- package/dist/table/factor-choice.d.ts.map +1 -1
- package/dist/table/gap-gate.d.ts +11 -0
- package/dist/table/gap-gate.d.ts.map +1 -0
- package/dist/table/index.js +926 -201
- package/dist/table/index.js.map +3 -3
- package/dist/table/lex-program.d.ts.map +1 -1
- package/dist/table/ops.d.ts +35 -16
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +6 -1
- package/dist/table/program.d.ts.map +1 -1
- package/dist/types.d.ts +16 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +2 -4
- package/src/analysis/duplication.ts +1 -1
- package/src/combinators/attempt.ts +1 -1
- package/src/combinators/dispatch.ts +30 -6
- package/src/combinators/first-set.ts +55 -12
- package/src/combinators/node.ts +1 -1
- package/src/combinators/regex.ts +9 -4
- package/src/combinators/repeat.ts +6 -6
- package/src/combinators/sequence.ts +1 -1
- package/src/combinators/trivia-boundary.ts +18 -0
- package/src/combinators/trivia-skip.ts +1 -1
- package/src/compiler/ir-serialize.ts +1 -1
- package/src/compiler/token-alphabet.ts +3 -1
- package/src/coverage.ts +1 -1
- package/src/index.ts +1 -1
- package/src/plugin/evaluator.ts +8 -3
- package/src/regex/first-set.ts +37 -3
- package/src/table/assemble.ts +176 -30
- package/src/table/child-slots.ts +8 -3
- package/src/table/compile-rule-map.ts +7 -4
- package/src/table/compile.ts +1 -1
- package/src/table/emit-assembly.ts +184 -33
- package/src/table/emit.ts +6 -4
- package/src/table/encode.ts +469 -74
- package/src/table/exec.ts +38 -82
- package/src/table/factor-choice.ts +62 -0
- package/src/table/fail-clean.ts +2 -2
- package/src/table/gap-gate.ts +55 -0
- package/src/table/hold-gap.ts +2 -2
- package/src/table/lex-program.ts +15 -6
- package/src/table/ops.ts +38 -16
- package/src/table/program.ts +7 -2
- package/src/table/select-hot.ts +7 -2
- package/src/types.ts +12 -5
- 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
|
@@ -21,8 +21,8 @@ import {
|
|
|
21
21
|
OP_LIT_TRACK, OP_RX_TRACK, OP_NODE_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_EXPECT, OP_SEQX, OP_SCAN,
|
|
22
22
|
OP_LIVE,
|
|
23
23
|
OP_FIELD, OP_DISPATCH, OP_ROUTED, OP_LIT_CI, OP_LIT_CI_TRACK, OP_TOKEN, OP_WITHCTX, OP_GUARD, OP_ATTEMPT, OP_LABEL,
|
|
24
|
-
OP_COV,
|
|
25
|
-
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE,
|
|
24
|
+
OP_COV, OP_GAPGATE, OP_STRICT,
|
|
25
|
+
OP_ADJ, OP_GREEDY, OP_REJECT, OP_ARMGATE, OP_RGATE, OP_RGATE2,
|
|
26
26
|
OP_LEX_BODY, OP_LEX_PROGRAM,
|
|
27
27
|
} from './ops.ts'
|
|
28
28
|
import { adjacencyHolds, adjacencyMisuse } from '../combinators/adjacency.ts'
|
|
@@ -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()']
|
|
@@ -375,7 +321,11 @@ function makeDriver(
|
|
|
375
321
|
}
|
|
376
322
|
|
|
377
323
|
function exec(ip: number, input: string, pos: number, ctx: ParseContext): unknown {
|
|
378
|
-
|
|
324
|
+
// A row that only hands on to one child at the same position (a gate that
|
|
325
|
+
// passes, a rule trampoline, `OP_STRICT`) continues here rather than
|
|
326
|
+
// recursing, so the reference driver's stack grows with the grammar's
|
|
327
|
+
// nesting, not with its gate rows.
|
|
328
|
+
rows: for (;;) {
|
|
379
329
|
switch (code[ip]) {
|
|
380
330
|
case OP_LIT: {
|
|
381
331
|
const s = k[code[ip + 1]!] as string
|
|
@@ -492,7 +442,7 @@ function makeDriver(
|
|
|
492
442
|
ctx._fe = pos; ctx._fx = fx[code[ip + 3]!] as string[]
|
|
493
443
|
return FAIL
|
|
494
444
|
}
|
|
495
|
-
|
|
445
|
+
ip = code[ip + 2]!; continue rows
|
|
496
446
|
}
|
|
497
447
|
|
|
498
448
|
case OP_LABEL: {
|
|
@@ -502,7 +452,7 @@ function makeDriver(
|
|
|
502
452
|
return v
|
|
503
453
|
}
|
|
504
454
|
case OP_RULE:
|
|
505
|
-
|
|
455
|
+
ip = code[ip + 1]!; continue rows
|
|
506
456
|
|
|
507
457
|
/**
|
|
508
458
|
* A COUNTER ROW IS TRANSPARENT HERE, and that is the whole of its handling.
|
|
@@ -518,8 +468,14 @@ function makeDriver(
|
|
|
518
468
|
* a second implementation of it here would be a second place for the phase
|
|
519
469
|
* semantics (entry for a rule, success for an arm) to drift.
|
|
520
470
|
*/
|
|
471
|
+
// The reference runs a gap-gated item as written: the gate is the repeat's,
|
|
472
|
+
// in the shipped engines, and this driver is the leg it is compared with.
|
|
521
473
|
case OP_COV:
|
|
522
|
-
|
|
474
|
+
case OP_GAPGATE:
|
|
475
|
+
ip = code[ip + 1]!; continue rows
|
|
476
|
+
|
|
477
|
+
case OP_STRICT:
|
|
478
|
+
ip = code[ctx._tolerant || ctx._probe !== undefined ? ip + 2 : ip + 1]!; continue rows
|
|
523
479
|
|
|
524
480
|
case OP_LIVE: {
|
|
525
481
|
// A HAND-WRITTEN COMBINATOR, run through its own `.parse` — mirrors
|
|
@@ -601,6 +557,7 @@ function makeDriver(
|
|
|
601
557
|
|
|
602
558
|
let target: number
|
|
603
559
|
let usesRouted: boolean
|
|
560
|
+
let commits = true
|
|
604
561
|
if (arm === undefined) {
|
|
605
562
|
const other = code[ip + 3]!
|
|
606
563
|
if (other < 0) {
|
|
@@ -610,7 +567,8 @@ function makeDriver(
|
|
|
610
567
|
return FAIL
|
|
611
568
|
}
|
|
612
569
|
target = other
|
|
613
|
-
usesRouted = code[ip + 4]! === 1
|
|
570
|
+
usesRouted = (code[ip + 4]! & 1) === 1
|
|
571
|
+
commits = (code[ip + 4]! & 2) === 0
|
|
614
572
|
} else {
|
|
615
573
|
target = code[armBase + arm]!
|
|
616
574
|
usesRouted = spec.routed[arm] === 1
|
|
@@ -637,8 +595,8 @@ function makeDriver(
|
|
|
637
595
|
rollbackTrivia(ctx, mark)
|
|
638
596
|
// The interpreter marks a failed dispatch branch COMMITTED: the
|
|
639
597
|
// selector already matched, so an enclosing choice must not treat this
|
|
640
|
-
// as "try the next arm".
|
|
641
|
-
ctx._fc = true
|
|
598
|
+
// as "try the next arm". An uncommitted fallback keeps its own flag.
|
|
599
|
+
if (commits) ctx._fc = true
|
|
642
600
|
return FAIL
|
|
643
601
|
}
|
|
644
602
|
// The end cell already holds the branch's end — dispatch's span runs from `pos`
|
|
@@ -878,7 +836,6 @@ function makeDriver(
|
|
|
878
836
|
// Running it through `exec` at the POST-scan position would find the gap
|
|
879
837
|
// already consumed and answer "adjacent" unconditionally.
|
|
880
838
|
if (code[child] === OP_ADJ) {
|
|
881
|
-
if (COUNT) { tableCounters.rows++; tableCounters.byOp[OP_ADJ]!++ }
|
|
882
839
|
const ki = code[child + 2]!
|
|
883
840
|
if (!adjacencyHolds(input, cur, ctx, code[child + 1] === 1, ki < 0 ? undefined : k[ki] as readonly string[])) {
|
|
884
841
|
ctx._fe = cur; ctx._fx = fx[code[child + 3]!] as string[]
|
|
@@ -914,10 +871,6 @@ function makeDriver(
|
|
|
914
871
|
// LIT/RX in place removes both. The duplication is in the DRIVER,
|
|
915
872
|
// which ships once for every grammar — the cost this design trades on.
|
|
916
873
|
const cop = code[child]
|
|
917
|
-
// The two inline terminals below execute a ROW without going through
|
|
918
|
-
// `exec`, so the counter has to see them or the row count understates
|
|
919
|
-
// exactly where the driver already won.
|
|
920
|
-
if (COUNT && (cop === OP_LIT || cop === OP_RX)) { tableCounters.rows++; tableCounters.byOp[cop]!++ }
|
|
921
874
|
if (cop === OP_LIT) {
|
|
922
875
|
const lit = k[code[child + 1]!] as string
|
|
923
876
|
if (!input.startsWith(lit, cur)) {
|
|
@@ -958,7 +911,6 @@ function makeDriver(
|
|
|
958
911
|
const reducer = code[ip + 1]!
|
|
959
912
|
if (reducer < 0) return values![~reducer]
|
|
960
913
|
const fn = fns[reducer] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
961
|
-
if (COUNT) siteFn('SEQX fn()', fn)
|
|
962
914
|
return fn(values, { start: pos, end: cur })
|
|
963
915
|
}
|
|
964
916
|
return values
|
|
@@ -990,7 +942,6 @@ function makeDriver(
|
|
|
990
942
|
}
|
|
991
943
|
if (arm >= 0) {
|
|
992
944
|
ctx._fc = false
|
|
993
|
-
if (COUNT) tableCounters.gatedEntries++
|
|
994
945
|
const v = exec(code[base + arm]!, input, pos, ctx)
|
|
995
946
|
if (v !== FAIL) return v
|
|
996
947
|
// THE CUT. `dispatch()` is the library's one true cut: once its
|
|
@@ -1029,6 +980,12 @@ function makeDriver(
|
|
|
1029
980
|
// its `g.X` arms were unresolved refs (choice.ts:35 — the same staleness
|
|
1030
981
|
// `encode.ts` recomputes around), so they firstMatch all seven arms where
|
|
1031
982
|
// this driver dispatches to one. The arm sets differ, not the rule.
|
|
983
|
+
//
|
|
984
|
+
// Under a completions probe the interpreter runs every arm on a miss
|
|
985
|
+
// (`choice.ts`), so each opener reaches the probe; so does this.
|
|
986
|
+
if (arm < 0 && ctx._probe !== undefined) {
|
|
987
|
+
for (let i = 0; i < code[ip + 2]!; i++) exec(code[base + i]!, input, pos, ctx)
|
|
988
|
+
}
|
|
1032
989
|
return failChoice()
|
|
1033
990
|
}
|
|
1034
991
|
// THE PER-ARM GATE. Arms in source order, each skipped when its own
|
|
@@ -1063,8 +1020,9 @@ function makeDriver(
|
|
|
1063
1020
|
const armFx = base + n
|
|
1064
1021
|
for (let i = 0; i < n; i++) {
|
|
1065
1022
|
const cls = armCls[i]
|
|
1066
|
-
|
|
1067
|
-
|
|
1023
|
+
// Under a completion probe every arm is entered, as the interpreter's
|
|
1024
|
+
// `firstMatch` enters it: its opener then reaches the probe (`failAt`).
|
|
1025
|
+
if (ctx._probe === undefined && cls !== undefined && cls !== null && !classHas(cls, c)) {
|
|
1068
1026
|
// THE GATED ARM STILL REPORTS. The interpreter has no char gate: it
|
|
1069
1027
|
// enters the arm, the arm's opener fails, and its static set is what
|
|
1070
1028
|
// lands in the accumulation. Declining to enter must not also decline
|
|
@@ -1073,11 +1031,8 @@ function makeDriver(
|
|
|
1073
1031
|
continue
|
|
1074
1032
|
}
|
|
1075
1033
|
ctx._fc = false
|
|
1076
|
-
if (COUNT) tableCounters.ungatedEntries++
|
|
1077
|
-
const rows0 = COUNT ? tableCounters.rows : 0
|
|
1078
1034
|
const v = exec(code[base + i]!, input, pos, ctx)
|
|
1079
1035
|
if (v !== FAIL) return v
|
|
1080
|
-
if (COUNT) { tableCounters.ungatedFails++; tableCounters.ungatedFailRows += tableCounters.rows - rows0 }
|
|
1081
1036
|
const at = ctx._fe ?? pos
|
|
1082
1037
|
if (at > best) { best = at; acc = undefined }
|
|
1083
1038
|
if (at === best) push(ctx._fx)
|
|
@@ -1119,14 +1074,17 @@ function makeDriver(
|
|
|
1119
1074
|
return sup
|
|
1120
1075
|
}
|
|
1121
1076
|
|
|
1122
|
-
|
|
1077
|
+
// The reference tests the first character only: the second is the shipped
|
|
1078
|
+
// engines' (`OP_RGATE2`), and this driver is the leg they are compared with.
|
|
1079
|
+
case OP_RGATE:
|
|
1080
|
+
case OP_RGATE2: {
|
|
1123
1081
|
const item = ctx._routed
|
|
1124
1082
|
if (item !== undefined && item.span.start === pos && ctx._probe === undefined && !ctx._tolerant
|
|
1125
1083
|
&& !classHas(cc[code[ip + 1]!]!, lead(input, item.span.end))) {
|
|
1126
1084
|
ctx._fe = item.span.end; ctx._fx = fx[code[ip + 3]!] as string[]
|
|
1127
1085
|
return FAIL
|
|
1128
1086
|
}
|
|
1129
|
-
|
|
1087
|
+
ip = code[ip + 2]!; continue rows
|
|
1130
1088
|
}
|
|
1131
1089
|
|
|
1132
1090
|
case OP_ARMGATE: {
|
|
@@ -1141,7 +1099,7 @@ function makeDriver(
|
|
|
1141
1099
|
ctx._fx = fx[code[ip + 3]!] as string[]
|
|
1142
1100
|
return FAIL
|
|
1143
1101
|
}
|
|
1144
|
-
|
|
1102
|
+
ip = code[ip + 2]!; continue rows
|
|
1145
1103
|
}
|
|
1146
1104
|
|
|
1147
1105
|
case OP_REJECT: {
|
|
@@ -1225,7 +1183,7 @@ function makeDriver(
|
|
|
1225
1183
|
// own sync. The item's expected set is at ip + 6 and the separator's
|
|
1226
1184
|
// sentinel class at ip + 7, both laid down only for a recovery table.
|
|
1227
1185
|
const mySync = REC ? ctx._sync : undefined
|
|
1228
|
-
const recFx = REC ? fx[code[ip + 6]!] as string[]
|
|
1186
|
+
const recFx = !REC ? undefined : (code[ip + 5]! & 16) !== 0 ? [] : fx[code[ip + 6]!] as string[]
|
|
1229
1187
|
const sepSent = REC ? sentinelFor(code[ip + 7]!) : undefined
|
|
1230
1188
|
const itemClassIndex = sep < 0 ? code[ip + 7]! : -1
|
|
1231
1189
|
const itemClass = REC && itemClassIndex >= 0 ? cc[itemClassIndex]! : undefined
|
|
@@ -1397,7 +1355,6 @@ function makeDriver(
|
|
|
1397
1355
|
const v = exec(code[ip + 2]!, input, pos, ctx)
|
|
1398
1356
|
if (v === FAIL) return FAIL
|
|
1399
1357
|
const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
1400
|
-
if (COUNT) siteFn('XFORM fn()', fn)
|
|
1401
1358
|
return fn(v, { start: pos, end: EC.e })
|
|
1402
1359
|
}
|
|
1403
1360
|
|
|
@@ -1439,7 +1396,6 @@ function makeDriver(
|
|
|
1439
1396
|
if (v === FAIL) return FAIL
|
|
1440
1397
|
const end = EC.e
|
|
1441
1398
|
const fn = fns[code[ip + 1]!] as (value: unknown, span: { start: number; end: number }) => unknown
|
|
1442
|
-
if (COUNT) siteFn('LEAF fn()', fn)
|
|
1443
1399
|
const out = fn(v, { start: pos, end })
|
|
1444
1400
|
if (wasCapturing) pushCstLeaf(ctx, {
|
|
1445
1401
|
_tag: 'leaf',
|
|
@@ -1542,7 +1498,6 @@ function makeDriver(
|
|
|
1542
1498
|
children: readonly unknown[], fields: FieldMap | undefined, span: { start: number; end: number },
|
|
1543
1499
|
rawChildren: readonly unknown[], triviaLog: readonly number[], state: unknown,
|
|
1544
1500
|
) => unknown
|
|
1545
|
-
if (COUNT) siteFn('NODE build()', build)
|
|
1546
1501
|
nd = build(kids, fieldMap, span, cap.rawChildren, (flags & 4) !== 0 || hostCst ? cap.triviaLog : EMPTY_TL, st)
|
|
1547
1502
|
}
|
|
1548
1503
|
} else if (host !== undefined) {
|
|
@@ -1644,6 +1599,7 @@ function makeDriver(
|
|
|
1644
1599
|
default:
|
|
1645
1600
|
throw new Error(`table driver: unknown opcode ${String(code[ip])} at ${ip}`)
|
|
1646
1601
|
}
|
|
1602
|
+
}
|
|
1647
1603
|
}
|
|
1648
1604
|
|
|
1649
1605
|
/**
|
|
@@ -35,8 +35,16 @@ import { sequence } from '../combinators/sequence.ts'
|
|
|
35
35
|
import { leaf, sourceLeaf } from '../combinators/token.ts'
|
|
36
36
|
import { noTrivia, parser } from '../combinators/grammar.ts'
|
|
37
37
|
import { attempt } from '../combinators/attempt.ts'
|
|
38
|
+
import { optional } from '../combinators/repeat.ts'
|
|
38
39
|
import { mayCommitFailure } from '../analysis/commitment.ts'
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The choices this module builds. The interpreter never runs them: it runs the
|
|
43
|
+
* authored choice, so a factored one's `disjoint` says nothing about which arms
|
|
44
|
+
* the interpreter lets in (`Encoder` classifies them by their derived sets).
|
|
45
|
+
*/
|
|
46
|
+
export const factoredChoices = new WeakSet<Combinator<unknown>>()
|
|
47
|
+
|
|
40
48
|
type ChoiceDef = Extract<ParserDef, { tag: 'choice' }>
|
|
41
49
|
|
|
42
50
|
/** Own keys every combinator may carry beside its definition; all derived from it. */
|
|
@@ -151,6 +159,7 @@ export function factorSharedEnds(d: ChoiceDef): Combinator<unknown> | undefined
|
|
|
151
159
|
// it walks everything the arms reach.
|
|
152
160
|
if (arms.some(a => mayCommitFailure(a))) return undefined
|
|
153
161
|
const middle = choice(...(shapes.map(s => s.terms[at]!) as [Combinator<unknown>, ...Combinator<unknown>[]]))
|
|
162
|
+
factoredChoices.add(middle)
|
|
154
163
|
// The inner choice must keep arm order: a strategy that reorders (longest
|
|
155
164
|
// literal first) or classifies would pick a different arm. Nor may it add an
|
|
156
165
|
// automatic lookahead the arms as written do not have: `'start'` before
|
|
@@ -171,3 +180,56 @@ export function factorSharedEnds(d: ChoiceDef): Combinator<unknown> | undefined
|
|
|
171
180
|
// `attempt`. The sequence alone would report where its inner choice failed.
|
|
172
181
|
return out === undefined ? undefined : attempt(out)
|
|
173
182
|
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* AN ARM WHOSE NEXT SIBLING IS ITS OWN PREFIX — `choice(sequence(X, a), X)`.
|
|
186
|
+
*
|
|
187
|
+
* On input where `X` matches and `a` does not, the choice parses `X`, fails `a`,
|
|
188
|
+
* rolls back, and parses `X` again for the next arm. Where the choice's value is
|
|
189
|
+
* unused (its sequence arms are `valueUnused`, so only their captures reach the
|
|
190
|
+
* enclosing node), the first arm may be `sequence(X, optional(a))`:
|
|
191
|
+
*
|
|
192
|
+
* - `X` then `a` matches: the arm as it was.
|
|
193
|
+
* - `X` matches, `a` fails uncommitted: the optional matches empty, rolls `a`'s
|
|
194
|
+
* captures back and leaves the gap before it unrecorded, so the arm ends where
|
|
195
|
+
* `X` did with `X`'s captures, which is what the retried next arm produced.
|
|
196
|
+
* - `a` fails committed: the arm fails committed, as it did.
|
|
197
|
+
* - `X` fails: the arm fails as before, and the next arm, kept as it is, fails the
|
|
198
|
+
* same way, so the choice merges the same sets in the same order.
|
|
199
|
+
*
|
|
200
|
+
* `X` is the next arm's sequence of terms, or that arm itself as a single term,
|
|
201
|
+
* and "the same" is `sameComb`. Gated arms, `autoNot` and strategies that reorder
|
|
202
|
+
* are left alone.
|
|
203
|
+
*/
|
|
204
|
+
export function factorPrefixArms(d: ChoiceDef): Combinator<unknown> | undefined {
|
|
205
|
+
const arms = d.parsers
|
|
206
|
+
if (arms.length < 2) return undefined
|
|
207
|
+
if (d.strategy.tag !== 'firstMatch' && d.strategy.tag !== 'sharedPrefix') return undefined
|
|
208
|
+
if (d.gates.some(g => g !== null) || d.autoNot.some(a => a !== null)) return undefined
|
|
209
|
+
const out = [...arms]
|
|
210
|
+
let changed = false
|
|
211
|
+
for (let i = 0; i + 1 < arms.length; i++) {
|
|
212
|
+
const arm = arms[i]!
|
|
213
|
+
if (arm._def.tag !== 'sequence' || arm._def.valueUnused !== true) continue
|
|
214
|
+
const terms = arm._def.parsers
|
|
215
|
+
const next = arms[i + 1]!
|
|
216
|
+
const prefix = next._def.tag === 'sequence' && next._def.valueUnused === true ? next._def.parsers : [next]
|
|
217
|
+
if (prefix.length >= terms.length) continue
|
|
218
|
+
if (!prefix.every((t, j) => sameComb(t, terms[j]!, 0))) continue
|
|
219
|
+
const rest = terms.slice(prefix.length)
|
|
220
|
+
// A rest that cannot fail (already `optional`, or `many`) has nothing to factor.
|
|
221
|
+
if (rest.every(never)) continue
|
|
222
|
+
const tail = rest.length === 1 ? rest[0]! : sequence(...(rest as [Combinator<unknown>, ...Combinator<unknown>[]]))
|
|
223
|
+
if (tail !== rest[0]) (tail._def as { valueUnused?: boolean }).valueUnused = true
|
|
224
|
+
const parts: Combinator<unknown>[] = [...terms.slice(0, prefix.length), optional(tail)]
|
|
225
|
+
const seq = sequence(...(parts as [Combinator<unknown>, ...Combinator<unknown>[]]))
|
|
226
|
+
;(seq._def as { valueUnused?: boolean }).valueUnused = true
|
|
227
|
+
out[i] = seq
|
|
228
|
+
changed = true
|
|
229
|
+
}
|
|
230
|
+
if (!changed) return undefined
|
|
231
|
+
const rebuilt = choice(...(out as [Combinator<unknown>, ...Combinator<unknown>[]]))
|
|
232
|
+
factoredChoices.add(rebuilt)
|
|
233
|
+
const st = rebuilt._def.tag === 'choice' ? rebuilt._def.strategy : undefined
|
|
234
|
+
return st !== undefined && (st.tag === 'firstMatch' || st.tag === 'sharedPrefix') ? rebuilt : undefined
|
|
235
|
+
}
|
package/src/table/fail-clean.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
OP_ATTEMPT, OP_CHOICE, OP_COV, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE, OP_GUARD, OP_LABEL,
|
|
25
25
|
OP_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK,
|
|
26
26
|
OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX, OP_RX_TRACK, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ,
|
|
27
|
-
OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM,
|
|
27
|
+
OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM, OP_GAPGATE,
|
|
28
28
|
} from './ops.ts'
|
|
29
29
|
|
|
30
30
|
export type FailClean = (ip: number) => boolean
|
|
@@ -35,7 +35,7 @@ export type FailClean = (ip: number) => boolean
|
|
|
35
35
|
*/
|
|
36
36
|
function passChild(code: ArrayLike<number>, ip: number): number {
|
|
37
37
|
switch (code[ip]) {
|
|
38
|
-
case OP_RULE: case OP_LABEL: case OP_COV: case OP_TOKEN:
|
|
38
|
+
case OP_RULE: case OP_LABEL: case OP_COV: case OP_TOKEN: case OP_GAPGATE:
|
|
39
39
|
return code[ip + 1]!
|
|
40
40
|
case OP_GATE: case OP_NODE: case OP_NODE_TRACK: case OP_XFORM: case OP_FIELD: case OP_SCOPE:
|
|
41
41
|
case OP_SCOPE_CAP: case OP_SCOPE_PLAIN: case OP_WITHCTX: case OP_LEAF: case OP_SOURCE_LEAF:
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AN OPTIONAL REPEAT ITEM THAT OPENS WITH A GAP (`OP_GAPGATE`).
|
|
3
|
+
*
|
|
4
|
+
* SCSS writes its operators as `sequence(pad, '*', pad)` with `pad` a regex that
|
|
5
|
+
* may match empty, and a value chain has a repeat of them at every level. After
|
|
6
|
+
* a value followed by a space, each level entered its operator, scanned the
|
|
7
|
+
* space and failed on the character after it: the same gap read once per level,
|
|
8
|
+
* and an operator node's frame opened each time.
|
|
9
|
+
*
|
|
10
|
+
* The repeat now reads the gap itself, ONCE, with the item's own regex, and
|
|
11
|
+
* looks at the character after it: an item that opens with the gap must
|
|
12
|
+
* continue with a character of `cls` there (and an arm without the gap must
|
|
13
|
+
* start with one of `other` where the gap starts). Where neither holds the item
|
|
14
|
+
* cannot match, so the repeat ends, exactly as it ends on a strict item whose
|
|
15
|
+
* class excludes the lead. Otherwise it enters the item and leaves the gap's end
|
|
16
|
+
* in the GAP SLOT, from which the regex row takes it instead of scanning again.
|
|
17
|
+
*
|
|
18
|
+
* The slot is a memo of one pure function: the end of regex row `row` matched
|
|
19
|
+
* at `at` in `input`. A row reads it only when all three are its own, so a
|
|
20
|
+
* different row, position or input cannot take it, and a slot left set can
|
|
21
|
+
* only ever give a row the end it would have computed. Only rows the table
|
|
22
|
+
* names as a gap (`gapRows`) test it at all. It is set for the item and cleared
|
|
23
|
+
* when the item returns.
|
|
24
|
+
*
|
|
25
|
+
* Not under tolerant recovery or a completions probe, which observe an item's
|
|
26
|
+
* failure: there the repeat enters the item as written. The reference driver
|
|
27
|
+
* (`exec.ts`) always does, which is what makes it the ungated leg.
|
|
28
|
+
*/
|
|
29
|
+
import { childSlots } from './child-slots.ts'
|
|
30
|
+
import { OP_GAPGATE } from './ops.ts'
|
|
31
|
+
|
|
32
|
+
/** The gap slot one assembly shares between its closure and emitted pieces. */
|
|
33
|
+
export type GapSlot = { row: number; at: number; end: number; input: string | undefined }
|
|
34
|
+
|
|
35
|
+
export function newGapSlot(): GapSlot {
|
|
36
|
+
return { row: -1, at: -1, end: -1, input: undefined }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The regex rows some reachable `OP_GAPGATE` names as its gap. */
|
|
40
|
+
export function gapRows(code: ArrayLike<number>, roots: Iterable<number>): Set<number> {
|
|
41
|
+
const rows = new Set<number>()
|
|
42
|
+
const seen = new Set<number>()
|
|
43
|
+
const stack = [...roots]
|
|
44
|
+
const kids: number[] = []
|
|
45
|
+
while (stack.length > 0) {
|
|
46
|
+
const ip = stack.pop()!
|
|
47
|
+
if (seen.has(ip)) continue
|
|
48
|
+
seen.add(ip)
|
|
49
|
+
if (code[ip] === OP_GAPGATE) rows.add(code[ip + 2]!)
|
|
50
|
+
kids.length = 0
|
|
51
|
+
childSlots(code, ip, kids)
|
|
52
|
+
for (const c of kids) stack.push(c)
|
|
53
|
+
}
|
|
54
|
+
return rows
|
|
55
|
+
}
|
package/src/table/hold-gap.ts
CHANGED
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
import {
|
|
18
18
|
OP_ADJ, OP_ATTEMPT, OP_CHOICE, OP_COV, OP_EXPECT, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_LIT_CI,
|
|
19
19
|
OP_NODE, OP_NOT, OP_PEEK, OP_REP, OP_REPV, OP_RULE, OP_RX, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ,
|
|
20
|
-
OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM,
|
|
20
|
+
OP_SEQV, OP_SEQX, OP_SOURCE_LEAF, OP_TOKEN, OP_WITHCTX, OP_XFORM, OP_GAPGATE,
|
|
21
21
|
} from './ops.ts'
|
|
22
22
|
import { childSlots } from './child-slots.ts'
|
|
23
23
|
import { leadingTriviaFieldAt } from './field-spec.ts'
|
|
24
24
|
import { regexCanMatchEmpty } from '../regex/first-set.ts'
|
|
25
25
|
|
|
26
26
|
const WRAPPERS = new Set([OP_NODE, OP_XFORM, OP_LEAF, OP_SOURCE_LEAF, OP_TOKEN, OP_SCOPE, OP_SCOPE_CAP,
|
|
27
|
-
OP_SCOPE_PLAIN, OP_FIELD, OP_LABEL, OP_EXPECT, OP_WITHCTX, OP_RULE, OP_ATTEMPT, OP_COV])
|
|
27
|
+
OP_SCOPE_PLAIN, OP_FIELD, OP_LABEL, OP_EXPECT, OP_WITHCTX, OP_RULE, OP_ATTEMPT, OP_COV, OP_GAPGATE])
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Can the row at `ip` match empty? `true` wherever it cannot tell. A first-set
|
package/src/table/lex-program.ts
CHANGED
|
@@ -187,7 +187,7 @@ function sequence5(
|
|
|
187
187
|
|
|
188
188
|
type ChoiceTail = (
|
|
189
189
|
input: string, pos: number, ctx: ParseContext, lead: number,
|
|
190
|
-
acc: string[] | undefined, best: number,
|
|
190
|
+
acc: string[] | undefined, best: number, entered: boolean,
|
|
191
191
|
) => number
|
|
192
192
|
|
|
193
193
|
function appendExpected(acc: string[] | undefined, values: readonly string[] | undefined): string[] | undefined {
|
|
@@ -211,19 +211,22 @@ function choiceNode(
|
|
|
211
211
|
return end < 0 ? next(input, pos) : end
|
|
212
212
|
}
|
|
213
213
|
}
|
|
214
|
-
let tail: ChoiceTail = (
|
|
214
|
+
let tail: ChoiceTail = (input, pos, ctx, _lead, acc, _best, entered) => {
|
|
215
|
+
// No arm was entered. A completion probe still sees each one fail on its
|
|
216
|
+
// opener, as the interpreter's entered arms leave it.
|
|
217
|
+
if (!entered && ctx._probe !== undefined) for (const arm of arms) arm.run(input, pos, ctx)
|
|
215
218
|
ctx._fe = pos
|
|
216
219
|
ctx._fx = acc ?? choiceExpected as string[]
|
|
217
220
|
return -1
|
|
218
221
|
}
|
|
219
222
|
for (let i = arms.length - 1; i >= 0; i--) {
|
|
220
223
|
const armRun = arms[i]!.run, cls = classes[i], staticExpected = armExpected[i]!, next = tail
|
|
221
|
-
tail = (input, pos, ctx, lead, acc, best) => {
|
|
224
|
+
tail = (input, pos, ctx, lead, acc, best, entered) => {
|
|
222
225
|
if (cls !== undefined && !classHas(cls, lead)) {
|
|
223
226
|
return next(
|
|
224
227
|
input, pos, ctx, lead,
|
|
225
228
|
best === pos ? appendExpected(acc, staticExpected) : acc,
|
|
226
|
-
best,
|
|
229
|
+
best, entered,
|
|
227
230
|
)
|
|
228
231
|
}
|
|
229
232
|
ctx._fc = false
|
|
@@ -236,13 +239,13 @@ function choiceNode(
|
|
|
236
239
|
if (nextAcc !== undefined) ctx._fx = nextAcc
|
|
237
240
|
return -1
|
|
238
241
|
}
|
|
239
|
-
return next(input, pos, ctx, lead, nextAcc, best)
|
|
242
|
+
return next(input, pos, ctx, lead, nextAcc, best, true)
|
|
240
243
|
}
|
|
241
244
|
}
|
|
242
245
|
return {
|
|
243
246
|
match: matchTail,
|
|
244
247
|
run: (input, pos, ctx) => tail(
|
|
245
|
-
input, pos, ctx, pos < input.length ? input.codePointAt(pos)! : -1, undefined, pos,
|
|
248
|
+
input, pos, ctx, pos < input.length ? input.codePointAt(pos)! : -1, undefined, pos, false,
|
|
246
249
|
),
|
|
247
250
|
}
|
|
248
251
|
}
|
|
@@ -290,6 +293,9 @@ function exclusiveChoiceNode(
|
|
|
290
293
|
const failed = ctx._fx
|
|
291
294
|
if (failed !== undefined && failed.length > 0) { ctx._fe = pos; return -1 }
|
|
292
295
|
}
|
|
296
|
+
// A miss under a completions probe: each arm fails on its opener, as the
|
|
297
|
+
// interpreter's arms do when it runs them all (`choice.ts`).
|
|
298
|
+
if (arm < 0 && ctx._probe !== undefined) for (const a of arms) a.run(input, pos, ctx)
|
|
293
299
|
ctx._fe = pos
|
|
294
300
|
ctx._fx = choiceFx as string[]
|
|
295
301
|
return -1
|
|
@@ -547,6 +553,9 @@ export function buildLexProgramRunner(
|
|
|
547
553
|
if (!classHas(cls, lead)) {
|
|
548
554
|
ctx._fe = pos
|
|
549
555
|
ctx._fx = choiceFx.slice()
|
|
556
|
+
// Each arm fails here; under a probe their openers are recorded as the
|
|
557
|
+
// interpreter's entered arms record them.
|
|
558
|
+
if (ctx._probe !== undefined) failAt(ctx, [...x0, ...x1, ...x2, ...x3], pos)
|
|
550
559
|
return -1
|
|
551
560
|
}
|
|
552
561
|
let acc: string[] | undefined
|
package/src/table/ops.ts
CHANGED
|
@@ -38,11 +38,15 @@ export const OP_SEQV = 4
|
|
|
38
38
|
*/
|
|
39
39
|
export const OP_CHOICE = 5
|
|
40
40
|
/**
|
|
41
|
-
* `REP c min max sep flags fx sepClass` —
|
|
42
|
-
*
|
|
43
|
-
* for a separated list,
|
|
44
|
-
* (or −1). `flags
|
|
45
|
-
* `
|
|
41
|
+
* `REP c min max sep flags fx sepClass [sepGate]` — EIGHT words, or NINE with
|
|
42
|
+
* flag bit 3. `sep` is a child offset or −1; `max` −1 = ∞. `fx` is the item's
|
|
43
|
+
* expected set. `sepClass` is the separator sentinel for a separated list,
|
|
44
|
+
* otherwise the finite/non-nullable optional-item class (or −1). `flags`: bit 0
|
|
45
|
+
* (`&1`) trailing separator allowed; bit 1 (`&2`) `keepSeparators`; bit 2 (`&4`)
|
|
46
|
+
* item expected reporting; bit 3 (`&8`) a ninth word at `ip + 8`, the class of
|
|
47
|
+
* characters the separator can start, so a strict list past its minimum ends
|
|
48
|
+
* where it cannot (`sepGate`); bit 4 (`&16`) the item's derived set is empty, so
|
|
49
|
+
* `fx` holds the item's tag for the minimum check and a resync error carries [].
|
|
46
50
|
*/
|
|
47
51
|
export const OP_REP = 6
|
|
48
52
|
/** `REPV …` — `REP` with `valueUnused`. */
|
|
@@ -70,13 +74,7 @@ export const OP_XFORM = 9
|
|
|
70
74
|
* from the reducer's declared arity by the same analysis codegen runs, and
|
|
71
75
|
* forced on under `hostMode: 'cst'`. The driver reads the bits and re-derives
|
|
72
76
|
* nothing.
|
|
73
|
-
|
|
74
|
-
* TWO ENCODERS SHARE THIS OPCODE and they do NOT agree on its length. The layout
|
|
75
|
-
* above is `encode.ts` / `exec.ts`. `encode-baseline.ts` / `exec-baseline.ts` emit
|
|
76
|
-
* the FOUR-word form `NODE b c flags` with bits 2 and 3 only, and refuse
|
|
77
|
-
* `collapse` / `unwrap` / `project` outright. Neither driver walks the stream by
|
|
78
|
-
* instruction length, so the divergence is contained — but read the pair you are
|
|
79
|
-
* in, not this comment alone. */
|
|
77
|
+
*/
|
|
80
78
|
export const OP_NODE = 10
|
|
81
79
|
/** `RULE r` — `r` indexes `prog.rules`. */
|
|
82
80
|
export const OP_RULE = 11
|
|
@@ -89,9 +87,6 @@ export const OP_NOT = 13
|
|
|
89
87
|
* `['peek(<child tag>)']`, because `peek.ts:60` DISCARDS the body's expectation:
|
|
90
88
|
* a lookahead's failure is "the guard did not hold", not a request for whatever
|
|
91
89
|
* token the body happened to stop on.
|
|
92
|
-
*
|
|
93
|
-
* `encode-baseline.ts` / `exec-baseline.ts` emit and read the two-word form and
|
|
94
|
-
* are internally consistent; read the pair you are in.
|
|
95
90
|
*/
|
|
96
91
|
export const OP_PEEK = 14
|
|
97
92
|
/** `LEAF f c` — `leaf()`: reduce, then record as a spanned leaf. */
|
|
@@ -486,11 +481,35 @@ export const OP_SOURCE_LEAF = 44
|
|
|
486
481
|
* at this position, a completions probe, or tolerant recovery enters the arm.
|
|
487
482
|
*/
|
|
488
483
|
export const OP_RGATE = 45
|
|
484
|
+
/**
|
|
485
|
+
* `GAPGATE c p cls other` — an optional repeat ITEM that opens with a gap: the
|
|
486
|
+
* pure regex row `p`, which may match empty, and then a term whose first set is
|
|
487
|
+
* the class `cls` (`other`: the class of any choice arm that does not open with
|
|
488
|
+
* `p`, or −1). `c` is the item. The row is TRANSPARENT wherever it runs: only
|
|
489
|
+
* the repeat that holds it reads its operands (`gap-gate.ts`).
|
|
490
|
+
*/
|
|
491
|
+
export const OP_GAPGATE = 46
|
|
492
|
+
/**
|
|
493
|
+
* `RGATE2 cls child fx first second` — `OP_RGATE` with a SECOND character: where
|
|
494
|
+
* the character after the routed token is in `first`, the one after it must be
|
|
495
|
+
* in `second` too (the next term reads both, `regexFirstTwo`), or the arm fails
|
|
496
|
+
* there with `fx`, as entering it would.
|
|
497
|
+
*/
|
|
498
|
+
export const OP_RGATE2 = 47
|
|
499
|
+
/**
|
|
500
|
+
* `STRICT strict tolerant` — one construct encoded two ways: `strict` for a
|
|
501
|
+
* strict parse, `tolerant` for tolerant recovery and a completions probe, which
|
|
502
|
+
* must see the construct as written (`factor-choice.ts` `factorPrefixArms`). An
|
|
503
|
+
* assembly resolves it to one child; it has no body of its own.
|
|
504
|
+
*/
|
|
505
|
+
export const OP_STRICT = 48
|
|
489
506
|
/**
|
|
490
507
|
* `DISPATCH sel d other otherRouted n a1 … an` — `dispatch()`.
|
|
491
508
|
*
|
|
492
509
|
* `sel` is the selector's offset, `d` indexes a dispatch table in `prog.dsp`,
|
|
493
|
-
* `other` is the `otherwise()` offset (or −1), `otherRouted`
|
|
510
|
+
* `other` is the `otherwise()` offset (or −1), `otherRouted` bit 1 (`&2`) is set
|
|
511
|
+
* when the fallback's failure is NOT committed (`otherwise(p, { commit: false })`)
|
|
512
|
+
* and bit 0 when the
|
|
494
513
|
* fallback consumes the routed token, and `a1…an` are the arm offsets. Arm
|
|
495
514
|
* `usesRouted` bits live in the `dsp` entry beside the key maps.
|
|
496
515
|
*
|
|
@@ -520,4 +539,7 @@ export const OP_NAMES: Record<number, string> = {
|
|
|
520
539
|
[OP_SCOPE_PLAIN]: 'SCOPE_PLAIN',
|
|
521
540
|
[OP_LEX_BODY]: 'LEX_BODY',
|
|
522
541
|
[OP_LEX_PROGRAM]: 'LEX_PROGRAM',
|
|
542
|
+
[OP_GAPGATE]: 'GAPGATE',
|
|
543
|
+
[OP_RGATE2]: 'RGATE2',
|
|
544
|
+
[OP_STRICT]: 'STRICT',
|
|
523
545
|
}
|
package/src/table/program.ts
CHANGED
|
@@ -17,10 +17,15 @@ import { buildLexProgramRunner, type LexProgramRunner } from './lex-program.ts'
|
|
|
17
17
|
* `EMITTED_PARAMS` in order. `plan` rebuilds the three data pools the factory
|
|
18
18
|
* expects (see `PoolPlan`), and `reached` is the emitter's site set, which
|
|
19
19
|
* `Assembly.reached` publishes and `test/unit/table-assemble.test.ts`
|
|
20
|
-
* asserts on.
|
|
20
|
+
* asserts on. `arity` is the number of helpers the factory was printed against
|
|
21
|
+
* (`EMITTED_PARAMS.length` of the printing parseman); the runtime compares it with
|
|
22
|
+
* its own list before calling the factory. It is printed data, not
|
|
23
|
+
* `factory.length`, which a minifier that drops unused trailing parameters lowers.
|
|
24
|
+
* An assembly printed before 0.60.0 has none.
|
|
21
25
|
*/
|
|
22
26
|
export type PrecompiledAssembly = {
|
|
23
27
|
readonly key: number
|
|
28
|
+
readonly arity?: number
|
|
24
29
|
readonly factory: EmittedFactory
|
|
25
30
|
readonly plan: PoolPlan
|
|
26
31
|
readonly reached: readonly number[]
|
|
@@ -433,7 +438,7 @@ export function validateDispatchSpec(
|
|
|
433
438
|
return false
|
|
434
439
|
}
|
|
435
440
|
if (spec === undefined || !Number.isInteger(arity) || arity < 0
|
|
436
|
-
|| (fallbackRouted
|
|
441
|
+
|| !Number.isInteger(fallbackRouted) || fallbackRouted < 0 || fallbackRouted > 3
|
|
437
442
|
|| spec.routed.length !== arity
|
|
438
443
|
|| spec.routed.some(value => value !== 0 && value !== 1)
|
|
439
444
|
|| invalid(spec.byKey.values()) || invalid(spec.byFold.values())
|
package/src/table/select-hot.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* A build given a corpus (`assemblyProfile`) ranks by measured calls instead.
|
|
16
16
|
*/
|
|
17
17
|
import { childSlots } from './child-slots.ts'
|
|
18
|
-
import { OP_CHOICE, OP_DISPATCH, OP_REP, OP_REPV } from './ops.ts'
|
|
18
|
+
import { OP_CHOICE, OP_DISPATCH, OP_GAPGATE, OP_REP, OP_REPV } from './ops.ts'
|
|
19
19
|
|
|
20
20
|
const LOOP = 8
|
|
21
21
|
const ROUNDS = 60
|
|
@@ -62,7 +62,12 @@ function staticWeights(code: readonly number[], entries: readonly number[]): Map
|
|
|
62
62
|
const scale = op === OP_REP || op === OP_REPV ? LOOP
|
|
63
63
|
: op === OP_CHOICE || op === OP_DISPATCH ? 1 / Math.max(1, kids.length)
|
|
64
64
|
: 1
|
|
65
|
-
for (const
|
|
65
|
+
for (const k of kids) {
|
|
66
|
+
// A gap gate has no body: weigh its item as the repeat's own child, so
|
|
67
|
+
// adding the row moves no other site's estimate (`gap-gate.ts`).
|
|
68
|
+
const kid = code[k] === OP_GAPGATE ? code[k + 1]! : k
|
|
69
|
+
next.set(kid, Math.min(CAP, (next.get(kid) ?? 0) + w * scale))
|
|
70
|
+
}
|
|
66
71
|
}
|
|
67
72
|
weight = next
|
|
68
73
|
}
|