kuebiko/src/parser.js
2025-07-02 11:13:04 -04:00

194 lines
4.9 KiB
JavaScript

import { Stream } from './iterator.js'
import { Result } from './result.js'
import { curry, range } from './utils.js'
import * as string from './string.js'
export class ParseError extends Error {
name
index
description
constructor(index, description) {
super()
this.name = 'ParseError'
this.index = index
this.description = description
}
}
export class EofError extends ParseError {
name = 'EofError'
description = 'End of stream reached'
}
// convenience factory functions and cached callbacks
const eofErr = index => () => new EofError(index)
const parseErr = (index, desc) => () => new ParseError(index, desc)
const eq = a => b => a === b
const toString = value => value.toString()
export const pure = curry((value, input) => Result.ok([value, input]))
export const fail = curry((error, input) => Result.err(new ParseError(input.index, error)))
export const anyItem = () => input => {
return input.peek().okOrElse(eofErr(input.index))
.map(value => [value, input.drop()])
}
export const satisfy = curry((predicate, input) => {
return input.peek()
.okOrElse(eofErr(input.index))
.filterOrElse(
predicate,
value => parseErr(input.index, `Value did not match predicate: ${value}`)
)
.map(value => [value, input.drop()])
})
export const literal = curry((value, input) => (
satisfy(eq(value), input)
))
export const bind = curry((parser, transform, input) =>
parser(input).andThen(([value, rest]) => transform(value)(rest))
)
export const map = curry((parser, morphism, input) => (
parser(input).map(([value, rest]) => [morphism(value), rest])
))
export const seq = curry((a, b, input) => (
bind(a, x => map(b, y => [x, y]), input)
))
export const alt = (...parsers) => input => {
for (const p of parsers) {
const result = p(input.clone())
if (result.isOk()) {
return result
}
}
return Result.err(new ParseError(input.index, "No parsers matched alt"))
}
export const many = curry((parser, input) => {
const results = []
let stream = input
while (true) {
const result = parser(stream.clone())
if (result.isOk()) {
const [value, rest] = result.unwrap()
results.push(value)
stream = rest
} else {
break
}
}
return Result.ok([results, stream])
})
export const many1 = curry((parser, input) => (
parser(input).andThen(([first, rest]) => (
many(parser, rest).map(([others, rest]) => (
[[first, ...others], rest]
))
))
))
export const optional = curry((parser, input) =>
parser(input.clone()).orElse(() => Result.ok([undefined, input]))
)
export const eof = () => input => (
anyItem()(input)
.andThen(([value, rest]) =>
Result.err(
new ParseError(rest.index, `Expected EOF, found ${value}`)
)
)
.orElse(err => {
if (err instanceof EofError) {
return Result.ok([null, input])
} else {
return Result.err(err)
}
})
)
export const take = curry((limit, input) => {
const result = []
const next = anyItem()
let stream = input
for (const _index of range(limit)) {
const nextResult = next(stream)
if (nextResult.isOk()) {
const [value, rest] = nextResult.unwrap()
result.push(value)
stream = rest
} else {
return nextResult
}
}
return Result.ok([result, stream])
})
export const drop = curry((limit, input) =>
skip(take(limit), input)
)
export const skip = curry((parser, input) =>
parser(input).map(([_, rest]) => [undefined, rest])
)
const streamInfo = (stream, n = 3) => {
const clone = stream.clone()
const values = clone.take(n).map(toString).join(', ')
const hasMore = clone.peek().isSome()
const ellip = hasMore ? '...' : ''
return `Stream { index = ${stream.index}, values = [${values}${ellip}]`
}
export const trace = curry((parser, label, input) => {
const name = label || parser.name || 'anonymous parser'
console.log(`trace(parser = ${name}, input = ${streamInfo(input)})`)
const result = parser(input)
if (result.isOk()) {
const [value, rest] = result.unwrap()
console.log(` success: value = ${toString(value)}, remaining = ${streamInfo(rest)}`)
} else {
const err = result.unwrapErr()
console.log(` fail: error = ${err}`)
}
})
// cached parsers
const LetterParser = alt(...Array.from(string.AsciiLetters).map(x => literal(x)))
const DigitParser = alt(...Array.from(string.Digits).map(x => literal(x)))
const WhitespaceParser = alt(...Array.from(string.Whitespace).map(x => literal(x)))
export const letter = () => LetterParser
export const digit = () => DigitParser
export const whitespace = () => WhitespaceParser
export const parseSome = curry((parser, value) =>
parser(new Stream(value))
)
export const parse = curry((parser, value) => {
const result = parseSome(seq(parser, eof()), value)
if (result.isOk()) {
const [[value, _rest], _stream] = result.unwrap()
return Result.ok(value)
} else {
return result
}
})