194 lines
4.9 KiB
JavaScript
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
|
|
}
|
|
})
|
|
|