parseman 0.48.0 → 0.49.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.
Files changed (45) hide show
  1. package/dist/analysis/diagnostics.js +158 -51
  2. package/dist/analysis/diagnostics.js.map +2 -2
  3. package/dist/cli/index.js +160 -52
  4. package/dist/cli/index.js.map +2 -2
  5. package/dist/combinators/choice.d.ts.map +1 -1
  6. package/dist/compiler/ir-serialize.d.ts.map +1 -1
  7. package/dist/compiler/linker.d.ts.map +1 -1
  8. package/dist/index.js +169 -55
  9. package/dist/index.js.map +2 -2
  10. package/dist/language-service/index.js.map +1 -1
  11. package/dist/plugin/direct-builder-static.d.ts +20 -0
  12. package/dist/plugin/direct-builder-static.d.ts.map +1 -1
  13. package/dist/plugin/evaluator.d.ts +13 -0
  14. package/dist/plugin/evaluator.d.ts.map +1 -1
  15. package/dist/plugin/index.d.ts.map +1 -1
  16. package/dist/plugin/index.js +365 -71
  17. package/dist/plugin/index.js.map +2 -2
  18. package/dist/spec/index.js +12 -2
  19. package/dist/spec/index.js.map +2 -2
  20. package/dist/table/assemble.d.ts.map +1 -1
  21. package/dist/table/emit-assembly.d.ts.map +1 -1
  22. package/dist/table/encode.d.ts.map +1 -1
  23. package/dist/table/exec.d.ts.map +1 -1
  24. package/dist/table/index.js +158 -51
  25. package/dist/table/index.js.map +1 -1
  26. package/dist/table/lex-program.d.ts.map +1 -1
  27. package/dist/table/ops.d.ts +4 -4
  28. package/dist/types.d.ts +6 -1
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/version.d.ts +1 -1
  31. package/package.json +1 -1
  32. package/src/combinators/choice.ts +10 -1
  33. package/src/compiler/ir-serialize.ts +15 -3
  34. package/src/compiler/linker.ts +14 -1
  35. package/src/plugin/direct-builder-static.ts +116 -12
  36. package/src/plugin/evaluator.ts +50 -2
  37. package/src/plugin/index.ts +140 -1
  38. package/src/table/assemble.ts +82 -59
  39. package/src/table/emit-assembly.ts +10 -5
  40. package/src/table/encode.ts +6 -10
  41. package/src/table/exec.ts +13 -18
  42. package/src/table/lex-program.ts +15 -6
  43. package/src/table/ops.ts +4 -4
  44. package/src/types.ts +1 -1
  45. package/src/version.ts +1 -1
@@ -23,7 +23,7 @@ import { createUnplugin } from 'unplugin'
23
23
  import { parseSync } from 'oxc-parser'
24
24
  import { ResolverFactory } from 'oxc-resolver'
25
25
  import MagicString from 'magic-string'
26
- import { evaluateExpr, evaluateCombinatorArray, evaluateParserFactory, evaluateStaticValue, evaluateWordFactory, evaluateWhenFactory, evaluateRefDeclaration, applyDefineStatement, referencesAny, setReducerResolver, propName, type Scope, type ScopeEntry } from './evaluator.ts'
26
+ import { evaluateExpr, evaluateCombinatorArray, evaluateParserFactory, evaluateStaticValue, evaluateWordFactory, evaluateWhenFactory, evaluateRefDeclaration, applyDefineStatement, referencesAny, setReducerResolver, setBuilderImportResolver, propName, type Scope, type ScopeEntry } from './evaluator.ts'
27
27
  import { classifyRuleMap } from '../analysis/commitment.ts'
28
28
  import { compile } from '../table/compile.ts'
29
29
  import { compileRuleMap } from '../table/compile-rule-map.ts'
@@ -516,6 +516,7 @@ export function transformMacro(
516
516
  return transformMacroImpl(code, id, moduleAliases, warnUnloweredRegex, recovery, grammarCoverage)
517
517
  } finally {
518
518
  setReducerResolver(null)
519
+ setBuilderImportResolver(null)
519
520
  // Both are idempotent: on the success path the body already released them and these
520
521
  // are no-ops. On an aborted transform they are what stops the leak.
521
522
  for (const d of unwindDegradationCapture(depth)) console.warn(formatDegradation(d))
@@ -666,6 +667,10 @@ function transformMacroImpl(
666
667
  */
667
668
  const reducerResolver = createReducerResolver(id, body as unknown[], code)
668
669
  setReducerResolver(reducerResolver, code)
670
+ /* A direct builder's free name is rescuable when THIS module imported it — the
671
+ * artifact carries `{ source, imported }` and a downstream compose() re-emits the
672
+ * import (see the re-lower pass below). Module-private consts stay refusals. */
673
+ setBuilderImportResolver(name => importBindings.get(name) ?? null)
669
674
 
670
675
  const topLevelFunction = (moduleBody: AnyNode[], name: string): AnyNode | null => {
671
676
  for (const st of moduleBody) {
@@ -797,6 +802,69 @@ function transformMacroImpl(
797
802
  * at the end of this transform is what proves it was actually emitted.
798
803
  */
799
804
  let usedTableRuntime = false
805
+ /**
806
+ * `source` -> (local -> imported) for direct-builder free names that rode in on a
807
+ * carried piece's IR (a composed base grammar whose reducers call imported AST
808
+ * factories). Re-emitted as real imports into THIS module so the inlined builder
809
+ * source has bindings to resolve. A name this module already imports is skipped.
810
+ */
811
+ const pendingBuilderImports = new Map<string, Map<string, string>>()
812
+ /**
813
+ * Harvest direct-builder import provenance off a MERGED rule graph so the re-lower
814
+ * pass re-emits those imports. `buildImports` rides on a `node()`'s `_def`, but a
815
+ * rule that ANOTHER rule references (`g.X`) is materialized in the evaluated map as
816
+ * a `lazy` proxy whose definition node sits behind `_def.thunk()` — never an own
817
+ * property. A generic `Object.values` descent therefore skips it entirely, silently
818
+ * dropping the imports of every cross-referenced rule (the css base's `VarCall`,
819
+ * referenced by its selector/value parents, is the canonical case: its `funcCall` /
820
+ * `isValueSlotValue` reads were lost and the fused downstream module threw "bound by
821
+ * nothing" / left a runtime compose()).
822
+ *
823
+ * The fused table encodes exactly the WINNER of each rule name and binds every
824
+ * reference BY NAME to that winner (`enc.winners`), so the imports it inlines are
825
+ * exactly the union over the winning rule bodies. Harvest that set: walk each rule
826
+ * entry's own body (following the entry lazy's thunk), and STOP at a named `lazy`
827
+ * reference inside a body — it resolves to a winner already covered as its own
828
+ * entry, so descending it would reach an OVERRIDDEN rule's shadowed original and
829
+ * re-emit a dead import. An anonymous `lazy` is inline recursion and is followed.
830
+ * One `seen` set is shared across every rule so a cyclic grammar terminates.
831
+ */
832
+ const collectBuilderImports = (ruleMap: ReadonlyArray<readonly [string, Combinator<unknown>]>): void => {
833
+ const seen = new Set<unknown>()
834
+ const walk = (v: unknown): void => {
835
+ if (v === null || typeof v !== 'object' || seen.has(v)) return
836
+ seen.add(v)
837
+ const def = (v as { _def?: { tag?: string; thunk?: () => unknown; buildImports?: ReadonlyArray<{ local: string; source: string; imported: string }> } })._def
838
+ for (const bi of def?.buildImports ?? []) {
839
+ let m = pendingBuilderImports.get(bi.source)
840
+ if (!m) { m = new Map(); pendingBuilderImports.set(bi.source, m) }
841
+ m.set(bi.local, bi.imported)
842
+ }
843
+ // An ANONYMOUS lazy is inline recursion — follow the thunk. A NAMED lazy is a
844
+ // rule reference bound by name to a winner covered as its own entry; do not
845
+ // descend it (that path reaches an overridden rule's shadowed original).
846
+ if (def?.tag === 'lazy' && (v as { _ruleName?: string })._ruleName === undefined && typeof def.thunk === 'function') {
847
+ let target: unknown = null
848
+ try { target = def.thunk() } catch { /* external ref — no definition to walk */ }
849
+ walk(target)
850
+ }
851
+ for (const child of Object.values(v as Record<string, unknown>)) {
852
+ if (Array.isArray(child)) { for (const c of child) walk(c) } else walk(child)
853
+ }
854
+ }
855
+ for (const [, rule] of ruleMap) {
856
+ walk(rule)
857
+ // A referenced rule entry is a named lazy whose body node (with its builder
858
+ // imports) is behind the thunk; `walk` skips a named lazy's thunk, so follow
859
+ // the WINNER'S body explicitly here. A raw-node entry was already fully walked.
860
+ const def = rule._def as { tag?: string; thunk?: () => unknown }
861
+ if (def?.tag === 'lazy' && typeof def.thunk === 'function') {
862
+ let body: unknown = null
863
+ try { body = def.thunk() } catch { /* unresolved winner — nothing to walk */ }
864
+ walk(body)
865
+ }
866
+ }
867
+ }
800
868
  /** Exported `rules()` factories, whose bodies are left verbatim. See the push site. */
801
869
  const exportedFactories: Array<{ name: string; pos: number }> = []
802
870
  let runtimeComposeFallback = false
@@ -1457,6 +1525,9 @@ function transformMacroImpl(
1457
1525
  const merged = mergedCarriedRules(carried)
1458
1526
  if (merged !== null) {
1459
1527
  if (composing) applyComposingTrivia(merged.rules, composing)
1528
+ // Harvest the direct-builder import provenance carried on the merged graph so
1529
+ // the re-lower pass at the end of this transform re-emits those imports.
1530
+ collectBuilderImports(merged.rules)
1460
1531
  const refusals: string[] = []
1461
1532
  const compiled = compileRuleMap(merged.rules, {
1462
1533
  ...(composing ? { trivia: composing } : {}),
@@ -2186,6 +2257,73 @@ function transformMacroImpl(
2186
2257
  if (usedTableRuntime && applied.length > 0) {
2187
2258
  ms.prepend(`import { tableRules } from ${JSON.stringify(TABLE_RUNTIME_SPECIFIER)}\n`)
2188
2259
  }
2260
+ /* Re-bind the direct-builder free names carried in on composed IR: a base
2261
+ * grammar's reducer calls `dimension` from '@jesscss/core/ast', that provenance
2262
+ * rode in on the carried piece, and the fused table inlines the reducer source
2263
+ * here — so this module must import the same binding.
2264
+ *
2265
+ * The inlined builder source spells the free name VERBATIM, so a re-emitted import
2266
+ * cannot be aliased away: the local name is load-bearing. Two things therefore make
2267
+ * a carried import unsatisfiable, and both are refused rather than silently mis-bound
2268
+ * or emitted as a duplicate declaration:
2269
+ *
2270
+ * - the name is ALREADY BOUND in this module to something else — a named import
2271
+ * from a DIFFERENT source/symbol (the inlined builder would call the wrong
2272
+ * binding), a default/namespace import, or a top-level const/function/class
2273
+ * (re-emitting the import would be a redeclaration `SyntaxError`). The
2274
+ * free-identifier net below cannot catch either: a wrong-binding read is bound,
2275
+ * and a redeclaration is the opposite of an unbound read.
2276
+ * - two DISTINCT carried sources need the SAME local name (the map is keyed by
2277
+ * source, so each would emit its own `import { X } from …`).
2278
+ *
2279
+ * A name this module already imports from the SAME source under the SAME symbol is
2280
+ * the benign case — the exact import the builder needs is already present, so it is
2281
+ * skipped (no duplicate). */
2282
+ if (applied.length > 0 && pendingBuilderImports.size > 0) {
2283
+ // Every top-level binding of THIS module, not just its named imports.
2284
+ const boundLocally = new Set<string>()
2285
+ for (const stmt of body as Statement[]) {
2286
+ if (stmt.type === 'ImportDeclaration') {
2287
+ for (const sp of (stmt as unknown as ImportDeclaration).specifiers) boundLocally.add(sp.local.name)
2288
+ continue
2289
+ }
2290
+ const decl = stmt.type === 'ExportNamedDeclaration'
2291
+ ? ((stmt as unknown as ExportNamedDeclaration).declaration as unknown as AnyNode | undefined)
2292
+ : (stmt as unknown as AnyNode)
2293
+ if (decl?.type === 'FunctionDeclaration' || decl?.type === 'ClassDeclaration') {
2294
+ const n = (decl as unknown as { id?: { name?: string } }).id?.name
2295
+ if (n) boundLocally.add(n)
2296
+ continue
2297
+ }
2298
+ const vd = unwrapVd(stmt)
2299
+ for (const d of vd?.declarations ?? []) {
2300
+ const n = (d.id as unknown as { name?: string }).name
2301
+ if (n) boundLocally.add(n)
2302
+ }
2303
+ }
2304
+ const emitted = new Set<string>()
2305
+ for (const [source, locals] of pendingBuilderImports) {
2306
+ const needed: Array<[string, string]> = []
2307
+ for (const [local, imported] of locals) {
2308
+ const own = importBindings.get(local)
2309
+ // Exact same import already present: the builder's need is satisfied. Skip.
2310
+ if (own && own.source === source && own.imported === imported) continue
2311
+ if (boundLocally.has(local) || emitted.has(local)) {
2312
+ throw new Error(
2313
+ `${id} — parseman will not emit this module: a fused builder needs \`${imported}\` from `
2314
+ + `${JSON.stringify(source)} bound as \`${local}\`, but this module already binds \`${local}\` `
2315
+ + `to something else. The inlined builder source names \`${local}\` verbatim, so the import `
2316
+ + `cannot be aliased; rename the conflicting local binding so the builder's import can be re-emitted.`,
2317
+ )
2318
+ }
2319
+ emitted.add(local)
2320
+ needed.push([local, imported])
2321
+ }
2322
+ if (needed.length === 0) continue
2323
+ const specs = needed.map(([local, imported]) => imported === local ? local : `${imported} as ${local}`)
2324
+ ms.prepend(`import { ${specs.join(', ')} } from ${JSON.stringify(source)}\n`)
2325
+ }
2326
+ }
2189
2327
  if (applied.length > 0) {
2190
2328
  ms.prepend(
2191
2329
  `// Generated by parseman v${PARSEMAN_VERSION} — DO NOT EDIT.\n` +
@@ -2259,6 +2397,7 @@ function transformMacroImpl(
2259
2397
  // caller's config does not break; it is now inert.
2260
2398
 
2261
2399
  setReducerResolver(null)
2400
+ setBuilderImportResolver(null)
2262
2401
  // Every place the compiler chose a correct-but-slower path for this module. Reported
2263
2402
  // on the ordinary warning channel and greppable on `[parseman] degraded`, so a
2264
2403
  // consumer's build gate can assert zero of them the way jess's `check:macro` already
@@ -224,13 +224,13 @@ type MaskedChoiceBlock = (
224
224
  input: string, pos: number, ctx: ParseContext, bits: number,
225
225
  need: boolean, mRaw: number, mTl: number, mLv: number, mFl: number,
226
226
  mEr: number, mLog: number, mRoot: number,
227
- acc: string[] | undefined, prev: number,
227
+ acc: string[] | undefined, prev: number, best: number,
228
228
  ) => unknown
229
229
  type GeneralChoiceBlock = (
230
230
  input: string, pos: number, ctx: ParseContext, c: number,
231
231
  need: boolean, mRaw: number, mTl: number, mLv: number, mFl: number,
232
232
  mEr: number, mLog: number, mRoot: number,
233
- acc: string[] | undefined, prev: number,
233
+ acc: string[] | undefined, prev: number, best: number,
234
234
  ) => unknown
235
235
  type ExclusiveChoiceBlock = (arm: number, input: string, pos: number, ctx: ParseContext) => unknown
236
236
  type DispatchMatcher = (key: string) => boolean
@@ -1008,19 +1008,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1008
1008
  return c._fc === true
1009
1009
  }
1010
1010
 
1011
- /**
1012
- * Accumulate one failed choice arm's expected set, in arm order.
1013
- *
1014
- * `choice.ts:167` is `expected.push(...result.expected)` per failed arm, and the
1015
- * accumulation — not the static union of the arms' OPENERS — is what a failing
1016
- * ordered choice reports. A left-factored or shared-prefix choice whose arms all
1017
- * failed PAST their common opener reported that opener alone: `['/::?/']` where
1018
- * the interpreter says `['"before"', '"hover"']`.
1019
- *
1020
- * Returns the accumulator so the common one-failing-arm case borrows the arm's
1021
- * own array (`slice` once) instead of building one per choice execution the way
1022
- * the interpreter's eager `const expected: string[] = []` does.
1023
- */
1011
+ /** Append an exact-depth failed arm set in source order. Callers own depth ranking. */
1024
1012
  function accSet(ax: readonly string[] | undefined, acc: string[] | undefined): string[] | undefined {
1025
1013
  if (ax === undefined || ax.length === 0) return acc
1026
1014
  if (acc === undefined) return ax.slice()
@@ -1062,13 +1050,16 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1062
1050
  total: number,
1063
1051
  choiceFx: string[],
1064
1052
  ): MaskedChoiceBlock {
1065
- return (input, pos, ctx, bits, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev) => {
1053
+ return (input, pos, ctx, bits, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best) => {
1066
1054
  if ((bits & (1 << start)) !== 0) {
1067
1055
  ctx._fc = false
1068
1056
  const v = a0(input, pos, ctx)
1069
1057
  if (v !== FAIL) return v
1070
- acc = through(start, prev, acc); prev = start + 1
1071
- acc = accSet(ctx._fx, acc)
1058
+ if (best === pos) acc = through(start, prev, acc)
1059
+ prev = start + 1
1060
+ const at = ctx._fe ?? pos
1061
+ if (at > best) { best = at; acc = undefined }
1062
+ if (at === best) acc = accSet(ctx._fx, acc)
1072
1063
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1073
1064
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1074
1065
  }
@@ -1076,8 +1067,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1076
1067
  ctx._fc = false
1077
1068
  const v = a1(input, pos, ctx)
1078
1069
  if (v !== FAIL) return v
1079
- acc = through(start + 1, prev, acc); prev = start + 2
1080
- acc = accSet(ctx._fx, acc)
1070
+ if (best === pos) acc = through(start + 1, prev, acc)
1071
+ prev = start + 2
1072
+ const at = ctx._fe ?? pos
1073
+ if (at > best) { best = at; acc = undefined }
1074
+ if (at === best) acc = accSet(ctx._fx, acc)
1081
1075
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1082
1076
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1083
1077
  }
@@ -1085,8 +1079,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1085
1079
  ctx._fc = false
1086
1080
  const v = a2(input, pos, ctx)
1087
1081
  if (v !== FAIL) return v
1088
- acc = through(start + 2, prev, acc); prev = start + 3
1089
- acc = accSet(ctx._fx, acc)
1082
+ if (best === pos) acc = through(start + 2, prev, acc)
1083
+ prev = start + 3
1084
+ const at = ctx._fe ?? pos
1085
+ if (at > best) { best = at; acc = undefined }
1086
+ if (at === best) acc = accSet(ctx._fx, acc)
1090
1087
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1091
1088
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1092
1089
  }
@@ -1094,15 +1091,18 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1094
1091
  ctx._fc = false
1095
1092
  const v = a3(input, pos, ctx)
1096
1093
  if (v !== FAIL) return v
1097
- acc = through(start + 3, prev, acc); prev = start + 4
1098
- acc = accSet(ctx._fx, acc)
1094
+ if (best === pos) acc = through(start + 3, prev, acc)
1095
+ prev = start + 4
1096
+ const at = ctx._fe ?? pos
1097
+ if (at > best) { best = at; acc = undefined }
1098
+ if (at === best) acc = accSet(ctx._fx, acc)
1099
1099
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1100
1100
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1101
1101
  }
1102
1102
  if (next !== undefined) {
1103
- return next(input, pos, ctx, bits, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev)
1103
+ return next(input, pos, ctx, bits, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best)
1104
1104
  }
1105
- acc = through(total, prev, acc)
1105
+ if (best === pos) acc = through(total, prev, acc)
1106
1106
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
1107
1107
  return FAIL
1108
1108
  }
@@ -1119,13 +1119,16 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1119
1119
  total: number,
1120
1120
  choiceFx: string[],
1121
1121
  ): GeneralChoiceBlock {
1122
- return (input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev) => {
1122
+ return (input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best) => {
1123
1123
  if (g0 === null || classHas(g0, c)) {
1124
1124
  ctx._fc = false
1125
1125
  const v = a0(input, pos, ctx)
1126
1126
  if (v !== FAIL) return v
1127
- acc = through(start, prev, acc); prev = start + 1
1128
- acc = accSet(ctx._fx, acc)
1127
+ if (best === pos) acc = through(start, prev, acc)
1128
+ prev = start + 1
1129
+ const at = ctx._fe ?? pos
1130
+ if (at > best) { best = at; acc = undefined }
1131
+ if (at === best) acc = accSet(ctx._fx, acc)
1129
1132
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1130
1133
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1131
1134
  }
@@ -1133,8 +1136,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1133
1136
  ctx._fc = false
1134
1137
  const v = a1(input, pos, ctx)
1135
1138
  if (v !== FAIL) return v
1136
- acc = through(start + 1, prev, acc); prev = start + 2
1137
- acc = accSet(ctx._fx, acc)
1139
+ if (best === pos) acc = through(start + 1, prev, acc)
1140
+ prev = start + 2
1141
+ const at = ctx._fe ?? pos
1142
+ if (at > best) { best = at; acc = undefined }
1143
+ if (at === best) acc = accSet(ctx._fx, acc)
1138
1144
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1139
1145
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1140
1146
  }
@@ -1142,8 +1148,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1142
1148
  ctx._fc = false
1143
1149
  const v = a2(input, pos, ctx)
1144
1150
  if (v !== FAIL) return v
1145
- acc = through(start + 2, prev, acc); prev = start + 3
1146
- acc = accSet(ctx._fx, acc)
1151
+ if (best === pos) acc = through(start + 2, prev, acc)
1152
+ prev = start + 3
1153
+ const at = ctx._fe ?? pos
1154
+ if (at > best) { best = at; acc = undefined }
1155
+ if (at === best) acc = accSet(ctx._fx, acc)
1147
1156
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1148
1157
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1149
1158
  }
@@ -1151,15 +1160,18 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
1151
1160
  ctx._fc = false
1152
1161
  const v = a3(input, pos, ctx)
1153
1162
  if (v !== FAIL) return v
1154
- acc = through(start + 3, prev, acc); prev = start + 4
1155
- acc = accSet(ctx._fx, acc)
1163
+ if (best === pos) acc = through(start + 3, prev, acc)
1164
+ prev = start + 4
1165
+ const at = ctx._fe ?? pos
1166
+ if (at > best) { best = at; acc = undefined }
1167
+ if (at === best) acc = accSet(ctx._fx, acc)
1156
1168
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
1157
1169
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
1158
1170
  }
1159
1171
  if (next !== undefined) {
1160
- return next(input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev)
1172
+ return next(input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, acc, prev, best)
1161
1173
  }
1162
- acc = through(total, prev, acc)
1174
+ if (best === pos) acc = through(total, prev, acc)
1163
1175
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
1164
1176
  return FAIL
1165
1177
  }
@@ -2690,10 +2702,8 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2690
2702
  // THE CUT — a committed failure fails the whole choice.
2691
2703
  if (committed(ctx)) return FAIL
2692
2704
  // THE DISPATCHED ARM'S OWN SET IS THE ANSWER, at the choice's own
2693
- // position — the rule both shipped engines apply (choice.ts:105,
2694
- // codegen.ts:1985). `_fx` already holds it; reporting the arm is
2695
- // declining to overwrite it. See `exec.ts` for why this is NOT
2696
- // furthest-failure merging and what the residual JSON gap is.
2705
+ // position. `_fx` already holds it; reporting the sole selected arm
2706
+ // is declining to overwrite it.
2697
2707
  const armFx = ctx._fx
2698
2708
  if (armFx !== undefined && armFx.length > 0) { ctx._fe = pos; return FAIL }
2699
2709
  }
@@ -2739,6 +2749,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2739
2749
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
2740
2750
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
2741
2751
  let acc: string[] | undefined
2752
+ let best = pos
2742
2753
  if (c < 128) {
2743
2754
  const bits = mask[c < 0 ? 128 : c]!
2744
2755
  let prev = 0
@@ -2747,7 +2758,9 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2747
2758
  const v = a0(input, pos, ctx)
2748
2759
  if (v !== FAIL) return v
2749
2760
  prev = 1
2750
- acc = accSet(ctx._fx, acc)
2761
+ const at = ctx._fe ?? pos
2762
+ if (at > best) { best = at; acc = undefined }
2763
+ if (at === best) acc = accSet(ctx._fx, acc)
2751
2764
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2752
2765
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2753
2766
  }
@@ -2755,9 +2768,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2755
2768
  ctx._fc = false
2756
2769
  const v = a1(input, pos, ctx)
2757
2770
  if (v !== FAIL) return v
2758
- if (prev < 1) acc = accSet(e0, acc)
2771
+ if (best === pos && prev < 1) acc = accSet(e0, acc)
2759
2772
  prev = 2
2760
- acc = accSet(ctx._fx, acc)
2773
+ const at = ctx._fe ?? pos
2774
+ if (at > best) { best = at; acc = undefined }
2775
+ if (at === best) acc = accSet(ctx._fx, acc)
2761
2776
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2762
2777
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2763
2778
  }
@@ -2765,16 +2780,18 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2765
2780
  ctx._fc = false
2766
2781
  const v = a2!(input, pos, ctx)
2767
2782
  if (v !== FAIL) return v
2768
- if (prev < 1) acc = accSet(e0, acc)
2769
- if (prev < 2) acc = accSet(e1, acc)
2783
+ if (best === pos && prev < 1) acc = accSet(e0, acc)
2784
+ if (best === pos && prev < 2) acc = accSet(e1, acc)
2770
2785
  prev = 3
2771
- acc = accSet(ctx._fx, acc)
2786
+ const at = ctx._fe ?? pos
2787
+ if (at > best) { best = at; acc = undefined }
2788
+ if (at === best) acc = accSet(ctx._fx, acc)
2772
2789
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2773
2790
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2774
2791
  }
2775
- if (prev < 1) acc = accSet(e0, acc)
2776
- if (prev < 2) acc = accSet(e1, acc)
2777
- if (n === 3 && prev < 3) acc = accSet(e2, acc)
2792
+ if (best === pos && prev < 1) acc = accSet(e0, acc)
2793
+ if (best === pos && prev < 2) acc = accSet(e1, acc)
2794
+ if (best === pos && n === 3 && prev < 3) acc = accSet(e2, acc)
2778
2795
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
2779
2796
  return FAIL
2780
2797
  }
@@ -2783,27 +2800,33 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2783
2800
  ctx._fc = false
2784
2801
  const v = a0(input, pos, ctx)
2785
2802
  if (v !== FAIL) return v
2786
- acc = accSet(ctx._fx, acc)
2803
+ const at = ctx._fe ?? pos
2804
+ if (at > best) { best = at; acc = undefined }
2805
+ if (at === best) acc = accSet(ctx._fx, acc)
2787
2806
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2788
2807
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2789
- } else acc = accSet(e0, acc)
2808
+ } else if (best === pos) acc = accSet(e0, acc)
2790
2809
  if (g1 === null || classHas(g1, c)) {
2791
2810
  ctx._fc = false
2792
2811
  const v = a1(input, pos, ctx)
2793
2812
  if (v !== FAIL) return v
2794
- acc = accSet(ctx._fx, acc)
2813
+ const at = ctx._fe ?? pos
2814
+ if (at > best) { best = at; acc = undefined }
2815
+ if (at === best) acc = accSet(ctx._fx, acc)
2795
2816
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2796
2817
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2797
- } else acc = accSet(e1, acc)
2818
+ } else if (best === pos) acc = accSet(e1, acc)
2798
2819
  if (n === 3) {
2799
2820
  if (g2 === null || classHas(g2, c)) {
2800
2821
  ctx._fc = false
2801
2822
  const v = a2!(input, pos, ctx)
2802
2823
  if (v !== FAIL) return v
2803
- acc = accSet(ctx._fx, acc)
2824
+ const at = ctx._fe ?? pos
2825
+ if (at > best) { best = at; acc = undefined }
2826
+ if (at === best) acc = accSet(ctx._fx, acc)
2804
2827
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }
2805
2828
  if (need) rollbackTriviaAt(ctx, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot)
2806
- } else acc = accSet(e2, acc)
2829
+ } else if (best === pos) acc = accSet(e2, acc)
2807
2830
  }
2808
2831
  ctx._fe = pos; ctx._fx = acc ?? choiceFx
2809
2832
  return FAIL
@@ -2869,11 +2892,11 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2869
2892
  if (c < 128) {
2870
2893
  return masked(
2871
2894
  input, pos, ctx, mask[c < 0 ? 128 : c]!,
2872
- need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0,
2895
+ need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
2873
2896
  )
2874
2897
  }
2875
2898
  return general(
2876
- input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0,
2899
+ input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
2877
2900
  )
2878
2901
  }
2879
2902
  }
@@ -2889,7 +2912,7 @@ export function assemble(t: ResolvedTable, prog: TableProgram, cfg: RunCfg): Ass
2889
2912
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
2890
2913
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
2891
2914
  return general(
2892
- input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0,
2915
+ input, pos, ctx, c, need, mRaw, mTl, mLv, mFl, mEr, mLog, mRoot, undefined, 0, pos,
2893
2916
  )
2894
2917
  }
2895
2918
  }
@@ -1463,9 +1463,11 @@ return FAIL
1463
1463
  ctx._fc=false
1464
1464
  {const v=${arm}(input,pos,ctx)
1465
1465
  if(v!==FAIL)return v}
1466
- acc=${catchName}(${i},prev,acc)
1466
+ if(best===pos)acc=${catchName}(${i},prev,acc)
1467
1467
  prev=${i + 1}
1468
- acc=_accSet(ctx._fx,acc)
1468
+ {const at=ctx._fe??pos
1469
+ if(at>best){best=at;acc=undefined}
1470
+ if(at===best)acc=_accSet(ctx._fx,acc)}
1469
1471
  if(ctx._fc===true){if(acc!==undefined)ctx._fx=acc;return FAIL}
1470
1472
  ${emitRollback(p, L.buf, sinks)}
1471
1473
  }`
@@ -1474,20 +1476,23 @@ ${emitRollback(p, L.buf, sinks)}
1474
1476
  ctx._fc=false
1475
1477
  {const v=${arm}(input,pos,ctx)
1476
1478
  if(v!==FAIL)return v}
1477
- acc=_accSet(ctx._fx,acc)
1479
+ {const at=ctx._fe??pos
1480
+ if(at>best){best=at;acc=undefined}
1481
+ if(at===best)acc=_accSet(ctx._fx,acc)}
1478
1482
  if(ctx._fc===true){if(acc!==undefined)ctx._fx=acc;return FAIL}
1479
1483
  ${emitRollback(p, L.buf, sinks)}
1480
- }else acc=_accSet(${expected[i]},acc)`).join('\n')
1484
+ }else if(best===pos)acc=_accSet(${expected[i]},acc)`).join('\n')
1481
1485
 
1482
1486
  return `${head}
1483
1487
  const c=lead(input,pos)
1484
1488
  ${emitMark(p, L.buf, sinks)}
1485
1489
  let acc
1490
+ let best=pos
1486
1491
  ${maskable ? `if(c<128){
1487
1492
  const bits=${maskName}[c<0?128:c]
1488
1493
  let prev=0
1489
1494
  ${maskArms}
1490
- acc=${catchName}(${n},prev,acc)
1495
+ if(best===pos)acc=${catchName}(${n},prev,acc)
1491
1496
  ctx._fe=pos;ctx._fx=acc??${choiceFx}
1492
1497
  return FAIL
1493
1498
  }
@@ -1078,20 +1078,16 @@ class Encoder {
1078
1078
  ? -1 : this.charClass(finalChoice.firstSets[i]!))
1079
1079
  const dispIdx = this.disp.length
1080
1080
  this.disp.push(classes)
1081
- // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. The ordered
1082
- // path reports the CONCATENATION of the arms' sets (`choice.ts:167`), and
1083
- // it must include the arms the driver's char gate declined to enter — the
1084
- // interpreter has no such gate, runs them, and gets exactly their static
1085
- // opener set back. Without these words a partially-gated site reported
1086
- // only the arms that happened to run.
1081
+ // PER-ARM EXPECTED SETS RIDE ALONG, after the arm offsets. While the best
1082
+ // failure remains at the choice position, an arm declined by the driver's
1083
+ // char gate contributes the static opener set the interpreter would get by
1084
+ // entering it. A deeper dynamic arm failure discards those shallower sets.
1087
1085
  const head = this.emitHead(OP_CHOICE, 3 + 2 * kids.length)
1088
1086
  this.code[head + 1] = dispIdx
1089
1087
  this.code[head + 2] = kids.length
1090
1088
  // THE CHOICE'S OWN SET IS THE ARMS' FLATMAP, NOT `deriveExpected(p)`.
1091
- // `deriveExpected` DEDUPES, and neither engine does: `choice.ts:110-118`
1092
- // is `parsers.flatMap(p => p.parse(...).expected)`, so a shared-prefix
1093
- // choice whose two arms both open with `/::?/` reports it TWICE. The
1094
- // deduped union silently collapsed that to one.
1089
+ // It is the pure-dispatch-miss and empty-dynamic fallback, where source
1090
+ // order and duplicates remain observable. `deriveExpected` would dedupe.
1095
1091
  this.code[head + 3] = this.expected(arms.flatMap(a => deriveExpected(a)))
1096
1092
  for (let i = 0; i < kids.length; i++) {
1097
1093
  this.code[head + 4 + i] = kids[i]!
package/src/table/exec.ts CHANGED
@@ -952,9 +952,8 @@ function makeDriver(
952
952
  const d = code[ip + 1]!
953
953
  const base = ip + 4
954
954
  const choiceFx = fx[code[ip + 3]!] as string[]
955
- // Report the union, at the choice's own position, on every exit that
956
- // fails — matching both engines and, more importantly, never handing a
957
- // user an empty expected set.
955
+ // A pure dispatch miss has no attempted arm to rank. Report the static
956
+ // opener union at the choice's own position so the result is meaningful.
958
957
  const failChoice = (): typeof FAIL => {
959
958
  ctx._fe = pos
960
959
  ctx._fx = choiceFx
@@ -1003,12 +1002,9 @@ function makeDriver(
1003
1002
  // union IS that flatMap's answer, without running seven parsers to
1004
1003
  // rediscover it.
1005
1004
  //
1006
- // NOT furthest-failure merging, which no engine on the `expected` path
1007
- // performs. `failAt` (combinators/probe.ts:13-24) and its compiled mirror
1008
- // `probeUpdate` (codegen.ts:692) are the only positional merges in the
1009
- // library, they are gated on `_ctx._probe` and they surface as
1010
- // `RunResult.furthestFail` under `{recover:true}` — a different field.
1011
- // Every `_fe`/`_fx` site in all three drivers is last-write-wins.
1005
+ // There is only one selected arm on this path, so ordered-arm depth
1006
+ // ranking does not apply. `furthestFail` under `{recover:true}` remains
1007
+ // a separate probe result.
1012
1008
  //
1013
1009
  // A residual JSON divergence survives this and is NOT a failure-reporting
1014
1010
  // bug: on `{"a":]` this driver reports `["}"]` (the Obj arm's own
@@ -1034,18 +1030,15 @@ function makeDriver(
1034
1030
  const mEr = need ? ctx._errors?.length ?? 0 : 0
1035
1031
  const mLog = need ? ctx._triviaLog?.length ?? 0 : 0
1036
1032
  const mRoot = need ? ctx._rootTriviaLog?.length ?? 0 : 0
1037
- // THE ORDERED PATH REPORTS THE ARMS THAT ACTUALLY RAN, accumulated in
1038
- // order — `choice.ts:167`'s `expected.push(...result.expected)` per failed
1039
- // arm, reported at the choice's own position. The static `choiceFx` union
1040
- // is a set of OPENERS, so a left-factored or shared-prefix choice whose
1041
- // arms all failed PAST their common opener reported that opener instead of
1042
- // the tokens the arms were actually waiting for: `['/::?/']` where the
1043
- // interpreter says `['"before"', '"hover"']`.
1033
+ // The ordered path keeps only arms at the deepest failure offset. Exact
1034
+ // ties merge in source order. The final failure is still anchored at the
1035
+ // choice's own position; the deeper offset ranks diagnostics only.
1044
1036
  //
1045
1037
  // `choiceFx` is still the answer when nothing ran or every arm reported
1046
1038
  // nothing — the char gate can skip arms the interpreter would have entered,
1047
1039
  // and the union is what those arms' start-failures would have contributed.
1048
1040
  let acc: string[] | undefined
1041
+ let best = pos
1049
1042
  const push = (ax: readonly string[] | undefined): void => {
1050
1043
  if (ax === undefined || ax.length === 0) return
1051
1044
  if (acc === undefined) acc = ax.slice()
@@ -1060,7 +1053,7 @@ function makeDriver(
1060
1053
  // enters the arm, the arm's opener fails, and its static set is what
1061
1054
  // lands in the accumulation. Declining to enter must not also decline
1062
1055
  // to say what the arm wanted.
1063
- push(fx[code[armFx + i]!] as string[])
1056
+ if (best === pos) push(fx[code[armFx + i]!] as string[])
1064
1057
  continue
1065
1058
  }
1066
1059
  ctx._fc = false
@@ -1069,7 +1062,9 @@ function makeDriver(
1069
1062
  const v = exec(code[base + i]!, input, pos, ctx)
1070
1063
  if (v !== FAIL) return v
1071
1064
  if (COUNT) { tableCounters.ungatedFails++; tableCounters.ungatedFailRows += tableCounters.rows - rows0 }
1072
- push(ctx._fx)
1065
+ const at = ctx._fe ?? pos
1066
+ if (at > best) { best = at; acc = undefined }
1067
+ if (at === best) push(ctx._fx)
1073
1068
  // A committed arm cuts, and the interpreter reports the accumulation at
1074
1069
  // the arm's OWN span (`choice.ts:170`), not at the choice's position.
1075
1070
  if (committed(ctx)) { if (acc !== undefined) ctx._fx = acc; return FAIL }