parseman 0.48.1 → 0.50.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/commitment.d.ts +2 -1
- package/dist/analysis/commitment.d.ts.map +1 -1
- package/dist/analysis/diagnostics.js +1931 -861
- package/dist/analysis/diagnostics.js.map +3 -3
- package/dist/analysis/rebuild.d.ts.map +1 -1
- package/dist/cli/index.js +1236 -164
- package/dist/cli/index.js.map +3 -3
- package/dist/combinators/trivia-skip.d.ts +1 -1
- package/dist/combinators/trivia-skip.d.ts.map +1 -1
- package/dist/compiler/build-arity.d.ts +16 -0
- package/dist/compiler/build-arity.d.ts.map +1 -1
- package/dist/compiler/degradation.d.ts +0 -2
- package/dist/compiler/degradation.d.ts.map +1 -1
- package/dist/compiler/ir-serialize.d.ts.map +1 -1
- package/dist/compiler/linker.d.ts.map +1 -1
- package/dist/compiler/token-alphabet.d.ts +11 -0
- package/dist/compiler/token-alphabet.d.ts.map +1 -1
- package/dist/compiler/token-capability.js +185 -20
- package/dist/compiler/token-capability.js.map +1 -1
- package/dist/cst/capture-buffer.d.ts +5 -1
- package/dist/cst/capture-buffer.d.ts.map +1 -1
- package/dist/index.js +1354 -278
- package/dist/index.js.map +3 -3
- package/dist/language-service/index.js +2 -1
- package/dist/language-service/index.js.map +1 -1
- package/dist/plugin/direct-builder-static.d.ts +20 -0
- package/dist/plugin/direct-builder-static.d.ts.map +1 -1
- package/dist/plugin/entry-supercompiler.d.ts +48 -0
- package/dist/plugin/entry-supercompiler.d.ts.map +1 -0
- package/dist/plugin/evaluator.d.ts +13 -0
- package/dist/plugin/evaluator.d.ts.map +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +2118 -376
- package/dist/plugin/index.js.map +3 -3
- package/dist/run/index.js +2 -1
- package/dist/run/index.js.map +1 -1
- package/dist/spec/index.js +29 -7
- package/dist/spec/index.js.map +1 -1
- package/dist/table/assemble.d.ts.map +1 -1
- package/dist/table/compile-rule-map.d.ts +6 -5
- package/dist/table/compile-rule-map.d.ts.map +1 -1
- package/dist/table/emit-assembly.d.ts +6 -1
- package/dist/table/emit-assembly.d.ts.map +1 -1
- package/dist/table/encode.d.ts.map +1 -1
- package/dist/table/index.js +1298 -231
- package/dist/table/index.js.map +3 -3
- package/dist/table/ops.d.ts +3 -2
- package/dist/table/ops.d.ts.map +1 -1
- package/dist/table/program.d.ts +15 -1
- package/dist/table/program.d.ts.map +1 -1
- package/dist/table/scalar-terminal.d.ts +30 -0
- package/dist/table/scalar-terminal.d.ts.map +1 -1
- package/dist/table/site-labels.d.ts +8 -0
- package/dist/table/site-labels.d.ts.map +1 -1
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/analysis/commitment.ts +19 -8
- package/src/analysis/rebuild.ts +1 -0
- package/src/combinators/trivia-skip.ts +2 -2
- package/src/compiler/build-arity.ts +50 -0
- package/src/compiler/degradation.ts +0 -6
- package/src/compiler/ir-serialize.ts +17 -3
- package/src/compiler/linker.ts +14 -1
- package/src/compiler/token-alphabet.ts +322 -28
- package/src/cst/capture-buffer.ts +34 -11
- package/src/plugin/direct-builder-static.ts +116 -12
- package/src/plugin/entry-supercompiler.ts +567 -0
- package/src/plugin/evaluator.ts +60 -2
- package/src/plugin/index.ts +147 -4
- package/src/table/assemble.ts +125 -42
- package/src/table/compile-rule-map.ts +8 -7
- package/src/table/compile.ts +7 -8
- package/src/table/emit-assembly.ts +646 -126
- package/src/table/emit.ts +1 -1
- package/src/table/encode.ts +132 -7
- package/src/table/ops.ts +3 -2
- package/src/table/program.ts +44 -3
- package/src/table/scalar-terminal.ts +213 -1
- package/src/table/site-labels.ts +22 -9
- package/src/types.ts +1 -1
- package/src/version.ts +1 -1
package/src/table/emit.ts
CHANGED
|
@@ -208,7 +208,7 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly RunCfg[]): string[] {
|
|
|
208
208
|
seen.add(key)
|
|
209
209
|
let em
|
|
210
210
|
try {
|
|
211
|
-
em = emitAssemblySource(t, prog, cfg, extraIps)
|
|
211
|
+
em = emitAssemblySource(t, prog, cfg, extraIps, true)
|
|
212
212
|
} catch (e) {
|
|
213
213
|
if (e instanceof Unemittable) continue
|
|
214
214
|
throw e
|
package/src/table/encode.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AutoNotCheck, Combinator, FirstSet, ParserDef } from '../types.ts'
|
|
2
|
-
import { classifyFinalChoice, firstSetOf, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
|
|
2
|
+
import { classifyFinalChoice, firstSetOf, intersects, matchesEmpty, union, type RefResolver } from '../combinators/first-set.ts'
|
|
3
3
|
import { childrenOf } from '../analysis/gating.ts'
|
|
4
|
+
import { capturesLeaf, mayCommitFailure, mayLeavePartialCapture } from '../analysis/commitment.ts'
|
|
4
5
|
import { getCoreLiteralValue } from '../combinators/choice.ts'
|
|
5
6
|
import { deriveExpected } from '../combinators/expect.ts'
|
|
6
7
|
import { assertionFailureExpected, directTerminalFailureExpected } from '../combinators/expected.ts'
|
|
@@ -33,7 +34,7 @@ import { covKindCode, encodeClassSpec, ownTableProgram } from './program.ts'
|
|
|
33
34
|
import type { GrammarCoveragePlan } from '../compiler/grammar-coverage-ids.ts'
|
|
34
35
|
import { directArrayProjection } from '../compiler/direct-projection.ts'
|
|
35
36
|
import {
|
|
36
|
-
|
|
37
|
+
collectLexicalCapabilities, winnerWrapsReference,
|
|
37
38
|
} from '../compiler/token-capability.ts'
|
|
38
39
|
import { directExecutableTokenBody, type ExecutableLexNode } from '../compiler/token-alphabet.ts'
|
|
39
40
|
|
|
@@ -141,6 +142,9 @@ class Encoder {
|
|
|
141
142
|
private lexIndex = new Map<string, number>()
|
|
142
143
|
lexPrograms: LexProgramSpec[] = []
|
|
143
144
|
private lexProgramIndex = new Map<string, number>()
|
|
145
|
+
private choiceRollbackMasks = new Map<number, number>()
|
|
146
|
+
private nonCommittingChoiceSites = new Set<number>()
|
|
147
|
+
private failureRollbackCleanSites = new Set<number>()
|
|
144
148
|
labels: readonly string[] | undefined = undefined
|
|
145
149
|
/** Trivia table in scope at the row being encoded — codegen's `ctx.activeTrivia`. */
|
|
146
150
|
activeTrivia: Combinator<unknown> | undefined = undefined
|
|
@@ -611,6 +615,77 @@ class Encoder {
|
|
|
611
615
|
return w === undefined ? undefined : (name: string) => w[name]
|
|
612
616
|
}
|
|
613
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Whether a failed ordered-choice arm can leave state that the enclosing
|
|
620
|
+
* choice must restore before trying its successor.
|
|
621
|
+
*
|
|
622
|
+
* `mayLeavePartialCapture` is the existing sound CST/trivia proof. The extra
|
|
623
|
+
* writer census covers the side sinks that proof deliberately does not model:
|
|
624
|
+
* fields, recovered errors, and interpreted scanners. Strict assemblies do
|
|
625
|
+
* not run repeat recovery, but these rows can still publish through a caller's
|
|
626
|
+
* context. Cycles are safe to stop: revisiting the same combinator cannot
|
|
627
|
+
* discover a writer the first traversal did not already reach.
|
|
628
|
+
*/
|
|
629
|
+
private failureNeedsRollback(
|
|
630
|
+
p: Combinator<unknown>,
|
|
631
|
+
autoNot: readonly AutoNotCheck[] | null | undefined,
|
|
632
|
+
): boolean {
|
|
633
|
+
const refName = (p as unknown as { _ruleName?: string })._ruleName
|
|
634
|
+
const winner = refName === undefined ? undefined : this.winners?.[refName]
|
|
635
|
+
const root = winner !== undefined && !winnerWrapsReference(winner, p) ? winner : p
|
|
636
|
+
const sideWriterMemo = new Map<Combinator<unknown>, boolean>()
|
|
637
|
+
const hasSideWriter = (
|
|
638
|
+
current: Combinator<unknown>,
|
|
639
|
+
seen = new Set<Combinator<unknown>>(),
|
|
640
|
+
): { found: boolean; complete: boolean } => {
|
|
641
|
+
const cached = sideWriterMemo.get(current)
|
|
642
|
+
if (cached !== undefined) return { found: cached, complete: true }
|
|
643
|
+
if (seen.has(current)) return { found: false, complete: false }
|
|
644
|
+
const next = new Set(seen)
|
|
645
|
+
next.add(current)
|
|
646
|
+
const def = current._def
|
|
647
|
+
let result: { found: boolean; complete: boolean }
|
|
648
|
+
switch (def.tag) {
|
|
649
|
+
case 'field':
|
|
650
|
+
case 'expect':
|
|
651
|
+
case 'recover':
|
|
652
|
+
case 'scanTo':
|
|
653
|
+
case 'unknown':
|
|
654
|
+
result = { found: true, complete: true }
|
|
655
|
+
break
|
|
656
|
+
case 'lazy': {
|
|
657
|
+
const name = (current as unknown as { _ruleName?: string })._ruleName
|
|
658
|
+
const resolvedWinner = name === undefined ? undefined : this.winners?.[name]
|
|
659
|
+
if (resolvedWinner !== undefined && !winnerWrapsReference(resolvedWinner, current)) {
|
|
660
|
+
result = hasSideWriter(resolvedWinner, next)
|
|
661
|
+
break
|
|
662
|
+
}
|
|
663
|
+
try { result = hasSideWriter(def.thunk(), next) }
|
|
664
|
+
catch { result = { found: true, complete: true } }
|
|
665
|
+
break
|
|
666
|
+
}
|
|
667
|
+
default: {
|
|
668
|
+
let complete = true
|
|
669
|
+
result = { found: false, complete: true }
|
|
670
|
+
for (const child of childrenOf(def)) {
|
|
671
|
+
const childResult = hasSideWriter(child, next)
|
|
672
|
+
if (childResult.found) {
|
|
673
|
+
result = { found: true, complete: true }
|
|
674
|
+
break
|
|
675
|
+
}
|
|
676
|
+
complete &&= childResult.complete
|
|
677
|
+
}
|
|
678
|
+
if (!result.found) result = { found: false, complete }
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (result.found || result.complete) sideWriterMemo.set(current, result.found)
|
|
682
|
+
return result
|
|
683
|
+
}
|
|
684
|
+
return mayLeavePartialCapture(root, new Set(), true)
|
|
685
|
+
|| hasSideWriter(root).found
|
|
686
|
+
|| ((autoNot?.length ?? 0) > 0 && capturesLeaf(root))
|
|
687
|
+
}
|
|
688
|
+
|
|
614
689
|
/** A first set becomes a char class string, or −1 for `any`. */
|
|
615
690
|
private charClass(fs: FirstSet): number {
|
|
616
691
|
if (fs.kind !== 'ranges' || fs.ranges.length === 0) return -1
|
|
@@ -1074,14 +1149,53 @@ class Encoder {
|
|
|
1074
1149
|
// Arm ORDER is preserved on both, which is what makes this a PEG-safe
|
|
1075
1150
|
// change rather than a reordering.
|
|
1076
1151
|
const finalChoice = classifyFinalChoice(arms, rr)
|
|
1077
|
-
|
|
1078
|
-
|
|
1152
|
+
// attempt() preserves its child's success language, but the broad
|
|
1153
|
+
// classifier deliberately treats the transaction as nullable/unknown so
|
|
1154
|
+
// it can never by itself authorize exclusive one-arm selection. Recover
|
|
1155
|
+
// only the narrower fact needed by an ORDERED choice: a non-nullable
|
|
1156
|
+
// child can receive an individual rejection gate. Keep that authority
|
|
1157
|
+
// out if it would make an otherwise open site fully disjoint, preserving
|
|
1158
|
+
// the transaction's standing exclusion from exclusive dispatch.
|
|
1159
|
+
const transactionalFirst = arms.map((arm, i): FirstSet | undefined => {
|
|
1160
|
+
if (!finalChoice.nullable[i]) return undefined
|
|
1161
|
+
const def = arm._def as ParserDef
|
|
1162
|
+
if (def.tag !== 'attempt' || matchesEmpty(def.parser, new Set(), rr)) return undefined
|
|
1163
|
+
return firstSetOf(def.parser, new Set(), rr)
|
|
1164
|
+
})
|
|
1165
|
+
const gateFirst = finalChoice.firstSets.map((first, i) => transactionalFirst[i] ?? first)
|
|
1166
|
+
const classes = gateFirst.map((first, i) => finalChoice.nullable[i] && transactionalFirst[i] === undefined
|
|
1167
|
+
? -1 : this.charClass(first))
|
|
1168
|
+
if (transactionalFirst.some(first => first !== undefined) && classes.every(cls => cls >= 0)) {
|
|
1169
|
+
let overlap = false
|
|
1170
|
+
for (let i = 0; i < gateFirst.length && !overlap; i++) {
|
|
1171
|
+
for (let j = i + 1; j < gateFirst.length; j++) {
|
|
1172
|
+
if (intersects(gateFirst[i]!, gateFirst[j]!)) { overlap = true; break }
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (!overlap) for (let i = 0; i < classes.length; i++) {
|
|
1176
|
+
if (transactionalFirst[i] !== undefined) classes[i] = -1
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1079
1179
|
const dispIdx = this.disp.length
|
|
1080
1180
|
this.disp.push(classes)
|
|
1081
1181
|
// PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
|
|
1082
1182
|
// failure remains at the choice position, an arm declined by the driver's
|
|
1083
1183
|
// char gate contributes the static opener set the interpreter would get by
|
|
1084
1184
|
// entering it. A deeper dynamic arm failure discards those shallower sets.
|
|
1185
|
+
// One compiler-only bit mask records which failed arms can actually
|
|
1186
|
+
// dirty a rollback sink. It is consumed while printing emitted strict
|
|
1187
|
+
// assemblies and never serialized; tolerant assemblies retain the
|
|
1188
|
+
// universal rollback because repeat recovery can publish an error from
|
|
1189
|
+
// inside any arm. Arity >31 falls back to -1 rather than truncating.
|
|
1190
|
+
let rollbackMask = kids.length > 31 ? -1 : 0
|
|
1191
|
+
if (rollbackMask === 0) {
|
|
1192
|
+
for (let i = 0; i < arms.length; i++) {
|
|
1193
|
+
const src = order[i]!
|
|
1194
|
+
if (this.failureNeedsRollback(arms[i]!, d.autoNot[src])) {
|
|
1195
|
+
rollbackMask |= 1 << i
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1085
1199
|
const head = this.emitHead(OP_CHOICE, 3 + 2 * kids.length)
|
|
1086
1200
|
this.code[head + 1] = dispIdx
|
|
1087
1201
|
this.code[head + 2] = kids.length
|
|
@@ -1093,6 +1207,10 @@ class Encoder {
|
|
|
1093
1207
|
this.code[head + 4 + i] = kids[i]!
|
|
1094
1208
|
this.code[head + 4 + kids.length + i] = this.expected(deriveExpected(arms[i]!))
|
|
1095
1209
|
}
|
|
1210
|
+
this.choiceRollbackMasks.set(head, rollbackMask)
|
|
1211
|
+
if (arms.every(arm => !mayCommitFailure(arm, new Set(), rr))) {
|
|
1212
|
+
this.nonCommittingChoiceSites.add(head)
|
|
1213
|
+
}
|
|
1096
1214
|
return head
|
|
1097
1215
|
}
|
|
1098
1216
|
case 'many':
|
|
@@ -1143,8 +1261,11 @@ class Encoder {
|
|
|
1143
1261
|
? this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags, this.expected(deriveExpected(d.parser)))
|
|
1144
1262
|
: this.emit(OP_REP, child, d.min, d.max ?? -1, sep, flags)
|
|
1145
1263
|
}
|
|
1146
|
-
case 'optional':
|
|
1147
|
-
|
|
1264
|
+
case 'optional': {
|
|
1265
|
+
const head = this.emit(OP_OPT, this.node(d.parser).ip)
|
|
1266
|
+
if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(head)
|
|
1267
|
+
return head
|
|
1268
|
+
}
|
|
1148
1269
|
case 'transform': {
|
|
1149
1270
|
// Declared on the def and not lowered here. Refuse rather than assume it is
|
|
1150
1271
|
// inert: a recognition-only transform suppresses its value, and a table that
|
|
@@ -1444,6 +1565,7 @@ class Encoder {
|
|
|
1444
1565
|
// lowering was correct only for a choice arm.
|
|
1445
1566
|
case 'attempt': {
|
|
1446
1567
|
const inner = this.emit(OP_ATTEMPT, this.node(d.parser).ip)
|
|
1568
|
+
if (!this.failureNeedsRollback(d.parser, undefined)) this.failureRollbackCleanSites.add(inner)
|
|
1447
1569
|
// THE FIRST-SET FAIL-FAST GUARD, lowered as the `OP_GATE` row the `node()`
|
|
1448
1570
|
// case already uses — it is the same guard, written twice in the
|
|
1449
1571
|
// interpreter (`attempt.ts:22`, `node.ts:239`). It is NOT merely an
|
|
@@ -1652,6 +1774,10 @@ class Encoder {
|
|
|
1652
1774
|
...(this.rec ? { rec: 1 as const } : {}),
|
|
1653
1775
|
...(this.cov === undefined ? {} : { cov: this.cov }),
|
|
1654
1776
|
lines: this.track ? 1 : 0,
|
|
1777
|
+
}, undefined, {
|
|
1778
|
+
choiceRollbackMasks: this.choiceRollbackMasks,
|
|
1779
|
+
failureRollbackCleanSites: this.failureRollbackCleanSites,
|
|
1780
|
+
nonCommittingChoiceSites: this.nonCommittingChoiceSites,
|
|
1655
1781
|
})
|
|
1656
1782
|
}
|
|
1657
1783
|
}
|
|
@@ -1748,7 +1874,6 @@ export function encodeTableProgram(
|
|
|
1748
1874
|
const lexicalRoots = [...names].sort().map(name => ruleMap[name]!)
|
|
1749
1875
|
const resolveLexical = (name: string): Combinator<unknown> | undefined => ruleMap[name]
|
|
1750
1876
|
const capabilities = collectLexicalCapabilities(lexicalRoots, resolveLexical)
|
|
1751
|
-
assertLexicalCapabilityClosure(lexicalRoots, capabilities, resolveLexical)
|
|
1752
1877
|
const enc = new Encoder(resolvedSettings, capabilities.capabilityComplete)
|
|
1753
1878
|
enc.winners = ruleMap
|
|
1754
1879
|
for (const name of names) enc.encodeRule(name, ruleMap[name]!)
|
package/src/table/ops.ts
CHANGED
|
@@ -27,8 +27,9 @@ export const OP_SEQ = 3
|
|
|
27
27
|
/** `SEQV n c1 … cn` — `SEQ` with `valueUnused`: terms run, no tuple is built. */
|
|
28
28
|
export const OP_SEQV = 4
|
|
29
29
|
/**
|
|
30
|
-
* `CHOICE d n fx c1 … cn` — `d` indexes a dispatch table (or −1 for
|
|
31
|
-
* try); `fx` indexes the choice's OWN expected set
|
|
30
|
+
* `CHOICE d n fx c1 … cn e1 … en` — `d` indexes a dispatch table (or −1 for
|
|
31
|
+
* ordered try); `fx` indexes the choice's OWN expected set and each `e` is one
|
|
32
|
+
* arm's set. Compiler-only rollback authority is deliberately not on the wire.
|
|
32
33
|
*
|
|
33
34
|
* Ordered failure keeps the expected sets at the deepest arm offset, merging
|
|
34
35
|
* exact ties in source order. Exclusive dispatch propagates its sole selected
|
package/src/table/program.ts
CHANGED
|
@@ -490,12 +490,53 @@ export type ResolvedTable = {
|
|
|
490
490
|
*/
|
|
491
491
|
const TABLE_RUNTIME = Symbol('parseman.tableRuntime')
|
|
492
492
|
type RuntimeTableProgram = TableProgram & {
|
|
493
|
-
readonly [TABLE_RUNTIME]?: {
|
|
493
|
+
readonly [TABLE_RUNTIME]?: {
|
|
494
|
+
resolved: ResolvedTable | undefined
|
|
495
|
+
readonly choiceRollbackMasks?: ReadonlyMap<number, number>
|
|
496
|
+
readonly nonCommittingChoiceSites?: ReadonlySet<number>
|
|
497
|
+
readonly failureRollbackCleanSites?: ReadonlySet<number>
|
|
498
|
+
}
|
|
494
499
|
}
|
|
495
500
|
|
|
496
501
|
/** Give an internally-created program its fixed-shape runtime owner. */
|
|
497
|
-
export function ownTableProgram(
|
|
498
|
-
|
|
502
|
+
export function ownTableProgram(
|
|
503
|
+
prog: TableProgram,
|
|
504
|
+
resolved?: ResolvedTable,
|
|
505
|
+
caches: {
|
|
506
|
+
readonly choiceRollbackMasks?: ReadonlyMap<number, number>
|
|
507
|
+
readonly failureRollbackCleanSites?: ReadonlySet<number>
|
|
508
|
+
readonly nonCommittingChoiceSites?: ReadonlySet<number>
|
|
509
|
+
} = {},
|
|
510
|
+
): TableProgram {
|
|
511
|
+
const runtime = (prog as RuntimeTableProgram)[TABLE_RUNTIME]
|
|
512
|
+
return {
|
|
513
|
+
...prog,
|
|
514
|
+
[TABLE_RUNTIME]: {
|
|
515
|
+
resolved,
|
|
516
|
+
choiceRollbackMasks: caches.choiceRollbackMasks ?? runtime?.choiceRollbackMasks,
|
|
517
|
+
nonCommittingChoiceSites: caches.nonCommittingChoiceSites ?? runtime?.nonCommittingChoiceSites,
|
|
518
|
+
failureRollbackCleanSites: caches.failureRollbackCleanSites ?? runtime?.failureRollbackCleanSites,
|
|
519
|
+
},
|
|
520
|
+
} as RuntimeTableProgram
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Compiler-only choice rollback authority, consumed while printing a static
|
|
524
|
+
* assembly and never serialized into the shipped table. */
|
|
525
|
+
export function choiceRollbackMask(prog: TableProgram, ip: number): number | undefined {
|
|
526
|
+
return (prog as RuntimeTableProgram)[TABLE_RUNTIME]?.choiceRollbackMasks?.get(ip)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Compiler-only proof that no arm at this choice can raise a committed
|
|
530
|
+
* failure. Static assembly may defer its diagnostic merge until total failure. */
|
|
531
|
+
export function choiceCannotCommit(prog: TableProgram, ip: number): boolean {
|
|
532
|
+
return (prog as RuntimeTableProgram)[TABLE_RUNTIME]?.nonCommittingChoiceSites?.has(ip) === true
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Compiler-only authority that a failed transactional child cannot leak any
|
|
536
|
+
* capture, trivia, field, or error sink. Static assembly consumes it to omit the
|
|
537
|
+
* enclosing mark and rollback; it is never serialized into the shipped table. */
|
|
538
|
+
export function failureRollbackClean(prog: TableProgram, ip: number): boolean {
|
|
539
|
+
return (prog as RuntimeTableProgram)[TABLE_RUNTIME]?.failureRollbackCleanSites?.has(ip) === true
|
|
499
540
|
}
|
|
500
541
|
|
|
501
542
|
/** A compiled entry, shaped exactly like a codegen rule function. */
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
OP_ATTEMPT, OP_CHOICE, OP_FIELD, OP_GATE, OP_LABEL, OP_LEAF, OP_LIT, OP_NODE, OP_NOT,
|
|
3
|
+
OP_PEEK, OP_RULE, OP_RX, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV,
|
|
4
|
+
OP_SEQX, OP_TOKEN, OP_XFORM,
|
|
5
|
+
} from './ops.ts'
|
|
2
6
|
|
|
3
7
|
/** Pure recognition only. Consumption owns value, CST, line, failure and cursor effects. */
|
|
4
8
|
export type ScalarRecognizer = (input: string, pos: number, seed?: number) => number
|
|
@@ -49,3 +53,211 @@ export function scalarTerminalNotChild(code: ArrayLike<number>, ip: number): num
|
|
|
49
53
|
const op = code[child]
|
|
50
54
|
return op === OP_RX || op === OP_LIT ? child : -1
|
|
51
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A direct scalar terminal that every successful execution of `ip` must match
|
|
59
|
+
* first, through wrappers that cannot consume or select before their child.
|
|
60
|
+
* Returns only after `minDepth` edges so a caller never duplicates recognition
|
|
61
|
+
* merely to avoid one already-cheap terminal call.
|
|
62
|
+
*/
|
|
63
|
+
export function leadingScalarTerminal(
|
|
64
|
+
code: ArrayLike<number>, ip: number, minDepth = 2, throughAttempt = true, throughPeek = false,
|
|
65
|
+
): number {
|
|
66
|
+
const seen = new Set<number>()
|
|
67
|
+
let at = ip
|
|
68
|
+
let depth = 0
|
|
69
|
+
while (!seen.has(at)) {
|
|
70
|
+
seen.add(at)
|
|
71
|
+
const op = code[at]
|
|
72
|
+
if (op === OP_LIT || op === OP_RX) return depth >= minDepth ? at : -1
|
|
73
|
+
if (op === OP_RULE || op === OP_LABEL || op === OP_TOKEN || (throughAttempt && op === OP_ATTEMPT)) {
|
|
74
|
+
at = code[at + 1]!
|
|
75
|
+
depth++
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
// A positive lookahead has exactly its child's success language and no
|
|
79
|
+
// effects on success. Choice predecisions may therefore execute a scalar
|
|
80
|
+
// child to reject the enclosing arm early. This is opt-in because callers
|
|
81
|
+
// that require a CONSUMING opener (notably exact start-failure diagnostics)
|
|
82
|
+
// must continue to treat peek() as zero-width.
|
|
83
|
+
if (throughPeek && op === OP_PEEK) {
|
|
84
|
+
at = code[at + 1]!
|
|
85
|
+
depth++
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
if (op === OP_GATE || op === OP_SCOPE || op === OP_SCOPE_CAP || op === OP_SCOPE_PLAIN
|
|
89
|
+
|| op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
|
|
90
|
+
at = code[at + 2]!
|
|
91
|
+
depth++
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if (op === OP_SEQ || op === OP_SEQV) {
|
|
95
|
+
if (code[at + 1]! < 1) return -1
|
|
96
|
+
at = code[at + 2]!
|
|
97
|
+
depth++
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
if (op === OP_SEQX) {
|
|
101
|
+
if (code[at + 2]! < 1) return -1
|
|
102
|
+
at = code[at + 3]!
|
|
103
|
+
depth++
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
return -1
|
|
107
|
+
}
|
|
108
|
+
return -1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type LeadingScalarSequence = {
|
|
112
|
+
readonly terminals: readonly number[]
|
|
113
|
+
/** The explicit ambient trivia slot between terms; -1 means no trivia. */
|
|
114
|
+
readonly trivia: number
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The first three complete scalar terms of a sequence under an explicit trivia
|
|
119
|
+
* scope. Unlike `leadingScalarTerminal`, each returned terminal is the WHOLE
|
|
120
|
+
* corresponding sequence term: recognizing it therefore identifies the exact
|
|
121
|
+
* position at which the enclosing sequence would scan trivia for the next one.
|
|
122
|
+
*
|
|
123
|
+
* This is rejection authority only. Gates, attempts, transforms and builders
|
|
124
|
+
* may still make the arm fail after the prefix matched, so callers must enter
|
|
125
|
+
* the ordinary arm on a positive result and may only skip it on a miss.
|
|
126
|
+
*/
|
|
127
|
+
export function leadingScalarSequence(
|
|
128
|
+
code: ArrayLike<number>, ip: number,
|
|
129
|
+
): LeadingScalarSequence | undefined {
|
|
130
|
+
const wholeScalar = (start: number): number => {
|
|
131
|
+
const seen = new Set<number>()
|
|
132
|
+
let at = start
|
|
133
|
+
while (!seen.has(at)) {
|
|
134
|
+
seen.add(at)
|
|
135
|
+
const op = code[at]
|
|
136
|
+
if (op === OP_LIT || op === OP_RX) return at
|
|
137
|
+
if (op === OP_RULE || op === OP_TOKEN || op === OP_ATTEMPT) {
|
|
138
|
+
at = code[at + 1]!
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
if (op === OP_GATE || op === OP_SCOPE_PLAIN
|
|
142
|
+
|| op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
|
|
143
|
+
at = code[at + 2]!
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
if (op === OP_SEQ || op === OP_SEQV) {
|
|
147
|
+
if (code[at + 1] !== 1) return -1
|
|
148
|
+
at = code[at + 2]!
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
if (op === OP_SEQX) {
|
|
152
|
+
if (code[at + 2] !== 1) return -1
|
|
153
|
+
at = code[at + 3]!
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
return -1
|
|
157
|
+
}
|
|
158
|
+
return -1
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const seen = new Set<number>()
|
|
162
|
+
let at = ip
|
|
163
|
+
let ambientTrivia: number | undefined
|
|
164
|
+
while (!seen.has(at)) {
|
|
165
|
+
seen.add(at)
|
|
166
|
+
const op = code[at]
|
|
167
|
+
if (op === OP_RULE) {
|
|
168
|
+
at = code[at + 1]!
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (op === OP_SCOPE_PLAIN) {
|
|
172
|
+
ambientTrivia = code[at + 1]!
|
|
173
|
+
at = code[at + 2]!
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
if (op === OP_GATE || op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
|
|
177
|
+
at = code[at + 2]!
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const n = op === OP_SEQ || op === OP_SEQV
|
|
182
|
+
? code[at + 1]!
|
|
183
|
+
: op === OP_SEQX ? code[at + 2]! : 0
|
|
184
|
+
if (n < 3 || ambientTrivia === undefined) return undefined
|
|
185
|
+
const base = op === OP_SEQX ? at + 3 : at + 2
|
|
186
|
+
const terminals = [
|
|
187
|
+
wholeScalar(code[base]!),
|
|
188
|
+
wholeScalar(code[base + 1]!),
|
|
189
|
+
wholeScalar(code[base + 2]!),
|
|
190
|
+
]
|
|
191
|
+
return terminals.every(terminal => terminal >= 0)
|
|
192
|
+
? { terminals, trivia: ambientTrivia }
|
|
193
|
+
: undefined
|
|
194
|
+
}
|
|
195
|
+
return undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* A bounded union of literal prefixes that every successful execution of `ip`
|
|
200
|
+
* must enter through. Unlike `leadingScalarTerminal`, this may cross nested
|
|
201
|
+
* choices, but only when EVERY arm yields literal authority. Longer literals
|
|
202
|
+
* covered by a shorter prefix are removed (`@{-}` is already covered by `@{`).
|
|
203
|
+
*/
|
|
204
|
+
export function leadingLiteralFamily(
|
|
205
|
+
code: ArrayLike<number>, constants: readonly unknown[], ip: number,
|
|
206
|
+
minDepth = 2, limit = 4,
|
|
207
|
+
): readonly number[] | undefined {
|
|
208
|
+
const active = new Set<number>()
|
|
209
|
+
|
|
210
|
+
const walk = (at: number, depth: number): number[] | undefined => {
|
|
211
|
+
if (active.has(at)) return undefined
|
|
212
|
+
active.add(at)
|
|
213
|
+
try {
|
|
214
|
+
const op = code[at]
|
|
215
|
+
if (op === OP_LIT) {
|
|
216
|
+
const value = constants[code[at + 1]!]
|
|
217
|
+
return depth >= minDepth && typeof value === 'string' && value.length > 0 ? [at] : undefined
|
|
218
|
+
}
|
|
219
|
+
if (op === OP_RULE || op === OP_LABEL || op === OP_TOKEN || op === OP_ATTEMPT || op === OP_PEEK) {
|
|
220
|
+
return walk(code[at + 1]!, depth + 1)
|
|
221
|
+
}
|
|
222
|
+
if (op === OP_GATE || op === OP_SCOPE || op === OP_SCOPE_CAP || op === OP_SCOPE_PLAIN
|
|
223
|
+
|| op === OP_XFORM || op === OP_NODE || op === OP_FIELD || op === OP_LEAF) {
|
|
224
|
+
return walk(code[at + 2]!, depth + 1)
|
|
225
|
+
}
|
|
226
|
+
if (op === OP_SEQ || op === OP_SEQV) {
|
|
227
|
+
return code[at + 1]! < 1 ? undefined : walk(code[at + 2]!, depth + 1)
|
|
228
|
+
}
|
|
229
|
+
if (op === OP_SEQX) {
|
|
230
|
+
return code[at + 2]! < 1 ? undefined : walk(code[at + 3]!, depth + 1)
|
|
231
|
+
}
|
|
232
|
+
if (op !== OP_CHOICE) return undefined
|
|
233
|
+
|
|
234
|
+
const n = code[at + 2]!
|
|
235
|
+
if (n < 1) return undefined
|
|
236
|
+
const out: number[] = []
|
|
237
|
+
for (let i = 0; i < n; i++) {
|
|
238
|
+
const branch = walk(code[at + 4 + i]!, depth + 1)
|
|
239
|
+
if (branch === undefined) return undefined
|
|
240
|
+
for (const terminal of branch) {
|
|
241
|
+
const value = constants[code[terminal + 1]!] as string
|
|
242
|
+
let covered = false
|
|
243
|
+
for (let j = 0; j < out.length; j++) {
|
|
244
|
+
const prior = constants[code[out[j]! + 1]!] as string
|
|
245
|
+
if (value.startsWith(prior)) { covered = true; break }
|
|
246
|
+
}
|
|
247
|
+
if (covered) continue
|
|
248
|
+
for (let j = out.length - 1; j >= 0; j--) {
|
|
249
|
+
const prior = constants[code[out[j]! + 1]!] as string
|
|
250
|
+
if (prior.startsWith(value)) out.splice(j, 1)
|
|
251
|
+
}
|
|
252
|
+
out.push(terminal)
|
|
253
|
+
if (out.length > limit) return undefined
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return out
|
|
257
|
+
} finally {
|
|
258
|
+
active.delete(at)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return walk(ip, 0)
|
|
263
|
+
}
|
package/src/table/site-labels.ts
CHANGED
|
@@ -79,6 +79,13 @@ export const CAP_OFF = 1
|
|
|
79
79
|
/** `ctx.captureTrivia === true` is guaranteed here. */
|
|
80
80
|
export const CAP_ON = 2
|
|
81
81
|
|
|
82
|
+
/** Buffer raw-entry mode is not provable at this site. */
|
|
83
|
+
export const RAW_UNKNOWN = 0
|
|
84
|
+
/** Buffer retains raw entry values. */
|
|
85
|
+
export const RAW_CAPTURE = 1
|
|
86
|
+
/** Buffer retains only a raw source-order count. */
|
|
87
|
+
export const RAW_OMIT = 2
|
|
88
|
+
|
|
82
89
|
export type SiteLabel = {
|
|
83
90
|
/** `TRI_UNKNOWN`, `TRI_NONE`, or the trivia slot the enclosing scope installed. */
|
|
84
91
|
readonly tri: number
|
|
@@ -87,19 +94,22 @@ export type SiteLabel = {
|
|
|
87
94
|
* asymmetry note in this file's header. Never read as "guaranteed absent".
|
|
88
95
|
*/
|
|
89
96
|
readonly buf: boolean
|
|
97
|
+
/** `RAW_UNKNOWN` / `RAW_CAPTURE` / `RAW_OMIT`. */
|
|
98
|
+
readonly raw: number
|
|
90
99
|
/** `CAP_UNKNOWN` / `CAP_OFF` / `CAP_ON`. */
|
|
91
100
|
readonly cap: number
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
/** The lattice's top: every entry point starts here. */
|
|
95
|
-
export const TOP: SiteLabel = { tri: TRI_UNKNOWN, buf: false, cap: CAP_UNKNOWN }
|
|
104
|
+
export const TOP: SiteLabel = { tri: TRI_UNKNOWN, buf: false, raw: RAW_UNKNOWN, cap: CAP_UNKNOWN }
|
|
96
105
|
|
|
97
106
|
function meet(a: SiteLabel, b: SiteLabel): SiteLabel {
|
|
98
107
|
const tri = a.tri === b.tri ? a.tri : TRI_UNKNOWN
|
|
99
108
|
const buf = a.buf && b.buf
|
|
109
|
+
const raw = a.raw === b.raw ? a.raw : RAW_UNKNOWN
|
|
100
110
|
const cap = a.cap === b.cap ? a.cap : CAP_UNKNOWN
|
|
101
|
-
if (tri === a.tri && buf === a.buf && cap === a.cap) return a
|
|
102
|
-
return { tri, buf, cap }
|
|
111
|
+
if (tri === a.tri && buf === a.buf && raw === a.raw && cap === a.cap) return a
|
|
112
|
+
return { tri, buf, raw, cap }
|
|
103
113
|
}
|
|
104
114
|
|
|
105
115
|
/**
|
|
@@ -145,7 +155,7 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
|
|
|
145
155
|
// (`encode.ts:1153-1158`), so it can only ever raise `cap` to `CAP_ON`.
|
|
146
156
|
const cap = op === OP_SCOPE_CAP ? CAP_ON : at.cap
|
|
147
157
|
if (tri === at.tri && cap === at.cap) return at
|
|
148
|
-
return { tri, buf: at.buf, cap }
|
|
158
|
+
return { tri, buf: at.buf, raw: at.raw, cap }
|
|
149
159
|
}
|
|
150
160
|
if (op === OP_NODE || op === OP_NODE_TRACK) {
|
|
151
161
|
const flags = code[ip + 3]!
|
|
@@ -156,14 +166,17 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
|
|
|
156
166
|
// arrays instead of a CstCaptureBuf. Descendants still capture, but they
|
|
157
167
|
// must take the generic collector branch rather than dereference `_cstBuf`.
|
|
158
168
|
// False means UNKNOWN in this lattice, which is exactly the safe answer.
|
|
159
|
-
return { tri: at.tri, buf: false, cap: CAP_OFF }
|
|
169
|
+
return { tri: at.tri, buf: false, raw: RAW_UNKNOWN, cap: CAP_OFF }
|
|
160
170
|
}
|
|
161
171
|
// `ctx._cstBuf = buf` and `ctx.captureTrivia = <literal>`, both unconditional
|
|
162
172
|
// — `emit-assembly.ts`'s `OP_NODE` body opens the buffer before it descends
|
|
163
173
|
// and closes it after, whatever the host mode is.
|
|
164
174
|
const cap = ((flags & 4) !== 0 || hostCst) ? CAP_ON : CAP_OFF
|
|
165
|
-
|
|
166
|
-
|
|
175
|
+
const raw = !hostCst && code[ip + 1]! >= 0 && code[ip + 4]! < 0 && (flags & 2) !== 0
|
|
176
|
+
? RAW_OMIT
|
|
177
|
+
: RAW_CAPTURE
|
|
178
|
+
if (at.buf && raw === at.raw && cap === at.cap) return at
|
|
179
|
+
return { tri: at.tri, buf: true, raw, cap }
|
|
167
180
|
}
|
|
168
181
|
// THE TWO BOUNDARIES clear every capture sink for their child and contribute
|
|
169
182
|
// ONE leaf for the whole match; `token()` clears `ctx.trivia` as well. So a
|
|
@@ -178,11 +191,11 @@ function transfer(code: Int32Array, ip: number, at: SiteLabel, hostCst: boolean)
|
|
|
178
191
|
// an option.
|
|
179
192
|
if (op === OP_TOKEN) {
|
|
180
193
|
if (at.tri === TRI_NONE && !at.buf) return at
|
|
181
|
-
return { tri: TRI_NONE, buf: false, cap: at.cap }
|
|
194
|
+
return { tri: TRI_NONE, buf: false, raw: RAW_UNKNOWN, cap: at.cap }
|
|
182
195
|
}
|
|
183
196
|
if (op === OP_LEAF) {
|
|
184
197
|
if (!at.buf) return at
|
|
185
|
-
return { tri: at.tri, buf: false, cap: at.cap }
|
|
198
|
+
return { tri: at.tri, buf: false, raw: RAW_UNKNOWN, cap: at.cap }
|
|
186
199
|
}
|
|
187
200
|
return at
|
|
188
201
|
}
|
package/src/types.ts
CHANGED
|
@@ -89,7 +89,7 @@ export type ParserDef =
|
|
|
89
89
|
// it requires, so it carries `parser`'s first-set and a leading `peek()` gates
|
|
90
90
|
// its choice arm (see `isPositiveLookahead` / `sequenceFirstSet`).
|
|
91
91
|
| { tag: 'peek'; parser: Combinator<unknown> }
|
|
92
|
-
| { tag: 'node'; type?: string; parser: Combinator<unknown>; build?: ((children: ReadonlyArray<unknown>, fields: FieldMap | undefined, span: { start: number; end: number }, rawChildren: ReadonlyArray<unknown>, triviaLog: readonly number[], state: unknown) => unknown) | undefined; buildSrc?: string; /** The IDENTIFIER the type argument was written as at the `node()` call site, when it was a plain identifier rather than a literal — ANALYSIS ONLY, never emitted. Lets the inline-`mk` matcher recognise `node(t, …, (…) => mk(t, …))` from a factory, where the two `t`s are provably one binding. */ typeSrc?: string; /** Resolved source of a NAMED `buildSrc` — ANALYSIS ONLY, never emitted. See `buildAnalysisSrc`. */ buildSigSrc?: string; /** Declared positional arity of `build`: author-declared via `node(..., { buildArity })`, or resolved from the reducer's declaration. Wins over source inspection. */ buildArity?: number; /** Why arity could not be resolved, for the diagnostic. */ buildArityUnresolved?: string; buildStaticError?: readonly string[]; unwrap?: boolean; collapse?: boolean; project?: number; captureTrivia?: boolean; trailingTrivia?: boolean; tags?: readonly string[] }
|
|
92
|
+
| { tag: 'node'; type?: string; parser: Combinator<unknown>; build?: ((children: ReadonlyArray<unknown>, fields: FieldMap | undefined, span: { start: number; end: number }, rawChildren: ReadonlyArray<unknown>, triviaLog: readonly number[], state: unknown) => unknown) | undefined; buildSrc?: string; /** The IDENTIFIER the type argument was written as at the `node()` call site, when it was a plain identifier rather than a literal — ANALYSIS ONLY, never emitted. Lets the inline-`mk` matcher recognise `node(t, …, (…) => mk(t, …))` from a factory, where the two `t`s are provably one binding. */ typeSrc?: string; /** Resolved source of a NAMED `buildSrc` — ANALYSIS ONLY, never emitted. See `buildAnalysisSrc`. */ buildSigSrc?: string; /** Macro analysis proved the fourth formal (`rawChildren`) unreferenced. Never inferred from an author-declared `buildArity`; carried through composed IR. */ buildRawUnused?: true; /** Declared positional arity of `build`: author-declared via `node(..., { buildArity })`, or resolved from the reducer's declaration. Wins over source inspection. */ buildArity?: number; /** Why arity could not be resolved, for the diagnostic. */ buildArityUnresolved?: string; buildStaticError?: readonly string[]; /** Import provenance for a direct builder's free names — each `{ local, source, imported }` lets a downstream compose() re-emit `import { imported as local } from source` so the inlined builder source can bind. Carried through the IR round-trip. */ buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }>; unwrap?: boolean; collapse?: boolean; project?: number; captureTrivia?: boolean; trailingTrivia?: boolean; tags?: readonly string[] }
|
|
93
93
|
// `predSrc`/`extraSrc` (set by the macro evaluator): SOURCE TEXT of the guard
|
|
94
94
|
// predicate / the withCtx `extra` value, so codegen inlines them into `_mf`
|
|
95
95
|
// rather than pushing a `null` source. Absent under runtime compile() (the real
|
package/src/version.ts
CHANGED