Skip to content

Commit 116c71e

Browse files
authored
Merge pull request #19 from mkantor/furthest-failure
Remember the furthest failure when backtracking
2 parents 25deea6 + 0410251 commit 116c71e

5 files changed

Lines changed: 277 additions & 53 deletions

File tree

‎src/combinators.ts‎

Lines changed: 110 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import * as either from '@matt.kantor/either'
22
import type {
3+
InvalidInputError,
34
Note,
45
Parser,
56
ParserResult,
67
ParserWhichAlwaysSucceeds,
78
Success,
89
} from './parser.js'
10+
import {
11+
deduplicateNotes,
12+
furthest,
13+
furthestOrUndefined,
14+
messageForExpectations,
15+
} from './internal.js'
916

1017
/**
1118
* Substitute the output of a successful parse.
@@ -17,6 +24,7 @@ export const as = <NewOutput>(
1724
const replaceOutput = (success: Success<unknown>) => ({
1825
output: newOutput,
1926
offset: success.offset,
27+
furthestFailure: success.furthestFailure,
2028
})
2129
return (input, offset = 0n) =>
2230
either.map(parser(input, offset), replaceOutput)
@@ -42,13 +50,18 @@ export const butNot = <Output>(
4250
either.flatMap(parser(input, offset), success => {
4351
const notResult = not(input, offset)
4452
if (!either.isLeft(notResult)) {
45-
return either.makeLeft({
53+
const newError = {
4654
source: input,
4755
offset,
4856
message: errorMessage,
4957
expected,
5058
notes: [],
51-
})
59+
}
60+
return either.makeLeft(
61+
success.furthestFailure === undefined
62+
? newError
63+
: furthest(success.furthestFailure, newError),
64+
)
5265
} else {
5366
return either.makeRight(success)
5467
}
@@ -66,7 +79,9 @@ export const flatMap =
6679
): Parser<NewOutput> =>
6780
(input, offset = 0n) =>
6881
either.flatMap(parser(input, offset), success =>
69-
f(success.output)(input, success.offset),
82+
withPotentiallyFurtherFailure(success.furthestFailure)(
83+
f(success.output)(input, success.offset),
84+
),
7085
)
7186

7287
/**
@@ -97,14 +112,20 @@ export const lookaheadNot = <Output>(
97112
either.flatMap(parser(input, offset), success =>
98113
either.match(notFollowedBy(input, success.offset), {
99114
left: _ => either.makeRight(success),
100-
right: _ =>
101-
either.makeLeft({
115+
right: _ => {
116+
const newError = {
102117
source: input,
103118
offset: success.offset,
104119
message: errorMessage,
105120
expected,
106121
notes: [],
107-
}),
122+
}
123+
return either.makeLeft(
124+
success.furthestFailure === undefined
125+
? newError
126+
: furthest(success.furthestFailure, newError),
127+
)
128+
},
108129
}),
109130
)
110131
}
@@ -119,6 +140,7 @@ export const map = <Output, NewOutput>(
119140
const applyF = (success: Success<Output>) => ({
120141
output: f(success.output),
121142
offset: success.offset,
143+
furthestFailure: success.furthestFailure,
122144
})
123145
return (input, offset = 0n) => either.map(parser(input, offset), applyF)
124146
}
@@ -160,38 +182,32 @@ export const oneOf =
160182
}
161183
} else {
162184
// Success!
163-
return result
185+
return mutableFurthestOffset < 0n
186+
? result
187+
: either.makeRight({
188+
...result.value,
189+
furthestFailure: furthestOrUndefined(
190+
result.value.furthestFailure,
191+
failureAt(
192+
input,
193+
mutableFurthestOffset,
194+
mutableFurthestExpectations,
195+
mutableFurthestNotes,
196+
),
197+
),
198+
})
164199
}
165200
}
166201

167-
const expected = new Set(mutableFurthestExpectations)
168-
169-
const [onlyExpectation, ...otherExpectations] = expected
170-
const message =
171-
onlyExpectation === undefined
172-
? 'unexpected input'
173-
: otherExpectations.length === 0
174-
? `expected ${onlyExpectation}`
175-
: `expected one of: ${[...expected].join(', ')}`
176-
177-
// Deduplicate notes.
178-
const notes = mutableFurthestNotes.filter(
179-
(note, index) =>
180-
mutableFurthestNotes.findIndex(
181-
otherNote =>
182-
otherNote.offset === note.offset &&
183-
otherNote.message === note.message,
184-
) === index,
185-
)
186-
187202
// If we haven't already returned then parsing failed.
188-
return either.makeLeft({
189-
source: input,
190-
offset: mutableFurthestOffset,
191-
message,
192-
expected,
193-
notes,
194-
})
203+
return either.makeLeft(
204+
failureAt(
205+
input,
206+
mutableFurthestOffset,
207+
mutableFurthestExpectations,
208+
mutableFurthestNotes,
209+
),
210+
)
195211
}
196212
type OneOfOutput<Parsers extends readonly Parser<unknown>[]> = {
197213
[Index in keyof Parsers]: OutputOf<Parsers[Index]>
@@ -226,12 +242,18 @@ export const sequence =
226242
parser,
227243
) =>
228244
either.isRight(results)
229-
? either.map(parser(input, results.value.offset), newSuccess => ({
230-
offset: newSuccess.offset,
231-
output: [...results.value.output, newSuccess.output],
232-
}))
245+
? either.map(
246+
withPotentiallyFurtherFailure(results.value.furthestFailure)(
247+
parser(input, results.value.offset),
248+
),
249+
newSuccess => ({
250+
offset: newSuccess.offset,
251+
output: [...results.value.output, newSuccess.output],
252+
furthestFailure: newSuccess.furthestFailure,
253+
}),
254+
)
233255
: results,
234-
either.makeRight({ offset, output: [] }), // `parsers` is non-empty so this is never returned
256+
either.makeRight({ offset, output: [], furthestFailure: undefined }), // `parsers` is non-empty so this is never returned
235257
)
236258
// The above `reduce` callback constructs `output` such that its
237259
// elements align with `Parsers`, but TypeScript doesn't know that.
@@ -250,17 +272,64 @@ export const zeroOrMore =
250272
// Uses a loop rather than recursion to avoid stack overflow.
251273
(input, offset = 0n) => {
252274
const output: Output[] = []
253-
const mutableState = { output, offset }
275+
const mutableState: {
276+
output: Output[]
277+
offset: bigint
278+
furthestFailure: InvalidInputError | undefined
279+
} = { output, offset, furthestFailure: undefined }
254280

255281
let result = parser(input, mutableState.offset)
256282
while (either.isRight(result)) {
257283
mutableState.output.push(result.value.output)
258284
mutableState.offset = result.value.offset
285+
mutableState.furthestFailure = furthestOrUndefined(
286+
mutableState.furthestFailure,
287+
result.value.furthestFailure,
288+
)
259289
result = parser(input, mutableState.offset)
260290
}
261291

262-
return either.makeRight(mutableState)
292+
return either.makeRight({
293+
...mutableState,
294+
furthestFailure: furthestOrUndefined(
295+
mutableState.furthestFailure,
296+
result.value,
297+
),
298+
})
299+
}
300+
301+
const failureAt = (
302+
input: string,
303+
offset: bigint,
304+
expectations: readonly string[],
305+
notes: readonly Note[],
306+
): InvalidInputError => {
307+
const expected = new Set(expectations)
308+
return {
309+
source: input,
310+
offset,
311+
message: messageForExpectations(expected),
312+
expected,
313+
notes: deduplicateNotes(notes),
263314
}
315+
}
316+
317+
const withPotentiallyFurtherFailure =
318+
(carried: InvalidInputError | undefined) =>
319+
<Output>(result: ParserResult<Output>): ParserResult<Output> =>
320+
carried === undefined
321+
? result
322+
: either.match(result, {
323+
left: error => either.makeLeft(furthest(carried, error)),
324+
right: success =>
325+
either.makeRight({
326+
...success,
327+
furthestFailure: furthestOrUndefined(
328+
carried,
329+
success.furthestFailure,
330+
),
331+
}),
332+
})
264333

265334
type OutputOf<SpecificParser extends Parser<unknown>> = Extract<
266335
ReturnType<SpecificParser>['value'],

‎src/constructors.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const anySingleCharacter: Parser<string> = (input, offset = 0n) => {
1616
return either.makeRight({
1717
output: firstCharacter,
1818
offset: offset + BigInt(firstCharacter.length),
19+
furthestFailure: undefined,
1920
})
2021
}
2122
}
@@ -28,6 +29,7 @@ export const literal = <Text extends string>(text: Text): Parser<Text> => {
2829
? either.makeRight({
2930
output: text,
3031
offset: offset + BigInt(text.length),
32+
furthestFailure: undefined,
3133
})
3234
: either.makeLeft({
3335
source: input,
@@ -45,6 +47,7 @@ export const nothing: ParserWhichAlwaysSucceeds<undefined> = (
4547
either.makeRight({
4648
output: undefined,
4749
offset,
50+
furthestFailure: undefined,
4851
})
4952

5053
export const regularExpression = (pattern: RegExp): Parser<string> => {
@@ -69,6 +72,7 @@ export const regularExpression = (pattern: RegExp): Parser<string> => {
6972
: either.makeRight({
7073
output: match[0],
7174
offset: offset + BigInt(match[0].length),
75+
furthestFailure: undefined,
7276
})
7377
}
7478
}

‎src/internal.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { InvalidInputError, Note } from './parser.js'
2+
3+
export const messageForExpectations = (
4+
expected: ReadonlySet<string>,
5+
): string => {
6+
const [onlyExpectation, ...otherExpectations] = expected
7+
return onlyExpectation === undefined
8+
? 'unexpected input'
9+
: otherExpectations.length === 0
10+
? `expected ${onlyExpectation}`
11+
: `expected one of: ${[...expected].join(', ')}`
12+
}
13+
14+
/**
15+
* The furthest of two failures, or a merged error when they tie.
16+
*/
17+
export const furthest = (
18+
first: InvalidInputError,
19+
second: InvalidInputError,
20+
): InvalidInputError =>
21+
first.offset > second.offset
22+
? first
23+
: second.offset > first.offset
24+
? second
25+
: mergeErrors(first, second)
26+
27+
export const furthestOrUndefined = (
28+
first: InvalidInputError | undefined,
29+
second: InvalidInputError | undefined,
30+
): InvalidInputError | undefined =>
31+
second === undefined
32+
? first
33+
: first === undefined
34+
? second
35+
: furthest(first, second)
36+
37+
export const deduplicateNotes = (notes: readonly Note[]): readonly Note[] =>
38+
notes.filter(
39+
(note, index) =>
40+
notes.findIndex(
41+
otherNote =>
42+
otherNote.offset === note.offset &&
43+
otherNote.message === note.message,
44+
) === index,
45+
)
46+
47+
const mergeErrors = (
48+
first: InvalidInputError,
49+
second: InvalidInputError,
50+
): InvalidInputError => {
51+
const expected = new Set([...first.expected, ...second.expected])
52+
return {
53+
source: first.source,
54+
offset: first.offset,
55+
message: messageForExpectations(expected),
56+
expected,
57+
notes:
58+
first.notes.length === 0
59+
? second.notes
60+
: second.notes.length === 0
61+
? first.notes
62+
: deduplicateNotes([...first.notes, ...second.notes]),
63+
}
64+
}

‎src/parser.ts‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Either, Right } from '@matt.kantor/either'
22
import * as either from '@matt.kantor/either'
3+
import { furthest } from './internal.js'
34

45
export type InvalidInputError = {
56
readonly source: string
@@ -33,6 +34,10 @@ export type ParserResult<Output> = Either<InvalidInputError, Success<Output>>
3334
export type Success<Output> = {
3435
readonly offset: bigint
3536
readonly output: Output
37+
/**
38+
* The furthest failure encountered while producing this success, if any.
39+
*/
40+
readonly furthestFailure: InvalidInputError | undefined
3641
}
3742

3843
/**
@@ -46,14 +51,19 @@ export const parse = <Output>(
4651
parser: Parser<Output>,
4752
input: string,
4853
): Either<InvalidInputError, Output> =>
49-
either.flatMap(parser(input, 0n), ({ output, offset }) =>
50-
Number(offset) !== input.length
51-
? either.makeLeft({
52-
source: input,
53-
offset,
54-
message: 'excess content followed valid input',
55-
expected: new Set(['end of input']),
56-
notes: [],
57-
})
58-
: either.makeRight(output),
59-
)
54+
either.flatMap(parser(input, 0n), ({ output, offset, furthestFailure }) => {
55+
const excessContent: InvalidInputError = {
56+
source: input,
57+
offset,
58+
message: 'excess content followed valid input',
59+
expected: new Set(['end of input']),
60+
notes: [],
61+
}
62+
return Number(offset) !== input.length
63+
? either.makeLeft(
64+
furthestFailure === undefined
65+
? excessContent
66+
: furthest(furthestFailure, excessContent),
67+
)
68+
: either.makeRight(output)
69+
})

0 commit comments

Comments
 (0)