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.
Files changed (94) hide show
  1. package/dist/analysis/diagnostics.js +876 -190
  2. package/dist/analysis/diagnostics.js.map +3 -3
  3. package/dist/cli/index.js +876 -190
  4. package/dist/cli/index.js.map +3 -3
  5. package/dist/combinators/dispatch.d.ts +13 -1
  6. package/dist/combinators/dispatch.d.ts.map +1 -1
  7. package/dist/combinators/first-set.d.ts +35 -2
  8. package/dist/combinators/first-set.d.ts.map +1 -1
  9. package/dist/combinators/regex.d.ts.map +1 -1
  10. package/dist/combinators/trivia-boundary.d.ts +11 -0
  11. package/dist/combinators/trivia-boundary.d.ts.map +1 -1
  12. package/dist/combinators/trivia-skip.d.ts +1 -1
  13. package/dist/combinators/trivia-skip.d.ts.map +1 -1
  14. package/dist/compiler/token-alphabet.d.ts +2 -0
  15. package/dist/compiler/token-alphabet.d.ts.map +1 -1
  16. package/dist/compiler/token-capability.js +65 -53
  17. package/dist/compiler/token-capability.js.map +2 -2
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +896 -199
  21. package/dist/index.js.map +3 -3
  22. package/dist/plugin/evaluator.d.ts.map +1 -1
  23. package/dist/plugin/index.js +903 -201
  24. package/dist/plugin/index.js.map +3 -3
  25. package/dist/regex/first-set.d.ts +19 -0
  26. package/dist/regex/first-set.d.ts.map +1 -1
  27. package/dist/table/assemble.d.ts +15 -1
  28. package/dist/table/assemble.d.ts.map +1 -1
  29. package/dist/table/child-slots.d.ts.map +1 -1
  30. package/dist/table/compile-rule-map.d.ts +1 -1
  31. package/dist/table/compile-rule-map.d.ts.map +1 -1
  32. package/dist/table/compile.d.ts +1 -1
  33. package/dist/table/emit-assembly.d.ts +8 -2
  34. package/dist/table/emit-assembly.d.ts.map +1 -1
  35. package/dist/table/emit.d.ts +4 -3
  36. package/dist/table/emit.d.ts.map +1 -1
  37. package/dist/table/encode.d.ts.map +1 -1
  38. package/dist/table/exec.d.ts +0 -35
  39. package/dist/table/exec.d.ts.map +1 -1
  40. package/dist/table/factor-choice.d.ts +27 -0
  41. package/dist/table/factor-choice.d.ts.map +1 -1
  42. package/dist/table/gap-gate.d.ts +11 -0
  43. package/dist/table/gap-gate.d.ts.map +1 -0
  44. package/dist/table/index.js +926 -201
  45. package/dist/table/index.js.map +3 -3
  46. package/dist/table/lex-program.d.ts.map +1 -1
  47. package/dist/table/ops.d.ts +35 -16
  48. package/dist/table/ops.d.ts.map +1 -1
  49. package/dist/table/program.d.ts +6 -1
  50. package/dist/table/program.d.ts.map +1 -1
  51. package/dist/types.d.ts +16 -4
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/package.json +2 -4
  55. package/src/analysis/duplication.ts +1 -1
  56. package/src/combinators/attempt.ts +1 -1
  57. package/src/combinators/dispatch.ts +30 -6
  58. package/src/combinators/first-set.ts +55 -12
  59. package/src/combinators/node.ts +1 -1
  60. package/src/combinators/regex.ts +9 -4
  61. package/src/combinators/repeat.ts +6 -6
  62. package/src/combinators/sequence.ts +1 -1
  63. package/src/combinators/trivia-boundary.ts +18 -0
  64. package/src/combinators/trivia-skip.ts +1 -1
  65. package/src/compiler/ir-serialize.ts +1 -1
  66. package/src/compiler/token-alphabet.ts +3 -1
  67. package/src/coverage.ts +1 -1
  68. package/src/index.ts +1 -1
  69. package/src/plugin/evaluator.ts +8 -3
  70. package/src/regex/first-set.ts +37 -3
  71. package/src/table/assemble.ts +176 -30
  72. package/src/table/child-slots.ts +8 -3
  73. package/src/table/compile-rule-map.ts +7 -4
  74. package/src/table/compile.ts +1 -1
  75. package/src/table/emit-assembly.ts +184 -33
  76. package/src/table/emit.ts +6 -4
  77. package/src/table/encode.ts +469 -74
  78. package/src/table/exec.ts +38 -82
  79. package/src/table/factor-choice.ts +62 -0
  80. package/src/table/fail-clean.ts +2 -2
  81. package/src/table/gap-gate.ts +55 -0
  82. package/src/table/hold-gap.ts +2 -2
  83. package/src/table/lex-program.ts +15 -6
  84. package/src/table/ops.ts +38 -16
  85. package/src/table/program.ts +7 -2
  86. package/src/table/select-hot.ts +7 -2
  87. package/src/types.ts +12 -5
  88. package/src/version.ts +1 -1
  89. package/dist/table/encode-baseline.d.ts +0 -16
  90. package/dist/table/encode-baseline.d.ts.map +0 -1
  91. package/dist/table/exec-baseline.d.ts +0 -9
  92. package/dist/table/exec-baseline.d.ts.map +0 -1
  93. package/src/table/encode-baseline.ts +0 -346
  94. package/src/table/exec-baseline.ts +0 -557
@@ -47,11 +47,11 @@
47
47
  import type { ParseContext } from '../types.ts'
48
48
  import { regexCanMatchEmpty } from '../regex/first-set.ts'
49
49
  import {
50
- OP_ADJ, OP_ARMGATE, OP_RGATE, OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE,
50
+ OP_ADJ, OP_ARMGATE, OP_RGATE, OP_RGATE2, OP_ATTEMPT, OP_CHOICE, OP_DISPATCH, OP_EMPTY, OP_EXPECT, OP_FIELD, OP_GATE,
51
51
  OP_LABEL, OP_LEAF, OP_SOURCE_LEAF, OP_LIT, OP_LIT_CI, OP_LIT_CI_TRACK, OP_LIT_TRACK, OP_NAMES,
52
52
  OP_NODE, OP_NODE_TRACK, OP_NOT, OP_OPT, OP_PEEK, OP_REP, OP_REPV, OP_ROUTED, OP_RULE, OP_RX,
53
53
  OP_RX_TRACK, OP_SCAN, OP_SCOPE, OP_SCOPE_CAP, OP_SCOPE_PLAIN, OP_SEQ, OP_SEQV, OP_SEQX, OP_TOKEN, OP_WITHCTX, OP_XFORM,
54
- OP_LEX_BODY, OP_LEX_PROGRAM,
54
+ OP_LEX_BODY, OP_LEX_PROGRAM, OP_GAPGATE, OP_STRICT,
55
55
  } from './ops.ts'
56
56
  import {
57
57
  choiceCannotCommit, choiceRollbackMask, failureRollbackClean, validateDispatchSpec,
@@ -67,10 +67,16 @@ import {
67
67
  scalarTerminalNodeChild, scalarTerminalNotChild, scalarTerminalPeekChild, type LeadingScalarSequence,
68
68
  } from './scalar-terminal.ts'
69
69
  import { childSlots } from './child-slots.ts'
70
- import { holdsGap } from './hold-gap.ts'
70
+ import { holdsGap, rowNullable } from './hold-gap.ts'
71
+ import { gapRows } from './gap-gate.ts'
71
72
  import { fieldSpec, leadingTriviaFieldAt } from './field-spec.ts'
72
73
  import { FIELD_LEADING_TRIVIA, FIELD_TRAILING_TRIVIA } from '../combinators/field-trivia.ts'
73
74
 
75
+ /** Does a gap's regex lower to an inline scan (`scanShapeFromRegex`)? */
76
+ function gapRegexLowers(re: unknown): boolean {
77
+ return re instanceof RegExp && scanShapeFromRegex(re.source, re.flags) !== null
78
+ }
79
+
74
80
  /** What the compiled factory hands back — the emitted twin of `Assembly`. */
75
81
  export type EmittedPiece = (input: string, pos: number, ctx: ParseContext) => unknown
76
82
  export type EmittedAssembly = {
@@ -109,12 +115,12 @@ export class Unemittable extends Error {
109
115
  * runs and is wrong, which no type in this file would catch.
110
116
  */
111
117
  export const EMITTED_PARAMS = [
112
- 'EC', 'FAIL', 'K', 'FX', 'FNS', 'MASK', 'CLS', 'AFX', 'TRIVIA', 'TRIVIALABELS', 'TRIVIASCAN',
118
+ 'EC', 'FAIL', 'K', 'FX', 'FNS', 'MASK', 'CLS', 'AFX', 'TRIVIA', 'TRIVIASCAN',
113
119
  'SCANS', 'DISP', 'DSP', 'EMPTY_FX', 'EMPTY_CH', 'EMPTY_TLOG', 'EMPTY_TL',
114
120
  'cstCaptureActive', 'pushCstLeaf', 'pushCstChild', 'rollbackTriviaAt', 'rollbackScannedTriviaAt', 'failAt',
115
121
  'classHas', 'consumeTrivia', 'buildFieldMap', 'projectChild', 'unwrapChild',
116
122
  'demoteCapturedToRaw', 'cstLeavesLen', 'skipTriviaScanned', 'needsDeferredTriviaCommit',
117
- 'scanTrivia', 'advanceTrivia', 'refuseUnclassifiedRootScope', 'spanLines', 'rawEntry', 'lead',
123
+ 'advanceTrivia', 'refuseUnclassifiedRootScope', 'spanLines', 'rawEntry', 'lead',
118
124
  'asciiFoldKey', 'ROUTED_FX',
119
125
  // RECOVERY, bound rather than reimplemented. `SENTS` is the sync sentinel per
120
126
  // char-class index — the emitted twin of `assemble.ts`'s `sentinelFor` memo,
@@ -124,27 +130,24 @@ export const EMITTED_PARAMS = [
124
130
  // engine and cannot drift.
125
131
  'SENTS', 'matchesAt', 'recoverScan', 'orSentinel', 'captureError',
126
132
  // Pure scalar terminal recognizers, indexed by the terminal's existing const
127
- // operand. Appended for compatibility with older precompiled factories.
128
- 'RECOG',
129
- // Appended so precompiled factories produced by earlier runtimes keep every
130
- // positional helper binding. Older factories ignore this trailing argument.
131
- 'commitTriviaScan', 'scanTriviaCompact', 'LEX',
132
- // Appended for old precompiled-factory ABI: old factories ignore it.
133
- 'adjacencyHolds',
134
- // Appended for selected composite lexical programs; old factories ignore it.
133
+ // operand; selected lexical bodies; the adjacency test.
134
+ 'RECOG', 'LEX', 'adjacencyHolds',
135
+ // Selected composite lexical programs.
135
136
  'LEXPROG',
136
- // Appended for trivia-attributing fields (`field(…, { leadingTrivia })`);
137
- // old factories ignore them.
137
+ // Trivia-attributing fields (`field(…, { leadingTrivia })`).
138
138
  'LEAD', 'triviaFieldCapture',
139
- // Appended for the deferred-commit trivia skip (`trivia-gap.ts`); old
140
- // factories ignore it.
139
+ // The deferred-commit trivia skip (`trivia-gap.ts`).
141
140
  'skipDeferredTrivia',
142
- // Appended for the pass-through collapse (`run-support.ts`); old factories
143
- // ignore it.
141
+ // The pass-through collapse (`run-support.ts`).
144
142
  'collapseEmpty',
145
- // Appended for constructor-built tree objects (`cst/plain.d.ts`): a `new` call has
146
- // no allocation site, so V8 cannot pretenure them. Old factories ignore them.
143
+ // Constructor-built tree objects (`cst/plain.d.ts`): a `new` call has no
144
+ // allocation site, so V8 cannot pretenure them.
147
145
  '_SP', '_LF', '_ND', '_FC', '_BUF',
146
+ // The gap gate's slot (`gap-gate.ts`).
147
+ 'GAP',
148
+ // Artifacts are version-locked (docs/design/artifact-format.md): a factory is
149
+ // only ever called by the runtime that emitted it, so a name nothing emitted
150
+ // reads is removed rather than kept for an older factory's positions.
148
151
  ] as const
149
152
 
150
153
  /**
@@ -605,6 +608,12 @@ export function emitAssemblySource(
605
608
  * then emitted with them.
606
609
  */
607
610
  scanSinkNeutral = false,
611
+ /**
612
+ * Lower every gap gate (`gap-gate.ts`) as the item it wraps. A selection is
613
+ * ranked on this lowering, so adding the gates moves no site's place in the
614
+ * order; the fit is judged by the source that ships, gates included.
615
+ */
616
+ gapNeutral = false,
608
617
  ): EmitResult {
609
618
  const { code, k, fx, disp, dsp, triviaLabelled } = t
610
619
  const swapLegal = !cfg.trackLines
@@ -643,6 +652,10 @@ export function emitAssemblySource(
643
652
  // emitted scope supplies a context this pass cannot see.
644
653
  const roots = [...Object.values(prog.rules), ...extraIps]
645
654
  const reachable = [...reachableSites(code, roots)]
655
+ // A gap whose regex lowers to an inline scan is scanned again by its row, for
656
+ // less than reading the slot costs; only a gap matched by its RegExp takes the
657
+ // slot (`gapRegexLowers`).
658
+ const GAP_ROWS = gapNeutral ? new Set<number>() : new Set([...gapRows(code, roots)].filter(row => !gapRegexLowers(k[code[row + 1]!])))
646
659
  const labels = computeSiteLabels(code, roots, hostCst)
647
660
  // Eligibility is pooled by the terminal's existing constant operand, matching
648
661
  // the closure recognizer pool: distinct terminal rows sharing one spec share
@@ -1172,7 +1185,8 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1172
1185
  // `OP_GATE` under a probe or a tolerant recovery is a no-op that forwards to
1173
1186
  // its child, exactly as `assemble.ts:952` resolves it.
1174
1187
  if (op === OP_GATE && (cfg.tolerant || cfg.probe)) return code[ip + 2]!
1175
- if (op === OP_RULE) return code[ip + 1]!
1188
+ if (op === OP_RULE || op === OP_GAPGATE) return code[ip + 1]!
1189
+ if (op === OP_STRICT) return code[cfg.tolerant || cfg.probe ? ip + 2 : ip + 1]!
1176
1190
  // A SCOPE THAT INSTALLS WHAT IS ALREADY INSTALLED. `encode.ts:520` wraps
1177
1191
  // EVERY rule of a `rules({ trivia }, …)` map in its own `OP_SCOPE_PLAIN`, so a
1178
1192
  // grammar with one ambient trivia re-installs the same slot at every rule
@@ -1191,6 +1205,7 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1191
1205
  function lower(ip: number, fname: string): string {
1192
1206
  const op = code[ip]
1193
1207
  const head = `function ${fname}(input,pos,ctx){`
1208
+ const outerHead = head
1194
1209
  const L = labels.at(ip)
1195
1210
  const sinks = sinksAt(ip)
1196
1211
  /**
@@ -1310,6 +1325,26 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1310
1325
  case OP_RX_TRACK: {
1311
1326
  const xf = fxRef(code[ip + 2]!)
1312
1327
  const track = op === OP_RX_TRACK
1328
+ // A gap row takes the end a repeat already scanned (`gap-gate.ts`), and
1329
+ // otherwise matches its sticky regex: one compact body, no second copy of
1330
+ // the success path.
1331
+ if (GAP_ROWS.has(ip)) {
1332
+ const re = hoist('gx', `K[${code[ip + 1]!}]`)
1333
+ return `${outerHead}
1334
+ let e
1335
+ if(GAP.row===${ip}&&GAP.at===pos&&GAP.input===input)e=GAP.end
1336
+ else{${re}.lastIndex=pos;e=${re}.test(input)?${re}.lastIndex:-1}
1337
+ if(e>=0){
1338
+ const v=input.slice(pos,e)
1339
+ ${captureLeaf('v')}
1340
+ ${track ? '_trackLines(ctx,input,e)\n' : ''}EC.e=e
1341
+ return v
1342
+ }
1343
+ ctx._fe=pos;ctx._fx=${xf}
1344
+ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1345
+ }`
1346
+ }
1347
+ const head = outerHead
1313
1348
  if (!track && scalarSpecs.has(code[ip + 1]!)) {
1314
1349
  const recognize = recognizerRef(code[ip + 1]!)
1315
1350
  return `${head}
@@ -1365,8 +1400,8 @@ ${cfg.probe ? `failAt(ctx,${xf},pos)\n` : ''}return FAIL
1365
1400
  }
1366
1401
 
1367
1402
  case OP_EMPTY:
1368
- // `''`, not `null` — the zero-width match's value. The other three engines
1369
- // (exec.ts, exec-baseline.ts, assemble.ts) all return `''`; this one returned
1403
+ // `''`, not `null` — the zero-width match's value. The other engines
1404
+ // (exec.ts, assemble.ts) both return `''`; this one returned
1370
1405
  // `null`, so the SAME program yielded a different `value` depending only on
1371
1406
  // which engine ran. Unreached from the combinator API today — `OP_EMPTY` is
1372
1407
  // emitted only as `finish()` padding for an EMPTY rule map, which then has no
@@ -1401,6 +1436,31 @@ return ${child}(input,pos,ctx)
1401
1436
  }`
1402
1437
  }
1403
1438
 
1439
+ case OP_RGATE2: {
1440
+ const child = link(code[ip + 2]!)
1441
+ if (cfg.tolerant || cfg.probe) return `${head}
1442
+ return ${child}(input,pos,ctx)
1443
+ }`
1444
+ // ASCII-only classes (the usual case: `:` then whitespace) are read as
1445
+ // code units against their ASCII tables, with no call.
1446
+ const idxs = [code[ip + 1]!, code[ip + 4]!, code[ip + 5]!]
1447
+ const ascii = idxs.every(i => isAsciiOnlyClass(t.cc[i]!))
1448
+ const ref = (idx: number, tag: string): string => {
1449
+ const ci = classes.push([t.cc[idx]!]) - 1
1450
+ classPlan.push([idx])
1451
+ return hoist(tag, `CLS[${ci}][0]`)
1452
+ }
1453
+ const [cls, first, second] = [ref(idxs[0]!, 'cl'), ref(idxs[1]!, 'c1'), ref(idxs[2]!, 'c2')]
1454
+ const has = (c: string, at: string): string => ascii ? `${c}.ascii[${at}]===1` : `classHas(${c},${at})`
1455
+ const xf = fxRef(code[ip + 3]!)
1456
+ return `${head}
1457
+ const it=ctx._routed
1458
+ if(it!==undefined&&it.span.start===pos){const e=it.span.end;const c=${ascii ? 'input.charCodeAt(e)' : 'lead(input,e)'}
1459
+ if(!(${has(cls, 'c')})||(${has(first, 'c')}&&!(${has(second, ascii ? 'input.charCodeAt(e+1)' : 'lead(input,e+1)')}))){ctx._fe=e;ctx._fx=${xf};return FAIL}}
1460
+ return ${child}(input,pos,ctx)
1461
+ }`
1462
+ }
1463
+
1404
1464
  case OP_ARMGATE: {
1405
1465
  // `choice({ gate, combinator })`, as `assemble.ts` lowers it. A blocked
1406
1466
  // arm is SKIPPED, not failed (choice.ts `continue`): it clears the cut,
@@ -1914,7 +1974,30 @@ if(ctx._fc===true)return FAIL
1914
1974
  const af=ctx._fx
1915
1975
  if(af!==undefined&&af.length>0){ctx._fe=pos;return FAIL}
1916
1976
  }
1917
- ctx._fe=pos;ctx._fx=${choiceFx}
1977
+ ${cfg.probe ? `if(arm<0){${arms.map(a => `${a}(input,pos,ctx)`).join(';')}}
1978
+ ` : ''}ctx._fe=pos;ctx._fx=${choiceFx}
1979
+ return FAIL
1980
+ }`
1981
+ }
1982
+
1983
+ if (cfg.probe) {
1984
+ // A probe assembly enters every arm in source order, as the
1985
+ // interpreter's `firstMatch` does, so each opener reaches `failAt`.
1986
+ const p = tmp()
1987
+ return `${head}
1988
+ ${emitMark(p, L.buf, L.raw, sinks)}
1989
+ let acc
1990
+ let best=pos
1991
+ ${arms.map(arm => `{ctx._fc=false
1992
+ const v=${arm}(input,pos,ctx)
1993
+ if(v!==FAIL)return v
1994
+ const at=ctx._fe??pos
1995
+ if(at>best){best=at;acc=undefined}
1996
+ if(at===best)acc=_accSet(ctx._fx,acc)
1997
+ if(ctx._fc===true){if(acc!==undefined)ctx._fx=acc;return FAIL}
1998
+ ${emitRollback(p, L.buf, L.raw, sinks)}
1999
+ }`).join('\n')}
2000
+ ctx._fe=pos;ctx._fx=acc??${choiceFx}
1918
2001
  return FAIL
1919
2002
  }`
1920
2003
  }
@@ -2194,7 +2277,8 @@ return FAIL
2194
2277
  const selector = link(code[ip + 1]!)
2195
2278
  const otherIp = code[ip + 3]!
2196
2279
  const other = otherIp >= 0 ? link(otherIp) : undefined
2197
- const otherRouted = code[ip + 4]! === 1
2280
+ const otherRouted = (code[ip + 4]! & 1) === 1
2281
+ const otherCommits = (code[ip + 4]! & 2) === 0
2198
2282
  const n = code[ip + 5]!
2199
2283
  validateDispatchSpec(spec, n, code[ip + 4]!)
2200
2284
  const armBase = ip + 6
@@ -2258,7 +2342,7 @@ ${fallbackCase}
2258
2342
  }
2259
2343
  if(v===FAIL){
2260
2344
  ${emitRollback(m2, L.buf, L.raw, sinks)}
2261
- ctx._fc=true
2345
+ ${other !== undefined && !otherCommits ? 'if(arm!==undefined)' : ''}ctx._fc=true
2262
2346
  return FAIL
2263
2347
  }
2264
2348
  return [key,v]
@@ -2293,6 +2377,69 @@ return [key,v]
2293
2377
  const itemMiss = (at: string): string => itemAscii
2294
2378
  ? `${itemCls}.ascii[input.charCodeAt(${at})]!==1`
2295
2379
  : `!classHas(${itemCls},lead(input,${at}))`
2380
+ // The separator's own first set (ip+7, `assemble.ts` `sepClass`).
2381
+ let sepCls: string | undefined
2382
+ let sepAscii = false
2383
+ if (!REC && sepIp >= 0 && (code[ip + 5]! & 8) !== 0 && !leadingTriviaFieldAt(code, k, sepIp) && !rowNullable(code, k, sepIp, nullable)) {
2384
+ const resolved = t.cc[code[ip + 8]!]!
2385
+ const ci = classes.push([resolved]) - 1
2386
+ classPlan.push([code[ip + 8]!])
2387
+ sepCls = hoist('rs', `CLS[${ci}][0]`)
2388
+ sepAscii = isAsciiOnlyClass(resolved)
2389
+ }
2390
+ // THE GAP GATE (`gap-gate.ts`, `assemble.ts`): the item's leading gap is
2391
+ // read once here and handed to its regex row through the slot.
2392
+ const gapIp = !gapNeutral && sepIp < 0 && min === 0 && !REC && code[code[ip + 1]!] === OP_GAPGATE ? code[ip + 1]! : -1
2393
+ let gapCode = (_rb: string): string => ''
2394
+ if (gapIp >= 0) {
2395
+ const row = code[gapIp + 2]!
2396
+ const rx = k[code[row + 1]!]
2397
+ const shape = gapRegexLowers(rx) ? scanShapeFromRegex((rx as RegExp).source, (rx as RegExp).flags) : null
2398
+ const cls = (idx: number): string => {
2399
+ const resolved = t.cc[idx]!
2400
+ const ci = classes.push([resolved]) - 1
2401
+ classPlan.push([idx])
2402
+ const ref = hoist('gc', `CLS[${ci}][0]`)
2403
+ return isAsciiOnlyClass(resolved) ? `${ref}.ascii[input.charCodeAt(${'@'})]!==1` : `!classHas(${ref},lead(input,${'@'}))`
2404
+ }
2405
+ const follow = cls(code[gapIp + 3]!).replaceAll('@', 'gq')
2406
+ const other = code[gapIp + 4]! >= 0 ? cls(code[gapIp + 4]!).replaceAll('@', 'itemStart') : 'true'
2407
+ if (shape !== null) {
2408
+ // Scanned inline, as the row scans it: no slot to fill or read.
2409
+ let n = 0
2410
+ const q = tmp()
2411
+ const m = emitShapeMatch(shape, 'itemStart', (prefix = '_v') => `${q}${prefix}${n++}`, '')
2412
+ gapCode = (rbCode: string): string => `let gapped=false
2413
+ if(ctx._probe===undefined){
2414
+ ${m.setup.join('\n')}
2415
+ const gq=${m.ok}?${m.end}:-1
2416
+ if((gq<0||${follow})&&${other}){
2417
+ ${rbCode}
2418
+ break
2419
+ }
2420
+ }
2421
+ `
2422
+ } else {
2423
+ const re = hoist('gr', `K[${code[row + 1]!}]`)
2424
+ gapCode = (rbCode: string): string => `let gapped=false
2425
+ if(ctx._probe===undefined){
2426
+ ${re}.lastIndex=itemStart
2427
+ const gq=${re}.test(input)?${re}.lastIndex:-1
2428
+ if((gq<0||${follow})&&${other}){
2429
+ ${rbCode}
2430
+ break
2431
+ }
2432
+ if(gq>=0){
2433
+ GAP.row=${row};GAP.at=itemStart;GAP.end=gq;GAP.input=input
2434
+ gapped=true
2435
+ }
2436
+ }
2437
+ `
2438
+ }
2439
+ }
2440
+ const sepMiss = (at: string): string => sepAscii
2441
+ ? `${sepCls}.ascii[input.charCodeAt(${at})]!==1`
2442
+ : `!classHas(${sepCls},lead(input,${at}))`
2296
2443
  const skipBeforeFirst = sepIp < 0 && min === 0
2297
2444
  const p = tmp()
2298
2445
  // `viaRepItem` is `sep === undefined && count >= min && (count > 0 ||
@@ -2387,7 +2534,7 @@ ${p}rt=ctx._rootTriviaLog!==undefined?ctx._rootTriviaLog.length:0${markSinks}
2387
2534
  // is taken unconditionally by the closure engine (`assemble.ts:2067`), so
2388
2535
  // it is resolved separately from `itemFx` — reusing that one would hand
2389
2536
  // `EMPTY_FX` to every recovered error in a grammar that does not report.
2390
- const recFx = REC ? fxRef(code[ip + 6]!) : ''
2537
+ const recFx = !REC ? '' : (flags & 16) !== 0 ? '[]' : fxRef(code[ip + 6]!)
2391
2538
  const sepSent = REC ? sentRef(code[ip + 7]!) : 'undefined'
2392
2539
  const recSent = sep === undefined
2393
2540
  ? my
@@ -2405,7 +2552,7 @@ continue
2405
2552
  `
2406
2553
  return `${head}
2407
2554
  const out=${collect ? '[]' : 'undefined'}
2408
- ${knownTrivia === undefined ? 'const hasTrivia=ctx.trivia!==undefined\n' : ''}${L.buf ? '' : 'const needMark=_rollbackNeeded(ctx)\n'}${REC ? `const ${my}=ctx._sync\n` : ''}${itemCls === undefined ? '' : `const ${p}gate=ctx._probe===undefined\n`}let cur=pos
2555
+ ${knownTrivia === undefined ? 'const hasTrivia=ctx.trivia!==undefined\n' : ''}${L.buf ? '' : 'const needMark=_rollbackNeeded(ctx)\n'}${REC ? `const ${my}=ctx._sync\n` : ''}${itemCls === undefined && sepCls === undefined ? '' : `const ${p}gate=ctx._probe===undefined\n`}let cur=pos
2409
2556
  let count=0
2410
2557
  for(;;){
2411
2558
  ${max >= 0 ? `if(count>=${max})break\n` : ''}${sep !== undefined ? `if(count>0&&count>=${min}&&cur>=input.length)break\n` : ''}${itemCls !== undefined && sep === undefined ? `if(count>=${min}&&${p}gate&&${hasTrivia === 'false' ? 'true' : hasTrivia === 'true' ? 'false' : '!hasTrivia'}&&${itemMiss('cur')})break\n` : ''}let ${p}raw=0,${p}tl=0,${p}lv=0,${p}lg=0,${p}rt=0${sinks.fd ? `,${p}fd=0` : ''}${sinks.er ? `,${p}er=0` : ''}
@@ -2415,7 +2562,11 @@ let sepEnd=-1
2415
2562
  ${sep !== undefined ? `if(count>0){
2416
2563
  const lb=cstLeavesLen(ctx)
2417
2564
  let sp=cur
2418
- ${hasTrivia === 'false' ? '' : hasTrivia === 'true' ? `sp=${skip}(input,sp,ctx)\n` : `if(hasTrivia)sp=${skip}(input,sp,ctx)\n`}ctx._fc=false
2565
+ ${hasTrivia === 'false' ? '' : hasTrivia === 'true' ? `sp=${skip}(input,sp,ctx)\n` : `if(hasTrivia)sp=${skip}(input,sp,ctx)\n`}${sepCls === undefined ? '' : `if(count>=${min}&&${p}gate&&${sepMiss('sp')}){
2566
+ ${rb}
2567
+ break
2568
+ }
2569
+ `}ctx._fc=false
2419
2570
  ${sepIp >= 0 && leadingTriviaFieldAt(code, k, sepIp) ? 'LEAD.at=cur\n' : ''}const sv=${sep}(input,sp,ctx)
2420
2571
  if(sv===FAIL){
2421
2572
  ${rb}
@@ -2430,13 +2581,13 @@ itemStart=${hasTrivia === 'false' ? 'EC.e' : hasTrivia === 'true' ? `${skip}(inp
2430
2581
  ${rb}
2431
2582
  ${trailingAllowed ? 'if(sepEnd>=0)cur=sepEnd\n' : ''}break
2432
2583
  }
2433
- ${itemCls === undefined ? '' : `if(count>=${min}&&${p}gate&&${itemMiss('itemStart')}){
2584
+ ${itemCls === undefined || (sep === undefined && hasTrivia === 'false') ? '' : `if(count>=${min}&&${p}gate&&${sep === undefined && hasTrivia === 'hasTrivia' ? 'hasTrivia&&' : ''}${itemMiss('itemStart')}){
2434
2585
  ${rb}
2435
2586
  ${trailingAllowed ? 'if(sepEnd>=0)cur=sepEnd\n' : ''}break
2436
2587
  }
2437
- `}ctx._fc=false
2588
+ `}${gapCode(rb)}ctx._fc=false
2438
2589
  const v=${child}(input,itemStart,ctx)
2439
- if(v===FAIL){
2590
+ ${gapIp >= 0 ? 'if(gapped)GAP.at=-1\n' : ''}if(v===FAIL){
2440
2591
  ${REC
2441
2592
  // THE ROLLBACK MOVES INSIDE THE ARMS. The strict loop rolls back
2442
2593
  // unconditionally before testing `_fc`, which is sound because every path out
package/src/table/emit.ts CHANGED
@@ -6,11 +6,12 @@ import { cfgKey, type RunCfg } from './assemble.ts'
6
6
  import { PARSEMAN_VERSION } from '../version.ts'
7
7
 
8
8
  /**
9
- * Opens every printed assembly factory. A runtime older than the printer passes fewer
10
- * helpers; refuse it when the assembly is built, instead of failing partway through the
11
- * first parse on whichever helper is reached first. Runs once per assembly.
9
+ * Opens every printed assembly factory. Helpers are bound BY POSITION, so a runtime that
10
+ * passes any other number of them, fewer or more, binds the wrong function to a name;
11
+ * refuse it when the assembly is built, instead of failing partway through the first parse
12
+ * on whichever helper is reached first. Runs once per assembly.
12
13
  */
13
- export const FACTORY_ARITY_GUARD = `if(arguments.length<${EMITTED_PARAMS.length})throw new Error(${JSON.stringify(`parseman: this parser was printed by parseman ${PARSEMAN_VERSION}; upgrade the parseman runtime to at least that version`)});`
14
+ export const FACTORY_ARITY_GUARD = `if(arguments.length!==${EMITTED_PARAMS.length})throw new Error(${JSON.stringify(`parseman: this parser was printed by parseman ${PARSEMAN_VERSION}; run it on parseman ${PARSEMAN_VERSION} or reprint it`)});`
14
15
 
15
16
  /**
16
17
  * Print a program as the module a build emits.
@@ -229,6 +230,7 @@ function emitAssemblies(prog: TableProgram, cfgs: readonly AssemblyCfg[]): strin
229
230
  const plan = em.plan
230
231
  out.push('{'
231
232
  + `key:${key},`
233
+ + `arity:${EMITTED_PARAMS.length},`
232
234
  + `factory:function(${EMITTED_PARAMS.join(',')}){${FACTORY_ARITY_GUARD}${em.source}\n},`
233
235
  + 'plan:{'
234
236
  + `classes:[${plan.classes.map(r => `[${r.join(',')}]`).join(',')}],`