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/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.4.7",
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": {
@@ -24,6 +24,11 @@
24
24
  "import": "./dist/parsers/markdown/index.js",
25
25
  "require": "./dist/parsers/markdown/index.js",
26
26
  "types": "./dist/parsers/markdown/index.d.ts"
27
+ },
28
+ "./parsers/bash": {
29
+ "import": "./dist/parsers/bash/index.js",
30
+ "require": "./dist/parsers/bash/index.js",
31
+ "types": "./dist/parsers/bash/index.d.ts"
27
32
  }
28
33
  },
29
34
  "type": "module",