tarsec 0.5.0 → 0.5.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.
@@ -65,6 +65,30 @@ export declare function many1WithJoin(parser: Parser<string>): Parser<string>;
65
65
  * @returns - a parser that tries each parser in order. Returns the result of the first parser that succeeds.
66
66
  */
67
67
  export declare function or<const T extends readonly GeneralParser<any, any>[]>(...parsers: T): PickParserType<T>;
68
+ /**
69
+ * Commit point: once `prefix` succeeds, the branch is committed — a
70
+ * failure of `rest` is marked `committed` and stops backtracking. `or()`
71
+ * returns it instead of trying later alternatives, `many`/`optional`
72
+ * fail through it, `label` passes it along untouched, and lookahead
73
+ * (`not`, `peek`) contains it. A failure of `prefix` itself remains an
74
+ * ordinary, backtrackable failure.
75
+ *
76
+ * Use it where a prefix uniquely identifies a construct, so errors in
77
+ * the construct's body are reported as such instead of being drowned
78
+ * out by other alternatives' complaints:
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * const codeLiteral = committed(str("[|"), literalBody);
83
+ * // On "[|broken", the error is literalBody's — no other parser in an
84
+ * // enclosing or() gets to reinterpret the text.
85
+ * ```
86
+ *
87
+ * @param prefix - parser identifying the construct; its result is discarded
88
+ * @param rest - parser for the remainder of the construct
89
+ * @returns a parser producing `rest`'s result
90
+ */
91
+ export declare function committed<T>(prefix: Parser<unknown>, rest: Parser<T>): Parser<T>;
68
92
  /**
69
93
  * Takes a parser and runs it. If the parser fails,
70
94
  * optional returns a success with a null result.
@@ -307,6 +331,20 @@ export declare function many1Till<T>(parser: Parser<T>): Parser<string>;
307
331
  export declare function manyTillOneOf(stops: string[], { insensitive }?: {
308
332
  insensitive?: boolean;
309
333
  }): Parser<string>;
334
+ /**
335
+ * Just like `manyTillOneOf`, but fails unless at least one character of
336
+ * input is consumed. Use this as the bulk "inert text" alternative inside
337
+ * an `or()` of chunk parsers: when a stop character is at the current
338
+ * position, this parser fails and `or()` moves on to the alternatives
339
+ * that know how to handle it.
340
+ *
341
+ * @param stops - the strings to stop at
342
+ * @param options - object of optional parameters. { insensitive: boolean }
343
+ * @returns a parser that consumes at least one character, up to a stop
344
+ */
345
+ export declare function many1TillOneOf(stops: string[], { insensitive }?: {
346
+ insensitive?: boolean;
347
+ }): Parser<string>;
310
348
  /**
311
349
  * `manyTillStr` is an optimized version of `manyTill`.
312
350
  * The `manyTill` combinator is slow because it runs the given parser
@@ -328,6 +366,53 @@ export declare function manyTillStr(str: string, { insensitive }?: {
328
366
  * @returns a parser that consumes the input string until the given string is found.
329
367
  */
330
368
  export declare function iManyTillStr(str: string): Parser<string>;
369
+ /**
370
+ * Runs `parser` and discards its structured result, producing the raw
371
+ * text it consumed instead. The slice is taken from the input by length
372
+ * arithmetic — escapes and whitespace come back exactly as written, and
373
+ * no intermediate strings are built.
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * const number = matchedText(many1(digit));
378
+ * number("123abc"); // success("123", "abc")
379
+ * ```
380
+ *
381
+ * @param parser - the parser whose consumed text to return
382
+ * @returns a parser producing the exact consumed slice
383
+ */
384
+ export declare function matchedText(parser: Parser<unknown>): Parser<string>;
385
+ /**
386
+ * Repeats `chunk` until `terminator` succeeds. The terminator is tried
387
+ * FIRST each round and is NOT consumed — parsing resumes at its start.
388
+ * Fails if the input ends before the terminator appears, if a chunk
389
+ * fails mid-scan, or if a chunk succeeds without consuming input (which
390
+ * would otherwise loop forever).
391
+ *
392
+ * Unlike `manyTill` (which scans character-by-character for a stop
393
+ * parser), `repeatTill` parses structured chunks — so regions where the
394
+ * terminator text is inert (strings, comments) can be skipped by making
395
+ * them chunks. For raw text output, wrap in `matchedText`. For speed,
396
+ * make the first chunk alternative a `many1TillOneOf` of every character
397
+ * that can begin a chunk or the terminator — it swallows runs of inert
398
+ * text in one indexOf-driven step.
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * const bodyChunk = or(
403
+ * many1TillOneOf(['"', "/", "|"]), // bulk inert text — caller's triggers
404
+ * stringParser, // terminator inside a string is inert
405
+ * commentParser,
406
+ * anyChar, // lone trigger char that started nothing
407
+ * );
408
+ * const bodyText = matchedText(repeatTill(bodyChunk, str("|]")));
409
+ * ```
410
+ *
411
+ * @param chunk - parser for one repeated unit
412
+ * @param terminator - parser marking the end of the repetition (not consumed)
413
+ * @returns a parser producing the array of chunk results
414
+ */
415
+ export declare function repeatTill<T>(chunk: Parser<T>, terminator: Parser<unknown>): Parser<T[]>;
331
416
  /**
332
417
  * `map` is a parser combinator that takes a parser and a mapper function.
333
418
  * If the parser succeeds, it maps its result using the mapper function.
@@ -501,9 +586,10 @@ export type OperatorInfo<T> = {
501
586
  */
502
587
  export declare function buildExpressionParser<T>(atom: Parser<T>, operatorTable: OperatorInfo<T>[][], parenParser?: Parser<T>): Parser<T>;
503
588
  /**
504
- * Clear every cache created by `memo`. Call this at the start of each
589
+ * Clear the current parse's memo caches. Call this at the start of each
505
590
  * top-level parse if your memoized parsers produce results that depend
506
591
  * on mutable global state (e.g. positional info from `setInputStr`).
592
+ * (`runNested` gives nested parses fresh caches automatically.)
507
593
  */
508
594
  export declare function resetMemos(): void;
509
595
  /**
@@ -1,8 +1,20 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
1
12
  import { within } from "./parsers/within.js";
2
13
  import { TarsecError } from "./tarsecError.js";
3
14
  import { getDiagnostics, trace } from "./trace.js";
4
- import { captureSuccess, failure, isCaptureResult, isSuccess, success, } from "./types.js";
15
+ import { captureSuccess, failure, isCaptureResult, isCommittedFailure, isSuccess, success, } from "./types.js";
5
16
  import { escape } from "./utils.js";
17
+ import { getParseState } from "./parseState.js";
6
18
  /**
7
19
  * Takes a parser and runs it zero or more times, returning the results as an array.
8
20
  * If the parser is a capture parser, it returns the captures as an array in this form:
@@ -25,6 +37,9 @@ export function many(parser) {
25
37
  while (true) {
26
38
  let parsed = parser(rest);
27
39
  if (!parsed.success) {
40
+ if (isCommittedFailure(parsed)) {
41
+ return parsed; // committed: fail the whole repetition
42
+ }
28
43
  if (Object.keys(captures).length) {
29
44
  return captureSuccess(results, rest, { captures });
30
45
  }
@@ -60,6 +75,9 @@ export function many(parser) {
60
75
  export function many1(parser) {
61
76
  return trace(`many1`, (input) => {
62
77
  let result = many(parser)(input);
78
+ if (!result.success && isCommittedFailure(result)) {
79
+ return result;
80
+ }
63
81
  // this logic doesn't work with optional and not
64
82
  if (result.rest !== input) {
65
83
  return result;
@@ -102,6 +120,9 @@ export function exactly(num, parser) {
102
120
  for (let i = 0; i < num; i++) {
103
121
  let parsed = parser(rest);
104
122
  if (!parsed.success) {
123
+ if (isCommittedFailure(parsed)) {
124
+ return parsed;
125
+ }
105
126
  return failure(`expected ${num} matches, got ${i}`, input);
106
127
  }
107
128
  results.push(parsed.result);
@@ -161,10 +182,53 @@ export function or(...parsers) {
161
182
  if (result.success) {
162
183
  return result;
163
184
  }
185
+ if (isCommittedFailure(result)) {
186
+ return result; // committed: do not try later alternatives
187
+ }
164
188
  }
165
189
  return failure(`all parsers failed`, input);
166
190
  });
167
191
  }
192
+ /**
193
+ * Commit point: once `prefix` succeeds, the branch is committed — a
194
+ * failure of `rest` is marked `committed` and stops backtracking. `or()`
195
+ * returns it instead of trying later alternatives, `many`/`optional`
196
+ * fail through it, `label` passes it along untouched, and lookahead
197
+ * (`not`, `peek`) contains it. A failure of `prefix` itself remains an
198
+ * ordinary, backtrackable failure.
199
+ *
200
+ * Use it where a prefix uniquely identifies a construct, so errors in
201
+ * the construct's body are reported as such instead of being drowned
202
+ * out by other alternatives' complaints:
203
+ *
204
+ * @example
205
+ * ```ts
206
+ * const codeLiteral = committed(str("[|"), literalBody);
207
+ * // On "[|broken", the error is literalBody's — no other parser in an
208
+ * // enclosing or() gets to reinterpret the text.
209
+ * ```
210
+ *
211
+ * @param prefix - parser identifying the construct; its result is discarded
212
+ * @param rest - parser for the remainder of the construct
213
+ * @returns a parser producing `rest`'s result
214
+ */
215
+ export function committed(prefix, rest) {
216
+ return trace("committed", (input) => {
217
+ const prefixResult = prefix(input);
218
+ if (!prefixResult.success)
219
+ return prefixResult;
220
+ const restResult = rest(prefixResult.rest);
221
+ if (!restResult.success) {
222
+ const committedResult = Object.assign(Object.assign({}, restResult), { committed: true });
223
+ // Preferred slot for error reporting: getErrorMessage returns this
224
+ // in preference to the rightmost record, so a fallback alternative
225
+ // that wanders deeper than the commit point can't win the message.
226
+ getParseState().committedFailure = committedResult;
227
+ return committedResult;
228
+ }
229
+ return restResult;
230
+ });
231
+ }
168
232
  /**
169
233
  * Takes a parser and runs it. If the parser fails,
170
234
  * optional returns a success with a null result.
@@ -179,6 +243,9 @@ export function optional(parser) {
179
243
  if (result.success) {
180
244
  return result;
181
245
  }
246
+ if (isCommittedFailure(result)) {
247
+ return result; // committed: not "optionally absent" — genuinely broken
248
+ }
182
249
  return success(null, input);
183
250
  });
184
251
  }
@@ -194,7 +261,11 @@ export function optional(parser) {
194
261
  */
195
262
  export function not(parser) {
196
263
  return trace("not", (input) => {
264
+ // The probe is speculation: a commit inside it must not escape,
265
+ // either through the result or through the committedFailure slot.
266
+ const slotBefore = getParseState().committedFailure;
197
267
  let result = parser(input);
268
+ getParseState().committedFailure = slotBefore;
198
269
  if (result.success) {
199
270
  return {
200
271
  success: false,
@@ -207,9 +278,14 @@ export function not(parser) {
207
278
  }
208
279
  export function peek(parser) {
209
280
  return trace("peek", (input) => {
281
+ // The probe is speculation: contain any commit inside it — restore
282
+ // the slot and strip the flag, keeping peek's rest-reset contract.
283
+ const slotBefore = getParseState().committedFailure;
210
284
  const result = parser(input);
285
+ getParseState().committedFailure = slotBefore;
211
286
  if (!result.success) {
212
- return Object.assign(Object.assign({}, result), { rest: input });
287
+ const { committed: _stripped } = result, plain = __rest(result, ["committed"]);
288
+ return Object.assign(Object.assign({}, plain), { rest: input });
213
289
  }
214
290
  return Object.assign(Object.assign({}, result), { rest: input });
215
291
  });
@@ -265,6 +341,9 @@ export function between(open, close, parser) {
265
341
  // the parser should keep succeeding until we find the closer
266
342
  // if it doesn't, we fail
267
343
  if (!parserResult.success) {
344
+ if (isCommittedFailure(parserResult)) {
345
+ return parserResult; // committed: pass through unwrapped
346
+ }
268
347
  return failure(parserResult.message, input);
269
348
  }
270
349
  successResult.push(parserResult.result);
@@ -308,12 +387,18 @@ export function sepBy(separator, parser) {
308
387
  while (true) {
309
388
  const result = parser(rest);
310
389
  if (!result.success) {
390
+ if (isCommittedFailure(result)) {
391
+ return result; // committed: fail the whole sepBy
392
+ }
311
393
  return success(results, rest);
312
394
  }
313
395
  results.push(result.result);
314
396
  rest = result.rest;
315
397
  const sepResult = separator(rest);
316
398
  if (!sepResult.success) {
399
+ if (isCommittedFailure(sepResult)) {
400
+ return sepResult;
401
+ }
317
402
  return success(results, rest);
318
403
  }
319
404
  rest = sepResult.rest;
@@ -478,14 +563,18 @@ export function captureCaptures(parser) {
478
563
  */
479
564
  export function manyTill(parser) {
480
565
  return (input) => {
566
+ // The stop probes are speculative; contain any commit they stash.
567
+ const slotBefore = getParseState().committedFailure;
481
568
  let current = 0;
482
569
  while (current < input.length) {
483
570
  const parsed = parser(input.slice(current));
484
571
  if (parsed.success) {
572
+ getParseState().committedFailure = slotBefore;
485
573
  return success(input.slice(0, current), input.slice(current));
486
574
  }
487
575
  current++;
488
576
  }
577
+ getParseState().committedFailure = slotBefore;
489
578
  return success(input, "");
490
579
  };
491
580
  }
@@ -496,10 +585,13 @@ export function manyTill(parser) {
496
585
  */
497
586
  export function many1Till(parser) {
498
587
  return (input) => {
588
+ // The stop probes are speculative; contain any commit they stash.
589
+ const slotBefore = getParseState().committedFailure;
499
590
  let current = 0;
500
591
  while (current < input.length) {
501
592
  const parsed = parser(input.slice(current));
502
593
  if (parsed.success) {
594
+ getParseState().committedFailure = slotBefore;
503
595
  if (current === 0) {
504
596
  return failure("expected to consume at least one character of input", input);
505
597
  }
@@ -507,6 +599,7 @@ export function many1Till(parser) {
507
599
  }
508
600
  current++;
509
601
  }
602
+ getParseState().committedFailure = slotBefore;
510
603
  if (current === 0) {
511
604
  return failure("expected to consume at least one character of input", input);
512
605
  }
@@ -546,6 +639,27 @@ export function manyTillOneOf(stops, { insensitive = false } = {}) {
546
639
  return success(input.slice(0, min), input.slice(min));
547
640
  });
548
641
  }
642
+ /**
643
+ * Just like `manyTillOneOf`, but fails unless at least one character of
644
+ * input is consumed. Use this as the bulk "inert text" alternative inside
645
+ * an `or()` of chunk parsers: when a stop character is at the current
646
+ * position, this parser fails and `or()` moves on to the alternatives
647
+ * that know how to handle it.
648
+ *
649
+ * @param stops - the strings to stop at
650
+ * @param options - object of optional parameters. { insensitive: boolean }
651
+ * @returns a parser that consumes at least one character, up to a stop
652
+ */
653
+ export function many1TillOneOf(stops, { insensitive = false } = {}) {
654
+ const scan = manyTillOneOf(stops, { insensitive });
655
+ return trace(`many1TillOneOf(${escape(stops.join(","))})`, (input) => {
656
+ const result = scan(input);
657
+ if (result.success && result.result.length === 0) {
658
+ return failure("expected to consume at least one character of input", input);
659
+ }
660
+ return result;
661
+ });
662
+ }
549
663
  /**
550
664
  * `manyTillStr` is an optimized version of `manyTill`.
551
665
  * The `manyTill` combinator is slow because it runs the given parser
@@ -571,6 +685,90 @@ export function manyTillStr(str, { insensitive = false } = {}) {
571
685
  export function iManyTillStr(str) {
572
686
  return manyTillStr(str, { insensitive: true });
573
687
  }
688
+ /**
689
+ * Runs `parser` and discards its structured result, producing the raw
690
+ * text it consumed instead. The slice is taken from the input by length
691
+ * arithmetic — escapes and whitespace come back exactly as written, and
692
+ * no intermediate strings are built.
693
+ *
694
+ * @example
695
+ * ```ts
696
+ * const number = matchedText(many1(digit));
697
+ * number("123abc"); // success("123", "abc")
698
+ * ```
699
+ *
700
+ * @param parser - the parser whose consumed text to return
701
+ * @returns a parser producing the exact consumed slice
702
+ */
703
+ export function matchedText(parser) {
704
+ return trace("matchedText", (input) => {
705
+ const result = parser(input);
706
+ if (!result.success)
707
+ return result;
708
+ // Invariant (shared with withSpan): a parser's `rest` is a suffix of
709
+ // its input, so the consumed length is the difference of lengths.
710
+ const consumedLength = input.length - result.rest.length;
711
+ return success(input.slice(0, consumedLength), result.rest);
712
+ });
713
+ }
714
+ /**
715
+ * Repeats `chunk` until `terminator` succeeds. The terminator is tried
716
+ * FIRST each round and is NOT consumed — parsing resumes at its start.
717
+ * Fails if the input ends before the terminator appears, if a chunk
718
+ * fails mid-scan, or if a chunk succeeds without consuming input (which
719
+ * would otherwise loop forever).
720
+ *
721
+ * Unlike `manyTill` (which scans character-by-character for a stop
722
+ * parser), `repeatTill` parses structured chunks — so regions where the
723
+ * terminator text is inert (strings, comments) can be skipped by making
724
+ * them chunks. For raw text output, wrap in `matchedText`. For speed,
725
+ * make the first chunk alternative a `many1TillOneOf` of every character
726
+ * that can begin a chunk or the terminator — it swallows runs of inert
727
+ * text in one indexOf-driven step.
728
+ *
729
+ * @example
730
+ * ```ts
731
+ * const bodyChunk = or(
732
+ * many1TillOneOf(['"', "/", "|"]), // bulk inert text — caller's triggers
733
+ * stringParser, // terminator inside a string is inert
734
+ * commentParser,
735
+ * anyChar, // lone trigger char that started nothing
736
+ * );
737
+ * const bodyText = matchedText(repeatTill(bodyChunk, str("|]")));
738
+ * ```
739
+ *
740
+ * @param chunk - parser for one repeated unit
741
+ * @param terminator - parser marking the end of the repetition (not consumed)
742
+ * @returns a parser producing the array of chunk results
743
+ */
744
+ export function repeatTill(chunk, terminator) {
745
+ return trace("repeatTill", (input) => {
746
+ const results = [];
747
+ let rest = input;
748
+ // Terminates: every iteration either returns or strictly shrinks
749
+ // `rest` (the progress guard below enforces it, even against a
750
+ // misbehaving chunk that returns a longer rest), and empty `rest`
751
+ // returns — at most input.length + 1 iterations.
752
+ while (true) {
753
+ const terminatorResult = terminator(rest);
754
+ if (terminatorResult.success) {
755
+ return success(results, rest);
756
+ }
757
+ if (rest.length === 0) {
758
+ return failure("input ended before terminator", rest);
759
+ }
760
+ const chunkResult = chunk(rest);
761
+ if (!chunkResult.success) {
762
+ return chunkResult;
763
+ }
764
+ if (chunkResult.rest.length >= rest.length) {
765
+ return failure("repeatTill chunk succeeded without consuming input", rest);
766
+ }
767
+ results.push(chunkResult.result);
768
+ rest = chunkResult.rest;
769
+ }
770
+ });
771
+ }
574
772
  /**
575
773
  * `map` is a parser combinator that takes a parser and a mapper function.
576
774
  * If the parser succeeds, it maps its result using the mapper function.
@@ -651,6 +849,9 @@ export function seq(parsers, transform, debugName = "") {
651
849
  for (let i = 0; i < parsers.length; i++) {
652
850
  const parsed = parsers[i](rest);
653
851
  if (!parsed.success) {
852
+ if (isCommittedFailure(parsed)) {
853
+ return parsed; // committed: keep the flag AND the failure position
854
+ }
654
855
  return Object.assign(Object.assign({}, parsed), { rest: input });
655
856
  }
656
857
  results.push(parsed.result);
@@ -847,8 +1048,11 @@ export function buildExpressionParser(atom, operatorTable, parenParser) {
847
1048
  if (!openResult.success)
848
1049
  return openResult;
849
1050
  const exprResult = expr(openResult.rest);
850
- if (!exprResult.success)
1051
+ if (!exprResult.success) {
1052
+ if (isCommittedFailure(exprResult))
1053
+ return exprResult;
851
1054
  return failure(exprResult.message, input);
1055
+ }
852
1056
  const closeResult = exprResult.rest[0] === ")" ? success(")", exprResult.rest.slice(1)) : failure("expected )", input);
853
1057
  if (!closeResult.success)
854
1058
  return closeResult;
@@ -889,6 +1093,10 @@ function buildLevel(nextLevel, ops) {
889
1093
  };
890
1094
  }
891
1095
  function parseLeft(left, rest, nextLevel, ops) {
1096
+ // The fold's probes are speculative: a failing operator or operand just
1097
+ // ends the chain. Discarding the failure must also discard any commit
1098
+ // it stashed, or the stale slot would mask later errors' messages.
1099
+ const slotBefore = getParseState().committedFailure;
892
1100
  while (true) {
893
1101
  const opMatch = tryOps(ops, rest);
894
1102
  if (!opMatch)
@@ -899,21 +1107,28 @@ function parseLeft(left, rest, nextLevel, ops) {
899
1107
  left = opMatch.apply(left, rightResult.result);
900
1108
  rest = rightResult.rest;
901
1109
  }
1110
+ getParseState().committedFailure = slotBefore;
902
1111
  return success(left, rest);
903
1112
  }
904
1113
  function parseRight(left, rest, nextLevel, ops) {
1114
+ // Same speculative-probe containment as parseLeft.
1115
+ const slotBefore = getParseState().committedFailure;
905
1116
  const opMatch = tryOps(ops, rest);
906
1117
  if (!opMatch)
907
1118
  return success(left, rest);
908
1119
  const rightResult = nextLevel(opMatch.rest);
909
- if (!rightResult.success)
1120
+ if (!rightResult.success) {
1121
+ getParseState().committedFailure = slotBefore;
910
1122
  return success(left, rest);
1123
+ }
911
1124
  // Recursively parse the right side to get right-associativity
912
1125
  const rightFolded = parseRight(rightResult.result, rightResult.rest, nextLevel, ops);
913
1126
  return success(opMatch.apply(left, rightFolded.result), rightFolded.rest);
914
1127
  }
915
1128
  function parseChain(left, rest, nextLevel, ops) {
916
1129
  // Fallback: treat everything as left-associative
1130
+ // Same speculative-probe containment as parseLeft.
1131
+ const slotBefore = getParseState().committedFailure;
917
1132
  while (true) {
918
1133
  const opMatch = tryOps(ops, rest);
919
1134
  if (!opMatch)
@@ -924,11 +1139,15 @@ function parseChain(left, rest, nextLevel, ops) {
924
1139
  left = opMatch.apply(left, rightResult.result);
925
1140
  rest = rightResult.rest;
926
1141
  }
1142
+ getParseState().committedFailure = slotBefore;
927
1143
  return success(left, rest);
928
1144
  }
929
1145
  function tryOps(ops, input) {
1146
+ // Op probes are speculative; contain any commit they stash.
1147
+ const slotBefore = getParseState().committedFailure;
930
1148
  for (const op of ops) {
931
1149
  const result = op.op(input);
1150
+ getParseState().committedFailure = slotBefore;
932
1151
  if (result.success) {
933
1152
  return { rest: result.rest, apply: op.apply };
934
1153
  }
@@ -936,26 +1155,32 @@ function tryOps(ops, input) {
936
1155
  return null;
937
1156
  }
938
1157
  const DEFAULT_MEMO_LIMIT = 10000;
939
- // Registry of all caches created by `memo`, so callers can clear them
940
- // between independent parses. This matters when memoized parsers produce
941
- // results derived from globally-set state (e.g. `withSpan` reads the
1158
+ // Caches created by `memo` live on the current parse state, keyed by a
1159
+ // per-memo numeric id. This matters when memoized parsers produce
1160
+ // results derived from the parse's state (e.g. `withSpan` reads the
942
1161
  // current input via `setInputStr`): the cached `loc` is only valid for
943
- // the source it was computed against, so reusing the cache across parses
944
- // with different sources would return stale positional info.
945
- const memoCaches = [];
1162
+ // the source it was computed against, so a nested parse must never see
1163
+ // (or pollute) the enclosing parse's entries swapping the state swaps
1164
+ // the caches with it.
1165
+ let nextMemoId = 0;
946
1166
  /**
947
- * Clear every cache created by `memo`. Call this at the start of each
1167
+ * Clear the current parse's memo caches. Call this at the start of each
948
1168
  * top-level parse if your memoized parsers produce results that depend
949
1169
  * on mutable global state (e.g. positional info from `setInputStr`).
1170
+ * (`runNested` gives nested parses fresh caches automatically.)
950
1171
  */
951
1172
  export function resetMemos() {
952
- for (const cache of memoCaches)
953
- cache.clear();
1173
+ getParseState().memoCaches.clear();
954
1174
  }
955
1175
  export function memo(name, parser) {
956
- const cache = new Map();
957
- memoCaches.push(cache);
1176
+ const memoId = nextMemoId++;
958
1177
  return trace(name ? `memo(${name})` : "memo", (input) => {
1178
+ const state = getParseState();
1179
+ let cache = state.memoCaches.get(memoId);
1180
+ if (cache === undefined) {
1181
+ cache = new Map();
1182
+ state.memoCaches.set(memoId, cache);
1183
+ }
959
1184
  const hit = cache.get(input);
960
1185
  if (hit !== undefined)
961
1186
  return hit;
package/dist/index.d.ts CHANGED
@@ -6,3 +6,5 @@ export * from "./tarsecError.js";
6
6
  export * from "./position.js";
7
7
  export * from "./rightmostFailure.js";
8
8
  export * from "./lexeme.js";
9
+ export * from "./parseState.js";
10
+ export * from "./runNested.js";
package/dist/index.js CHANGED
@@ -6,3 +6,5 @@ export * from "./tarsecError.js";
6
6
  export * from "./position.js";
7
7
  export * from "./rightmostFailure.js";
8
8
  export * from "./lexeme.js";
9
+ export * from "./parseState.js";
10
+ export * from "./runNested.js";
@@ -0,0 +1,25 @@
1
+ import type { Position } from "./position.js";
2
+ import type { ParserFailure, ParserResult } from "./types.js";
3
+ /**
4
+ * All mutable state belonging to one running parse. Everything tarsec
5
+ * used to keep in scattered module-level variables lives here, so a
6
+ * nested parse (see `runNested`) can swap in a fresh state and restore
7
+ * the old one by reassigning a single reference.
8
+ */
9
+ export type ParseState = {
10
+ inputStr: string;
11
+ rightmostFailurePos: number;
12
+ rightmostFailureExpected: string[];
13
+ /** Per-`memo`-instance caches, keyed by each memo's numeric id. */
14
+ memoCaches: Map<number, Map<string, ParserResult<any>>>;
15
+ /** Offset added to every derived position, so a nested parse reports
16
+ * positions in the enclosing parse's coordinates. Zero at top level. */
17
+ basePosition: Position;
18
+ /** The most recent committed failure of this parse, if any. Preferred
19
+ * over the rightmost record by `getErrorMessage`. See `committed`. */
20
+ committedFailure: ParserFailure | null;
21
+ };
22
+ export declare function createParseState(inputStr: string, basePosition?: Position): ParseState;
23
+ export declare function getParseState(): ParseState;
24
+ /** Replace the current parse state, returning the previous one. */
25
+ export declare function swapParseState(next: ParseState): ParseState;
@@ -0,0 +1,21 @@
1
+ const ZERO_POSITION = { offset: 0, line: 0, column: 0 };
2
+ export function createParseState(inputStr, basePosition = ZERO_POSITION) {
3
+ return {
4
+ inputStr,
5
+ rightmostFailurePos: -1,
6
+ rightmostFailureExpected: [],
7
+ memoCaches: new Map(),
8
+ basePosition: Object.assign({}, basePosition),
9
+ committedFailure: null,
10
+ };
11
+ }
12
+ let currentState = createParseState("");
13
+ export function getParseState() {
14
+ return currentState;
15
+ }
16
+ /** Replace the current parse state, returning the previous one. */
17
+ export function swapParseState(next) {
18
+ const previous = currentState;
19
+ currentState = next;
20
+ return previous;
21
+ }
@@ -1,5 +1,6 @@
1
1
  import { trace } from "../trace.js";
2
2
  import { success } from "../types.js";
3
+ import { getParseState } from "../parseState.js";
3
4
  /**
4
5
  * `within` is a funny combinator. It finds zero or more instances of `parser` within the input.
5
6
  * It always succeeds and returns an array of results, each one being a matched or unmatched string.
@@ -59,6 +60,9 @@ import { success } from "../types.js";
59
60
  */
60
61
  export function within(parser) {
61
62
  return trace("within", (input) => {
63
+ // A search is speculation: probes at every offset must not leak
64
+ // commits into the committedFailure slot.
65
+ const slotBefore = getParseState().committedFailure;
62
66
  let start = 0;
63
67
  let current = 0;
64
68
  const results = [];
@@ -89,6 +93,7 @@ export function within(parser) {
89
93
  value: input.slice(start, current),
90
94
  });
91
95
  }
96
+ getParseState().committedFailure = slotBefore;
92
97
  return success(results, "");
93
98
  });
94
99
  }
package/dist/parsers.d.ts CHANGED
@@ -138,8 +138,19 @@ export declare function takeWhile1(charsOrPred: string | CharPredicate, expected
138
138
  export declare const anyChar: Parser<string>;
139
139
  /**
140
140
  * Wraps a parser with a human-readable label for error reporting.
141
- * On failure, suppresses any inner failure recordings and records only the label.
142
- * This produces clean error messages like `expected a digit` instead of `expected one of "0123456789"`.
141
+ *
142
+ * If the wrapped parser fails at the label's own position (it never got
143
+ * anywhere), its internal failure records are suppressed and only the
144
+ * label is recorded — producing clean messages like `expected a digit`
145
+ * instead of `expected one of "0123456789"`.
146
+ *
147
+ * If the wrapped parser fails STRICTLY INSIDE the labeled region (it
148
+ * consumed input first), its deeper, more specific record is kept and
149
+ * the label stays out of the way — so a grammar labeled at every level
150
+ * still surfaces the deepest thing the parser actually knew.
151
+ * (Comparing against the label's own position, rather than the previous
152
+ * record, is what keeps shallow labels clean while deep knowledge
153
+ * survives.)
143
154
  *
144
155
  * @param name - human-readable description of what the parser expects
145
156
  * @param parser - the parser to wrap
package/dist/parsers.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { many1WithJoin } from "./combinators.js";
2
- import { trace } from "./trace.js";
2
+ import { trace, getInputStr } from "./trace.js";
3
3
  import { recordFailure, saveRightmostFailure, restoreRightmostFailure } from "./rightmostFailure.js";
4
4
  import { captureSuccess, failure, success, } from "./types.js";
5
5
  import { escape } from "./utils.js";
@@ -241,8 +241,19 @@ export const anyChar = trace("anyChar", (input) => {
241
241
  });
242
242
  /**
243
243
  * Wraps a parser with a human-readable label for error reporting.
244
- * On failure, suppresses any inner failure recordings and records only the label.
245
- * This produces clean error messages like `expected a digit` instead of `expected one of "0123456789"`.
244
+ *
245
+ * If the wrapped parser fails at the label's own position (it never got
246
+ * anywhere), its internal failure records are suppressed and only the
247
+ * label is recorded — producing clean messages like `expected a digit`
248
+ * instead of `expected one of "0123456789"`.
249
+ *
250
+ * If the wrapped parser fails STRICTLY INSIDE the labeled region (it
251
+ * consumed input first), its deeper, more specific record is kept and
252
+ * the label stays out of the way — so a grammar labeled at every level
253
+ * still surfaces the deepest thing the parser actually knew.
254
+ * (Comparing against the label's own position, rather than the previous
255
+ * record, is what keeps shallow labels clean while deep knowledge
256
+ * survives.)
246
257
  *
247
258
  * @param name - human-readable description of what the parser expects
248
259
  * @param parser - the parser to wrap
@@ -252,6 +263,21 @@ export function label(name, parser) {
252
263
  return (input) => {
253
264
  const saved = saveRightmostFailure();
254
265
  const result = parser(input);
266
+ if (!result.success && result.committed === true) {
267
+ // Committed failures carry their own story — no re-labeling,
268
+ // no restore games.
269
+ return result;
270
+ }
271
+ const afterChild = saveRightmostFailure();
272
+ const labelPos = getInputStr().length - input.length;
273
+ const childGotStrictlyDeeper = afterChild.pos > labelPos;
274
+ if (!result.success && childGotStrictlyDeeper) {
275
+ // The child FAILED somewhere specific past this label's start —
276
+ // keep its record; adding the label here would only blur it.
277
+ // On success the restore below still runs, scrubbing speculative
278
+ // records left by backtracked alternatives inside the region.
279
+ return result;
280
+ }
255
281
  restoreRightmostFailure(saved);
256
282
  if (!result.success)
257
283
  recordFailure(input, name);
@@ -18,14 +18,24 @@ export declare function buildLineTable(source: string): number[];
18
18
  * Both line and column are 0-based.
19
19
  */
20
20
  export declare function offsetToPosition(lineTable: number[], offset: number): Position;
21
+ /**
22
+ * Compose an inner-parse position with a base position (the point in the
23
+ * enclosing input where the inner input begins). Offsets and lines add;
24
+ * the base column applies only while still on the base line (inner line 0).
25
+ */
26
+ export declare function composePosition(base: Position, pos: Position): Position;
21
27
  /**
22
28
  * A zero-width parser that returns the current offset into the input string.
23
29
  * Requires `setInputStr` to have been called with the full input.
30
+ * Inside `runNested`, the offset is reported in the enclosing parse's
31
+ * coordinates (the state's `basePosition` is added).
24
32
  */
25
33
  export declare const getOffset: Parser<number>;
26
34
  /**
27
35
  * A zero-width parser that returns the current position (offset, line, column).
28
36
  * Requires `setInputStr` to have been called with the full input.
37
+ * Inside `runNested`, the position is reported in the enclosing parse's
38
+ * coordinates (composed with the state's `basePosition`).
29
39
  */
30
40
  export declare const getPosition: Parser<Position>;
31
41
  /**
package/dist/position.js CHANGED
@@ -1,5 +1,9 @@
1
- import { getInputStr } from "./trace.js";
1
+ // NOTE: this module deliberately imports nothing from trace.ts — it reads
2
+ // the input via getParseState().inputStr. trace.ts imports composePosition
3
+ // from here, so an import in the other direction would create a runtime
4
+ // module cycle (fragile TDZ hazard under refactoring/bundlers).
2
5
  import { success } from "./types.js";
6
+ import { getParseState } from "./parseState.js";
3
7
  /**
4
8
  * Build a lookup table of line-start offsets for a given source string.
5
9
  * This allows O(log n) offset-to-position conversion via binary search.
@@ -17,6 +21,9 @@ export function buildLineTable(source) {
17
21
  // table on every invocation. Parses run sequentially with a single
18
22
  // `setInputStr` source, so a most-recently-seen cache is sufficient and
19
23
  // avoids O(num_calls * source.length) work during a parse.
24
+ // Deliberately NOT part of ParseState: keyed by source-string identity,
25
+ // so a nested parse's source misses and rebuilds — it can never serve a
26
+ // stale table across parse states.
20
27
  let cachedSource = null;
21
28
  let cachedLineTable = [0];
22
29
  function getLineTable(source) {
@@ -49,23 +56,45 @@ export function offsetToPosition(lineTable, offset) {
49
56
  column: offset - lineTable[lo],
50
57
  };
51
58
  }
59
+ /**
60
+ * Compose an inner-parse position with a base position (the point in the
61
+ * enclosing input where the inner input begins). Offsets and lines add;
62
+ * the base column applies only while still on the base line (inner line 0).
63
+ */
64
+ export function composePosition(base, pos) {
65
+ if (base.offset === 0 && base.line === 0 && base.column === 0) {
66
+ return pos;
67
+ }
68
+ const column = pos.line === 0 ? base.column + pos.column : pos.column;
69
+ return {
70
+ offset: base.offset + pos.offset,
71
+ line: base.line + pos.line,
72
+ column,
73
+ };
74
+ }
52
75
  /**
53
76
  * A zero-width parser that returns the current offset into the input string.
54
77
  * Requires `setInputStr` to have been called with the full input.
78
+ * Inside `runNested`, the offset is reported in the enclosing parse's
79
+ * coordinates (the state's `basePosition` is added).
55
80
  */
56
81
  export const getOffset = (input) => {
57
- const source = getInputStr();
58
- return success(source.length - input.length, input);
82
+ const source = getParseState().inputStr;
83
+ const base = getParseState().basePosition;
84
+ return success(base.offset + (source.length - input.length), input);
59
85
  };
60
86
  /**
61
87
  * A zero-width parser that returns the current position (offset, line, column).
62
88
  * Requires `setInputStr` to have been called with the full input.
89
+ * Inside `runNested`, the position is reported in the enclosing parse's
90
+ * coordinates (composed with the state's `basePosition`).
63
91
  */
64
92
  export const getPosition = (input) => {
65
- const source = getInputStr();
93
+ const source = getParseState().inputStr;
66
94
  const offset = source.length - input.length;
67
95
  const lineTable = getLineTable(source);
68
- return success(offsetToPosition(lineTable, offset), input);
96
+ const base = getParseState().basePosition;
97
+ return success(composePosition(base, offsetToPosition(lineTable, offset)), input);
69
98
  };
70
99
  /**
71
100
  * Wraps a parser so that its result includes span information (start and end positions).
@@ -80,18 +109,19 @@ export const getPosition = (input) => {
80
109
  */
81
110
  export function withSpan(parser) {
82
111
  return (input) => {
83
- const source = getInputStr();
112
+ const source = getParseState().inputStr;
84
113
  const lineTable = getLineTable(source);
85
114
  const startOffset = source.length - input.length;
86
115
  const result = parser(input);
87
116
  if (!result.success)
88
117
  return result;
89
118
  const endOffset = source.length - result.rest.length;
119
+ const base = getParseState().basePosition;
90
120
  return success({
91
121
  value: result.result,
92
122
  span: {
93
- start: offsetToPosition(lineTable, startOffset),
94
- end: offsetToPosition(lineTable, endOffset),
123
+ start: composePosition(base, offsetToPosition(lineTable, startOffset)),
124
+ end: composePosition(base, offsetToPosition(lineTable, endOffset)),
95
125
  },
96
126
  }, result.rest);
97
127
  };
@@ -25,6 +25,16 @@ export declare function saveRightmostFailure(): SavedRightmostFailure;
25
25
  export declare function restoreRightmostFailure(saved: SavedRightmostFailure): void;
26
26
  /**
27
27
  * Formats the rightmost failure into a human-readable error message with line and column info.
28
+ * A committed failure (see the `committed` combinator) takes precedence
29
+ * over the rightmost record — the committed error wins reporting even
30
+ * when some fallback alternative failed deeper into the input.
31
+ *
32
+ * Scope: the preference is per parse state. A commit inside `runNested`
33
+ * lives in the inner state and surfaces through the returned result's
34
+ * `committed` flag, never through the enclosing parse's getErrorMessage.
35
+ * The position math assumes the committed failure's `rest` is a suffix
36
+ * of the CURRENT state's source — don't stash a `runNested` result
37
+ * (inner-coordinate `rest`) into an outer-state commit.
28
38
  * Returns `null` if no failures have been recorded.
29
39
  * Requires `setInputStr` to have been called.
30
40
  */
@@ -1,10 +1,11 @@
1
1
  import { getInputStr } from "./trace.js";
2
- import { buildLineTable, offsetToPosition } from "./position.js";
3
- let rightmostFailurePos = -1;
4
- let rightmostFailureExpected = [];
2
+ import { buildLineTable, offsetToPosition, composePosition } from "./position.js";
3
+ import { getParseState } from "./parseState.js";
5
4
  export function resetRightmostFailure() {
6
- rightmostFailurePos = -1;
7
- rightmostFailureExpected = [];
5
+ const state = getParseState();
6
+ state.rightmostFailurePos = -1;
7
+ state.rightmostFailureExpected = [];
8
+ state.committedFailure = null;
8
9
  }
9
10
  /**
10
11
  * Record an expected alternative at the current failure position.
@@ -16,17 +17,18 @@ export function resetRightmostFailure() {
16
17
  * @param expected - a human-readable description of what was expected
17
18
  */
18
19
  export function recordFailure(input, expected) {
20
+ const state = getParseState();
19
21
  const source = getInputStr();
20
22
  if (source.length === 0)
21
23
  return;
22
24
  const pos = source.length - input.length;
23
- if (pos > rightmostFailurePos) {
24
- rightmostFailurePos = pos;
25
- rightmostFailureExpected = [expected];
25
+ if (pos > state.rightmostFailurePos) {
26
+ state.rightmostFailurePos = pos;
27
+ state.rightmostFailureExpected = [expected];
26
28
  }
27
- else if (pos === rightmostFailurePos) {
28
- if (!rightmostFailureExpected.includes(expected)) {
29
- rightmostFailureExpected.push(expected);
29
+ else if (pos === state.rightmostFailurePos) {
30
+ if (!state.rightmostFailureExpected.includes(expected)) {
31
+ state.rightmostFailureExpected.push(expected);
30
32
  }
31
33
  }
32
34
  }
@@ -35,16 +37,25 @@ export function recordFailure(input, expected) {
35
37
  * or `null` if no failures have been recorded.
36
38
  */
37
39
  export function getRightmostFailure() {
38
- if (rightmostFailurePos < 0)
40
+ const state = getParseState();
41
+ if (state.rightmostFailurePos < 0)
39
42
  return null;
40
- return { pos: rightmostFailurePos, expected: [...rightmostFailureExpected] };
43
+ return {
44
+ pos: state.rightmostFailurePos,
45
+ expected: [...state.rightmostFailureExpected],
46
+ };
41
47
  }
42
48
  export function saveRightmostFailure() {
43
- return { pos: rightmostFailurePos, expected: [...rightmostFailureExpected] };
49
+ const state = getParseState();
50
+ return {
51
+ pos: state.rightmostFailurePos,
52
+ expected: [...state.rightmostFailureExpected],
53
+ };
44
54
  }
45
55
  export function restoreRightmostFailure(saved) {
46
- rightmostFailurePos = saved.pos;
47
- rightmostFailureExpected = [...saved.expected];
56
+ const state = getParseState();
57
+ state.rightmostFailurePos = saved.pos;
58
+ state.rightmostFailureExpected = [...saved.expected];
48
59
  }
49
60
  function formatExpected(expected) {
50
61
  if (expected.length === 1)
@@ -55,16 +66,34 @@ function formatExpected(expected) {
55
66
  }
56
67
  /**
57
68
  * Formats the rightmost failure into a human-readable error message with line and column info.
69
+ * A committed failure (see the `committed` combinator) takes precedence
70
+ * over the rightmost record — the committed error wins reporting even
71
+ * when some fallback alternative failed deeper into the input.
72
+ *
73
+ * Scope: the preference is per parse state. A commit inside `runNested`
74
+ * lives in the inner state and surfaces through the returned result's
75
+ * `committed` flag, never through the enclosing parse's getErrorMessage.
76
+ * The position math assumes the committed failure's `rest` is a suffix
77
+ * of the CURRENT state's source — don't stash a `runNested` result
78
+ * (inner-coordinate `rest`) into an outer-state commit.
58
79
  * Returns `null` if no failures have been recorded.
59
80
  * Requires `setInputStr` to have been called.
60
81
  */
61
82
  export function getErrorMessage() {
62
- if (rightmostFailurePos < 0)
83
+ const state = getParseState();
84
+ const committed = state.committedFailure;
85
+ if (committed !== null) {
86
+ const source = getInputStr();
87
+ const lineTable = buildLineTable(source);
88
+ const pos = composePosition(state.basePosition, offsetToPosition(lineTable, source.length - committed.rest.length));
89
+ return `Line ${pos.line + 1}, col ${pos.column + 1}: ${committed.message}`;
90
+ }
91
+ if (state.rightmostFailurePos < 0)
63
92
  return null;
64
93
  const source = getInputStr();
65
94
  const lineTable = buildLineTable(source);
66
- const pos = offsetToPosition(lineTable, rightmostFailurePos);
95
+ const pos = composePosition(state.basePosition, offsetToPosition(lineTable, state.rightmostFailurePos));
67
96
  const line = pos.line + 1;
68
97
  const column = pos.column + 1;
69
- return `Line ${line}, col ${column}: expected ${formatExpected(rightmostFailureExpected)}`;
98
+ return `Line ${line}, col ${column}: expected ${formatExpected(state.rightmostFailureExpected)}`;
70
99
  }
@@ -0,0 +1,24 @@
1
+ import { Parser, ParserResult } from "./types.js";
2
+ import type { Position } from "./position.js";
3
+ export type NestedOptions = {
4
+ /** Positions in the sub-parse's results are offset by this, so spans
5
+ * and error messages come out in the ENCLOSING parse's coordinates.
6
+ * Defaults to zero (positions relative to `input`). */
7
+ basePosition?: Position;
8
+ };
9
+ /**
10
+ * Run a complete parse of `input` with its own input string, rightmost-
11
+ * failure record, and memo caches, restoring the enclosing parse's state
12
+ * on exit — success, failure, or throw. The one supported way to run a
13
+ * parse inside another parse.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // While parsing a file, parse an embedded snippet as its own program,
18
+ * // reporting positions relative to the enclosing file:
19
+ * const embedded = runNested(exprParser, snippetText, {
20
+ * basePosition: openingPosition, // where the snippet starts in the file
21
+ * });
22
+ * ```
23
+ */
24
+ export declare function runNested<T>(parser: Parser<T>, input: string, opts?: NestedOptions): ParserResult<T>;
@@ -0,0 +1,28 @@
1
+ import { createParseState, swapParseState } from "./parseState.js";
2
+ /**
3
+ * Run a complete parse of `input` with its own input string, rightmost-
4
+ * failure record, and memo caches, restoring the enclosing parse's state
5
+ * on exit — success, failure, or throw. The one supported way to run a
6
+ * parse inside another parse.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // While parsing a file, parse an embedded snippet as its own program,
11
+ * // reporting positions relative to the enclosing file:
12
+ * const embedded = runNested(exprParser, snippetText, {
13
+ * basePosition: openingPosition, // where the snippet starts in the file
14
+ * });
15
+ * ```
16
+ */
17
+ export function runNested(parser, input, opts) {
18
+ // Deliberately does NOT call setInputStr: createParseState pre-sets
19
+ // inputStr and a fresh rightmost record (replicating setInputStr's
20
+ // reset), and calling it here would clobber the fresh state's fields.
21
+ const savedState = swapParseState(createParseState(input, opts === null || opts === void 0 ? void 0 : opts.basePosition));
22
+ try {
23
+ return parser(input);
24
+ }
25
+ finally {
26
+ swapParseState(savedState);
27
+ }
28
+ }
package/dist/trace.js CHANGED
@@ -2,6 +2,8 @@ import { escape, round, shorten } from "./utils.js";
2
2
  import process from "process";
3
3
  import { execSync } from "child_process";
4
4
  import { resetRightmostFailure } from "./rightmostFailure.js";
5
+ import { getParseState } from "./parseState.js";
6
+ import { composePosition } from "./position.js";
5
7
  const isNode = typeof process !== "undefined" &&
6
8
  process.versions != null &&
7
9
  process.versions.node != null;
@@ -186,7 +188,6 @@ export function limitSteps(limit, callback) {
186
188
  callback();
187
189
  stepLimit = -1;
188
190
  }
189
- let inputStr = "";
190
191
  /**
191
192
  * Use this function in conjunction with the parseError combinator. Before running your parser,
192
193
  * call this function, giving it the entire input. Then, if the parseError combinator throws an error,
@@ -194,11 +195,11 @@ let inputStr = "";
194
195
  * @param s full string to parse
195
196
  */
196
197
  export function setInputStr(s) {
197
- inputStr = s;
198
+ getParseState().inputStr = s;
198
199
  resetRightmostFailure();
199
200
  }
200
201
  export function getInputStr() {
201
- return inputStr;
202
+ return getParseState().inputStr;
202
203
  }
203
204
  export function getDiagnostics(result, input, _message) {
204
205
  const inputStr = getInputStr();
@@ -222,9 +223,14 @@ export function getDiagnostics(result, input, _message) {
222
223
  }
223
224
  const linesIndex = Math.max(0, i - 1);
224
225
  const column = lines[linesIndex].length - (acc - index);
225
- return {
226
+ const composed = composePosition(getParseState().basePosition, {
227
+ offset: index,
226
228
  line: i - 1,
227
229
  column,
230
+ });
231
+ return {
232
+ line: composed.line,
233
+ column: composed.column,
228
234
  length: 1,
229
235
  prettyMessage: messages.join("\n"),
230
236
  message: message,
package/dist/types.d.ts CHANGED
@@ -18,6 +18,11 @@ export type ParserFailure = {
18
18
  success: false;
19
19
  rest: string;
20
20
  message: string;
21
+ /** Set when the failure occurred past a commit point (see `committed`).
22
+ * Backtracking combinators (`or`, `many`, `optional`) propagate a
23
+ * committed failure instead of trying alternatives; lookahead
24
+ * combinators (`not`, `peek`) contain it. */
25
+ committed?: true;
21
26
  };
22
27
  export type ParserResult<T> = ParserSuccess<T> | ParserFailure;
23
28
  export type CaptureParserResult<T, C extends PlainObject> = CaptureParserSuccess<T, C> | ParserFailure;
@@ -50,6 +55,14 @@ export declare function success<T>(result: T, rest: string): ParserSuccess<T>;
50
55
  export declare function captureSuccess<T, C extends PlainObject>(result: T, rest: string, captures: C): CaptureParserSuccess<T, C>;
51
56
  /** Convenience function to return a ParserFailure */
52
57
  export declare function failure(message: string, rest: string): ParserFailure;
58
+ /** Convenience function to return a committed ParserFailure — a failure
59
+ * that stops backtracking. See the `committed` combinator. */
60
+ export declare function committedFailure(message: string, rest: string): ParserFailure;
61
+ /** True when the result is a failure that occurred past a commit point. */
62
+ export declare function isCommittedFailure(result: {
63
+ success: boolean;
64
+ committed?: true;
65
+ }): boolean;
53
66
  /** Prettify an intersected type, to make it easier to read. */
54
67
  export type Prettify<T> = {
55
68
  [K in keyof T]: T[K];
package/dist/types.js CHANGED
@@ -30,3 +30,12 @@ export function captureSuccess(result, rest, captures) {
30
30
  export function failure(message, rest) {
31
31
  return { success: false, message, rest };
32
32
  }
33
+ /** Convenience function to return a committed ParserFailure — a failure
34
+ * that stops backtracking. See the `committed` combinator. */
35
+ export function committedFailure(message, rest) {
36
+ return { success: false, message, rest, committed: true };
37
+ }
38
+ /** True when the result is a failure that occurred past a commit point. */
39
+ export function isCommittedFailure(result) {
40
+ return !result.success && result.committed === true;
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tarsec",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "A parser combinator library for TypeScript, inspired by Parsec.",
5
5
  "homepage": "https://github.com/egonSchiele/tarsec",
6
6
  "scripts": {