185 lines
4.3 KiB
JavaScript
185 lines
4.3 KiB
JavaScript
import { Result } from './result.js'
|
|
import { Iterator, Stream } from './stream.js'
|
|
import { curry, join, of } from './fn.js'
|
|
|
|
class ParseError extends Error {
|
|
constructor(message, state, source) {
|
|
super(message)
|
|
this.state = state
|
|
this.source = source
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @typedef Iterator
|
|
*/
|
|
|
|
/**
|
|
* @template T
|
|
* @typedef {[T[], Stream]} ParserState
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} Peekable
|
|
* @property {Iterator} iterator
|
|
*/
|
|
|
|
/**
|
|
* @template T, E
|
|
* @typedef {Object} Result
|
|
* @property {T} value
|
|
* @property {E} error
|
|
*/
|
|
|
|
/**
|
|
* Create a ParserState from input
|
|
*
|
|
* @template T
|
|
* @param {T[]} value - Any iterator or value which can become an iterator
|
|
* @return {ParserState} - A new ParserState
|
|
*/
|
|
const ParserState = value => ([[], new Stream(value)])
|
|
const clone = ([a, b]) => ([a.slice(), b.clone()])
|
|
|
|
/**
|
|
* Update ParserState with parsed tokens
|
|
*
|
|
* @template T
|
|
* @param {T[]} - parsed tokens
|
|
* @param {ParserState}
|
|
* @return {Result} - Result.Ok with updated ParserState
|
|
*/
|
|
const succeed = curry((v, [p, rest]) => Result.Ok([p.concat(of(v)), rest]))
|
|
|
|
/**
|
|
* Indicate parser failure
|
|
*
|
|
* @template E
|
|
* @param {string} - error message
|
|
* @param {ParserState}
|
|
* @param {E=} error
|
|
* @return {Result} - Error with ParserState information and error source
|
|
*/
|
|
const fail = curry((msg, state, err = undefined) => Result.Err(new ParseError(msg, state, err)))
|
|
|
|
const next = ([, rest]) => rest.next()
|
|
// b.len - a.len
|
|
// b.slice(-diff)
|
|
//const difflen = result.value[0].length - parsed.length
|
|
//console.log(difflen, result.value[0], result.value[0].slice(-difflen))
|
|
const diff = ([a], [b]) => b.slice(-Math.max(0, b.length - a.length))
|
|
const tokenize = str => str.split('')
|
|
const tokenizeInto = curry((fn, str) => tokenize(str).map(v => fn(v)))
|
|
|
|
const LowerAlpha = 'abcdefghijklmnopqrstuvwxyz'
|
|
const UpperAlpha = LowerAlpha.toUpperCase()
|
|
const Alpha = LowerAlpha + UpperAlpha
|
|
const Digits = '1234567890'
|
|
const Alphanumeric = Alpha + Digits
|
|
|
|
export const seq = (...parsers) => state => {
|
|
let acc = Result.Ok(state)
|
|
for (const parser of parsers) {
|
|
if (acc.isOk()) {
|
|
acc = parser(acc.value)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return acc
|
|
}
|
|
|
|
export const any = (...parsers) => (state) => {
|
|
for (const parser of parsers) {
|
|
const result = parser(clone(state))
|
|
if (result.isOk()) {
|
|
return result
|
|
}
|
|
}
|
|
|
|
return fail('no matching parsers', state)
|
|
}
|
|
|
|
export const many = curry((parser, state) => {
|
|
let result = Result.Ok(state)
|
|
|
|
while (true) {
|
|
const res = parser(clone(result.value))
|
|
if (res.isOk()) {
|
|
result = res
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return result
|
|
})
|
|
|
|
export const skip = curry((parser, state) => {
|
|
const [parsed] = state
|
|
const result = parser(state)
|
|
|
|
if (result.isOk()) {
|
|
return Result.Ok([parsed, result.value[1]])
|
|
} else {
|
|
return result
|
|
}
|
|
})
|
|
|
|
export const map = curry((fn, parser, state) => {
|
|
const result = parser(state)
|
|
|
|
if (result.isOk()) {
|
|
const [parsed] = state
|
|
const [, stream] = result.value
|
|
const backtrack = [parsed, stream]
|
|
|
|
try {
|
|
const parsedValue = diff(state, result.value)
|
|
return succeed(fn(parsedValue), backtrack)
|
|
} catch (e) {
|
|
return fail('map failed', state, e)
|
|
}
|
|
}
|
|
|
|
return result
|
|
})
|
|
|
|
|
|
export const char = curry((ch, state) => {
|
|
return next(state) === ch ? succeed(ch, state) : fail(`could not parse ${ch}`, state)
|
|
})
|
|
|
|
export const noCaseChar = curry((ch, state) => (
|
|
any(char(ch.toLowerCase()), char(ch.toUpperCase()))(state)
|
|
))
|
|
|
|
export const string = curry((str, state) =>
|
|
map(join(''), seq(...tokenizeInto(char, str)))(state)
|
|
)
|
|
|
|
export const anyChar = curry((str, state) =>
|
|
any(...tokenizeInto(char, str))(state)
|
|
)
|
|
|
|
export const digit = anyChar(Digits)
|
|
export const lowerAlpha = anyChar(LowerAlpha)
|
|
export const upperAlpha = anyChar(UpperAlpha)
|
|
export const alpha = anyChar(Alpha)
|
|
export const alphanumeric = anyChar(Alphanumeric)
|
|
|
|
export const noCaseString = curry((str, state) => (
|
|
seq(...tokenizeInto(noCaseChar, str))(state)
|
|
))
|
|
|
|
export const maybe = curry((parser, state) => {
|
|
const result = parser(state)
|
|
return result.isOk() ? result : succeed([], state)
|
|
})
|
|
|
|
export const eof = (state) => {
|
|
return rest[1].done() ? succeed([], state) : fail('eof did not match', state)
|
|
}
|
|
|
|
export const parse = curry((parser, input) => parser(ParserState(input)))
|
|
|