split up parser.js
This commit is contained in:
parent
28e213d5c2
commit
8323c9f6a1
14 changed files with 703 additions and 128 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,3 +1,6 @@
|
|||
[submodule "vendor/kojima"]
|
||||
path = vendor/kojima
|
||||
url = https://git.kitsu.cafe/rowan/kojima.git
|
||||
[submodule "vendor/izuna"]
|
||||
path = vendor/izuna
|
||||
url = https://git.kitsu.cafe/rowan/izuna.git
|
||||
|
|
16
jsconfig.json
Normal file
16
jsconfig.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"module": "es2020",
|
||||
"target": "es6",
|
||||
"lib": ["es2022", "dom"],
|
||||
"checkJs": false,
|
||||
"paths": {
|
||||
"/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
36
src/char.js
Normal file
36
src/char.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { State } from './state.js'
|
||||
import { anyOf } from './combinator.js'
|
||||
import { Alpha, Alphanumeric, Digits, LowerAlpha, UpperAlpha } from './const.js'
|
||||
import { fail, join, mapStr, next, succeed } from './fn.js'
|
||||
import { map, seq } from './seq.js'
|
||||
import { curry } from '../vendor/izuna/src/curry.js'
|
||||
|
||||
/** @import { ParserState } from './state.js' */
|
||||
|
||||
export const char = curry(
|
||||
/**
|
||||
* @param {string} ch
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
(ch, state) => (
|
||||
next(state) === ch ? succeed(ch, state) : fail(`could not parse ${ch} `, state)
|
||||
))
|
||||
|
||||
export const str = curry(
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {State} state
|
||||
*/
|
||||
(str, state) => (
|
||||
map(
|
||||
join(''),
|
||||
seq(...mapStr(char, str))
|
||||
)(state)
|
||||
))
|
||||
|
||||
export const digit = anyOf(Digits)
|
||||
export const lowerAlpha = anyOf(LowerAlpha)
|
||||
export const upperAlpha = anyOf(UpperAlpha)
|
||||
export const alpha = anyOf(Alpha)
|
||||
export const alphanumeric = anyOf(Alphanumeric)
|
||||
|
34
src/combinator.js
Normal file
34
src/combinator.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { char } from './char.js'
|
||||
import { fail, fork, mapStr } from './fn.js'
|
||||
import { curry } from '../vendor/izuna/src/curry.js'
|
||||
|
||||
/** @import { ParserState } from './state.js' */
|
||||
|
||||
/**
|
||||
* @param {...any} parsers
|
||||
*/
|
||||
export const any = (...parsers) =>
|
||||
/**
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
state => {
|
||||
for (const parser of parsers) {
|
||||
const [original, clone] = fork(state)
|
||||
const result = parser(clone)
|
||||
if (result.isOk) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return fail('no matching parsers', state)
|
||||
}
|
||||
|
||||
export const anyOf = curry(
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
(str, state) => (
|
||||
any(...mapStr(char, str))(state)
|
||||
))
|
||||
|
18
src/cond.js
Normal file
18
src/cond.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { ParseError } from './state.js'
|
||||
import { curry } from '../vendor/izuna/src/index.js'
|
||||
import { fork, succeed } from './fn.js'
|
||||
|
||||
/** @import { Result } from '../vendor/kojima/src/index.js' */
|
||||
/** @import { ParserState } from './state.js' */
|
||||
|
||||
export const maybe = curry(
|
||||
/**
|
||||
* @param {(...args: any[]) => Result<ParserState, ParseError>} parser
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
(parser, state) => {
|
||||
const [original, clone] = fork(state)
|
||||
const result = parser(clone)
|
||||
return result.isOk() ? result : succeed([], original)
|
||||
})
|
||||
|
6
src/const.js
Normal file
6
src/const.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const LowerAlpha = 'abcdefghijklmnopqrstuvwxyz'
|
||||
export const UpperAlpha = LowerAlpha.toUpperCase()
|
||||
export const Alpha = LowerAlpha + UpperAlpha
|
||||
export const Digits = '1234567890'
|
||||
export const Alphanumeric = Alpha + Digits
|
||||
|
99
src/fn.js
Normal file
99
src/fn.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
import { ParseError } from './state.js'
|
||||
import { Iter } from './iter.js'
|
||||
import { err, ok } from '../vendor/kojima/src/index.js'
|
||||
import { curry } from '/vendor/izuna/src/index.js'
|
||||
|
||||
/** @import { ParserState } from './state.js'* /
|
||||
|
||||
/**
|
||||
* @param {...any} values
|
||||
*/
|
||||
export const Tuple = (...values) => Object.freeze(values)
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Iterator<T> | Iterable<T>} iterable
|
||||
* @param {number} [n=2]
|
||||
*/
|
||||
function tee(iterable, n = 2) {
|
||||
const iterator = Iter.from(iterable)
|
||||
|
||||
/**
|
||||
* @param {{ next: any, value: T }} current
|
||||
*/
|
||||
function* gen(current) {
|
||||
while (true) {
|
||||
if (!current.next) {
|
||||
const { done, value } = iterator.next()
|
||||
if (done) return
|
||||
current.next = { value }
|
||||
}
|
||||
|
||||
current = current.next
|
||||
yield current.value
|
||||
}
|
||||
}
|
||||
|
||||
return Array(n).fill({}).map(gen)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./state.js').ParserState} state
|
||||
*/
|
||||
export const fork = ([tokens, state]) => {
|
||||
const [a, b] = tee(state)
|
||||
return Tuple(
|
||||
Tuple(tokens.slice(), a),
|
||||
Tuple(tokens.slice(), b),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T | T[]} v
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
export const succeed = (v, [x, y]) => ok(Tuple(x.concat(v), y))
|
||||
|
||||
/**
|
||||
* @param {string} msg
|
||||
* @param {ParserState} state
|
||||
* @param {Error} [e]
|
||||
*/
|
||||
export const fail = (msg, state, e = undefined) => err(new ParseError(msg, state, e))
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {number & keyof T} n
|
||||
* @param {T[]} iter
|
||||
*/
|
||||
export const nth = (n, iter) => iter[n]
|
||||
|
||||
/**
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
export const next = state => state[1].next().value
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T[]} a
|
||||
* @param {T[]} b
|
||||
*/
|
||||
export const diff = (a, b) => b.slice(-Math.max(0, b.length - a.length))
|
||||
|
||||
export const join = curry(
|
||||
/**
|
||||
* @param {string} delim
|
||||
* @param {string[]} val
|
||||
*/
|
||||
(delim, val) => val.join(delim)
|
||||
)
|
||||
|
||||
export const mapStr = curry(
|
||||
/**
|
||||
* @param {(...args: any[]) => any} fn
|
||||
* @param {string} str
|
||||
*/
|
||||
(fn, str) => Array.from(str).map(v => fn(v))
|
||||
)
|
||||
|
|
@ -1,2 +1,6 @@
|
|||
export * from './parser.js'
|
||||
export * from './char.js'
|
||||
export * from './combinator.js'
|
||||
export * from './cond.js'
|
||||
export * from './seq.js'
|
||||
export * from './state.js'
|
||||
|
||||
|
|
406
src/iter.js
Normal file
406
src/iter.js
Normal file
|
@ -0,0 +1,406 @@
|
|||
/**
|
||||
* @template T
|
||||
* @implements Iterator<T>
|
||||
* @implements Iterable<T>
|
||||
*/
|
||||
export class Iter {
|
||||
_iterator
|
||||
|
||||
/**
|
||||
* @param {Iterator<T>} iterator
|
||||
*/
|
||||
constructor(iterator) {
|
||||
this._iterator = iterator
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {any} value
|
||||
* @returns {value is Iterable<T>}
|
||||
*/
|
||||
static _isIterable(value) {
|
||||
return Object.hasOwn(value, Symbol.iterator)
|
||||
&& typeof value[Symbol.iterator] === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} value
|
||||
*/
|
||||
static from(value) {
|
||||
if (value instanceof Iter) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (Iter._isIterable(value)) {
|
||||
const iterator = value[Symbol.iterator]()
|
||||
|
||||
if (iterator instanceof Iter) {
|
||||
return iterator
|
||||
}
|
||||
|
||||
return new Iter(iterator)
|
||||
}
|
||||
|
||||
throw new TypeError('object is not an iterator')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} [value]
|
||||
*/
|
||||
next(value) {
|
||||
return this._iterator.next(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} [value]
|
||||
*/
|
||||
return(value) {
|
||||
// @ts-ignore
|
||||
return this._iterator.return(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} err
|
||||
*/
|
||||
throw(err) {
|
||||
// @ts-ignore
|
||||
return this._iterator.throw(err)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} limit
|
||||
*/
|
||||
drop(limit) {
|
||||
return new DropIter(this, limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(value: T, index: number) => boolean} callbackFn
|
||||
*/
|
||||
every(callbackFn) {
|
||||
let next = this.next()
|
||||
let index = 0
|
||||
let result = true
|
||||
|
||||
while (!next.done) {
|
||||
if (!callbackFn(next.value, index)) {
|
||||
result = false
|
||||
break
|
||||
}
|
||||
|
||||
next = this.next()
|
||||
index += 1
|
||||
}
|
||||
|
||||
this.return()
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(value: T, index: number) => boolean} callbackFn
|
||||
*/
|
||||
filter(callbackFn) {
|
||||
return new FilterIter(this, callbackFn)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(value: T, index: number) => boolean} callbackFn
|
||||
*/
|
||||
find(callbackFn) {
|
||||
let next = this.next()
|
||||
let index = 0
|
||||
|
||||
while (!next.done) {
|
||||
if (callbackFn(next.value, index)) {
|
||||
this.return()
|
||||
return next.value
|
||||
}
|
||||
|
||||
next = this.next()
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {<U>(value: T, index: number) => U} callbackFn
|
||||
*/
|
||||
flatMap(callbackFn) {
|
||||
return new FlatMapIter(this, callbackFn)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(value: T, index: number) => void} callbackFn
|
||||
*/
|
||||
forEach(callbackFn) {
|
||||
let next = this.next()
|
||||
let index = 0
|
||||
|
||||
while (!next.done) {
|
||||
callbackFn(next.value, index)
|
||||
next = this.next()
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {<U>(value: T, index: number) => U} callbackFn
|
||||
*/
|
||||
map(callbackFn) {
|
||||
return new MapIter(this, callbackFn)
|
||||
}
|
||||
|
||||
/**
|
||||
* @template U
|
||||
* @param {(accumulator: U, value: T, index: number) => U} callbackFn
|
||||
* @param {U} init
|
||||
*/
|
||||
reduce(callbackFn, init) {
|
||||
let next = this.next()
|
||||
let index = 0
|
||||
let acc = init
|
||||
|
||||
while (!next.done) {
|
||||
acc = callbackFn(acc, next.value, index)
|
||||
|
||||
next = this.next()
|
||||
index += 1
|
||||
}
|
||||
|
||||
this.return()
|
||||
return acc
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(value: T, index: number) => boolean} callbackFn
|
||||
*/
|
||||
some(callbackFn) {
|
||||
let next = this.next()
|
||||
let index = 0
|
||||
let result = false
|
||||
|
||||
while (!next.done) {
|
||||
if (callbackFn(next.value, index)) {
|
||||
result = true
|
||||
break
|
||||
}
|
||||
|
||||
next = this.next()
|
||||
index += 1
|
||||
}
|
||||
|
||||
this.return()
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} limit
|
||||
*/
|
||||
take(limit) {
|
||||
return new TakeIter(this, limit)
|
||||
}
|
||||
|
||||
/*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
toArray() {
|
||||
/** @type {T[]} */
|
||||
const result = []
|
||||
|
||||
for (const item of this) {
|
||||
result.push(item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @extends Iter<T>
|
||||
*/
|
||||
class DropIter extends Iter {
|
||||
_limit
|
||||
|
||||
/**
|
||||
* @param {Iterator<T>} iterator
|
||||
* @param {number} limit
|
||||
*/
|
||||
constructor(iterator, limit) {
|
||||
super(iterator)
|
||||
this._limit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
*/
|
||||
next(value) {
|
||||
for (let i = this._limit; i > 0; i--) {
|
||||
const next = super.next(value)
|
||||
|
||||
if (next.done) {
|
||||
return next
|
||||
}
|
||||
}
|
||||
|
||||
return super.next(value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @extends Iter<T>
|
||||
*/
|
||||
class FilterIter extends Iter {
|
||||
_filter
|
||||
_index = 0
|
||||
|
||||
/**
|
||||
* @param {Iterator<T>} iterator
|
||||
* @param {(value: T, index: number) => boolean} callbackFn
|
||||
*/
|
||||
constructor(iterator, callbackFn) {
|
||||
super(iterator)
|
||||
this._filter = callbackFn
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} [value]
|
||||
* @returns {IteratorResult<T>}
|
||||
*/
|
||||
next(value) {
|
||||
let next = super.next(value)
|
||||
|
||||
while (!next.done && !this._filter(next.value, this._index)) {
|
||||
next = super.next(value)
|
||||
this._index += 1
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @extends Iter<T>
|
||||
*/
|
||||
class FlatMapIter extends Iter {
|
||||
_flatMap
|
||||
_index = 0
|
||||
|
||||
/** @type {Iterator<T> | undefined} */
|
||||
_inner = undefined
|
||||
|
||||
/**
|
||||
* @param {Iterator<T>} iterator
|
||||
* @param {<U>(value: T, index: number) => Iterator<U> | Iterable<U>} callbackFn
|
||||
*/
|
||||
constructor(iterator, callbackFn) {
|
||||
super(iterator)
|
||||
this._flatMap = callbackFn
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @returns {IteratorResult<T>}
|
||||
*/
|
||||
next(value) {
|
||||
if (this._inner) {
|
||||
const innerResult = this._inner.next(value)
|
||||
|
||||
if (!innerResult.done) {
|
||||
this._index += 1
|
||||
return { value: innerResult.value, done: false }
|
||||
}
|
||||
|
||||
this._inner = undefined
|
||||
}
|
||||
|
||||
const outerResult = super.next(value)
|
||||
if (outerResult.done) {
|
||||
return { value: undefined, done: true }
|
||||
}
|
||||
|
||||
const nextIterable = this._flatMap(outerResult.value, this._index || 0)
|
||||
if (Iter._isIterable(nextIterable)) {
|
||||
this._inner = Iter.from(nextIterable)
|
||||
return this.next(value)
|
||||
} else {
|
||||
throw new TypeError('value is not an iterator')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @extends Iter<T>
|
||||
*/
|
||||
class MapIter extends Iter {
|
||||
_map
|
||||
_index = 0
|
||||
|
||||
/**
|
||||
* @param {Iterator<T>} iterator
|
||||
* @param {<U>(value: T, index: number) => U} callbackFn
|
||||
*/
|
||||
constructor(iterator, callbackFn) {
|
||||
super(iterator)
|
||||
this._map = callbackFn
|
||||
}
|
||||
|
||||
/** @param {any} value */
|
||||
next(value) {
|
||||
let next = super.next(value)
|
||||
|
||||
if (next.done) {
|
||||
return next
|
||||
}
|
||||
|
||||
const result = {
|
||||
done: false,
|
||||
value: this._map(next.value, this._index)
|
||||
}
|
||||
|
||||
this._index += 1
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @extends Iter<T>
|
||||
*/
|
||||
class TakeIter extends Iter {
|
||||
_limit
|
||||
|
||||
/**
|
||||
* @param {Iterator<T>} iterator
|
||||
* @param {number} limit
|
||||
*/
|
||||
constructor(iterator, limit) {
|
||||
super(iterator)
|
||||
this._limit = limit
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @returns {IteratorResult<T>}
|
||||
*/
|
||||
next(value) {
|
||||
if (this._limit > 0) {
|
||||
const next = super.next(value)
|
||||
if (!next.done) {
|
||||
this._limit -= 1
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
return { value: undefined, done: true }
|
||||
}
|
||||
}
|
||||
|
126
src/parser.js
126
src/parser.js
|
@ -1,126 +0,0 @@
|
|||
import { Ok, Err, curry } from '../vendor/kojima/src/index.js'
|
||||
|
||||
export class ParseError extends Error {
|
||||
constructor(message, state, source) {
|
||||
super(message)
|
||||
this.state = state
|
||||
this.source = source
|
||||
}
|
||||
}
|
||||
|
||||
const Tuple = (...values) => Object.freeze(values)
|
||||
const State = value => Tuple([], Iterator.from(value))
|
||||
|
||||
const LowerAlpha = 'abcdefghijklmnopqrstuvwxyz'
|
||||
const UpperAlpha = LowerAlpha.toUpperCase()
|
||||
const Alpha = LowerAlpha + UpperAlpha
|
||||
const Digits = '1234567890'
|
||||
const Alphanumeric = Alpha + Digits
|
||||
|
||||
|
||||
const tee = (iterator, n = 2) => {
|
||||
iterator = Iterator.from(iterator)
|
||||
|
||||
function* gen(current) {
|
||||
while (true) {
|
||||
if (!current.next) {
|
||||
const { done, value } = iterator.next()
|
||||
if (done) { return }
|
||||
current.next = { value }
|
||||
}
|
||||
current = current.next
|
||||
yield current.value
|
||||
}
|
||||
}
|
||||
|
||||
return Array(n).fill({}).map(gen)
|
||||
}
|
||||
|
||||
const fork = ([tokens, state]) => {
|
||||
const [a, b] = tee(state)
|
||||
return Tuple(
|
||||
Tuple(tokens.slice(), a),
|
||||
Tuple(tokens.slice(), b),
|
||||
)
|
||||
}
|
||||
|
||||
export const succeed = (v, [x, y]) => Ok(Tuple(x.concat(v), y))
|
||||
export const fail = (msg, state, err = undefined) => Err(new ParseError(msg, state, err))
|
||||
const nth = (n, iter) => iter[n]
|
||||
const next = state => nth(1, state).next().value
|
||||
const diff = (a, b) => b.slice(-Math.max(0, b.length - a.length))
|
||||
const join = curry((delim, val) => val.join(delim))
|
||||
const mapStr = curry((fn, str) => Array.from(str).map(v => fn(v)))
|
||||
|
||||
export const any = (...parsers) => state => {
|
||||
for (const parser of parsers) {
|
||||
const [original, clone] = fork(state)
|
||||
const result = parser(clone)
|
||||
if (result.isOk) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return fail('no matching parsers', state)
|
||||
}
|
||||
|
||||
export const anyOf = curry((str, state) => (
|
||||
any(...mapStr(char, str))(state)
|
||||
))
|
||||
|
||||
export const seq = (...parsers) => state => {
|
||||
let acc = Ok(state)
|
||||
|
||||
for (const parser of parsers) {
|
||||
if (acc.isOk) {
|
||||
acc = acc.bind(parser)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
export const map = curry((fn, parser, state) => {
|
||||
return parser(state).bind(result => {
|
||||
try {
|
||||
const parsed = diff(state[0], result[0])
|
||||
const backtrack = Tuple(state[0], result[1])
|
||||
return succeed(fn(parsed), backtrack)
|
||||
} catch (e) {
|
||||
return fail('failed to map', state, e)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export const char = curry((ch, state) => (
|
||||
next(state) === ch ? succeed(ch, state) : fail(`could not parse ${ch} `, state)
|
||||
))
|
||||
|
||||
export const anyChar = state => {
|
||||
const ch = next(state)
|
||||
return !!ch ? succeed(ch, state) : fail(`could not parse ${ch}`, state)
|
||||
}
|
||||
|
||||
export const str = curry((str, state) => (
|
||||
map(
|
||||
join(''),
|
||||
seq(...mapStr(char, str))
|
||||
)(state)
|
||||
))
|
||||
|
||||
export const digit = anyOf(Digits)
|
||||
export const lowerAlpha = anyOf(LowerAlpha)
|
||||
export const upperAlpha = anyOf(UpperAlpha)
|
||||
export const alpha = anyOf(Alpha)
|
||||
export const alphanumeric = anyOf(Alphanumeric)
|
||||
|
||||
export const maybe = curry((parser, state) => {
|
||||
const [original, clone] = fork(state)
|
||||
const result = parser(clone)
|
||||
return result.isOk ? result : succeed([], original)
|
||||
})
|
||||
|
||||
export const parse = curry((parser, input) => parser(State(input)))
|
||||
|
55
src/seq.js
Normal file
55
src/seq.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { diff, fail, succeed, Tuple } from './fn.js'
|
||||
import { ok } from '../vendor/kojima/src/index.js'
|
||||
import { curry } from '../vendor/izuna/src/curry.js'
|
||||
|
||||
/** @import { ParseError, ParserState } from './state.js' */
|
||||
/** @import { Result } from '../vendor/kojima/src/index.js' */
|
||||
|
||||
/**
|
||||
* @typedef {(value: any) => Result<ParserState, ParseError>} Parser
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {...Parser} parsers
|
||||
*/
|
||||
export const seq = (...parsers) =>
|
||||
/** @param {ParserState} state */
|
||||
state => {
|
||||
let acc = ok(state)
|
||||
|
||||
for (const parser of parsers) {
|
||||
if (acc.isOk()) {
|
||||
acc = acc.bind(parser)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
export const map = curry(
|
||||
/**
|
||||
* @param {(...args: any[]) => any} fn
|
||||
* @param {Parser} parser
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
(fn, parser, state) => {
|
||||
return parser(state).map(result => {
|
||||
try {
|
||||
/** @type {Result<ParserState, ParseError>} */
|
||||
const parsed = result.chain(otherState =>
|
||||
fn(diff(state[0], otherState[0]))
|
||||
)
|
||||
|
||||
const backtrack = result.chain(otherState =>
|
||||
Tuple(state[0], otherState[1])
|
||||
)
|
||||
|
||||
return succeed(parsed, backtrack)
|
||||
} catch (e) {
|
||||
return fail('failed to map', state, e)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
23
src/state.js
Normal file
23
src/state.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Iter } from './iter.js'
|
||||
/**
|
||||
* @typedef {Readonly<[any[], Iterator<any>]>} ParserState
|
||||
*/
|
||||
|
||||
export class ParseError extends Error {
|
||||
/**
|
||||
* @param {string} message
|
||||
* @param {ParserState} state
|
||||
* @param {Error} [cause]
|
||||
*/
|
||||
constructor(message, state, cause) {
|
||||
super(message, { cause })
|
||||
this.state = state
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @returns {ParserState}
|
||||
*/
|
||||
export const State = value => Object.freeze([[], Iter.from(value)])
|
||||
|
1
vendor/izuna
vendored
Submodule
1
vendor/izuna
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit aa70427c8c349bbfe4576cba878f5b44859007d4
|
2
vendor/kojima
vendored
2
vendor/kojima
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 1402219aad22f55c39ac448bff372474a17f5c00
|
||||
Subproject commit d6615248572d2e5c16661d8aab0650ae28aeb6c2
|
Loading…
Add table
Reference in a new issue