Skip to content

Commit 4726887

Browse files
authored
Merge pull request #1 from mkantor/optimizations
Optimize constructors & combinators
2 parents af6769e + f3c2f00 commit 4726887

File tree

3 files changed

+123
-120
lines changed

3 files changed

+123
-120
lines changed

src/combinators.ts

Lines changed: 107 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
1-
import type { Either } from '@matt.kantor/either'
1+
import type { Either, Right } from '@matt.kantor/either'
22
import * as either from '@matt.kantor/either'
33
import { nothing } from './constructors.js'
44
import type {
55
InvalidInputError,
66
Parser,
7+
ParserResult,
78
ParserWhichAlwaysSucceeds,
89
Success,
910
} from './parser.js'
1011

1112
/**
1213
* Substitute the output of a successful parse.
1314
*/
14-
export const as =
15-
<NewOutput>(
16-
parser: Parser<unknown>,
17-
newOutput: NewOutput,
18-
): Parser<NewOutput> =>
19-
input =>
20-
either.map(parser(input), success => ({
21-
output: newOutput,
22-
remainingInput: success.remainingInput,
23-
}))
15+
export const as = <NewOutput>(
16+
parser: Parser<unknown>,
17+
newOutput: NewOutput,
18+
): Parser<NewOutput> => {
19+
const replaceOutput = (success: Success<unknown>) => ({
20+
output: newOutput,
21+
remainingInput: success.remainingInput,
22+
})
23+
return input => either.map(parser(input), replaceOutput)
24+
}
2425

2526
/**
2627
* Attempt to parse input with `parser`. If successful, ensure the same input
@@ -31,39 +32,40 @@ export const as =
3132
* butNot(anySingleCharacter, literal('a'), 'the letter a') // parses any character besides 'a'
3233
* ```
3334
*/
34-
export const butNot =
35-
<Output>(
36-
parser: Parser<Output>,
37-
not: Parser<unknown>,
38-
notName: string,
39-
): Parser<Output> =>
40-
input =>
35+
export const butNot = <Output>(
36+
parser: Parser<Output>,
37+
not: Parser<unknown>,
38+
notName: string,
39+
): Parser<Output> => {
40+
const errorMessage = `input was unexpectedly ${notName}`
41+
return input =>
4142
either.flatMap(parser(input), success => {
4243
const notResult = not(input)
4344
if (!either.isLeft(notResult)) {
4445
return either.makeLeft({
4546
input,
46-
message: `input was unexpectedly ${notName}`,
47+
message: errorMessage,
4748
})
4849
} else {
4950
return either.makeRight(success)
5051
}
5152
})
53+
}
5254

5355
/**
5456
* Map the output of `parser` to another `Parser` which is then applied to the
5557
* remaining input, flattening the parse results.
5658
*/
57-
export const flatMap =
58-
<Output, NewOutput>(
59-
parser: Parser<Output>,
60-
f: (output: Output) => Parser<NewOutput>,
61-
): Parser<NewOutput> =>
62-
input =>
63-
either.flatMap(parser(input), success => {
64-
const nextParser = f(success.output)
65-
return nextParser(success.remainingInput)
66-
})
59+
export const flatMap = <Output, NewOutput>(
60+
parser: Parser<Output>,
61+
f: (output: Output) => Parser<NewOutput>,
62+
): Parser<NewOutput> => {
63+
const applyF = (success: Success<Output>) => {
64+
const nextParser = f(success.output)
65+
return nextParser(success.remainingInput)
66+
}
67+
return input => either.flatMap(parser(input), applyF)
68+
}
6769

6870
/**
6971
* Create a `Parser` from a thunk. This can be useful for recursive parsers.
@@ -82,60 +84,61 @@ export const lazy =
8284
* lookaheadNot(anySingleCharacter, literal('a'), 'the letter a') // parses the first character of 'ab', but not 'aa'
8385
* ```
8486
*/
85-
export const lookaheadNot =
86-
<Output>(
87-
parser: Parser<Output>,
88-
notFollowedBy: Parser<unknown>,
89-
followedByName: string,
90-
): Parser<Output> =>
91-
input =>
87+
export const lookaheadNot = <Output>(
88+
parser: Parser<Output>,
89+
notFollowedBy: Parser<unknown>,
90+
followedByName: string,
91+
): Parser<Output> => {
92+
const errorMessage = `input was unexpectedly followed by ${followedByName}`
93+
return input =>
9294
either.flatMap(parser(input), success =>
9395
either.match(notFollowedBy(success.remainingInput), {
9496
left: _ => either.makeRight(success),
9597
right: _ =>
9698
either.makeLeft({
9799
input,
98-
message: `input was unexpectedly followed by ${followedByName}`,
100+
message: errorMessage,
99101
}),
100102
}),
101103
)
104+
}
102105

103106
/**
104107
* Map the output of `parser` to new output.
105108
*/
106-
export const map =
107-
<Output, NewOutput>(
108-
parser: Parser<Output>,
109-
f: (output: Output) => NewOutput,
110-
): Parser<NewOutput> =>
111-
input =>
112-
either.map(parser(input), success => ({
113-
output: f(success.output),
114-
remainingInput: success.remainingInput,
115-
}))
109+
export const map = <Output, NewOutput>(
110+
parser: Parser<Output>,
111+
f: (output: Output) => NewOutput,
112+
): Parser<NewOutput> => {
113+
const applyF = (success: Success<Output>) => ({
114+
output: f(success.output),
115+
remainingInput: success.remainingInput,
116+
})
117+
return input => either.map(parser(input), applyF)
118+
}
116119

117120
/**
118121
* Apply the given `parsers` to the same input until one succeeds or all fail.
119122
*/
120-
export const oneOf =
121-
<
122-
Parsers extends readonly [
123-
Parser<unknown>,
124-
Parser<unknown>,
125-
...(readonly Parser<unknown>[]),
126-
],
127-
>(
128-
parsers: Parsers,
129-
): Parser<OneOfOutput<Parsers>> =>
130-
input =>
131-
parsers.reduce(
123+
export const oneOf = <
124+
Parsers extends readonly [
125+
Parser<unknown>,
126+
Parser<unknown>,
127+
...(readonly Parser<unknown>[]),
128+
],
129+
>(
130+
parsers: Parsers,
131+
): Parser<OneOfOutput<Parsers>> => {
132+
const [firstParser, ...otherParsers] = parsers
133+
return input => {
134+
const firstResult = firstParser(input)
135+
return otherParsers.reduce(
132136
(result: ReturnType<Parser<OneOfOutput<Parsers>>>, parser) =>
133-
either.match(result, {
134-
right: either.makeRight,
135-
left: _ => parser(input),
136-
}),
137-
either.makeLeft({ input, message: '' }), // `parsers` is non-empty so this is never returned
137+
either.isLeft(result) ? parser(input) : result,
138+
firstResult,
138139
)
140+
}
141+
}
139142
type OneOfOutput<Parsers extends readonly Parser<unknown>[]> = {
140143
[Index in keyof Parsers]: OutputOf<Parsers[Index]>
141144
}[number]
@@ -162,62 +165,55 @@ export const sequence =
162165
>(
163166
parsers: Parsers,
164167
): Parser<SequenceOutput<Parsers>> =>
165-
input =>
166-
either.map(
167-
parsers.reduce(
168-
(
169-
results: ReturnType<
170-
Parser<readonly SequenceOutput<Parsers>[number][]>
171-
>,
172-
parser,
173-
) =>
174-
either.match(results, {
175-
right: successes =>
176-
either.map(parser(successes.remainingInput), newSuccess => ({
177-
remainingInput: newSuccess.remainingInput,
178-
output: [...successes.output, newSuccess.output],
179-
})),
180-
left: either.makeLeft,
181-
}),
182-
either.makeRight({ remainingInput: input, output: [] }), // `parsers` is non-empty so this is never returned
183-
),
184-
({ output, remainingInput }) => ({
185-
// The above `reduce` callback constructs `output` such that its
186-
// elements align with `Parsers`, but TypeScript doesn't know that.
187-
output: output as SequenceOutput<Parsers>,
188-
remainingInput,
189-
}),
168+
input => {
169+
const parseResult = parsers.reduce(
170+
(
171+
results: ReturnType<Parser<readonly SequenceOutput<Parsers>[number][]>>,
172+
parser,
173+
) =>
174+
either.isRight(results)
175+
? either.map(parser(results.value.remainingInput), newSuccess => ({
176+
remainingInput: newSuccess.remainingInput,
177+
output: [...results.value.output, newSuccess.output],
178+
}))
179+
: results,
180+
either.makeRight({ remainingInput: input, output: [] }), // `parsers` is non-empty so this is never returned
190181
)
182+
// The above `reduce` callback constructs `output` such that its
183+
// elements align with `Parsers`, but TypeScript doesn't know that.
184+
return parseResult as ParserResult<SequenceOutput<Parsers>>
185+
}
191186
type SequenceOutput<Parsers extends readonly Parser<unknown>[]> = {
192187
[Index in keyof Parsers]: OutputOf<Parsers[Index]>
193188
}
194189

195190
/**
196191
* Refine/transform the output of `parser` via a function which may fail.
197192
*/
198-
export const transformOutput =
199-
<Output, NewOutput>(
200-
parser: Parser<Output>,
201-
f: (output: Output) => Either<InvalidInputError, NewOutput>,
202-
): Parser<NewOutput> =>
203-
input =>
204-
either.flatMap(parser(input), success =>
205-
either.map(f(success.output), output => ({
206-
output,
207-
remainingInput: success.remainingInput,
208-
})),
209-
)
193+
export const transformOutput = <Output, NewOutput>(
194+
parser: Parser<Output>,
195+
f: (output: Output) => Either<InvalidInputError, NewOutput>,
196+
): Parser<NewOutput> => {
197+
const transformation = (success: Success<Output>) =>
198+
either.map(f(success.output), output => ({
199+
output,
200+
remainingInput: success.remainingInput,
201+
}))
202+
return input => either.flatMap(parser(input), transformation)
203+
}
210204

211205
/**
212206
* Repeatedly apply `parser` to the input as long as it keeps succeeding.
213207
* Outputs are collected in an array.
214208
*/
215-
export const zeroOrMore =
216-
<Output>(
217-
parser: Parser<Output>,
218-
): ParserWhichAlwaysSucceeds<readonly Output[]> =>
219-
input => {
220-
const result = oneOf([parser, nothing])(input)
209+
export const zeroOrMore = <Output>(
210+
parser: Parser<Output>,
211+
): ParserWhichAlwaysSucceeds<readonly Output[]> => {
212+
const parserOrNothing = oneOf([parser, nothing])
213+
214+
// Give this a name so it can be recursively referenced.
215+
const thisParser = (input: string): Right<Success<readonly Output[]>> => {
216+
const result = parserOrNothing(input)
221217
const success = either.match(result, {
222218
left: _ => ({
223219
output: [],
@@ -230,7 +226,7 @@ export const zeroOrMore =
230226
remainingInput: lastSuccess.remainingInput,
231227
}
232228
} else {
233-
const nextResult = zeroOrMore(parser)(lastSuccess.remainingInput)
229+
const nextResult = thisParser(lastSuccess.remainingInput)
234230
return {
235231
output: [lastSuccess.output, ...nextResult.value.output],
236232
remainingInput: nextResult.value.remainingInput,
@@ -241,6 +237,9 @@ export const zeroOrMore =
241237
return either.makeRight(success)
242238
}
243239

240+
return thisParser
241+
}
242+
244243
type OutputOf<SpecificParser extends Parser<unknown>> = Extract<
245244
ReturnType<SpecificParser>['value'],
246245
Success<unknown>

src/constructors.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,33 @@ export const anySingleCharacter: Parser<string> = input => {
1717
}
1818
}
1919

20-
export const literal =
21-
<Text extends string>(text: Text): Parser<Text> =>
22-
input =>
20+
export const literal = <Text extends string>(text: Text): Parser<Text> => {
21+
const errorMessage = `input did not begin with "${text}"`
22+
return input =>
2323
input.startsWith(text)
2424
? either.makeRight({
2525
remainingInput: input.slice(text.length),
2626
output: text,
2727
})
2828
: either.makeLeft({
2929
input,
30-
message: `input did not begin with "${text}"`,
30+
message: errorMessage,
3131
})
32+
}
3233

3334
export const nothing: ParserWhichAlwaysSucceeds<undefined> = input =>
3435
either.makeRight({
3536
remainingInput: input,
3637
output: undefined,
3738
})
3839

39-
export const regularExpression =
40-
(pattern: RegExp): Parser<string> =>
41-
input => {
42-
const match = pattern.exec(input)
43-
return match === null || match.index !== 0
40+
export const regularExpression = (pattern: RegExp): Parser<string> => {
41+
const patternAnchoredToStartOfString = pattern.source.startsWith('^')
42+
? pattern
43+
: new RegExp(`^${pattern.source}`, pattern.flags)
44+
return input => {
45+
const match = patternAnchoredToStartOfString.exec(input)
46+
return match === null
4447
? either.makeLeft({
4548
input,
4649
message: 'input did not match regular expression',
@@ -50,3 +53,4 @@ export const regularExpression =
5053
output: match[0],
5154
})
5255
}
56+
}

src/parser.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ export type InvalidInputError = {
66
readonly message: string
77
}
88

9-
export type Parser<Output> = (
10-
input: string,
11-
) => Either<InvalidInputError, Success<Output>>
9+
export type Parser<Output> = (input: string) => ParserResult<Output>
1210

1311
export type ParserWhichAlwaysSucceeds<Output> = (
1412
input: string,
1513
) => Right<Success<Output>>
1614

15+
export type ParserResult<Output> = Either<InvalidInputError, Success<Output>>
16+
1717
export type Success<Output> = {
1818
readonly remainingInput: string
1919
readonly output: Output

0 commit comments

Comments
 (0)