diff --git a/src/fn.js b/src/fn.js new file mode 100644 index 0000000..b7996b5 --- /dev/null +++ b/src/fn.js @@ -0,0 +1,42 @@ +import { Result } from './result.js' + +export function curry(func) { + return function curried(...args) { + if (args.length >= func.length) { + return func.apply(this, args) + } else { + return function(...args2) { + return curried.apply(this, args.concat(args2)) + } + } + } +} + +// predicates +export const and = (...booleans) => booleans.every(identity) +export const or = (...booleans) => booleans.some(identity) +export const isOk = r => !is(Result, r) || r.isOk() + +export const of = value => Array.isArray(value) ? value : [value] +export const head = value => value[0] +export const tail = value => value.slice(1) +export const prop = curry((p, v) => v[p]) +export const apply = curry((v, f) => f(v)) +export const reduce = curry((fn, init, v) => v.reduce(fn, init)) +export const map = curry((fn, v) => v.map(fn)) +export const filter = curry((fn, v) => v.filter(fn)) +export const pipe = (...f) => v => f.reduce(apply, v) + +export const identity = x => x + +export const ifElse = curry((p, ft, ff, v) => p(v) ? ft(v) : ff(v)) +export const when = curry((p, f, v) => ifElse(p, f, identity, v)) +export const unless = curry((p, f, v) => ifElse(p, identity, f, v)) + +export const isNil = v => !v + +export const take = curry((n, value) => value.slice(0, n)) +export const drop = curry((n, value) => value.slice(n)) + +export const is = curry((type, value) => value instanceof type) + diff --git a/src/parser.js b/src/parser.js new file mode 100644 index 0000000..d812609 --- /dev/null +++ b/src/parser.js @@ -0,0 +1,161 @@ +import { Result } from './result.js' +import { Iterator, Peekable, Stream } from './stream.js' +import { curry, is, of, tryPipe } 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)]) + +/** + * Create a Peekable from an existing ParserState + * @param {ParserState} + * @return {Peekable} + */ +const peekable = ([a, b]) => ([a, new Peekable(b)]) + +/** + * 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() +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) => { + const peekState = peekable(state) + for (const parser of parsers) { + const result = parser(peekState) + if (result.isOk()) { + return result + } + } + + return fail('no matching parsers', state) +} + +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 { + return succeed(fn(result.value, 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) => + 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))) + diff --git a/src/query-old.js b/src/query-old.js new file mode 100644 index 0000000..0c6eb9d --- /dev/null +++ b/src/query-old.js @@ -0,0 +1,49 @@ +import { defineQuery, hasComponent } from 'bitecs' +/// danm this rules !!! +const re = /\((?\w+)?:(?\w+)\)(-\[(?\w+)?:(?\w+)\]->\((?\w+)?:(?\w+)\))*/ + +const node = (name, label) => ({ name, label }) + +const parse = s => { + const result = s.match(re) + const groups = result.groups + + return { + from: node(groups.fromName || 'a', groups.fromLabel), + to: node(groups.toName || 'b', groups.toLabel), + edge: node(groups.edgeName, groups.edgeLabel) + } +} + +export const query = (world, s) => { + const meta = parse(s) + const from = world.components[meta.from.label] + const to = world.components[meta.to.label] + const edge = world.components[meta.edge.label] + + const fromQuery = defineQuery([from]) + const toQuery = defineQuery([to]) + const edgeQuery = defineQuery([edge]) + + return (world) => { + const result = [] + + const fq = fromQuery(world) + const tq = toQuery(world) + const eq = edgeQuery(world) + + for(const feid of fq) { + const targets = [] + + for(const eeid of eq) { + if(edge.from[eeid] === feid && hasComponent(world, to, edge.to[eeid])) { + targets.push(edge.to[eeid]) + } + } + + result.push({ [feid]: targets }) + } + + return result + } +} diff --git a/src/query.js b/src/query.js index 0c6eb9d..4659096 100644 --- a/src/query.js +++ b/src/query.js @@ -1,49 +1,6 @@ -import { defineQuery, hasComponent } from 'bitecs' -/// danm this rules !!! -const re = /\((?\w+)?:(?\w+)\)(-\[(?\w+)?:(?\w+)\]->\((?\w+)?:(?\w+)\))*/ +import { } from './parser.js' -const node = (name, label) => ({ name, label }) +// MATCH (a:Label)-[e:Label]->(b:Label) +// RETURN a, b -const parse = s => { - const result = s.match(re) - const groups = result.groups - return { - from: node(groups.fromName || 'a', groups.fromLabel), - to: node(groups.toName || 'b', groups.toLabel), - edge: node(groups.edgeName, groups.edgeLabel) - } -} - -export const query = (world, s) => { - const meta = parse(s) - const from = world.components[meta.from.label] - const to = world.components[meta.to.label] - const edge = world.components[meta.edge.label] - - const fromQuery = defineQuery([from]) - const toQuery = defineQuery([to]) - const edgeQuery = defineQuery([edge]) - - return (world) => { - const result = [] - - const fq = fromQuery(world) - const tq = toQuery(world) - const eq = edgeQuery(world) - - for(const feid of fq) { - const targets = [] - - for(const eeid of eq) { - if(edge.from[eeid] === feid && hasComponent(world, to, edge.to[eeid])) { - targets.push(edge.to[eeid]) - } - } - - result.push({ [feid]: targets }) - } - - return result - } -} diff --git a/src/stream.js b/src/stream.js new file mode 100644 index 0000000..3027fba --- /dev/null +++ b/src/stream.js @@ -0,0 +1,115 @@ +export class NotImplementedError extends Error { + constructor(msg) { + super(msg) + } +} + +export class Iterator { + static handledTypes = new Map() + + static from(value) { + if (!value) { return } + const types = Iterator.handledTypes + const ctr = value.constructor || value.constructor + + if (types.has(ctr)) { + const handler = types.get(ctr) + return new handler(value) + } else { + throw new NotImplementedError(`${value} is not an iterator`) + } + } + + next() { + throw new NotImplementedError('Iterator::next not implemented') + } + + peek() { + throw new NotImplementedError('Iterator::peek not implemented') + } + + done() { + return !!this.peek() + } + + take(n) { + let accumulator = [] + + for (let i = 0; i < n; i++) { + accumulator.push(this.next()) + } + + return accumulator + } + + drop(n) { + this.take(n) + return this + } +} + +export class Peekable extends Iterator { + constructor(iterator) { + super() + this.iterator = iterator + } + + next() { + return this.iterator.peek() + } + + peek() { + return this.iterator.peek() + } +} + +class ArrayIterator extends Iterator { + static { Iterator.handledTypes.set(Array, ArrayIterator) } + + constructor(value) { + super() + this.value = value + } + + next() { + if (this.value.length > 0) { + return this.value.shift() + } + } + + peek() { + return this.value.length > 0 ? this.value[0] : undefined + } +} + +class StringIterator extends ArrayIterator { + static { Iterator.handledTypes.set(String, StringIterator) } + + constructor(value) { + super(value.split('')) + } +} + +export class Stream extends Iterator { + constructor(value) { + super() + if (!value.next || !typeof value.next === 'function') { + this.iterator = Iterator.from(value) + + if (!value) { + throw new Error(`${value} is not an iterator`) + } + } else { + this.iterator = value + } + } + + next() { + return this.iterator.next() + } + + peek() { + return this.iterator.peek() + } +} +