parse
bun add @stopcock/parseParser 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: '' }vs. hand-written parsers
Section titled “vs. hand-written parsers”// Regex: fragile, no error messages, unreadable at scaleconst 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 reportingconst 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 } }Real-world patterns
Section titled “Real-world patterns”CSV line parser
Section titled “CSV line parser”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'] }Key-value config file
Section titled “Key-value config file”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')JSON subset (recursive)
Section titled “JSON subset (recursive)”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>Primitives
Section titled “Primitives”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>Combinators
Section titled “Combinators”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.
Built-in parsers
Section titled “Built-in parsers”integer: Parser<number>float: Parser<number>quotedString: Parser<string>identifier: Parser<string>Running
Section titled “Running”run<A>(parser: Parser<A>, input: string): ParseResult<A>