UNPKG

@masala/parser

Version:
329 lines (282 loc) 8.24 kB
/* * Masala Parser * https://github.com/masala/masala-parser * * Copyright (c) 2016-2025 Didier Plaindoux & Nicolas Zozol * Licensed under the LGPL3 license. */ /* * Parsec: Direct Style Monadic Parser Combinators For The Real World * * http://research.microsoft.com/en-us/um/people/daan/download/papers/parsec-paper.pdf */ import stream from '../stream/index.js' import option from '../data/option.js' import response from './response.js' import unit from '../data/unit.js' import { NEUTRAL, Tuple, isTuple } from '../data/tuple.js' /** * Parser class */ export default class Parser { // (Stream 'c -> number -> Response 'a 'c) -> Parser 'a 'c constructor(parse) { this.parse = parse.bind(this) } val(text) { return this.parse(stream.ofString(text)).value } // Parser 'a 'c => ('a -> Parser 'b 'c) -> Parser 'b 'c flatMap(f) { return bind(this, f) } // Parser 'a 'c => ('a -> 'b) -> Parser 'b 'c map(f) { var self = this return new Parser((input, index = 0) => self.parse(input, index).map(f)) } // Parser 'a 'c => ('a -> boolean) -> Parser 'a 'c filter(p) { var self = this return new Parser((input, index = 0) => self.parse(input, index).filter(p), ) } // Parser 'a 'c => Comparable 'a -> Parser 'a 'c match(v) { return this.filter((a) => a === v) } // Parser 'a 'c => Parser 'b 'c -> Parser ('a,'b) 'c // Parser 'a 'c => Parser 'b 'c -> Parser ('a,'b) 'c then(p) { return this.flatMap((a) => p.map((b) => new Tuple([]).append(a).append(b)), ) } single() { return this.map((tuple) => tuple.single()) } last() { return this.map((tuple) => tuple.last()) } first() { return this.map((tuple) => tuple.first()) } // Should be called only on ListParser ; Always returns an array array() { return this.map((value) => { if (!isTuple(value)) { throw 'array() is called only on TupleParser' } return value.array() }) } join(j = '') { return this.map((value) => { if (!isTuple(value)) { throw 'Error: join() is called only on TupleParser' } return value.join(j) }) } thenEos() { return this.then(eos().drop()) } eos() { return new Parser((input, index = 0) => this.parse(input, index).fold( (accept) => { return input.endOfStream(accept.offset) ? response.accept( accept.value, accept.input, accept.offset, true, ) : response.reject( accept.input, accept.offset, accept.consumed, ) }, (reject) => response.reject( reject.input, reject.offset, reject.consumed, ), ), ) } concat(p) { return this.then(p) } drop() { return this.map(() => NEUTRAL) } // Parser 'a 'c => Parser 'b 'c -> Parser 'a 'c thenLeft(p) { return this.then(p.drop()) } // Parser 'a 'c => Parser 'b 'c -> Parser 'b 'c thenRight(p) { return this.drop().then(p) } // Parser 'a 'c => 'b -> Parser 'b 'c returns(v) { return this.drop().map(() => v) } // Parser 'a 'c -> Parser 'a 'c or(p) { return choice(this, p) } /** * Must be used with F.layer() * @param p * @returns {Parser} */ and(p) { return both(this, p) } // Parser 'a 'c => unit -> Parser (Option 'a) 'c opt() { return this.map(option.some).or(returns(option.none())) } // Parser 'a 'c => unit -> Parser (List 'a) 'c rep() { return repeatable( this, () => true, (l) => l !== 0, ) } // Parser 'a 'c => number -> Parser (List 'a) 'c occurrence(occurrence) { return repeatable( this, (l) => l < occurrence, (l) => l === occurrence, ) } // Parser 'a 'c => unit -> Parser (List 'a) 'c optrep() { return repeatable( this, () => true, () => true, ) } // Parser 'a 'c => Parser 'b 'a -> Parser 'b 'c chain(p) { var self = this return new Parser((input, index = 0) => p.parse(stream.buffered(stream.ofParser(self, input)), index), ) } /** * Prints a hint if the parser enters in this step * @param hint * @param details * @returns the equivalent Parser */ // TODO: set details default at false; check tests debug(hint, details = true) { var f = (p) => { if (details) { console.log('[debug] : ', hint, p) } else { console.log('[debug] : ', hint) } return p } return this.map(f) } } // Response 'a 'c -> ('a -> Parser 'b 'c) -> Response 'b 'c function bindAccepted(accept_a, f) { return f(accept_a.value) .parse(accept_a.input, accept_a.offset) .fold( (accept_b) => response.accept( accept_b.value, accept_b.input, accept_b.offset, accept_a.consumed || accept_b.consumed, ), (reject_b) => response.reject( reject_b.input, reject_b.offset, accept_a.consumed || reject_b.consumed, ), ) } // Parser 'a 'c -> ('a -> Parser 'b 'c) -> Parser 'b 'c function bind(self, f) { return new Parser((input, index = 0) => self.parse(input, index).fold( (accept_a) => bindAccepted(accept_a, f), (reject_a) => reject_a, ), ) } // Parser 'a 'c -> Parser 'a 'c -> Parser 'a 'c // TODO logger representing which choice is made function choice(self, f) { return new Parser((input, index = 0) => self.parse(input, index).fold( (accept) => accept, (reject) => (reject.consumed ? reject : f.parse(input, index)), ), ) } // Parser 'a 'c -> Parser 'a 'c -> Parser 'a 'c function both(self, f) { return new Parser((input, index = 0) => self.parse(input, index).fold( (accept) => f.parse(input, index).map((r) => accept.value.append(r)), (reject) => reject, ), ) } // Parser 'a 'c -> unit -> Parser (List 'a) 'c function repeatable(self, occurrences, accept) { return new Parser((input, index = 0) => { var consumed = false, value = new Tuple([]), offset = index, current = self.parse(input, index), occurrence = 0 while (current.isAccepted() && occurrences(occurrence)) { occurrence += 1 value = value.append(current.value) consumed = consumed || current.consumed offset = current.offset current = self.parse(input, current.offset) } if (accept(occurrence)) { return response.accept(value, input, offset, consumed) } return response.reject(input, offset, consumed) }) } /* * Builders */ function returns(v) { return new Parser((input, index = 0) => response.accept(v, input, index, false), ) } // unit -> Parser unit 'c export function eos() { return new Parser((input, index = 0) => { if (input.endOfStream(index)) { return response.accept(unit, input, index, false) } else { return response.reject(input, index, false) } }) }