parseman 0.7.1 → 0.9.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/index.cjs CHANGED
@@ -37,6 +37,7 @@ __export(index_exports, {
37
37
  choice: () => choice,
38
38
  compile: () => compile,
39
39
  completionsAt: () => completionsAt,
40
+ expect: () => expect,
40
41
  guard: () => guard,
41
42
  isParseError: () => isParseError,
42
43
  keywords: () => keywords,
@@ -61,6 +62,7 @@ __export(index_exports, {
61
62
  sepBy: () => sepBy,
62
63
  sequence: () => sequence,
63
64
  skip: () => skip,
65
+ staticExpected: () => staticExpected,
64
66
  transform: () => transform,
65
67
  trivia: () => trivia,
66
68
  triviaEntries: () => triviaEntries,
@@ -1349,13 +1351,15 @@ function noTrivia(root) {
1349
1351
  function parse(combinator, input, opts = {}) {
1350
1352
  const trackLines = opts.trackLines ?? false;
1351
1353
  const _errors = opts.recover ? [] : void 0;
1354
+ const _probe = opts.recover ? { offset: input.length, best: null } : void 0;
1352
1355
  const ctx = {
1353
1356
  trackLines,
1354
- ..._errors !== void 0 ? { _errors } : {}
1357
+ ..._errors !== void 0 ? { _errors } : {},
1358
+ ..._probe !== void 0 ? { _probe } : {}
1355
1359
  };
1356
1360
  const result = combinator.parse(input, 0, ctx);
1357
1361
  if (!result.ok) return result;
1358
- const withErrors = _errors !== void 0 ? { ...result, errors: _errors } : result;
1362
+ const withErrors = _errors !== void 0 ? { ...result, errors: _errors, furthestFail: _probe?.best ?? null } : result;
1359
1363
  if (trackLines) {
1360
1364
  const idx = buildLineIndex(input);
1361
1365
  return { ...withErrors, span: annotateSpan(withErrors.span, idx) };
@@ -1363,14 +1367,140 @@ function parse(combinator, input, opts = {}) {
1363
1367
  return withErrors;
1364
1368
  }
1365
1369
 
1370
+ // src/compiler/scannable-run.ts
1371
+ var CLASS_ESCAPES = { t: 9, n: 10, r: 13, f: 12, v: 11, "0": 0 };
1372
+ var META = new Set("()[]{}*+?|^$.".split(""));
1373
+ function parseClassRanges(body) {
1374
+ const ranges = [];
1375
+ let i = 0;
1376
+ const readCp = () => {
1377
+ const ch = body[i];
1378
+ if (ch === void 0) return null;
1379
+ if (ch === "\\") {
1380
+ const e = body[i + 1];
1381
+ if (e === void 0) return null;
1382
+ i += 2;
1383
+ return e in CLASS_ESCAPES ? CLASS_ESCAPES[e] : e.codePointAt(0);
1384
+ }
1385
+ i += ch.length;
1386
+ return ch.codePointAt(0);
1387
+ };
1388
+ while (i < body.length) {
1389
+ const lo = readCp();
1390
+ if (lo === null) return null;
1391
+ if (body[i] === "-" && body[i + 1] !== void 0 && body[i + 1] !== "]") {
1392
+ i += 1;
1393
+ const hi = readCp();
1394
+ if (hi === null) return null;
1395
+ ranges.push([lo, hi]);
1396
+ } else {
1397
+ ranges.push([lo, lo]);
1398
+ }
1399
+ }
1400
+ return ranges.length ? ranges : null;
1401
+ }
1402
+ function literalCodePoints(frag) {
1403
+ const out = [];
1404
+ let i = 0;
1405
+ while (i < frag.length) {
1406
+ const ch = frag[i];
1407
+ if (ch === "\\") {
1408
+ const e = frag[i + 1];
1409
+ if (e === void 0) return null;
1410
+ out.push(e in CLASS_ESCAPES ? CLASS_ESCAPES[e] : e.codePointAt(0));
1411
+ i += 2;
1412
+ continue;
1413
+ }
1414
+ if (META.has(ch)) return null;
1415
+ out.push(ch.codePointAt(0));
1416
+ i += 1;
1417
+ }
1418
+ return out.length ? out : null;
1419
+ }
1420
+ function parseScanShape(source) {
1421
+ let m = /^\[((?:\\.|[^\]])+)\]([+*])$/.exec(source);
1422
+ if (m) {
1423
+ if (m[1].startsWith("^")) return null;
1424
+ const ranges = parseClassRanges(m[1]);
1425
+ return ranges ? { kind: "chars", ranges } : null;
1426
+ }
1427
+ m = /^(.*?)\[\^((?:\\.|[^\]])+)\]\*$/.exec(source);
1428
+ if (m) {
1429
+ const open = literalCodePoints(m[1]);
1430
+ const stop = parseClassRanges(m[2]);
1431
+ if (open && stop) return { kind: "until", open, stop };
1432
+ return null;
1433
+ }
1434
+ if (!source.includes("\\\\")) {
1435
+ m = /^(.*?)\((?:\?:)?[\s\S]*\)\*(.*?)$/.exec(source);
1436
+ if (m && m[1] && m[2]) {
1437
+ const open = literalCodePoints(m[1]);
1438
+ const close = literalCodePoints(m[2]);
1439
+ if (open && close) return { kind: "delimited", open, close };
1440
+ }
1441
+ }
1442
+ return null;
1443
+ }
1444
+ var classCond = (cVar, ranges) => ranges.map(([lo, hi]) => lo === hi ? `${cVar} === ${lo}` : `(${cVar} >= ${lo} && ${cVar} <= ${hi})`).join(" || ");
1445
+ var litCond = (base, cps, firstVar) => cps.map(
1446
+ (cp, k) => k === 0 && firstVar ? `${firstVar} === ${cp}` : `input.charCodeAt(${base}${k ? ` + ${k}` : ""}) === ${cp}`
1447
+ ).join(" && ");
1448
+ function advanceExpr(shape) {
1449
+ if (shape.kind === "chars") {
1450
+ return {
1451
+ setup: [
1452
+ ` _e++`,
1453
+ ` while (_e < input.length) { const c2 = input.charCodeAt(_e); if (${classCond("c2", shape.ranges)}) _e++; else break }`
1454
+ ],
1455
+ endVar: "_e"
1456
+ };
1457
+ }
1458
+ if (shape.kind === "until") {
1459
+ return {
1460
+ setup: [
1461
+ ` let j = _e + ${shape.open.length}`,
1462
+ ` while (j < input.length && !(${classCond("input.charCodeAt(j)", shape.stop)})) j++`
1463
+ ],
1464
+ endVar: "j"
1465
+ };
1466
+ }
1467
+ const n = shape.close.length;
1468
+ return {
1469
+ setup: [
1470
+ ` let j = _e + ${shape.open.length}`,
1471
+ ` while (j + ${n - 1} < input.length && !(${litCond("j", shape.close)})) j++`
1472
+ ],
1473
+ endVar: n === 1 ? "(j + 1 <= input.length ? j + 1 : input.length)" : `(j + ${n} <= input.length ? j + ${n} : input.length)`
1474
+ };
1475
+ }
1476
+ function guardLine(shape) {
1477
+ const cond = shape.kind === "chars" ? classCond("c", shape.ranges) : litCond("_e", shape.open, "c");
1478
+ return ` if (${cond}) {`;
1479
+ }
1480
+ function scanBranch(shape) {
1481
+ if (shape.kind === "chars") {
1482
+ return ` if (${classCond("c", shape.ranges)}) { _e++; continue }`;
1483
+ }
1484
+ const { setup, endVar } = advanceExpr(shape);
1485
+ return [guardLine(shape), ...setup, ` _e = ${endVar}`, ` continue`, ` }`].join("\n");
1486
+ }
1487
+ function captureChunk(startVar, kindIndex) {
1488
+ return [
1489
+ ` if (_cap) {`,
1490
+ ` if (_ctx._triviaLog !== undefined) _ctx._triviaLog.push(${startVar}, _e, ${kindIndex})`,
1491
+ ` if (_ctx._cstTriviaLog !== undefined) _ctx._cstTriviaLog.push(${startVar}, _e, _ctx._cstRawChildren ? _ctx._cstRawChildren.length : 0, ${kindIndex})`,
1492
+ ` }`
1493
+ ];
1494
+ }
1495
+ function scanBranchLabeled(shape, kindIndex) {
1496
+ const { setup, endVar } = advanceExpr(shape);
1497
+ const lines = [guardLine(shape), ` const _cs = _e`, ...setup];
1498
+ if (endVar !== "_e") lines.push(` _e = ${endVar}`);
1499
+ lines.push(...captureChunk("_cs", kindIndex), ` continue`, ` }`);
1500
+ return lines.join("\n");
1501
+ }
1502
+
1366
1503
  // src/compiler/trivia-fast-path.ts
1367
- var BLOCK_COMMENT = String.raw`\/\*(?:[^*]|\*(?!\/))*\*\/`;
1368
- var WS_CLASS_SOURCES = /* @__PURE__ */ new Set([
1369
- String.raw`[ \t\n\r\f]+`,
1370
- String.raw`[ \t\n]+`,
1371
- String.raw`[ \t]+`,
1372
- String.raw`[\t\n\f\r ]+`
1373
- ]);
1374
1504
  function unwrapTrivia2(p) {
1375
1505
  let cur = p;
1376
1506
  while (cur._def.tag === "trivia") cur = cur._def.parser;
@@ -1386,36 +1516,30 @@ function choiceArms(p) {
1386
1516
  if (p._def.tag === "choice") return p._def.parsers;
1387
1517
  return null;
1388
1518
  }
1389
- function isWsClassSource(source) {
1390
- return WS_CLASS_SOURCES.has(source);
1391
- }
1392
- function isBlockCommentSource(source) {
1393
- return source === BLOCK_COMMENT;
1519
+ function armShape(arm) {
1520
+ const src = getCoreRegexDef(arm)?.source;
1521
+ return src ? parseScanShape(src) : null;
1394
1522
  }
1395
1523
  function analyzeTriviaFastPath(trivia2) {
1396
1524
  const core = unwrapTrivia2(trivia2);
1397
- const direct = getCoreRegexDef(core)?.source;
1398
- if (direct) {
1399
- if (isWsClassSource(direct)) return "wsOnly";
1400
- return null;
1525
+ const directSrc = getCoreRegexDef(core)?.source;
1526
+ if (directSrc) {
1527
+ const shape = parseScanShape(directSrc);
1528
+ return shape && shape.kind === "chars" ? [shape] : null;
1401
1529
  }
1402
1530
  const inner = oneOrMoreInner(core);
1403
1531
  if (!inner) return null;
1404
- const innerSrc = getCoreRegexDef(inner)?.source;
1405
- if (innerSrc && isWsClassSource(innerSrc)) return "wsOnly";
1532
+ const innerShape = armShape(inner);
1533
+ if (innerShape) return innerShape.kind === "chars" ? [innerShape] : null;
1406
1534
  const arms = choiceArms(inner);
1407
- if (!arms || arms.length !== 2) return null;
1408
- let hasWs = false;
1409
- let hasComment = false;
1535
+ if (!arms || arms.length < 2) return null;
1536
+ const shapes = [];
1410
1537
  for (const arm of arms) {
1411
- const src = getCoreRegexDef(arm)?.source;
1412
- if (!src) return null;
1413
- if (isWsClassSource(src)) hasWs = true;
1414
- else if (isBlockCommentSource(src)) hasComment = true;
1415
- else return null;
1538
+ const shape = armShape(arm);
1539
+ if (!shape) return null;
1540
+ shapes.push(shape);
1416
1541
  }
1417
- if (hasWs && hasComment) return "wsComments";
1418
- return null;
1542
+ return shapes;
1419
1543
  }
1420
1544
  var CAP_RECORD = [
1421
1545
  ` if (_cap && _e > _pos) {`,
@@ -1423,80 +1547,49 @@ var CAP_RECORD = [
1423
1547
  ` if (_ctx._cstTriviaLog !== undefined) _ctx._cstTriviaLog.push(_pos, _e, _ctx._cstRawChildren ? _ctx._cstRawChildren.length : 0)`,
1424
1548
  ` }`
1425
1549
  ].join("\n");
1426
- var WS_ONLY_LOOP = [
1427
- ` while (_e < input.length) {`,
1428
- ` const c = input.charCodeAt(_e)`,
1429
- ` if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12) { _e++; continue }`,
1430
- ` break`,
1431
- ` }`
1432
- ].join("\n");
1433
- var WS_COMMENTS_LOOP = [
1434
- ` while (_e < input.length) {`,
1435
- ` const c = input.charCodeAt(_e)`,
1436
- ` if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12) { _e++; continue }`,
1437
- ` if (c === 47 && input.charCodeAt(_e + 1) === 42) {`,
1438
- ` let j = _e + 2`,
1439
- ` while (j + 1 < input.length && !(input.charCodeAt(j) === 42 && input.charCodeAt(j + 1) === 47)) j++`,
1440
- ` _e = j + 2 <= input.length ? j + 2 : input.length`,
1441
- ` continue`,
1442
- ` }`,
1443
- ` break`,
1444
- ` }`
1445
- ].join("\n");
1446
- var CAP_CHUNK = (wsKind, commentKind) => [
1447
- ` if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12) {`,
1448
- ` const _cs = _e`,
1449
- ` _e++`,
1450
- ` while (_e < input.length) {`,
1451
- ` const c2 = input.charCodeAt(_e)`,
1452
- ` if (c2 === 32 || c2 === 9 || c2 === 10 || c2 === 13 || c2 === 12) _e++`,
1453
- ` else break`,
1454
- ` }`,
1455
- ` if (_cap) {`,
1456
- ` if (_ctx._triviaLog !== undefined) _ctx._triviaLog.push(_cs, _e, ${wsKind})`,
1457
- ` if (_ctx._cstTriviaLog !== undefined) _ctx._cstTriviaLog.push(_cs, _e, _ctx._cstRawChildren ? _ctx._cstRawChildren.length : 0, ${wsKind})`,
1458
- ` }`,
1459
- ` continue`,
1460
- ` }`,
1461
- ` if (c === 47 && input.charCodeAt(_e + 1) === 42) {`,
1462
- ` const _cs = _e`,
1463
- ` let j = _e + 2`,
1464
- ` while (j + 1 < input.length && !(input.charCodeAt(j) === 42 && input.charCodeAt(j + 1) === 47)) j++`,
1465
- ` _e = j + 2 <= input.length ? j + 2 : input.length`,
1466
- ` if (_cap) {`,
1467
- ` if (_ctx._triviaLog !== undefined) _ctx._triviaLog.push(_cs, _e, ${commentKind})`,
1468
- ` if (_ctx._cstTriviaLog !== undefined) _ctx._cstTriviaLog.push(_cs, _e, _ctx._cstRawChildren ? _ctx._cstRawChildren.length : 0, ${commentKind})`,
1469
- ` }`,
1470
- ` continue`,
1471
- ` }`
1472
- ].join("\n");
1473
- var WS_COMMENTS_LOOP_LABELED = (wsKind, commentKind) => [
1474
- ` while (_e < input.length) {`,
1475
- ` const c = input.charCodeAt(_e)`,
1476
- CAP_CHUNK(wsKind, commentKind),
1477
- ` break`,
1478
- ` }`
1479
- ].join("\n");
1480
- function buildFastTriviaFnDecl(fnName, kind, kindIndices) {
1481
- const loop = kind === "wsComments" && kindIndices ? WS_COMMENTS_LOOP_LABELED(kindIndices.ws, kindIndices.comment) : kind === "wsComments" ? WS_COMMENTS_LOOP : WS_ONLY_LOOP;
1482
- const cap = kindIndices ? "" : CAP_RECORD;
1483
- const lines = [
1550
+ function composeFastLoop(shapes) {
1551
+ return [
1552
+ ` while (_e < input.length) {`,
1553
+ ` const c = input.charCodeAt(_e)`,
1554
+ ...shapes.map(scanBranch),
1555
+ ` break`,
1556
+ ` }`
1557
+ ].join("\n");
1558
+ }
1559
+ function buildFastTriviaFnDecl(fnName, shapes) {
1560
+ return [
1484
1561
  `function ${fnName}(input, _pos, _ctx, _cap) {`,
1485
1562
  ` let _e = _pos`,
1486
- loop
1487
- ];
1488
- if (cap) lines.push(cap);
1489
- lines.push(` return _e`, `}`);
1490
- return lines.join("\n");
1563
+ composeFastLoop(shapes),
1564
+ CAP_RECORD,
1565
+ ` return _e`,
1566
+ `}`
1567
+ ].join("\n");
1491
1568
  }
1492
- function regexArmIndex(spec, isComment) {
1569
+ function analyzeLabeledScannableRun(trivia2) {
1570
+ const spec = analyzeLabeledTrivia(trivia2);
1571
+ if (!spec) return null;
1572
+ const out = [];
1493
1573
  for (const arm of spec.arms) {
1494
1574
  const src = getCoreRegexDef(arm.parser)?.source;
1495
- if (!src) return null;
1496
- const comment = src.includes("\\*") || src.startsWith("\\/\\/");
1497
- if (comment === isComment) return arm.kindIndex;
1575
+ const shape = src ? parseScanShape(src) : null;
1576
+ if (!shape) return null;
1577
+ out.push({ shape, kindIndex: arm.kindIndex });
1498
1578
  }
1499
- return null;
1579
+ return out;
1580
+ }
1581
+ function buildLabeledScannableTriviaFnDecl(fnName, arms) {
1582
+ return [
1583
+ `function ${fnName}(input, _pos, _ctx, _cap) {`,
1584
+ ` let _e = _pos`,
1585
+ ` while (_e < input.length) {`,
1586
+ ` const c = input.charCodeAt(_e)`,
1587
+ ...arms.map((a) => scanBranchLabeled(a.shape, a.kindIndex)),
1588
+ ` break`,
1589
+ ` }`,
1590
+ ` return _e`,
1591
+ `}`
1592
+ ].join("\n");
1500
1593
  }
1501
1594
  function buildLabeledRegexTriviaFnDecl(fnName, spec, reNames) {
1502
1595
  const tryArms = spec.arms.map((arm, i) => {
@@ -1529,14 +1622,6 @@ function buildLabeledRegexTriviaFnDecl(fnName, spec, reNames) {
1529
1622
  `}`
1530
1623
  ].join("\n");
1531
1624
  }
1532
- function labeledTriviaKindIndices(trivia2) {
1533
- const spec = analyzeLabeledTrivia(trivia2);
1534
- if (!spec || spec.arms.length !== 2) return null;
1535
- const ws = regexArmIndex(spec, false);
1536
- const comment = regexArmIndex(spec, true);
1537
- if (ws === null || comment === null) return null;
1538
- return { ws, comment };
1539
- }
1540
1625
  function labeledTriviaRegexArms(trivia2) {
1541
1626
  const spec = analyzeLabeledTrivia(trivia2);
1542
1627
  if (!spec) return null;
@@ -1784,14 +1869,19 @@ function ensureTriviaFn(ctx) {
1784
1869
  const fnName = `_tf${ctx.triviaFnNames.size}`;
1785
1870
  ctx.triviaFnNames.set(trivia2, fnName);
1786
1871
  ctx.triviaCaptureNames.set(trivia2, fnName);
1787
- const fastKind = analyzeTriviaFastPath(trivia2);
1788
- const kindIndices = labeledTriviaKindIndices(trivia2);
1789
- if (fastKind) {
1790
- ctx.namedFnDecls.push(buildFastTriviaFnDecl(fnName, fastKind, kindIndices ?? void 0));
1791
- return fnName;
1792
- }
1793
1872
  const labeledSpec = analyzeLabeledTrivia(trivia2);
1794
- if (labeledSpec) {
1873
+ if (!labeledSpec) {
1874
+ const fastShapes = analyzeTriviaFastPath(trivia2);
1875
+ if (fastShapes) {
1876
+ ctx.namedFnDecls.push(buildFastTriviaFnDecl(fnName, fastShapes));
1877
+ return fnName;
1878
+ }
1879
+ } else {
1880
+ const labeledShapes = analyzeLabeledScannableRun(trivia2);
1881
+ if (labeledShapes) {
1882
+ ctx.namedFnDecls.push(buildLabeledScannableTriviaFnDecl(fnName, labeledShapes));
1883
+ return fnName;
1884
+ }
1795
1885
  const regexSpec = labeledTriviaRegexArms(trivia2);
1796
1886
  if (regexSpec) {
1797
1887
  const reNames = [];
@@ -1851,6 +1941,119 @@ function firstSetCond(codeVar, fs) {
1851
1941
  (r) => r.lo === r.hi ? `${codeVar} === ${r.lo}` : `(${codeVar} >= ${r.lo} && ${codeVar} <= ${r.hi})`
1852
1942
  ).join(" || ");
1853
1943
  }
1944
+ function captureRestoreBody(mL, mR, mTl, mLg) {
1945
+ const base = `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${mL}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${mR}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${mTl}`;
1946
+ return mLg ? `${base}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${mLg}` : base;
1947
+ }
1948
+ function mayLeavePartialCapture(p, seen = /* @__PURE__ */ new Set()) {
1949
+ if (seen.has(p)) return false;
1950
+ seen.add(p);
1951
+ const d = p._def;
1952
+ switch (d.tag) {
1953
+ // Atomic / non-capturing-then-failing: a failure happens before any push.
1954
+ case "literal":
1955
+ case "regex":
1956
+ case "keywords":
1957
+ case "guard":
1958
+ case "not":
1959
+ case "trivia":
1960
+ case "scanTo":
1961
+ case "unknown":
1962
+ return false;
1963
+ // node() captures into its own private buffers and rolls them back on
1964
+ // failure (emitNode restores _ctx.* and never pushes on the !ok path).
1965
+ case "node":
1966
+ return false;
1967
+ // choice/firstMatch already roll back each failed arm; on overall failure
1968
+ // nothing committed remains.
1969
+ case "choice":
1970
+ return false;
1971
+ // optional never fails; many/oneOrMore only "fail" with zero captured items.
1972
+ case "optional":
1973
+ case "many":
1974
+ case "oneOrMore":
1975
+ return false;
1976
+ // sepBy emits its own per-iteration rollback in emitSepBy.
1977
+ case "sepBy":
1978
+ return false;
1979
+ // A sequence is the real case: an earlier capturing term followed by a term
1980
+ // that can fail leaves the earlier captures buffered.
1981
+ case "sequence": {
1982
+ const parts = d.parsers;
1983
+ if (parts.length < 2) return parts.some((x) => mayLeavePartialCapture(x, seen));
1984
+ for (let i = 0; i < parts.length - 1; i++) {
1985
+ if (hasNodeDef(parts[i]) || capturesLeaf(parts[i])) return true;
1986
+ }
1987
+ return false;
1988
+ }
1989
+ // Delegating wrappers: defer to the wrapped parser.
1990
+ case "transform":
1991
+ case "label":
1992
+ case "expect":
1993
+ case "withCtx":
1994
+ case "grammar":
1995
+ return mayLeavePartialCapture(d.parser, seen);
1996
+ case "skip":
1997
+ return mayLeavePartialCapture(d.main, seen);
1998
+ case "recover":
1999
+ return mayLeavePartialCapture(d.parser, seen);
2000
+ case "lazy": {
2001
+ try {
2002
+ return mayLeavePartialCapture(d.thunk(), seen);
2003
+ } catch {
2004
+ return true;
2005
+ }
2006
+ }
2007
+ // Unknown shapes: be safe and keep the rollback.
2008
+ default:
2009
+ return true;
2010
+ }
2011
+ }
2012
+ function capturesLeaf(p, seen = /* @__PURE__ */ new Set()) {
2013
+ if (seen.has(p)) return false;
2014
+ seen.add(p);
2015
+ const d = p._def;
2016
+ switch (d.tag) {
2017
+ case "literal":
2018
+ case "regex":
2019
+ case "keywords":
2020
+ case "node":
2021
+ return true;
2022
+ case "not":
2023
+ case "guard":
2024
+ case "trivia":
2025
+ case "unknown":
2026
+ return false;
2027
+ case "sequence":
2028
+ case "choice":
2029
+ return d.parsers.some((x) => capturesLeaf(x, seen));
2030
+ case "sepBy":
2031
+ return capturesLeaf(d.parser, seen) || capturesLeaf(d.separator, seen);
2032
+ case "many":
2033
+ case "oneOrMore":
2034
+ case "optional":
2035
+ case "transform":
2036
+ case "label":
2037
+ case "expect":
2038
+ case "withCtx":
2039
+ case "grammar":
2040
+ case "recover":
2041
+ return capturesLeaf(d.parser, seen);
2042
+ case "skip":
2043
+ return capturesLeaf(d.main, seen);
2044
+ case "scanTo":
2045
+ return true;
2046
+ case "lazy": {
2047
+ try {
2048
+ return capturesLeaf(d.thunk(), seen);
2049
+ } catch {
2050
+ return true;
2051
+ }
2052
+ }
2053
+ default:
2054
+ return true;
2055
+ }
2056
+ }
1854
2057
  function emitFallible(inner, ctx, pos) {
1855
2058
  const lbl = v(ctx, "_lbl");
1856
2059
  const okV = `${lbl}ok`;
@@ -1864,26 +2067,23 @@ function emitFallible(inner, ctx, pos) {
1864
2067
  ctx.failLabel = savedLabel;
1865
2068
  ctx.indent = savedIndent;
1866
2069
  const ind0 = ind(ctx);
1867
- const mL = ctx.capturing ? v(ctx, "_fcl") : null;
1868
- const mR = ctx.capturing ? v(ctx, "_fcr") : null;
1869
- const mTl = ctx.capturing ? v(ctx, "_fctl") : null;
1870
- const mLg = ctx.capturing ? v(ctx, "_fclg") : null;
2070
+ const needsRollback = ctx.capturing && mayLeavePartialCapture(inner);
2071
+ const mL = needsRollback ? v(ctx, "_fcl") : null;
2072
+ const mR = needsRollback ? v(ctx, "_fcr") : null;
2073
+ const mTl = needsRollback ? v(ctx, "_fctl") : null;
1871
2074
  const stmts = [
1872
2075
  `${ind0}let ${okV} = false, ${valV}, ${endV} = ${pos}`,
1873
2076
  ...mL ? [
1874
2077
  `${ind0}const ${mL} = _ctx._cstLeaves?.length ?? 0`,
1875
2078
  `${ind0}const ${mR} = _ctx._cstRawChildren?.length ?? 0`,
1876
- `${ind0}const ${mTl} = _ctx._cstTriviaLog?.length ?? 0`,
1877
- `${ind0}const ${mLg} = _ctx._triviaLog?.length ?? 0`
2079
+ `${ind0}const ${mTl} = _ctx._cstTriviaLog?.length ?? 0`
1878
2080
  ] : [],
1879
2081
  `${ind0}${lbl}: {`,
1880
2082
  ...r.stmts,
1881
- `${ind0} ${valV} = ${r.valueVar}`,
1882
- `${ind0} ${endV} = ${r.endVar}`,
1883
- `${ind0} ${okV} = true`,
2083
+ `${ind0} ${valV} = ${r.valueVar}; ${endV} = ${r.endVar}; ${okV} = true`,
1884
2084
  `${ind0}}`,
1885
2085
  ...mL ? [
1886
- `${ind0}if (!${okV}) { if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${mL}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${mR}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${mTl}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${mLg} }`
2086
+ `${ind0}if (!${okV}) { ${captureRestoreBody(mL, mR, mTl, null)} }`
1887
2087
  ] : []
1888
2088
  ];
1889
2089
  return { stmts, okVar: okV, valVar: valV, endVar: endV };
@@ -2119,11 +2319,11 @@ function emitLiteralsLongestFirst(def, sortedIndices, allExpected, ctx, pos) {
2119
2319
  const p = def.parsers[idx];
2120
2320
  const litVal = getCoreLiteralValue(p);
2121
2321
  const litLen = litVal.length;
2122
- const litCond = emitLiteralCondition(litVal, pos);
2322
+ const litCond2 = emitLiteralCondition(litVal, pos);
2123
2323
  const kw = first ? "if" : "else if";
2124
2324
  first = false;
2125
2325
  const litEnd = `${pos} + ${litLen}`;
2126
- stmts.push(`${ind(ctx)}${kw} (${litCond}) {`);
2326
+ stmts.push(`${ind(ctx)}${kw} (${litCond2}) {`);
2127
2327
  ctx.indent++;
2128
2328
  const tR = emitTransformChain(p, JSON.stringify(litVal), litEnd, pos, ctx);
2129
2329
  stmts.push(
@@ -2157,11 +2357,13 @@ function emitFirstMatch(def, allExpected, ctx, pos) {
2157
2357
  gateCond = `_mf[${gateIdx}](_ctx.state)`;
2158
2358
  }
2159
2359
  const skipCond = gateCond ? `!${resOkV} && ${gateCond}` : `!${resOkV}`;
2160
- const markLeaves = ctx.capturing ? v(ctx, "_cml") : null;
2161
- const markRaw = ctx.capturing ? v(ctx, "_cmr") : null;
2162
- const markTl = ctx.capturing ? v(ctx, "_cmtl") : null;
2163
- const markLog = ctx.capturing ? v(ctx, "_cmlg") : null;
2164
- const rollback = markLeaves ? `if (_ctx._cstLeaves) _ctx._cstLeaves.length = ${markLeaves}; if (_ctx._cstRawChildren) _ctx._cstRawChildren.length = ${markRaw}; if (_ctx._cstTriviaLog) _ctx._cstTriviaLog.length = ${markTl}; if (_ctx._triviaLog) _ctx._triviaLog.length = ${markLog}` : "";
2360
+ const armHasAutoNot = !!(autoNot && autoNot.length > 0);
2361
+ const armNeedsRollback = ctx.capturing && (mayLeavePartialCapture(p) || armHasAutoNot && capturesLeaf(p));
2362
+ const markLeaves = armNeedsRollback ? v(ctx, "_cml") : null;
2363
+ const markRaw = armNeedsRollback ? v(ctx, "_cmr") : null;
2364
+ const markTl = armNeedsRollback ? v(ctx, "_cmtl") : null;
2365
+ const markLog = armNeedsRollback ? v(ctx, "_cmlg") : null;
2366
+ const rollback = markLeaves ? captureRestoreBody(markLeaves, markRaw, markTl, markLog) : "";
2165
2367
  stmts.push(`${ind0}if (${skipCond}) {`);
2166
2368
  ctx.indent++;
2167
2369
  const ind1 = ind(ctx);
@@ -2191,11 +2393,7 @@ function emitFirstMatch(def, allExpected, ctx, pos) {
2191
2393
  stmts.push(`${ind0} }`);
2192
2394
  if (rollback) stmts.push(`${ind0} if (!${resOkV}) { ${rollback} }`);
2193
2395
  } else {
2194
- stmts.push(`${ind0} if (${okVar}) {`);
2195
- stmts.push(`${ind0} ${resValV} = ${valVar}`);
2196
- stmts.push(`${ind0} ${resEndV} = ${endVar}`);
2197
- stmts.push(`${ind0} ${resOkV} = true`);
2198
- stmts.push(`${ind0} }`);
2396
+ stmts.push(`${ind0} if (${okVar}) { ${resValV} = ${valVar}; ${resEndV} = ${endVar}; ${resOkV} = true }`);
2199
2397
  if (rollback) stmts.push(`${ind0} else { ${rollback} }`);
2200
2398
  }
2201
2399
  stmts.push(`${ind0}}`);
@@ -2493,6 +2691,16 @@ function emitRuntimeFallback(parser2, ctx, pos) {
2493
2691
  return { stmts, valueVar: vv, endVar: ev };
2494
2692
  }
2495
2693
  function emitLazy(p, def, ctx, pos) {
2694
+ const usage = ctx.lazyUsage;
2695
+ if (usage && (usage.counts.get(p) ?? 0) <= 1 && !usage.recursive.has(p)) {
2696
+ let resolved;
2697
+ try {
2698
+ resolved = def.thunk();
2699
+ } catch {
2700
+ return emitRuntimeFallback(p, ctx, pos);
2701
+ }
2702
+ return emit(resolved, ctx, pos);
2703
+ }
2496
2704
  if (!ctx.namedParsers.has(p)) {
2497
2705
  const fnName2 = `_pf${ctx.namedParsers.size}`;
2498
2706
  ctx.namedParsers.set(p, fnName2);
@@ -2540,6 +2748,22 @@ function emitRecover(def, ctx, pos) {
2540
2748
  ];
2541
2749
  return { stmts, valueVar: valVar, endVar };
2542
2750
  }
2751
+ function emitExpect(def, ctx, pos) {
2752
+ const { stmts: innerStmts, okVar, valVar, endVar } = emitFallible(def.parser, ctx, pos);
2753
+ const ind0 = ind(ctx);
2754
+ const errV = v(ctx, "_err");
2755
+ const stmts = [
2756
+ ...innerStmts,
2757
+ `${ind0}if (!${okVar}) {`,
2758
+ `${ind0} const ${errV} = { _tag: 'parseError', span: { start: ${pos}, end: ${pos} }, expected: ${JSON.stringify(def.expected)} }`,
2759
+ `${ind0} if (_ctx._errors) _ctx._errors.push(${errV})`,
2760
+ `${ind0} ${valVar} = ${errV}`,
2761
+ `${ind0} ${endVar} = ${pos}`,
2762
+ `${ind0} ${okVar} = true`,
2763
+ `${ind0}}`
2764
+ ];
2765
+ return { stmts, valueVar: valVar, endVar };
2766
+ }
2543
2767
  function emit(p, ctx, pos) {
2544
2768
  const def = p._def;
2545
2769
  switch (def.tag) {
@@ -2648,6 +2872,8 @@ function emit(p, ctx, pos) {
2648
2872
  return emitScanTo(def, ctx, pos);
2649
2873
  case "recover":
2650
2874
  return emitRecover(def, ctx, pos);
2875
+ case "expect":
2876
+ return emitExpect(def, ctx, pos);
2651
2877
  case "guard": {
2652
2878
  const fnIdx = ctx.mapFns.length;
2653
2879
  ctx.mapFns.push(def.predicate);
@@ -2731,12 +2957,88 @@ function hasNodeDef(p, seen = /* @__PURE__ */ new Set()) {
2731
2957
  return hasNodeDef(d.sentinel, seen) || d.skip.some((x) => hasNodeDef(x, seen));
2732
2958
  case "recover":
2733
2959
  return hasNodeDef(d.parser, seen) || hasNodeDef(d.sentinel, seen);
2960
+ case "expect":
2961
+ return hasNodeDef(d.parser, seen);
2734
2962
  case "withCtx":
2735
2963
  return hasNodeDef(d.parser, seen);
2736
2964
  default:
2737
2965
  return false;
2738
2966
  }
2739
2967
  }
2968
+ function childrenOf(def) {
2969
+ switch (def.tag) {
2970
+ case "sequence":
2971
+ case "choice":
2972
+ return def.parsers;
2973
+ case "many":
2974
+ case "optional":
2975
+ case "transform":
2976
+ case "trivia":
2977
+ case "label":
2978
+ case "grammar":
2979
+ case "not":
2980
+ case "node":
2981
+ case "withCtx":
2982
+ case "expect":
2983
+ return [def.parser];
2984
+ // emitMany's oneOrMore branch and emitSepBy both codegen `def.parser` TWICE
2985
+ // (a mandatory first match, then again inside the repeat loop) — two real
2986
+ // emit() call sites, so the usage analysis must see two edges here or it
2987
+ // undercounts a single-use `parser` ref as inline-safe when it's actually
2988
+ // referenced from two positions within this one compiled function.
2989
+ case "oneOrMore":
2990
+ return [def.parser, def.parser];
2991
+ case "sepBy":
2992
+ return [def.parser, def.parser, def.separator];
2993
+ case "skip":
2994
+ return [def.main, def.skipped];
2995
+ case "recover":
2996
+ return [def.parser, def.sentinel];
2997
+ case "scanTo":
2998
+ return [def.sentinel, ...def.skip];
2999
+ case "lazy":
3000
+ case "literal":
3001
+ case "regex":
3002
+ case "keywords":
3003
+ case "guard":
3004
+ case "unknown":
3005
+ return [];
3006
+ }
3007
+ }
3008
+ function analyzeLazyUsage(root) {
3009
+ return analyzeLazyUsageMulti([root]);
3010
+ }
3011
+ function analyzeLazyUsageMulti(roots) {
3012
+ const counts = /* @__PURE__ */ new Map();
3013
+ const recursive = /* @__PURE__ */ new Set();
3014
+ const descended = /* @__PURE__ */ new Set();
3015
+ const active = /* @__PURE__ */ new Set();
3016
+ function walk(p) {
3017
+ const def = p._def;
3018
+ if (def.tag === "lazy") {
3019
+ counts.set(p, (counts.get(p) ?? 0) + 1);
3020
+ if (active.has(p)) {
3021
+ recursive.add(p);
3022
+ return;
3023
+ }
3024
+ if (descended.has(p)) return;
3025
+ descended.add(p);
3026
+ let resolved;
3027
+ try {
3028
+ resolved = def.thunk();
3029
+ } catch {
3030
+ return;
3031
+ }
3032
+ active.add(p);
3033
+ walk(resolved);
3034
+ active.delete(p);
3035
+ return;
3036
+ }
3037
+ for (const child of childrenOf(def)) walk(child);
3038
+ }
3039
+ for (const root of roots) walk(root);
3040
+ return { counts, recursive };
3041
+ }
2740
3042
  function compile(combinator, mapFnSources) {
2741
3043
  const ctx = {
2742
3044
  vars: 0,
@@ -2753,7 +3055,8 @@ function compile(combinator, mapFnSources) {
2753
3055
  triviaCaptureNames: /* @__PURE__ */ new Map(),
2754
3056
  triviaFnNames: /* @__PURE__ */ new Map(),
2755
3057
  namedFnDecls: [],
2756
- capturing: hasNodeDef(combinator)
3058
+ capturing: hasNodeDef(combinator),
3059
+ lazyUsage: analyzeLazyUsage(combinator)
2757
3060
  };
2758
3061
  const r = emit(combinator, ctx, "_pos");
2759
3062
  const collatorDecl = ctx.needsCollator ? `const _collator = new Intl.Collator(undefined, { sensitivity: 'accent' })
@@ -2799,6 +3102,11 @@ function compile(combinator, mapFnSources) {
2799
3102
  parseWithContext(input, parseCtx, pos = 0) {
2800
3103
  return fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, parseCtx);
2801
3104
  },
3105
+ // Note: collects recover()/expect() errors via _errors. Unlike interpreter
3106
+ // parse({recover:true}) it does NOT populate furthestFail — the compiled path
3107
+ // inlines failures for throughput and deliberately skips _probe bookkeeping.
3108
+ // Callers wanting a furthest-position diagnostic detect unconsumed input
3109
+ // (span.end < input.length) instead, which is mode-agnostic.
2802
3110
  parseWithErrors(input, pos = 0) {
2803
3111
  const errors = [];
2804
3112
  const result = fn(input, pos, ctx.runtimeParsers, ctx.mapFns, ctx.buildFns, { ...defaultCtx, _errors: errors });
@@ -2910,6 +3218,58 @@ function isParseError(value) {
2910
3218
  return typeof value === "object" && value !== null && value._tag === "parseError";
2911
3219
  }
2912
3220
 
3221
+ // src/combinators/expect.ts
3222
+ function staticExpected(c) {
3223
+ const def = c._def;
3224
+ switch (def.tag) {
3225
+ case "literal":
3226
+ return [JSON.stringify(def.value)];
3227
+ case "regex":
3228
+ return [`/${def.source}/`];
3229
+ case "keywords":
3230
+ return def.words.map((w) => JSON.stringify(w));
3231
+ case "label":
3232
+ return [def.label];
3233
+ case "choice":
3234
+ return def.parsers.flatMap(staticExpected);
3235
+ case "sequence":
3236
+ return def.parsers.length > 0 ? staticExpected(def.parsers[0]) : [];
3237
+ case "node":
3238
+ case "grammar":
3239
+ case "trivia":
3240
+ case "optional":
3241
+ case "many":
3242
+ case "oneOrMore":
3243
+ case "transform":
3244
+ case "not":
3245
+ return staticExpected(def.parser);
3246
+ case "lazy":
3247
+ return staticExpected(def.thunk());
3248
+ default:
3249
+ return [];
3250
+ }
3251
+ }
3252
+ function expect(combinator, label2) {
3253
+ const expected = label2 !== void 0 ? [label2] : staticExpected(combinator);
3254
+ const meta = {
3255
+ firstSet: combinator._meta.firstSet,
3256
+ canMatchNewline: combinator._meta.canMatchNewline,
3257
+ isTrivia: false
3258
+ };
3259
+ return {
3260
+ _tag: "expect",
3261
+ _meta: meta,
3262
+ _def: { tag: "expect", parser: combinator, label: label2, expected },
3263
+ parse(input, pos, ctx) {
3264
+ const result = combinator.parse(input, pos, ctx);
3265
+ if (result.ok) return result;
3266
+ const error = { _tag: "parseError", span: { start: pos, end: pos }, expected };
3267
+ ctx._errors?.push(error);
3268
+ return { ok: true, value: error, span: { start: pos, end: pos } };
3269
+ }
3270
+ };
3271
+ }
3272
+
2913
3273
  // src/combinators/completions.ts
2914
3274
  function completionsAt(combinator, input, offset) {
2915
3275
  const probe = { offset, best: null };
@@ -2938,7 +3298,11 @@ function scanTo(sentinel, { skip: skip2 = [], orEOF = false } = {}) {
2938
3298
  _def: { tag: "scanTo", sentinel, skip: skip2, orEOF },
2939
3299
  parse(input, pos, ctx) {
2940
3300
  let cur = pos;
2941
- const probeCtx = { trackLines: ctx.trackLines, state: ctx.state };
3301
+ const probeCtx = {
3302
+ trackLines: ctx.trackLines,
3303
+ state: ctx.state,
3304
+ ...ctx._errors !== void 0 ? { _errors: ctx._errors } : {}
3305
+ };
2942
3306
  const emit2 = (end) => {
2943
3307
  if (end > pos && cstCaptureActive(ctx)) {
2944
3308
  const leaf = { _tag: "leaf", value: input.slice(pos, end), span: { start: pos, end } };
@@ -2974,20 +3338,46 @@ function scanTo(sentinel, { skip: skip2 = [], orEOF = false } = {}) {
2974
3338
  }
2975
3339
  function balanced(open, close, options = {}) {
2976
3340
  const self = ref();
2977
- const inner = scanTo(literal(close), {
2978
- ...options,
2979
- skip: [self, ...options.skip ?? []]
2980
- });
3341
+ const skips = options.skip ?? [];
3342
+ const stop = /* @__PURE__ */ new Set([open, close]);
3343
+ let bounded = true;
3344
+ for (const sk of skips) {
3345
+ const cs = firstSetClassChars(sk);
3346
+ if (cs === null) {
3347
+ bounded = false;
3348
+ break;
3349
+ }
3350
+ for (const ch of cs) stop.add(ch);
3351
+ }
3352
+ const cls = [...stop].map(escapeClassChar).join("");
3353
+ const content = bounded ? regex(new RegExp(`[^${cls}]+`)) : regex(new RegExp(`[^${escapeClassChar(open)}${escapeClassChar(close)}]`));
3354
+ const inner = many(choice(self, ...skips, content));
2981
3355
  const combi = transform(
2982
- sequence(literal(open), inner, literal(close)),
2983
- ([o, content, c]) => o + content + c
3356
+ sequence(literal(open), inner, expect(literal(close), close)),
3357
+ // parts: strings (content/self) or arrays (a sequence-shaped skip) or a
3358
+ // ParseError (recovered close). `c` is the close string or a ParseError.
3359
+ ([o, parts, c]) => o + parts.map((p) => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")
2984
3360
  );
2985
3361
  if (combi._def.tag === "transform") {
2986
- combi._def.fnSrc = "([o, content, c]) => o + content + c";
3362
+ combi._def.fnSrc = '([o, parts, c]) => o + parts.map(p => typeof p === "string" ? p : Array.isArray(p) ? p.join("") : "").join("") + (typeof c === "string" ? c : "")';
2987
3363
  }
2988
3364
  self.define(combi);
2989
3365
  return combi;
2990
3366
  }
3367
+ function firstSetClassChars(c) {
3368
+ const fs = c._meta.firstSet;
3369
+ if (fs.kind === "empty") return [];
3370
+ if (fs.kind !== "ranges") return null;
3371
+ const out = [];
3372
+ for (const { lo, hi } of fs.ranges) {
3373
+ if (hi - lo > 8) return null;
3374
+ for (let cp = lo; cp <= hi; cp++) out.push(String.fromCodePoint(cp));
3375
+ }
3376
+ return out;
3377
+ }
3378
+ function escapeClassChar(ch) {
3379
+ return ch.replace(/[\\\]^-]/g, "\\$&");
3380
+ }
2991
3381
 
2992
3382
  // src/functional/doc.ts
2993
3383
  function isNode(x) {