Skip to content

parse

Terminal window
bun add @stopcock/parse

Parser combinators. Parsers are just values, so you compose them with seq, alt, map like anything else.

Types flow through composition: seq(integer, string('='), integer) infers [number, string, number]. Failed parses tell you what was expected and where. lazy(() => parser) handles recursive grammars.

import { string, digit, many1, seq, map, run } from '@stopcock/parse'
const number = map(many1(digit), chars => parseInt(chars.join(''), 10))
const assignment = seq(identifier, string(' = '), number)
run(assignment, 'x = 42')
// { success: true, value: ['x', ' = ', 42], rest: '' }
// Regex: fragile, no error messages, unreadable at scale
const match = input.match(/^([a-z]+)\s*=\s*(\d+)/)
if (!match) throw new Error('parse failed')
const [, key, value] = match
// Parser combinator: composable, typed, clear error reporting
const assignment = pipe(
seq(identifier, many(whitespace), string('='), many(whitespace), integer),
map(([key, , , , value]) => ({ key, value })),
)
run(assignment, 'count = 42')
// { success: true, value: { key: 'count', value: 42 } }
const quoted = between(char('"'), char('"'), regex(/[^"]*/))
const unquoted = regex(/[^,\n]*/)
const field = alt(quoted, unquoted)
const csvLine = sepBy(field, char(','))
run(csvLine, 'Alice,"New York",30')
// { success: true, value: ['Alice', 'New York', '30'] }
const comment = seq(char('#'), regex(/[^\n]*/))
const key = regex(/[a-zA-Z_][a-zA-Z0-9_]*/)
const value = regex(/[^\n]+/)
const entry = pipe(
seq(key, many(whitespace), char('='), many(whitespace), value),
map(([k, , , , v]) => [k, v.trim()] as const),
)
const config = many(alt(
pipe(comment, map(() => null)),
entry,
))
run(config, 'host = localhost\nport = 3000\n# comment')
const jsonValue: Parser<unknown> = lazy(() => alt(
map(quotedString, s => s),
float,
map(string('true'), () => true),
map(string('false'), () => false),
map(string('null'), () => null),
jsonArray,
jsonObject,
))
const jsonArray = between(
seq(char('['), many(whitespace)),
seq(many(whitespace), char(']')),
sepBy(jsonValue, seq(many(whitespace), char(','), many(whitespace))),
)
type ParseResult<A> = { success: true; value: A; rest: string } | { success: false; expected: string; at: string }
type Parser<A> = (input: string) => ParseResult<A>
string(s: string): Parser<string>
char(c: string): Parser<string>
regex(re: RegExp): Parser<string>
digit: Parser<string>
letter: Parser<string>
whitespace: Parser<string>
eof: Parser<void>
seq<T extends Parser<any>[]>(...parsers: T): Parser<[...inferred]>
alt<T extends Parser<any>[]>(...parsers: T): Parser<T[number] extends Parser<infer U> ? U : never>
many<A>(parser: Parser<A>): Parser<A[]>
many1<A>(parser: Parser<A>): Parser<A[]>
optional<A>(parser: Parser<A>): Parser<A | undefined>
sepBy<A, S>(parser: Parser<A>, sep: Parser<S>): Parser<A[]>
between<L, A, R>(left: Parser<L>, parser: Parser<A>, right: Parser<R>): Parser<A>
map<A, B>(parser: Parser<A>, f: (a: A) => B): Parser<B>
flatMap<A, B>(parser: Parser<A>, f: (a: A) => Parser<B>): Parser<B>
lazy<A>(f: () => Parser<A>): Parser<A>

lazy is for recursive grammars. Delays parser construction to avoid circular references.

integer: Parser<number>
float: Parser<number>
quotedString: Parser<string>
identifier: Parser<string>
run<A>(parser: Parser<A>, input: string): ParseResult<A>