graph-ecs/src/parser.js

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)))