diff --git a/src/fn.js b/src/fn.js index b7996b5..a6e7886 100644 --- a/src/fn.js +++ b/src/fn.js @@ -25,6 +25,7 @@ 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 join = curry((sep, v) => v.join(sep)) export const pipe = (...f) => v => f.reduce(apply, v) export const identity = x => x diff --git a/src/parser.js b/src/parser.js index b50d5e6..b129cde 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1,6 +1,6 @@ import { Result } from './result.js' -import { Iterator, Peekable, Stream } from './stream.js' -import { curry, of } from './fn.js' +import { Iterator, Stream } from './stream.js' +import { curry, join, of } from './fn.js' class ParseError extends Error { constructor(message, state, source) { @@ -39,13 +39,7 @@ class ParseError extends Error { * @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)]) +const clone = ([a, b]) => ([a.slice(), b.clone()]) /** * Update ParserState with parsed tokens @@ -69,6 +63,11 @@ const succeed = curry((v, [p, rest]) => Result.Ok([p.concat(of(v)), rest])) 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))) @@ -91,9 +90,8 @@ export const seq = (...parsers) => state => { } export const any = (...parsers) => (state) => { - const peekState = peekable(state) for (const parser of parsers) { - const result = parser(peekState) + const result = parser(clone(state)) if (result.isOk()) { return result } @@ -102,6 +100,32 @@ export const any = (...parsers) => (state) => { 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) @@ -111,7 +135,8 @@ export const map = curry((fn, parser, state) => { const backtrack = [parsed, stream] try { - return succeed(fn(result.value, backtrack)) + const parsedValue = diff(state, result.value) + return succeed(fn(parsedValue), backtrack) } catch (e) { return fail('map failed', state, e) } @@ -121,7 +146,6 @@ export const map = curry((fn, parser, state) => { }) - export const char = curry((ch, state) => { return next(state) === ch ? succeed(ch, state) : fail(`could not parse ${ch}`, state) }) @@ -131,7 +155,7 @@ export const noCaseChar = curry((ch, state) => ( )) export const string = curry((str, state) => - seq(...tokenizeInto(char, str))(state) + map(join(''), seq(...tokenizeInto(char, str)))(state) ) export const anyChar = curry((str, state) => diff --git a/src/query.js b/src/query.js index 4659096..8d4e56b 100644 --- a/src/query.js +++ b/src/query.js @@ -1,6 +1,73 @@ -import { } from './parser.js' +import { join } from './fn.js' +import { alpha, alphanumeric, any, char, many, map, noCaseString, parse, seq, skip } from './parser.js' // MATCH (a:Label)-[e:Label]->(b:Label) // RETURN a, b +const Identifier = (name, label) => ({ name, label }) + +const ObjectType = Object.freeze({ + Node: 0, + Edge: 1 +}) + +const GraphObject = ({ name, label }, type, properties = []) => Object.freeze({ + name, + label, + type, + properties +}) + +const whitespace = char(' ') +const matchKeyword = noCaseString('match') +const returnKeyword = noCaseString('return') + +const name = map( + join(''), + seq(alpha, many(alphanumeric)) +) +const colon = char(':') +const label = seq(skip(colon), name) + +const id = any( + map( + ([name, label]) => Identifier(name, label), + seq(name, label) + ), + map( + ([name]) => Identifier(name), + name + ), + map( + ([label]) => Identifier(undefined, label), + label + ) +) + +const lparen = char('(') +const rparen = char(')') +const node = map( + ([id]) => GraphObject(id, ObjectType.Node), + seq(skip(lparen), id, skip(rparen)) +) + +const lsquare = char('[') +const rsquare = char(']') +const edge = map( + ([id]) => GraphObject(id, ObjectType.Edge), + seq(skip(lsquare), id, skip(rsquare)) +) + +const hyphen = char('-') +const rchevron = char('>') +const arrow = seq(hyphen, rchevron) +const relationship = seq(skip(hyphen), edge, skip(arrow), node) + +const matchParameters = seq(node, many(relationship)) +const matchStmt = seq(skip(matchKeyword), skip(many(whitespace)), matchParameters) + +const query = 'MATCH (:Label)-[e:Label]->(b:Label)' +const result = parse(matchStmt, query) +console.log(result.value[0]) + diff --git a/src/stream.js b/src/stream.js index 1333f81..51fc305 100644 --- a/src/stream.js +++ b/src/stream.js @@ -16,7 +16,7 @@ export class Iterator { const handler = types.get(ctr) return new handler(value) } else { - throw new NotImplementedError(`${value} is not an iterator`) + throw new NotImplementedError(`${value} is not an Iterator`) } } @@ -28,6 +28,10 @@ export class Iterator { throw new NotImplementedError('Iterator::peek not implemented') } + clone() { + throw new NotImplementedError('Iterator::clone not implemented') + } + done() { return !!this.peek() } @@ -48,37 +52,35 @@ export class Iterator { } } -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) } + #index = -1 + #value - constructor(value) { + constructor(value, index = -1) { super() - this.value = value + + if (!Array.isArray) { + throw new Error(`${value} is not an Array`) + } + + this.#index = index + this.#value = value } next() { - if (this.value.length > 0) { - return this.value.shift() + if (this.#index <= this.#value.length) { + this.#index += 1 + return this.#value[this.#index] } } peek() { - return this.value.length > 0 ? this.value[0] : undefined + return this.#value[this.#index] + } + + clone() { + return new ArrayIterator(this.#value, this.#index) } } @@ -111,5 +113,9 @@ export class Stream extends Iterator { peek() { return this.iterator.peek() } + + clone() { + return new Stream(this.iterator.clone()) + } }