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.
- package/dist/combinators.d.ts +87 -1
- package/dist/combinators.js +240 -15
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/parseState.d.ts +25 -0
- package/dist/parseState.js +21 -0
- package/dist/parsers/within.js +5 -0
- package/dist/parsers.d.ts +13 -2
- package/dist/parsers.js +29 -3
- package/dist/position.d.ts +10 -0
- package/dist/position.js +38 -8
- package/dist/rightmostFailure.d.ts +10 -0
- package/dist/rightmostFailure.js +48 -19
- package/dist/runNested.d.ts +24 -0
- package/dist/runNested.js +28 -0
- package/dist/trace.js +10 -4
- package/dist/types.d.ts +13 -0
- package/dist/types.js +9 -0
- package/package.json +1 -1
package/dist/combinators.d.ts
CHANGED
|
@@ -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
|
|
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
|
/**
|
package/dist/combinators.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
940
|
-
//
|
|
941
|
-
// results derived from
|
|
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
|
|
944
|
-
//
|
|
945
|
-
|
|
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
|
|
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
|
-
|
|
953
|
-
cache.clear();
|
|
1173
|
+
getParseState().memoCaches.clear();
|
|
954
1174
|
}
|
|
955
1175
|
export function memo(name, parser) {
|
|
956
|
-
const
|
|
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
package/dist/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/parsers/within.js
CHANGED
|
@@ -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
|
-
*
|
|
142
|
-
*
|
|
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
|
-
*
|
|
245
|
-
*
|
|
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);
|
package/dist/position.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
58
|
-
|
|
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 =
|
|
93
|
+
const source = getParseState().inputStr;
|
|
66
94
|
const offset = source.length - input.length;
|
|
67
95
|
const lineTable = getLineTable(source);
|
|
68
|
-
|
|
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 =
|
|
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
|
*/
|
package/dist/rightmostFailure.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { getInputStr } from "./trace.js";
|
|
2
|
-
import { buildLineTable, offsetToPosition } from "./position.js";
|
|
3
|
-
|
|
4
|
-
let rightmostFailureExpected = [];
|
|
2
|
+
import { buildLineTable, offsetToPosition, composePosition } from "./position.js";
|
|
3
|
+
import { getParseState } from "./parseState.js";
|
|
5
4
|
export function resetRightmostFailure() {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
40
|
+
const state = getParseState();
|
|
41
|
+
if (state.rightmostFailurePos < 0)
|
|
39
42
|
return null;
|
|
40
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
pos: state.rightmostFailurePos,
|
|
45
|
+
expected: [...state.rightmostFailureExpected],
|
|
46
|
+
};
|
|
41
47
|
}
|
|
42
48
|
export function saveRightmostFailure() {
|
|
43
|
-
|
|
49
|
+
const state = getParseState();
|
|
50
|
+
return {
|
|
51
|
+
pos: state.rightmostFailurePos,
|
|
52
|
+
expected: [...state.rightmostFailureExpected],
|
|
53
|
+
};
|
|
44
54
|
}
|
|
45
55
|
export function restoreRightmostFailure(saved) {
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|