unit tests; improved iterator
This commit is contained in:
parent
81a33e4a98
commit
fa822ea3ac
17 changed files with 450 additions and 290 deletions
|
@ -1,13 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"moduleResolution": "node",
|
||||
"module": "es2020",
|
||||
"target": "es6",
|
||||
"lib": ["es2022", "dom"],
|
||||
"checkJs": false,
|
||||
"paths": {
|
||||
"/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
|
34
package-lock.json
generated
34
package-lock.json
generated
|
@ -24,7 +24,6 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -41,7 +40,6 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -58,7 +56,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -75,7 +72,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -92,7 +88,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -109,7 +104,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -126,7 +120,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -143,7 +136,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -160,7 +152,6 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -177,7 +168,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -194,7 +184,6 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -211,7 +200,6 @@
|
|||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -228,7 +216,6 @@
|
|||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -245,7 +232,6 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -262,7 +248,6 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -279,7 +264,6 @@
|
|||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -296,7 +280,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -313,7 +296,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -330,7 +312,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -347,7 +328,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -364,7 +344,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -381,7 +360,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -398,7 +376,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -415,7 +392,6 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -432,7 +408,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
@ -446,7 +421,6 @@
|
|||
"version": "0.25.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
|
||||
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
@ -485,20 +459,22 @@
|
|||
},
|
||||
"node_modules/folktest": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+https://git.kitsu.cafe/rowan/folktest.git#708d44f1215be33fcceba426029f44b4f963dbe5",
|
||||
"resolved": "git+https://git.kitsu.cafe/rowan/folktest.git#cbf48ff3b1334eb883f202a77a5bc89d24534520",
|
||||
"dev": true,
|
||||
"license": "GPL-3.0-or-later"
|
||||
},
|
||||
"node_modules/izuna": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+https://git.kitsu.cafe/rowan/izuna.git#e11a0870c27eeb5ea1a4ae3fedccca008eda15c2",
|
||||
"resolved": "git+https://git.kitsu.cafe/rowan/izuna.git#4ab7c265d83856f2dc527780a3ac87b3d54676f1",
|
||||
"license": "GPL-3.0-or-later"
|
||||
},
|
||||
"node_modules/kojima": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "git+https://git.kitsu.cafe/rowan/kojima.git#d6615248572d2e5c16661d8aab0650ae28aeb6c2",
|
||||
"resolved": "git+https://git.kitsu.cafe/rowan/kojima.git#b997bca5e31323bb81b38fafe5f823cae6e574de",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.2",
|
||||
"izuna": "git+https://git.kitsu.cafe/rowan/izuna.git",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
},
|
||||
|
|
83
src/byte.js
Normal file
83
src/byte.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { chain, curry } from 'izuna'
|
||||
import { any, map, seq } from './combinator.js'
|
||||
import { not } from './cond.js'
|
||||
import { Digits, LowerAlpha, UpperAlpha } from './const.js'
|
||||
import { clone, fail, join, mapStr, next, succeed } from './fn.js'
|
||||
import { ok } from 'kojima'
|
||||
|
||||
/** @import { ParserState } from './state.js' */
|
||||
|
||||
export const char = curry(
|
||||
/**
|
||||
* @param {string} ch
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
(ch, state) => {
|
||||
return next(state) === ch ? succeed(ch, state) : fail(`could not parse ${ch} `, state)
|
||||
})
|
||||
|
||||
export const tag = curry(
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
(str, state) => (
|
||||
map(
|
||||
join(''),
|
||||
seq(...mapStr(char, str))
|
||||
)(state)
|
||||
))
|
||||
|
||||
export const charNoCase = curry((ch, state) => {
|
||||
return any(
|
||||
char(ch.toLowerCase()),
|
||||
char(ch.toUpperCase())
|
||||
)(state)
|
||||
})
|
||||
|
||||
export const tagNoCase = curry(
|
||||
(str, state) => (
|
||||
map(
|
||||
join(''),
|
||||
seq(...mapStr(charNoCase, str))
|
||||
)(state)
|
||||
))
|
||||
|
||||
export const anyChar = state => {
|
||||
const ch = next(state)
|
||||
return !!ch ? succeed(ch, state) : fail('end of input', state)
|
||||
}
|
||||
|
||||
export const take = curry(
|
||||
(limit, state) => {
|
||||
let res = ok(state)
|
||||
|
||||
while (res.isOk() && limit > 0) {
|
||||
res = chain(anyChar, res)
|
||||
limit -= 1
|
||||
}
|
||||
return res
|
||||
}
|
||||
)
|
||||
|
||||
export const oneOf = curry(
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
(str, state) => (
|
||||
any(...mapStr(char, str))(state)
|
||||
))
|
||||
|
||||
export const noneOf = curry((str, state) => seq(not(any(oneOf(str), eof)), anyChar)(state))
|
||||
|
||||
export const digit = oneOf(Digits)
|
||||
export const lowerAlpha = oneOf(LowerAlpha)
|
||||
export const upperAlpha = oneOf(UpperAlpha)
|
||||
export const alpha = any(lowerAlpha, upperAlpha)
|
||||
export const alphanumeric = any(alpha, digit)
|
||||
|
||||
export const eof = state => {
|
||||
return clone(state).next().done ? succeed([], state) : fail('not end of stream', state)
|
||||
}
|
||||
|
41
src/char.js
41
src/char.js
|
@ -1,41 +0,0 @@
|
|||
import { State } from './state.js'
|
||||
import { any, anyOf, map } from './combinator.js'
|
||||
import { Alpha, Alphanumeric, Digits, LowerAlpha, UpperAlpha } from './const.js'
|
||||
import { fail, join, mapStr, next, succeed } from './fn.js'
|
||||
import { seq } from './seq.js'
|
||||
import { curry } from 'izuna'
|
||||
|
||||
/** @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 {ParserState} state
|
||||
*/
|
||||
(str, state) => (
|
||||
map(
|
||||
join(''),
|
||||
seq(...mapStr(char, str))
|
||||
)(state)
|
||||
))
|
||||
|
||||
export const anyChar = state => {
|
||||
const ch = next(state)
|
||||
return !!ch ? succeed(ch, state) : fail('end of input', state)
|
||||
}
|
||||
|
||||
export const digit = anyOf(Digits)
|
||||
export const lowerAlpha = anyOf(LowerAlpha)
|
||||
export const upperAlpha = anyOf(UpperAlpha)
|
||||
export const alpha = any(lowerAlpha, upperAlpha)
|
||||
export const alphanumeric = any(alpha, digit)
|
||||
|
11
src/clone.js
Normal file
11
src/clone.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export const Clone = Symbol('clone')
|
||||
|
||||
export const clone = target => {
|
||||
if (target[Clone]) {
|
||||
return target[Clone]()
|
||||
} else {
|
||||
return structuredClone(target)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,9 +1,28 @@
|
|||
import { char } from './char.js'
|
||||
import { clone, diff, fail, mapStr, succeed, Tuple } from './fn.js'
|
||||
import { curry } from 'izuna'
|
||||
import { chain, curry } from 'izuna'
|
||||
import { ok } from 'kojima'
|
||||
import { clone, diff, fail, succeed } from './fn.js'
|
||||
import { state as State } from './state.js'
|
||||
|
||||
/** @import { ParserState } from './state.js' */
|
||||
|
||||
/**
|
||||
* @param {...Parser} parsers
|
||||
*/
|
||||
export const seq = (...parsers) =>
|
||||
/** @param {ParserState} state */
|
||||
state => {
|
||||
let acc = ok(state)
|
||||
for (const parser of parsers) {
|
||||
if (acc.isOk()) {
|
||||
acc = chain(parser, acc)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {...any} parsers
|
||||
*/
|
||||
|
@ -13,6 +32,7 @@ export const any = (...parsers) =>
|
|||
*/
|
||||
state => {
|
||||
for (const parser of parsers) {
|
||||
//console.log('combinator.js::35/any:', state)
|
||||
const result = parser(clone(state))
|
||||
if (result.isOk()) {
|
||||
return result
|
||||
|
@ -22,15 +42,6 @@ export const any = (...parsers) =>
|
|||
return fail('no matching parsers', state)
|
||||
}
|
||||
|
||||
export const anyOf = curry(
|
||||
/**
|
||||
* @param {string} str
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
(str, state) => (
|
||||
any(...mapStr(char, str))(state)
|
||||
))
|
||||
|
||||
export const map = curry(
|
||||
/**
|
||||
* @param {(...args: any[]) => any} fn
|
||||
|
@ -42,8 +53,7 @@ export const map = curry(
|
|||
try {
|
||||
/** @type {Result<ParserState, ParseError>} */
|
||||
const parsed = fn(diff(state[0], result[0]))
|
||||
const backtrack = Tuple(state[0], result[1])
|
||||
|
||||
const backtrack = State(result[1], state[0])
|
||||
|
||||
return succeed(parsed, backtrack)
|
||||
} catch (e) {
|
||||
|
@ -52,7 +62,3 @@ export const map = curry(
|
|||
})
|
||||
})
|
||||
|
||||
export const eof = state => {
|
||||
return clone(state).next().done ? succeed([], state) : fail('not end of stream', state)
|
||||
}
|
||||
|
||||
|
|
16
src/cond.js
16
src/cond.js
|
@ -1,8 +1,8 @@
|
|||
import { ParseError } from './state.js'
|
||||
import { clone, fail, fork, succeed } from './fn.js'
|
||||
import { anyChar } from './char.js'
|
||||
import { clone, fail, succeed } from './fn.js'
|
||||
import { anyChar } from './byte.js'
|
||||
import { ok } from 'kojima'
|
||||
import { curry, pipe } from 'izuna'
|
||||
import { chain, curry, pipe } from 'izuna'
|
||||
|
||||
/** @import { Result } from '../vendor/kojima/src/index.js' */
|
||||
/** @import { ParserState } from './state.js' */
|
||||
|
@ -21,7 +21,7 @@ export const not = curry((parser, state) => {
|
|||
const result = parser(clone(state))
|
||||
|
||||
if (result.isOk()) {
|
||||
return fail(`'not' parser failed for ${parser.name}`, state)
|
||||
return fail(`'not' parser failed`, state)
|
||||
} else {
|
||||
return succeed([], state)
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ export const until = curry((parser, state) => {
|
|||
let result = ok(state)
|
||||
|
||||
while (result.isOk()) {
|
||||
console.log(parser.name, state)
|
||||
result = result.chain(pipe(clone, parser))
|
||||
if (result.isOk()) {
|
||||
break
|
||||
|
@ -43,6 +42,13 @@ export const until = curry((parser, state) => {
|
|||
return result
|
||||
})
|
||||
|
||||
export const verify = curry((parser, predicate, state) => {
|
||||
const result = chain(parser, clone(state))
|
||||
return chain(x => (
|
||||
predicate(x) ? result : fail('verification failed', state)
|
||||
), result)
|
||||
})
|
||||
|
||||
export const skip = curry((parser, state) => {
|
||||
})
|
||||
|
||||
|
|
60
src/fn.js
60
src/fn.js
|
@ -1,64 +1,26 @@
|
|||
import { ParseError } from './state.js'
|
||||
import { Iter } from './iter.js'
|
||||
import { err, ok } from 'kojima'
|
||||
import { curry } from 'izuna'
|
||||
import { concat, curry } from 'izuna'
|
||||
import { ParseError, state } from './state.js'
|
||||
import { IndexableIterator } from './iter.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),
|
||||
)
|
||||
}
|
||||
export const iter = value => new IndexableIterator(value)
|
||||
|
||||
/**
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
export const clone = ([h, iter]) => [h, iter.clone()]
|
||||
export const clone = (state) => state.clone()
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T | T[]} v
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
export const succeed = (v, [x, y]) => ok(Tuple(x.concat(v), y))
|
||||
export const succeed = (v, [x, y]) => {
|
||||
return ok(state(y, concat(v, x)))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} msg
|
||||
|
@ -77,7 +39,7 @@ export const nth = (n, iter) => iter[n]
|
|||
/**
|
||||
* @param {ParserState} state
|
||||
*/
|
||||
export const next = state => state[1].next().value
|
||||
export const next = state => state.next().value
|
||||
|
||||
/**
|
||||
* @template T
|
||||
|
@ -99,7 +61,9 @@ export const mapStr = curry(
|
|||
* @param {(...args: any[]) => any} fn
|
||||
* @param {string} str
|
||||
*/
|
||||
(fn, str) => Array.from(str).map(v => fn(v))
|
||||
(fn, str) => Array.from(str).flatMap(x => {
|
||||
return fn(x)
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export * from './char.js'
|
||||
export * from './byte.js'
|
||||
export * from './combinator.js'
|
||||
export * from './cond.js'
|
||||
export * from './seq.js'
|
||||
export * from './multi.js'
|
||||
export * from './state.js'
|
||||
|
||||
|
|
93
src/iter.js
93
src/iter.js
|
@ -1,21 +1,50 @@
|
|||
import { Clone } from './clone.js'
|
||||
|
||||
export class IndexableIterator extends Iterator {
|
||||
_value
|
||||
_index
|
||||
|
||||
constructor(value, index = 0) {
|
||||
super()
|
||||
|
||||
this._value = value
|
||||
this._index = index
|
||||
}
|
||||
|
||||
clone() {
|
||||
return this[Clone]()
|
||||
}
|
||||
|
||||
done() {
|
||||
return this._index >= this._value.length
|
||||
}
|
||||
|
||||
next() {
|
||||
if (this.done()) {
|
||||
return { value: undefined, done: true }
|
||||
} else {
|
||||
return { value: this._value[this._index++], done: false }
|
||||
}
|
||||
}
|
||||
|
||||
[Clone]() {
|
||||
return new IndexableIterator(
|
||||
this._value,
|
||||
this._index
|
||||
)
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @implements Iterator<T>
|
||||
* @implements Iterable<T>
|
||||
*/
|
||||
export class Iter {
|
||||
_iterator
|
||||
_source
|
||||
|
||||
/**
|
||||
* @param {Iterator<T>} iterator
|
||||
* @param {Iterabl<T>} [source]
|
||||
*/
|
||||
constructor(iterator, source) {
|
||||
this._iterator = iterator
|
||||
this._source = source
|
||||
}
|
||||
|
||||
class Iter {
|
||||
/**
|
||||
* @template T
|
||||
* @param {any} value
|
||||
|
@ -34,6 +63,10 @@ export class Iter {
|
|||
return value
|
||||
}
|
||||
|
||||
if (Array.isArray(value) || typeof value === 'string') {
|
||||
return new IndexableIterator(value)
|
||||
}
|
||||
|
||||
if (Iter._isIterable(value)) {
|
||||
const iterator = value[Symbol.iterator]()
|
||||
|
||||
|
@ -41,44 +74,12 @@ export class Iter {
|
|||
return iterator
|
||||
}
|
||||
|
||||
return new Iter(iterator, value)
|
||||
return new this(iterator, value)
|
||||
}
|
||||
|
||||
throw new TypeError('object is not an iterator')
|
||||
}
|
||||
|
||||
clone() {
|
||||
if (this._source) {
|
||||
return Iter.from(this._source)
|
||||
}
|
||||
|
||||
throw new Error('Cannot clone Iterator: not created from an iterable')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} [value]
|
||||
*/
|
||||
next(value) {
|
||||
const n = this._iterator.next(value)
|
||||
return n
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
|
48
src/multi.js
Normal file
48
src/multi.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { ok } from 'kojima'
|
||||
import { curry, } from 'izuna'
|
||||
import { clone } from './fn.js'
|
||||
import { verify } from './cond.js'
|
||||
import { anyChar } from './byte.js'
|
||||
|
||||
/** @import { ParseError, ParserState } from './state.js' */
|
||||
/** @import { Result } from '../vendor/kojima/src/index.js' */
|
||||
|
||||
export const takeWhile = curry((predicate, state) => {
|
||||
let result = ok(state)
|
||||
|
||||
while (result.isOk()) {
|
||||
result = verify(anyChar, predicate, state)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
export const takeUntil = curry((parser, state) => not(takeWhile(parser, state)))
|
||||
|
||||
export const skip = curry((parser, state) => {
|
||||
const tokens = state[0]
|
||||
const result = parser(state)
|
||||
if (result.isOk()) {
|
||||
return result.map(other => [tokens, other[1]])
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
export const many = curry((parser, state) => {
|
||||
let result = ok(state)
|
||||
|
||||
while (true) {
|
||||
const res = parser(clone(state))
|
||||
if (res.isOk()) {
|
||||
result = res
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
export const many1 = parser => seq(parser, many(parser))
|
||||
|
71
src/seq.js
71
src/seq.js
|
@ -1,71 +0,0 @@
|
|||
import { clone, diff, fail, succeed, Tuple } from './fn.js'
|
||||
import { anyChar } from './char.js'
|
||||
import { ok } from 'kojima'
|
||||
import { curry } from 'izuna'
|
||||
|
||||
/** @import { ParseError, ParserState } from './state.js' */
|
||||
/** @import { Result } from '../vendor/kojima/src/index.js' */
|
||||
|
||||
/**
|
||||
* @typedef {(value: any) => Result<ParserState, ParseError>} Parser
|
||||
*/
|
||||
|
||||
export const take = n => state => {
|
||||
let result = anyChar(state)
|
||||
for (let i = n; i > 0; i--) {
|
||||
if (result.isErr()) {
|
||||
return result.chain(e => fail(`"take(${n})" failed`, state, e))
|
||||
}
|
||||
|
||||
result = result.chain(anyChar)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const skip = parser => state => {
|
||||
const tokens = state[0]
|
||||
const result = parser(state)
|
||||
if (result.isOk()) {
|
||||
return result.map(other => [tokens, other[1]])
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.chain(parser)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}
|
||||
|
||||
|
||||
export const many = curry((parser, state) => {
|
||||
let result = ok(state)
|
||||
|
||||
while (true) {
|
||||
const res = parser(clone(state))
|
||||
if (res.isOk()) {
|
||||
result = res
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
export const many1 = parser => seq(parser, many(parser))
|
||||
|
57
src/state.js
57
src/state.js
|
@ -1,8 +1,9 @@
|
|||
import { Iter } from './iter.js'
|
||||
import { seq } from './seq.js'
|
||||
import { until } from './cond.js'
|
||||
import { eof } from './combinator.js'
|
||||
import { curry, pipe } from 'izuna'
|
||||
import { ok } from 'kojima'
|
||||
import { chain, compose, curry, pipe } from 'izuna'
|
||||
import { eof } from './byte.js'
|
||||
import { Tuple } from './tuple.js'
|
||||
import { iter } from './fn.js'
|
||||
import { Clone } from './clone.js'
|
||||
|
||||
/**
|
||||
* @typedef {Readonly<[any[], Iterator<any>]>} ParserState
|
||||
|
@ -20,18 +21,40 @@ export class ParseError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value
|
||||
* @returns {ParserState}
|
||||
*/
|
||||
export const State = value => Object.freeze([[], Iter.from(value)])
|
||||
export class State extends Tuple {
|
||||
constructor(remaining, read = []) {
|
||||
super([['read', read], ['remaining', remaining]])
|
||||
}
|
||||
|
||||
export const parse = curry((parser, input) => parser(State(input)))
|
||||
export const parseAll = curry((parser, input) => pipe(
|
||||
State,
|
||||
seq(
|
||||
parser,
|
||||
until(eof)
|
||||
)
|
||||
static from(values) {
|
||||
return new State(iter(values))
|
||||
}
|
||||
|
||||
next() {
|
||||
return this.remaining.next()
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `State(${this.read}, ${[...this.clone().remaining]})`
|
||||
}
|
||||
|
||||
[Clone]() {
|
||||
return Object.freeze(new State(this.remaining.clone(), this.read))
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return super[Symbol.iterator]()
|
||||
}
|
||||
}
|
||||
|
||||
export const state = (remaining, read) => new State(remaining, read)
|
||||
|
||||
export const parse = curry((parser, input) => pipe(
|
||||
iter,
|
||||
state,
|
||||
ok,
|
||||
chain(parser),
|
||||
)(input))
|
||||
|
||||
export const parseAll = curry((parser, input) => compose(eof, parse(parser), input))
|
||||
|
||||
|
|
61
src/tuple.js
Normal file
61
src/tuple.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { entries } from 'izuna'
|
||||
import { Clone } from './clone.js'
|
||||
import { IndexableIterator } from './iter.js'
|
||||
|
||||
/**
|
||||
* @param {...any} values
|
||||
*/
|
||||
export class Tuple {
|
||||
_values
|
||||
_length
|
||||
|
||||
get length() {
|
||||
return this._length
|
||||
}
|
||||
|
||||
constructor(values) {
|
||||
if (values.length > 0) {
|
||||
this._length = values.length
|
||||
values.forEach(this._setEntry.bind(this))
|
||||
}
|
||||
|
||||
this._values = values
|
||||
}
|
||||
|
||||
_setEntry([key, value], index) {
|
||||
if (typeof key !== 'number') {
|
||||
Object.defineProperty(this, key, {
|
||||
get() {
|
||||
return this[index]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this[index] = value
|
||||
}
|
||||
|
||||
static from(values) {
|
||||
return Object.freeze(new this(entries(values)))
|
||||
}
|
||||
|
||||
|
||||
toString() {
|
||||
return `(${this._values.map(([_, v]) => v.toString())})`
|
||||
}
|
||||
|
||||
clone() {
|
||||
return this[Clone]()
|
||||
}
|
||||
|
||||
[Clone]() {
|
||||
return Object.freeze(new this.constructor(this._values))
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return new IndexableIterator(this._values.map(([_, v]) => v))
|
||||
}
|
||||
}
|
||||
|
||||
export const tuple = Tuple.from
|
||||
|
||||
|
81
tests/units/bytes.js
Normal file
81
tests/units/bytes.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { it, assert, assertEq } from 'folktest'
|
||||
import { char, charNoCase, noneOf, oneOf, parse, tag, tagNoCase, take, takeWhile } from '../../src/index.js'
|
||||
import { Alphanumeric } from '../../src/const.js'
|
||||
import { chain, curry, map } from 'izuna'
|
||||
|
||||
const assertState = expected => state => {
|
||||
assertEq(expected, state[0])
|
||||
}
|
||||
|
||||
const parseEq = curry((parser, input, expected) =>
|
||||
chain(assertState(expected), parse(parser, input))
|
||||
)
|
||||
|
||||
const parseErr = curry((parser, input) =>
|
||||
assert(parse(parser, input).isErr(), `expected an error but "${input}" parsed successfully`)
|
||||
)
|
||||
|
||||
export const Byte = [
|
||||
it('char', () => {
|
||||
const parser = char('a')
|
||||
|
||||
parseEq(parser, 'abc', ['a'])
|
||||
assert(parse(parser, ' abc').isErr())
|
||||
assert(parse(parser, 'bc').isErr())
|
||||
assert(parse(parser, '').isErr())
|
||||
}),
|
||||
|
||||
it('oneOf', () => {
|
||||
parse(oneOf('abc'), 'b').chain(assertState('b'))
|
||||
assert(parse(oneOf('a'), 'bc').isErr())
|
||||
assert(parse(oneOf('a'), '').isErr())
|
||||
}),
|
||||
|
||||
it('noneOf', () => {
|
||||
parse(noneOf('abc'), 'z').chain(assertState(['z']))
|
||||
assert(parse(noneOf('ab'), 'a').isErr())
|
||||
assert(parse(noneOf('ab'), '').isErr())
|
||||
}),
|
||||
|
||||
it('tag', () => {
|
||||
const parser = tag('Hello')
|
||||
parse(parser, 'Hello, World!').chain(assertState(['Hello']))
|
||||
assert(parse(parser, 'Something').isErr())
|
||||
assert(parse(parser, '').isErr())
|
||||
}),
|
||||
|
||||
it('charNoCase', () => {
|
||||
const parser = charNoCase('a')
|
||||
parse(parser, 'abc').chain(assertState(['a']))
|
||||
parse(parser, 'Abc').chain(assertState(['A']))
|
||||
assert(parse(parser, ' abc').isErr())
|
||||
assert(parse(parser, 'bc').isErr())
|
||||
assert(parse(parser, '').isErr())
|
||||
}),
|
||||
|
||||
it('tagNoCase', () => {
|
||||
const parser = tagNoCase('hello')
|
||||
parse(parser, 'Hello, World!').chain(assertState(['Hello']))
|
||||
parse(parser, 'hello, World!').chain(assertState(['hello']))
|
||||
parse(parser, 'HeLlO, World!').chain(assertState(['HeLlO']))
|
||||
assert(parse(parser, 'Something').isErr())
|
||||
assert(parse(parser, '').isErr())
|
||||
}),
|
||||
|
||||
it('take', () => {
|
||||
const parser = take(6)
|
||||
parse(parser, '1234567').chain(assertState('123456'.split('')))
|
||||
parse(parser, 'things').chain(assertState('things'.split('')))
|
||||
assert(parse(parser, 'short').isErr())
|
||||
assert(parse(parser, '').isErr())
|
||||
}),
|
||||
|
||||
it('takeWhile', () => {
|
||||
const parser = takeWhile(x => Alphanumeric.includes(x))
|
||||
parse(parser, 'latin123').chain(assertState(['latin']))
|
||||
parse(parser, '123').chain(assertState([]))
|
||||
parse(parser, 'latin').chain(assertState(['latin']))
|
||||
parse(parser, '').chain(assertState([]))
|
||||
})
|
||||
]
|
||||
|
|
@ -1,10 +1,3 @@
|
|||
import { it, assert } from 'folktest'
|
||||
import { map, not, until, char, parse, State, seq, str } from '../../src/index.js'
|
||||
import { mapStr, join } from '../../src/fn.js'
|
||||
|
||||
export const Tests = [
|
||||
it('whatever', () => {
|
||||
|
||||
})
|
||||
]
|
||||
export * from './bytes.js'
|
||||
export * from './iter.js'
|
||||
|
||||
|
|
21
tests/units/iter.js
Normal file
21
tests/units/iter.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { it, assert, assertEq } from 'folktest'
|
||||
import { IndexableIterator } from '../../src/iter.js'
|
||||
import { state } from '../../src/state.js'
|
||||
|
||||
export const Iterator = [
|
||||
it('should be cloneable', () => {
|
||||
const hi = new IndexableIterator('hi :3')
|
||||
const hihi = hi.clone()
|
||||
|
||||
assertEq(hi.next().value, 'h')
|
||||
|
||||
assertEq(hi.next().value, 'i')
|
||||
assertEq(hihi.next().value, 'h')
|
||||
}),
|
||||
|
||||
it('should have iterator helpers', () => {
|
||||
const aaaaa = new IndexableIterator('awawawawa').filter(x => x !== 'w')
|
||||
assertEq([...aaaaa].join(''), 'aaaaa')
|
||||
})
|
||||
]
|
||||
|
Loading…
Add table
Reference in a new issue