tarsec 0.4.7 → 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.
package/README.md CHANGED
@@ -36,6 +36,7 @@ parser("hello there"); // failure
36
36
  ## Learning tarsec
37
37
  - [A five minute introduction](/tutorials/5-minute-intro.md)
38
38
  - [The three building blocks in tarsec](/tutorials/three-building-blocks.md)
39
+ - [Lexemes: your grammar's token vocabulary](/tutorials/lexemes.md)
39
40
  - [API reference](https://egonschiele.github.io/tarsec/)
40
41
 
41
42
  ## Features
@@ -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
@@ -5,3 +5,6 @@ export * from "./types.js";
5
5
  export * from "./tarsecError.js";
6
6
  export * from "./position.js";
7
7
  export * from "./rightmostFailure.js";
8
+ export * from "./lexeme.js";
9
+ export * from "./parseState.js";
10
+ export * from "./runNested.js";
package/dist/index.js CHANGED
@@ -5,3 +5,6 @@ export * from "./types.js";
5
5
  export * from "./tarsecError.js";
6
6
  export * from "./position.js";
7
7
  export * from "./rightmostFailure.js";
8
+ export * from "./lexeme.js";
9
+ export * from "./parseState.js";
10
+ export * from "./runNested.js";
@@ -0,0 +1,71 @@
1
+ import { CharPredicate } from "./parsers.js";
2
+ import { CaptureParser, Parser, PlainObject } from "./types.js";
3
+ /** Configuration for `makeLexemes`. */
4
+ export type LexemeConfig = {
5
+ /** Characters eaten after every lexeme, e.g. " \t". */
6
+ whitespace: string;
7
+ /** Line-comment marker (e.g. "#" or "//"); comments are eaten as whitespace,
8
+ * up to but NOT including the newline (newlines may be significant). */
9
+ lineComment?: string;
10
+ /** When true, `\` followed by a newline is also eaten as whitespace. */
11
+ lineContinuation?: boolean;
12
+ /** Charset or predicate for an identifier's first character.
13
+ * Defaults to letters and "_". */
14
+ identStart?: string | CharPredicate;
15
+ /** Charset or predicate for subsequent identifier characters.
16
+ * Defaults to letters, digits, and "_". */
17
+ identRest?: string | CharPredicate;
18
+ /** Words `identifier` refuses to match (and `keyword` accepts). */
19
+ keywords?: string[];
20
+ /** Charset or predicate for characters that can appear in operators.
21
+ * `operator` uses it for longest-match rejection: `operator("<")` refuses
22
+ * to match the front of `<=`. Defaults to common symbol characters. */
23
+ operatorChars?: string | CharPredicate;
24
+ };
25
+ export type Lexemes = {
26
+ /** Whitespace/comment skipper as a parser. Always succeeds. Use once at the
27
+ * top of a grammar to eat leading whitespace; lexemes handle the rest. */
28
+ whitespace: Parser<null>;
29
+ /** Whitespace/comment skipper as a total function: returns the input with
30
+ * leading whitespace removed. Handy where a parser result would force
31
+ * callers to handle a failure that cannot happen. */
32
+ skipWhitespace: (input: string) => string;
33
+ /** Run a parser, then eat trailing whitespace/comments. Capture-preserving. */
34
+ lexeme: {
35
+ <T, C extends PlainObject>(parser: CaptureParser<T, C>): CaptureParser<T, C>;
36
+ <T>(parser: Parser<T>): Parser<T>;
37
+ };
38
+ /** `lexeme(str(s))` — match a literal, eat trailing whitespace. */
39
+ symbol: <const S extends string>(s: S) => Parser<S>;
40
+ /** Match an identifier (per identStart/identRest), rejecting keywords. */
41
+ identifier: Parser<string>;
42
+ /** Match `s` only when not followed by an identRest character, so
43
+ * `keyword("if")` rejects "ifx". */
44
+ keyword: (s: string) => Parser<string>;
45
+ /** Match `s` only when not followed by an operator character, so
46
+ * `operator("<")` rejects the front of "<=". The symbolic counterpart
47
+ * of `keyword`. */
48
+ operator: (s: string) => Parser<string>;
49
+ /** Run a parser between "(" and ")", whitespace handled. Returns the
50
+ * inner result. */
51
+ parens: <T>(parser: Parser<T>) => Parser<T>;
52
+ /** Like `parens`, with "[" and "]". */
53
+ brackets: <T>(parser: Parser<T>) => Parser<T>;
54
+ /** Like `parens`, with "{" and "}". */
55
+ braces: <T>(parser: Parser<T>) => Parser<T>;
56
+ /** Zero or more of `parser`, separated by commas. */
57
+ commaSep: <T>(parser: Parser<T>) => Parser<T[]>;
58
+ /** One or more of `parser`, separated by commas. */
59
+ commaSep1: <T>(parser: Parser<T>) => Parser<T[]>;
60
+ };
61
+ /**
62
+ * Build a set of lexeme helpers: ordinary parsers that handle whitespace,
63
+ * comments, and keywords in one place. This is a *scannerless* lexeme layer
64
+ * (like Parsec's `makeTokenParser`), not a lexer — there is no separate pass
65
+ * and no token array.
66
+ *
67
+ * The discipline: every token-shaped parser eats its own *trailing*
68
+ * whitespace; eat *leading* whitespace once at the top with `whitespace`
69
+ * (or `skipWhitespace`).
70
+ */
71
+ export declare function makeLexemes(config: LexemeConfig): Lexemes;