diff --git a/dist/regexer.js b/dist/regexer.js index f6bbc80..ec2104a 100644 --- a/dist/regexer.js +++ b/dist/regexer.js @@ -1,10 +1,105 @@ +/** @template T */ +class PairMap { + + /** @type {Map, Map, T>>} */ + #map = new Map() + + /** + * @param {Parser} first + * @param {Parser} second + */ + get(first, second) { + return this.#map.get(first)?.get(second) + } + + /** + * @param {Parser} first + * @param {Parser} second + * @param {T} value + */ + set(first, second, value) { + let map = this.#map.get(first); + if (!map) { + map = new Map(); + this.#map.set(first, map); + } + map.set(second, value); + return this + } + + /** + * @param {Parser} first + * @param {Parser} second + * @param {T} value + */ + setGet(first, second, value) { + this.set(first, second, value); + return value + } +} + +/** + * @template Value + * @typedef {{ + * status: Boolean, + * value: Value, + * position: Number, + * }} Result + */ + + +class Reply { + + /** + * @template Value + * @param {Number} position + * @param {Value} value + */ + static makeSuccess(position, value) { + return /** @type {Result} */({ + status: true, + value: value, + position: position, + }) + } + + /** + * @template Value + * @param {Number} position + */ + static makeFailure(position) { + return /** @type {Result} */({ + status: false, + value: null, + position: position, + }) + } + + /** @param {Regexer>} regexer */ + static makeContext(regexer, input = "") { + return /** @type {Context} */({ + regexer: regexer, + input: input, + equals: new PairMap(), + visited: new Set(), + }) + } +} + /** @template T */ class Parser { + static isTerminal = false static indentation = " " + /** @type {Boolean?} */ + #matchesEmptyFlag + + /** @type {Parser[]} */ + #starterList + /** Calling parse() can make it change the overall parsing outcome */ - static isActualParser = true + isActualParser = true /** * @param {Result} a @@ -21,6 +116,56 @@ class Parser { }) } + matchesEmpty() { + if (this.#matchesEmptyFlag === undefined) { + return this.#matchesEmptyFlag = this.doMatchesEmpty() + } + return this.#matchesEmptyFlag + } + + /** + * @protected + * @returns {Boolean} + */ + doMatchesEmpty() { + const children = this.unwrap(); + if (children.length === 1) { + return children[0].doMatchesEmpty() + } + return false + } + + /** + * List of starting terminal parsers + * @param {Parser[]} additional Additional non terminal parsers that will be considered part of the starter list when encounter even though non terminals + */ + starterList(context = Reply.makeContext(null, ""), additional = []) { + if (!this.#starterList && !context.visited.has(this)) { + context.visited.add(this); + this.#starterList = this.doStarterList(context, additional); + if (additional.length) { + this.#starterList = this.#starterList + .filter(v => !/** @type {typeof Parser} */(v.constructor).isTerminal && additional.includes(v)); + } + } + let result = this.#starterList; + if (!/** @type {typeof Parser} */(this.constructor).isTerminal && additional.includes(this)) { + result = [this, ...result]; + } + return result + } + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + let unwrapped = this.unwrap(); + return unwrapped?.length === 1 + ? unwrapped[0].starterList(context, additional) + : [] + } + /** * In an alternative, this would always match parser could might * @param {Parser} parser @@ -58,8 +203,7 @@ class Parser { * @returns {Parser} */ actualParser(traverse = [], opaque = []) { - const self = /** @type {typeof Parser} */(this.constructor); - let isTraversable = (!self.isActualParser || traverse.find(type => this instanceof type)) + let isTraversable = (!this.isActualParser || traverse.find(type => this instanceof type)) && !opaque.find(type => this instanceof type); let unwrapped = isTraversable ? this.unwrap() : undefined; isTraversable &&= unwrapped?.length === 1; @@ -73,8 +217,7 @@ class Parser { * @returns {Parser} */ withActualParser(other, traverse = [], opaque = []) { - const self = /** @type {typeof Parser} */(this.constructor); - let isTraversable = (!self.isActualParser || traverse.some(type => this instanceof type)) + let isTraversable = (!this.isActualParser || traverse.some(type => this instanceof type)) && !opaque.some(type => this instanceof type); let unwrapped = isTraversable ? this.unwrap() : undefined; isTraversable &&= unwrapped?.length === 1; @@ -107,18 +250,19 @@ class Parser { lhs = rhs; rhs = temp; } - let memoized = context.visited.get(lhs, rhs); + let memoized = context.equals.get(lhs, rhs); if (memoized !== undefined) { return memoized } else if (memoized === undefined) { - context.visited.set(lhs, rhs, true); + context.equals.set(lhs, rhs, true); memoized = lhs.doEquals(context, rhs, strict); - context.visited.set(lhs, rhs, memoized); + context.equals.set(lhs, rhs, memoized); } return memoized } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -127,95 +271,160 @@ class Parser { return false } - toString(indent = 0) { + /** @param {Context} context */ + toString(context, indent = 0) { + if (context.visited.has(this)) { + return "<...>" // Recursive parser + } + context.visited.add(this); + return this.doToString(context, indent) + } + + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return `${this.constructor.name} does not implement toString()` } } -/** @template T */ -class PairMap { +/** + * @template T + * @extends Parser + */ +class SuccessParser extends Parser { - /** @type {Map, Map, T>>} */ - #map = new Map() + static isTerminal = true + static instance = new SuccessParser("") + + #value + + /** @param {T} value */ + constructor(value) { + super(); + this.#value = value; + } + + /** @protected */ + doMatchesEmpty() { + return true + } /** - * @param {Parser} first - * @param {Parser} second + * @protected + * @param {Context} context */ - get(first, second) { - return this.#map.get(first)?.get(second) + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return [SuccessParser.instance] } /** - * @param {Parser} first - * @param {Parser} second - * @param {T} value + * @param {Context} context + * @param {Number} position */ - set(first, second, value) { - let map = this.#map.get(first); - if (!map) { - map = new Map(); - this.#map.set(first, map); - } - map.set(second, value); - return this + parse(context, position) { + return Reply.makeSuccess(position, this.#value) } /** - * @param {Parser} first - * @param {Parser} second - * @param {T} value + * @protected + * @param {Context} context + * @param {Parser} other + * @param {Boolean} strict */ - setGet(first, second, value) { - this.set(first, second, value); - return value + doEquals(context, other, strict) { + return other instanceof SuccessParser + } + + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return "" } } /** - * @template Value - * @typedef {{ - * status: Boolean, - * value: Value, - * position: Number, - * }} Result + * @template {String} T + * @extends {Parser} */ +class StringParser extends Parser { + static isTerminal = true -class Reply { + #value + get value() { + return this.#value + } + + /** @param {T} value */ + constructor(value) { + super(); + this.#value = value; + } + + /** @protected */ + doMatchesEmpty() { + return this.#value === "" + } /** - * @template Value - * @param {Number} position - * @param {Value} value + * @protected + * @param {Context} context */ - static makeSuccess(position, value) { - return /** @type {Result} */({ - status: true, - value: value, - position: position, - }) + doStarterList(context, additional = /** @type {Parser[]} */([])) { + if (this.#value === "") { + return [SuccessParser.instance] + } + return [this] } /** - * @template Value + * In an alternative, this would always match parser could might + * @param {Parser} parser + */ + dominates(parser) { + parser = parser.actualParser(); + if (parser instanceof StringParser) { + const otherValue = /** @type {String} */(parser.#value); + return otherValue.startsWith(this.#value) + } + } + + /** + * @param {Context} context * @param {Number} position */ - static makeFailure(position) { - return /** @type {Result} */({ - status: false, - value: null, - position: position, - }) + parse(context, position) { + const end = position + this.#value.length; + const value = context.input.substring(position, end); + return this.#value === value + ? Reply.makeSuccess(end, this.#value) + : /** @type {Result} */(Reply.makeFailure(position)) } - /** @param {Regexer>} regexer */ - static makeContext(regexer, input = "") { - return /** @type {Context} */({ - regexer: regexer, - input: input, - visited: new PairMap() - }) + /** + * @protected + * @param {Context} context + * @param {Parser} other + * @param {Boolean} strict + */ + doEquals(context, other, strict) { + return other instanceof StringParser && this.#value === other.#value + || !strict && this.#value === "" && other instanceof SuccessParser + } + + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + const inlined = this.value.replaceAll("\n", "\\n"); + return this.value.length > 1 || this.value[0] === " " + ? `"${inlined.replaceAll('"', '\\"')}"` + : inlined } } @@ -239,6 +448,27 @@ class AlternativeParser extends Parser { constructor(...parsers) { super(); this.#parsers = parsers; + if (this.#parsers.length === 1) { + this.isActualParser = false; + } + } + + /** @protected */ + doMatchesEmpty() { + return this.#parsers.some(p => p.matchesEmpty()) + } + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return this.#parsers + .flatMap(p => p.starterList(context)) + .reduce( + (acc, cur) => acc.some(p => p.equals(context, cur, true)) ? acc : (acc.push(cur), acc), + /** @type {Parser[]} */([]) + ) } unwrap() { @@ -279,6 +509,7 @@ class AlternativeParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -299,13 +530,25 @@ class AlternativeParser extends Parser { return true } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { const indentation = Parser.indentation.repeat(indent); const deeperIndentation = Parser.indentation.repeat(indent + 1); + if (this.#parsers.length === 2 && this.#parsers[1] instanceof SuccessParser) { + let result = this.#parsers[0].toString(context, indent); + if (!(this.#parsers[0] instanceof StringParser) && !context.visited.has(this.#parsers[0])) { + result = "<" + result + ">"; + } + result += "?"; + return result + } return "ALT<\n" - + this.#parsers - .map(p => deeperIndentation + p.toString(indent + 1)) - .join("\n" + deeperIndentation + "|\n") + + deeperIndentation + this.#parsers + .map(p => p.toString(context, indent + 1)) + .join("\n" + deeperIndentation + "| ") + "\n" + indentation + ">" } } @@ -334,6 +577,11 @@ class ChainedParser extends Parser { this.#fn = chained; } + /** @protected */ + doMatchesEmpty() { + return false + } + unwrap() { return [this.#parser] } @@ -361,6 +609,7 @@ class ChainedParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -371,14 +620,29 @@ class ChainedParser extends Parser { && this.#parser.equals(context, other.parser, strict) } - toString(indent = 0) { - return this.#parser.toString(indent) + " => chained" + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return this.#parser.toString(context, indent) + " => chained" } } /** @extends Parser */ class FailureParser extends Parser { + static isTerminal = true + static instance = new FailureParser() + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return [this] + } + /** * @param {Context} context * @param {Number} position @@ -388,6 +652,7 @@ class FailureParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -396,7 +661,11 @@ class FailureParser extends Parser { return other instanceof FailureParser } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return "" } } @@ -408,7 +677,7 @@ class FailureParser extends Parser { class LazyParser extends Parser { #parser - static isActualParser = false + isActualParser = false /** @type {T} */ #resolvedPraser @@ -451,6 +720,7 @@ class LazyParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -461,13 +731,19 @@ class LazyParser extends Parser { return true } other = other.resolve(); + } else if (strict) { + return false } this.resolve(); return this.#resolvedPraser.equals(context, other, strict) } - toString(indent = 0) { - return this.resolve().toString(indent) + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return this.resolve().toString(context, indent) } } @@ -536,6 +812,7 @@ class LookaroundParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -547,8 +824,12 @@ class LookaroundParser extends Parser { && this.#parser.equals(context, other.#parser, strict) } - toString(indent = 0) { - return "(" + this.#type + this.#parser.toString(indent) + ")" + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return "(" + this.#type + this.#parser.toString(context, indent) + ")" } } @@ -559,8 +840,6 @@ class LookaroundParser extends Parser { */ class MapParser extends Parser { - static isActualParser = false - #parser get parser() { return this.#parser @@ -571,6 +850,8 @@ class MapParser extends Parser { return this.#mapper } + isActualParser = false + /** * @param {P} parser * @param {(v: ParserValue

) => R} mapper @@ -607,6 +888,7 @@ class MapParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -617,12 +899,16 @@ class MapParser extends Parser { && this.#parser.equals(context, other.#parser, strict) } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { let serializedMapper = this.#mapper.toString(); if (serializedMapper.length > 80 || serializedMapper.includes("\n")) { serializedMapper = "( ... ) => { ... }"; } - return this.#parser.toString(indent) + ` -> map<${serializedMapper}>` + return this.#parser.toString(context, indent) + ` -> map<${serializedMapper}>` } } @@ -632,6 +918,8 @@ class MapParser extends Parser { */ class RegExpParser extends Parser { + static isTerminal = true + /** @type {RegExp} */ #regexp get regexp() { @@ -665,6 +953,14 @@ class RegExpParser extends Parser { this.#group = group; } + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return [this] + } + isFullyGenerated() { return this.regexpFullyGenerated } @@ -681,6 +977,7 @@ class RegExpParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -691,7 +988,11 @@ class RegExpParser extends Parser { && this.#regexp.source === other.#regexp.source } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return "/" + this.#regexp.source + "/" } } @@ -711,6 +1012,29 @@ class SequenceParser extends Parser { constructor(...parsers) { super(); this.#parsers = parsers; + if (this.#parsers.length === 1) { + this.isActualParser = false; + } + } + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + const result = this.#parsers[0].starterList(context); + for (let i = 1; i < this.#parsers.length && this.#parsers[i - 1].matchesEmpty(); ++i) { + this.#parsers[i].starterList(context).reduce( + (acc, cur) => acc.some(p => p.equals(context, cur, true)) ? acc : (acc.push(cur), acc), + result + ); + } + return result + } + + /** @protected */ + doMatchesEmpty() { + return this.#parsers.every(p => p.matchesEmpty()) } unwrap() { @@ -744,6 +1068,7 @@ class SequenceParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -760,111 +1085,21 @@ class SequenceParser extends Parser { return true } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { const indentation = Parser.indentation.repeat(indent); const deeperIndentation = Parser.indentation.repeat(indent + 1); return "SEQ<\n" + this.#parsers - .map(p => deeperIndentation + p.toString(indent + 1)) + .map(p => deeperIndentation + p.toString(context, indent + 1)) .join("\n") + "\n" + indentation + ">" } } -/** - * @template {String} T - * @extends {Parser} - */ -class StringParser extends Parser { - - #value - get value() { - return this.#value - } - - /** @param {T} value */ - constructor(value) { - super(); - this.#value = value; - } - - /** - * In an alternative, this would always match parser could might - * @param {Parser} parser - */ - dominates(parser) { - parser = parser.actualParser(); - if (parser instanceof StringParser) { - const otherValue = /** @type {String} */(parser.#value); - return otherValue.startsWith(this.#value) - } - } - - /** - * @param {Context} context - * @param {Number} position - */ - parse(context, position) { - const end = position + this.#value.length; - const value = context.input.substring(position, end); - return this.#value === value - ? Reply.makeSuccess(end, this.#value) - : /** @type {Result} */(Reply.makeFailure(position)) - } - - /** - * @param {Context} context - * @param {Parser} other - * @param {Boolean} strict - */ - doEquals(context, other, strict) { - return other instanceof StringParser && this.#value === other.#value - } - - toString(indent = 0) { - const inlined = this.value.replaceAll("\n", "\\n"); - return this.value.length > 1 || this.value[0] === " " - ? `"${inlined.replaceAll('"', '\\"')}"` - : inlined - } -} - -/** - * @template T - * @extends Parser - */ -class SuccessParser extends Parser { - - #value - - /** @param {T} value */ - constructor(value) { - super(); - this.#value = value; - } - - /** - * @param {Context} context - * @param {Number} position - */ - parse(context, position) { - return Reply.makeSuccess(position, this.#value) - } - - /** - * @param {Context} context - * @param {Parser} other - * @param {Boolean} strict - */ - doEquals(context, other, strict) { - return other instanceof SuccessParser - } - - toString(indent = 0) { - return "" - } -} - /** * @template {Parser} T * @extends {Parser[]>} @@ -902,6 +1137,23 @@ class TimesParser extends Parser { this.#max = max; } + /** @protected */ + doMatchesEmpty() { + return this.#min === 0 + } + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + const result = this.#parser.starterList(context); + if (this.matchesEmpty() && !result.some(p => SuccessParser.instance.equals(context, p, false))) { + result.push(SuccessParser.instance); + } + return result + } + unwrap() { return [this.#parser] } @@ -945,6 +1197,7 @@ class TimesParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -957,8 +1210,12 @@ class TimesParser extends Parser { && this.#parser.equals(context, other.#parser, strict) } - toString(indent = 0) { - return this.parser.toString(indent) + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return this.parser.toString(context, indent) + ( this.#min === 0 && this.#max === 1 ? "?" : this.#min === 0 && this.#max === Number.POSITIVE_INFINITY ? "*" @@ -980,18 +1237,17 @@ class Regexer { /** @type {(new (parser: Parser) => Regexer) & typeof Regexer} */ Self - static #numberTransformer = v => Number(v) + static #numberMapper = v => Number(v) /** @param {[any, ...any]|RegExpExecArray} param0 */ static #firstElementGetter = ([v, _]) => v /** @param {[any, any, ...any]|RegExpExecArray} param0 */ static #secondElementGetter = ([_, v]) => v static #arrayFlatter = ([first, rest]) => [first, ...rest] - static #joiner = - /** @param {any} v */ - v => - v instanceof Array - ? v.join("") - : v + /** @param {any} v */ + static #joiner = v => + v instanceof Array + ? v.join("") + : v static #createEscapeable = character => String.raw`[^${character}\\]*(?:\\.[^${character}\\]*)*` static #numberRegex = /[-\+]?(?:\d*\.)?\d+/ @@ -999,19 +1255,19 @@ class Regexer { /** Parser accepting any valid decimal, possibly signed number */ static number = this.regexp(new RegExp(this.#numberRegex.source + String.raw`(?!\.)`)) - .map(this.#numberTransformer) + .map(this.#numberMapper) /** Parser accepting any digits only number */ - static numberNatural = this.regexp(/\d+/).map(this.#numberTransformer) + static numberNatural = this.regexp(/\d+/).map(this.#numberMapper) /** Parser accepting any valid decimal, possibly signed, possibly in the exponential form number */ static numberExponential = this.regexp(new RegExp( this.#numberRegex.source + String.raw`(?:[eE][\+\-]?\d+)?(?!\.)`) - ).map(this.#numberTransformer) + ).map(this.#numberMapper) /** Parser accepting any valid decimal number between 0 and 1 */ static numberUnit = this.regexp(/\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/) - .map(this.#numberTransformer) + .map(this.#numberMapper) /** Parser accepting whitespace */ static whitespace = this.regexp(/\s+/) @@ -1054,13 +1310,17 @@ class Regexer { } /** - * @param {Regexer>} lhs - * @param {Regexer>} rhs + * @param {Regexer> | Parser} lhs + * @param {Regexer> | Parser} rhs */ static equals(lhs, rhs, strict = false) { - const a = lhs.getParser(); - const b = rhs.getParser(); - return a.equals(Reply.makeContext(lhs), b, strict) + const a = lhs instanceof Regexer ? lhs.getParser() : lhs; + const b = rhs instanceof Regexer ? rhs.getParser() : rhs; + return a.equals( + Reply.makeContext(lhs instanceof Regexer ? lhs : rhs instanceof Regexer ? rhs : null), + b, + strict + ) } getParser() { @@ -1106,11 +1366,11 @@ class Regexer { } static success(value = undefined) { - return new this(new SuccessParser(value)) + return new this(value === undefined ? SuccessParser.instance : new SuccessParser(value)) } static failure() { - return new this(new FailureParser()) + return new this(FailureParser.instance) } // Combinators @@ -1235,7 +1495,8 @@ class Regexer { } toString(indent = 0, newline = false) { - return (newline ? "\n" + Parser.indentation.repeat(indent) : "") + this.#parser.toString(indent) + return (newline ? "\n" + Parser.indentation.repeat(indent) : "") + + this.#parser.toString(Reply.makeContext(this, ""), indent) } } diff --git a/dist/regexer.min.js b/dist/regexer.min.js index bdd59be..13f24d4 100644 --- a/dist/regexer.min.js +++ b/dist/regexer.min.js @@ -1 +1 @@ -class e{static indentation=" ";static isActualParser=!0;static mergeResults(e,t){return t?{status:e.status,position:e.position,value:e.value}:e}dominates(e){}unwrap(){return[]}wrap(...e){return null}parse(e,t){return null}actualParser(e=[],t=[]){let r=(!this.constructor.isActualParser||e.find((e=>this instanceof e)))&&!t.find((e=>this instanceof e)),s=r?this.unwrap():void 0;return r&&=1===s?.length,r?s[0].actualParser(e,t):this}withActualParser(e,t=[],r=[]){let s=(!this.constructor.isActualParser||t.some((e=>this instanceof e)))&&!r.some((e=>this instanceof e)),a=s?this.unwrap():void 0;return s&&=1===a?.length,s?this.wrap(a[0].withActualParser(e,t,r)):e}equals(e,t,r){let s=this;if(s===t)return!0;if(r||(s=this.actualParser(),t=t.actualParser()),t instanceof s.constructor&&!(s instanceof t.constructor)||t.resolve&&!s.resolve){const e=s;s=t,t=e}let a=e.visited.get(s,t);return void 0!==a||void 0===a&&(e.visited.set(s,t,!0),a=s.doEquals(e,t,r),e.visited.set(s,t,a)),a}doEquals(e,t,r){return!1}toString(e=0){return`${this.constructor.name} does not implement toString()`}}class t{#e=new Map;get(e,t){return this.#e.get(e)?.get(t)}set(e,t,r){let s=this.#e.get(e);return s||(s=new Map,this.#e.set(e,s)),s.set(t,r),this}setGet(e,t,r){return this.set(e,t,r),r}}class r{static makeSuccess(e,t){return{status:!0,value:t,position:e}}static makeFailure(e){return{status:!1,value:null,position:e}}static makeContext(e,r=""){return{regexer:e,input:r,visited:new t}}}class s extends e{#t=!1;get backtracking(){return this.#t}#r;get parsers(){return this.#r}constructor(...e){super(),this.#r=e}unwrap(){return[...this.#r]}wrap(...e){const t=new s(...e);return this.#t&&(t.#t=!0),t}asBacktracking(){const e=this.wrap(...this.#r);return e.#t=!0,e}parse(e,t){let s;for(let r=0;rs+e.toString(t+1))).join("\n"+s+"|\n")+"\n"+r+">"}}class a extends e{#s;get parser(){return this.#s}#a;constructor(e,t){super(),this.#s=e,this.#a=t}unwrap(){return[this.#s]}wrap(...e){return new a(e[0],this.#a)}parse(e,t){let s=this.#s.parse(e,t);return s.status?(s=this.#a(s.value,e.input,s.position)?.getParser().parse(e,s.position)??r.makeFailure(s.position),s):s}doEquals(e,t,r){return t instanceof a&&this.#a===t.#a&&this.#s.equals(e,t.parser,r)}toString(e=0){return this.#s.toString(e)+" => chained"}}class i extends e{parse(e,t){return r.makeFailure(t)}doEquals(e,t,r){return t instanceof i}toString(e=0){return""}}class n extends e{#s;static isActualParser=!1;#i;constructor(e){super(),this.#s=e}resolve(){return this.#i||(this.#i=this.#s().getParser()),this.#i}unwrap(){return[this.resolve()]}wrap(...e){const t=this.#s().constructor;return new n((()=>new t(e[0])))}parse(e,t){return this.resolve(),this.#i.parse(e,t)}doEquals(e,t,r){if(t instanceof n){if(this.#s===t.#s)return!0;t=t.resolve()}return this.resolve(),this.#i.equals(e,t,r)}toString(e=0){return this.resolve().toString(e)}}class u extends e{#s;get parser(){return this.#s}#n;get type(){return this.#n}static Type={NEGATIVE_AHEAD:"?!",NEGATIVE_BEHIND:"?80||t.includes("\n"))&&(t="( ... ) => { ... }"),this.#s.toString(e)+` -> map<${t}>`}}class h extends e{#p;get regexp(){return this.#p}#h;#o;regexpGenerated=!1;regexpFullyGenerated=!0;cyclomaticComplexity=1;constructor(e,t){super(),e instanceof RegExp?(this.#p=e,this.#h=new RegExp(`^(?:${e.source})`,e.flags)):e instanceof h&&(this.#p=e.#p,this.#h=e.#h,this.regexpGenerated=e.regexpGenerated,this.regexpFullyGenerated=e.regexpFullyGenerated,this.cyclomaticComplexity=e.cyclomaticComplexity),this.#o=t}isFullyGenerated(){return this.regexpFullyGenerated}parse(e,t){const s=this.#h.exec(e.input.substring(t));return s?r.makeSuccess(t+s[0].length,this.#o>=0?s[this.#o]:s):r.makeFailure(t)}doEquals(e,t,r){return t instanceof h&&(!r||this.#o===t.#o)&&this.#p.source===t.#p.source}toString(e=0){return"/"+this.#p.source+"/"}}class o extends e{#r;get parsers(){return this.#r}constructor(...e){super(),this.#r=e}unwrap(){return[...this.#r]}wrap(...e){return new o(...e)}parse(e,t){const s=new Array(this.#r.length),a=r.makeSuccess(t,s);for(let t=0;ts+e.toString(t+1))).join("\n")+"\n"+r+">"}}class c extends e{#c;get value(){return this.#c}constructor(e){super(),this.#c=e}dominates(e){if((e=e.actualParser())instanceof c){return e.#c.startsWith(this.#c)}}parse(e,t){const s=t+this.#c.length,a=e.input.substring(t,s);return this.#c===a?r.makeSuccess(s,this.#c):r.makeFailure(t)}doEquals(e,t,r){return t instanceof c&&this.#c===t.#c}toString(e=0){const t=this.value.replaceAll("\n","\\n");return this.value.length>1||" "===this.value[0]?`"${t.replaceAll('"','\\"')}"`:t}}class l extends e{#c;constructor(e){super(),this.#c=e}parse(e,t){return r.makeSuccess(t,this.#c)}doEquals(e,t,r){return t instanceof l}toString(e=0){return""}}class g extends e{#t=!1;get backtracking(){return this.#t}#s;get parser(){return this.#s}#l;get min(){return this.#l}#g;get max(){return this.#g}constructor(e,t=0,r=Number.POSITIVE_INFINITY){if(super(),t>r)throw new Error("Min is more than max");this.#s=e,this.#l=t,this.#g=r}unwrap(){return[this.#s]}wrap(...e){const t=new g(e[0],this.#l,this.#g);return this.#t&&(t.#t=!0),t}asBacktracking(){const e=new g(this.#s,this.#l,this.#g);return e.#t=!0,e}parse(e,t){const s=r.makeSuccess(t,[]);for(let t=0;t=this.#l?s:r;s.value.push(r.value),s.position=r.position}return s}doEquals(e,t,r){return t instanceof g&&this.#t===t.#t&&this.#l===t.#l&&this.#g===t.#g&&this.#s.equals(e,t.#s,r)}toString(e=0){return this.parser.toString(e)+(0===this.#l&&1===this.#g?"?":0===this.#l&&this.#g===Number.POSITIVE_INFINITY?"*":1===this.#l&&this.#g===Number.POSITIVE_INFINITY?"+":"{"+this.#l+(this.#l!==this.#g?",":this.#g!==Number.POSITIVE_INFINITY?this.#g:"")+"}")}}class m{#s;#m;#d=new Map;Self;static#x=e=>Number(e);static#w=([e,t])=>e;static#f=([e,t])=>t;static#S=([e,t])=>[e,...t];static#E=e=>e instanceof Array?e.join(""):e;static#k=e=>String.raw`[^${e}\\]*(?:\\.[^${e}\\]*)*`;static#v=/[-\+]?(?:\d*\.)?\d+/;static number=this.regexp(new RegExp(this.#v.source+String.raw`(?!\.)`)).map(this.#x);static numberNatural=this.regexp(/\d+/).map(this.#x);static numberExponential=this.regexp(new RegExp(this.#v.source+String.raw`(?:[eE][\+\-]?\d+)?(?!\.)`)).map(this.#x);static numberUnit=this.regexp(/\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/).map(this.#x);static whitespace=this.regexp(/\s+/);static whitespaceInline=this.regexp(/[^\S\n]+/);static whitespaceMultiline=this.regexp(/\s*?\n\s*/);static optWhitespace=this.regexp(/\s*/);static doubleQuotedString=this.regexpGroups(new RegExp(`"(${this.#k('"')})"`)).map(this.#f);static singleQuotedString=this.regexpGroups(new RegExp(`'(${this.#k("'")})'`)).map(this.#f);static backtickQuotedString=this.regexpGroups(new RegExp(`\`(${this.#k("`")})\``)).map(this.#f);constructor(e,t=!1){this.Self=this.constructor,this.#s=e,this.#m=t}static optimize(e){}static equals(e,t,s=!1){const a=e.getParser(),i=t.getParser();return a.equals(r.makeContext(e),i,s)}getParser(){return this.#s}run(e){const t=this.#s.parse(r.makeContext(this,e),0);return t.status&&t.position===e.length?t:r.makeFailure(t.position)}parse(e){const t=this.run(e);if(!t.status)throw new Error("Parsing error");return t.value}static str(e){return new this(new c(e))}static regexp(e,t=0){return new this(new h(e,t))}static regexpGroups(e){return new this(new h(e,-1))}static success(e=void 0){return new this(new l(e))}static failure(){return new this(new i)}static seq(...e){return new this(new o(...e.map((e=>e.getParser()))))}static alt(...e){return new this(new s(...e.map((e=>e.getParser()))))}static lookahead(e){return new this(new u(e.getParser(),u.Type.POSITIVE_AHEAD))}static lazy(e){return new this(new n(e))}times(e,t=e){return new this.Self(new g(this.#s,e,t))}many(){return this.times(0,Number.POSITIVE_INFINITY)}atLeast(e){return this.times(e,Number.POSITIVE_INFINITY)}atMost(e){return this.times(0,e)}opt(){return this.Self.alt(this,this.Self.success(null))}sepBy(e,t=!1){return this.Self.seq(this,this.Self.seq(e,this).map(m.#f).many()).map(m.#S)}skipSpace(){return this.Self.seq(this,this.Self.optWhitespace).map(m.#w)}map(e){return new this.Self(new p(this.#s,e))}chain(e){return new this.Self(new a(this.#s,e))}assert(e){return this.chain(((t,r,s)=>e(t,r,s)?this.Self.success(t):this.Self.failure()))}join(e=""){return this.map(m.#E)}toString(t=0,r=!1){return(r?"\n"+e.indentation.repeat(t):"")+this.#s.toString(t)}}export{m as default}; +class t{#t=new Map;get(t,e){return this.#t.get(t)?.get(e)}set(t,e,r){let s=this.#t.get(t);return s||(s=new Map,this.#t.set(t,s)),s.set(e,r),this}setGet(t,e,r){return this.set(t,e,r),r}}class e{static makeSuccess(t,e){return{status:!0,value:e,position:t}}static makeFailure(t){return{status:!1,value:null,position:t}}static makeContext(e,r=""){return{regexer:e,input:r,equals:new t,visited:new Set}}}class r{static isTerminal=!1;static indentation=" ";#e;#r;isActualParser=!0;static mergeResults(t,e){return e?{status:t.status,position:t.position,value:t.value}:t}matchesEmpty(){return void 0===this.#e?this.#e=this.doMatchesEmpty():this.#e}doMatchesEmpty(){const t=this.unwrap();return 1===t.length&&t[0].doMatchesEmpty()}starterList(t=e.makeContext(null,""),r=[]){this.#r||t.visited.has(this)||(t.visited.add(this),this.#r=this.doStarterList(t,r),r.length&&(this.#r=this.#r.filter((t=>!t.constructor.isTerminal&&r.includes(t)))));let s=this.#r;return!this.constructor.isTerminal&&r.includes(this)&&(s=[this,...s]),s}doStarterList(t,e=[]){let r=this.unwrap();return 1===r?.length?r[0].starterList(t,e):[]}dominates(t){}unwrap(){return[]}wrap(...t){return null}parse(t,e){return null}actualParser(t=[],e=[]){let r=(!this.isActualParser||t.find((t=>this instanceof t)))&&!e.find((t=>this instanceof t)),s=r?this.unwrap():void 0;return r&&=1===s?.length,r?s[0].actualParser(t,e):this}withActualParser(t,e=[],r=[]){let s=(!this.isActualParser||e.some((t=>this instanceof t)))&&!r.some((t=>this instanceof t)),a=s?this.unwrap():void 0;return s&&=1===a?.length,s?this.wrap(a[0].withActualParser(t,e,r)):t}equals(t,e,r){let s=this;if(s===e)return!0;if(r||(s=this.actualParser(),e=e.actualParser()),e instanceof s.constructor&&!(s instanceof e.constructor)||e.resolve&&!s.resolve){const t=s;s=e,e=t}let a=t.equals.get(s,e);return void 0!==a||void 0===a&&(t.equals.set(s,e,!0),a=s.doEquals(t,e,r),t.equals.set(s,e,a)),a}doEquals(t,e,r){return!1}toString(t,e=0){return t.visited.has(this)?"<...>":(t.visited.add(this),this.doToString(t,e))}doToString(t,e=0){return`${this.constructor.name} does not implement toString()`}}class s extends r{static isTerminal=!0;static instance=new s("");#s;constructor(t){super(),this.#s=t}doMatchesEmpty(){return!0}doStarterList(t,e=[]){return[s.instance]}parse(t,r){return e.makeSuccess(r,this.#s)}doEquals(t,e,r){return e instanceof s}doToString(t,e=0){return""}}class a extends r{static isTerminal=!0;#s;get value(){return this.#s}constructor(t){super(),this.#s=t}doMatchesEmpty(){return""===this.#s}doStarterList(t,e=[]){return""===this.#s?[s.instance]:[this]}dominates(t){if((t=t.actualParser())instanceof a){return t.#s.startsWith(this.#s)}}parse(t,r){const s=r+this.#s.length,a=t.input.substring(r,s);return this.#s===a?e.makeSuccess(s,this.#s):e.makeFailure(r)}doEquals(t,e,r){return e instanceof a&&this.#s===e.#s||!r&&""===this.#s&&e instanceof s}doToString(t,e=0){const r=this.value.replaceAll("\n","\\n");return this.value.length>1||" "===this.value[0]?`"${r.replaceAll('"','\\"')}"`:r}}class i extends r{#a=!1;get backtracking(){return this.#a}#i;get parsers(){return this.#i}constructor(...t){super(),this.#i=t,1===this.#i.length&&(this.isActualParser=!1)}doMatchesEmpty(){return this.#i.some((t=>t.matchesEmpty()))}doStarterList(t,e=[]){return this.#i.flatMap((e=>e.starterList(t))).reduce(((e,r)=>(e.some((e=>e.equals(t,r,!0)))||e.push(r),e)),[])}unwrap(){return[...this.#i]}wrap(...t){const e=new i(...t);return this.#a&&(e.#a=!0),e}asBacktracking(){const t=this.wrap(...this.#i);return t.#a=!0,t}parse(t,r){let s;for(let e=0;e"),r+="?",r}return"ALT<\n"+n+this.#i.map((r=>r.toString(t,e+1))).join("\n"+n+"| ")+"\n"+i+">"}}class n extends r{#n;get parser(){return this.#n}#u;constructor(t,e){super(),this.#n=t,this.#u=e}doMatchesEmpty(){return!1}unwrap(){return[this.#n]}wrap(...t){return new n(t[0],this.#u)}parse(t,r){let s=this.#n.parse(t,r);return s.status?(s=this.#u(s.value,t.input,s.position)?.getParser().parse(t,s.position)??e.makeFailure(s.position),s):s}doEquals(t,e,r){return e instanceof n&&this.#u===e.#u&&this.#n.equals(t,e.parser,r)}doToString(t,e=0){return this.#n.toString(t,e)+" => chained"}}class u extends r{static isTerminal=!0;static instance=new u;doStarterList(t,e=[]){return[this]}parse(t,r){return e.makeFailure(r)}doEquals(t,e,r){return e instanceof u}doToString(t,e=0){return""}}class p extends r{#n;isActualParser=!1;#p;constructor(t){super(),this.#n=t}resolve(){return this.#p||(this.#p=this.#n().getParser()),this.#p}unwrap(){return[this.resolve()]}wrap(...t){const e=this.#n().constructor;return new p((()=>new e(t[0])))}parse(t,e){return this.resolve(),this.#p.parse(t,e)}doEquals(t,e,r){if(e instanceof p){if(this.#n===e.#n)return!0;e=e.resolve()}else if(r)return!1;return this.resolve(),this.#p.equals(t,e,r)}doToString(t,e=0){return this.resolve().toString(t,e)}}class h extends r{#n;get parser(){return this.#n}#h;get type(){return this.#h}static Type={NEGATIVE_AHEAD:"?!",NEGATIVE_BEHIND:"?80||r.includes("\n"))&&(r="( ... ) => { ... }"),this.#n.toString(t,e)+` -> map<${r}>`}}class c extends r{static isTerminal=!0;#c;get regexp(){return this.#c}#l;#m;regexpGenerated=!1;regexpFullyGenerated=!0;cyclomaticComplexity=1;constructor(t,e){super(),t instanceof RegExp?(this.#c=t,this.#l=new RegExp(`^(?:${t.source})`,t.flags)):t instanceof c&&(this.#c=t.#c,this.#l=t.#l,this.regexpGenerated=t.regexpGenerated,this.regexpFullyGenerated=t.regexpFullyGenerated,this.cyclomaticComplexity=t.cyclomaticComplexity),this.#m=e}doStarterList(t,e=[]){return[this]}isFullyGenerated(){return this.regexpFullyGenerated}parse(t,r){const s=this.#l.exec(t.input.substring(r));return s?e.makeSuccess(r+s[0].length,this.#m>=0?s[this.#m]:s):e.makeFailure(r)}doEquals(t,e,r){return e instanceof c&&(!r||this.#m===e.#m)&&this.#c.source===e.#c.source}doToString(t,e=0){return"/"+this.#c.source+"/"}}class l extends r{#i;get parsers(){return this.#i}constructor(...t){super(),this.#i=t,1===this.#i.length&&(this.isActualParser=!1)}doStarterList(t,e=[]){const r=this.#i[0].starterList(t);for(let e=1;e(e.some((e=>e.equals(t,r,!0)))||e.push(r),e)),r);return r}doMatchesEmpty(){return this.#i.every((t=>t.matchesEmpty()))}unwrap(){return[...this.#i]}wrap(...t){return new l(...t)}parse(t,r){const s=new Array(this.#i.length),a=e.makeSuccess(r,s);for(let e=0;ea+r.toString(t,e+1))).join("\n")+"\n"+s+">"}}class m extends r{#a=!1;get backtracking(){return this.#a}#n;get parser(){return this.#n}#g;get min(){return this.#g}#d;get max(){return this.#d}constructor(t,e=0,r=Number.POSITIVE_INFINITY){if(super(),e>r)throw new Error("Min is more than max");this.#n=t,this.#g=e,this.#d=r}doMatchesEmpty(){return 0===this.#g}doStarterList(t,e=[]){const r=this.#n.starterList(t);return this.matchesEmpty()&&!r.some((e=>s.instance.equals(t,e,!1)))&&r.push(s.instance),r}unwrap(){return[this.#n]}wrap(...t){const e=new m(t[0],this.#g,this.#d);return this.#a&&(e.#a=!0),e}asBacktracking(){const t=new m(this.#n,this.#g,this.#d);return t.#a=!0,t}parse(t,r){const s=e.makeSuccess(r,[]);for(let e=0;e=this.#g?s:r;s.value.push(r.value),s.position=r.position}return s}doEquals(t,e,r){return e instanceof m&&this.#a===e.#a&&this.#g===e.#g&&this.#d===e.#d&&this.#n.equals(t,e.#n,r)}doToString(t,e=0){return this.parser.toString(t,e)+(0===this.#g&&1===this.#d?"?":0===this.#g&&this.#d===Number.POSITIVE_INFINITY?"*":1===this.#g&&this.#d===Number.POSITIVE_INFINITY?"+":"{"+this.#g+(this.#g!==this.#d?",":this.#d!==Number.POSITIVE_INFINITY?this.#d:"")+"}")}}class g{#n;#x;#w=new Map;Self;static#E=t=>Number(t);static#f=([t,e])=>t;static#S=([t,e])=>e;static#k=([t,e])=>[t,...e];static#v=t=>t instanceof Array?t.join(""):t;static#y=t=>String.raw`[^${t}\\]*(?:\\.[^${t}\\]*)*`;static#I=/[-\+]?(?:\d*\.)?\d+/;static number=this.regexp(new RegExp(this.#I.source+String.raw`(?!\.)`)).map(this.#E);static numberNatural=this.regexp(/\d+/).map(this.#E);static numberExponential=this.regexp(new RegExp(this.#I.source+String.raw`(?:[eE][\+\-]?\d+)?(?!\.)`)).map(this.#E);static numberUnit=this.regexp(/\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/).map(this.#E);static whitespace=this.regexp(/\s+/);static whitespaceInline=this.regexp(/[^\S\n]+/);static whitespaceMultiline=this.regexp(/\s*?\n\s*/);static optWhitespace=this.regexp(/\s*/);static doubleQuotedString=this.regexpGroups(new RegExp(`"(${this.#y('"')})"`)).map(this.#S);static singleQuotedString=this.regexpGroups(new RegExp(`'(${this.#y("'")})'`)).map(this.#S);static backtickQuotedString=this.regexpGroups(new RegExp(`\`(${this.#y("`")})\``)).map(this.#S);constructor(t,e=!1){this.Self=this.constructor,this.#n=t,this.#x=e}static optimize(t){}static equals(t,r,s=!1){const a=t instanceof g?t.getParser():t,i=r instanceof g?r.getParser():r;return a.equals(e.makeContext(t instanceof g?t:r instanceof g?r:null),i,s)}getParser(){return this.#n}run(t){const r=this.#n.parse(e.makeContext(this,t),0);return r.status&&r.position===t.length?r:e.makeFailure(r.position)}parse(t){const e=this.run(t);if(!e.status)throw new Error("Parsing error");return e.value}static str(t){return new this(new a(t))}static regexp(t,e=0){return new this(new c(t,e))}static regexpGroups(t){return new this(new c(t,-1))}static success(t=void 0){return new this(void 0===t?s.instance:new s(t))}static failure(){return new this(u.instance)}static seq(...t){return new this(new l(...t.map((t=>t.getParser()))))}static alt(...t){return new this(new i(...t.map((t=>t.getParser()))))}static lookahead(t){return new this(new h(t.getParser(),h.Type.POSITIVE_AHEAD))}static lazy(t){return new this(new p(t))}times(t,e=t){return new this.Self(new m(this.#n,t,e))}many(){return this.times(0,Number.POSITIVE_INFINITY)}atLeast(t){return this.times(t,Number.POSITIVE_INFINITY)}atMost(t){return this.times(0,t)}opt(){return this.Self.alt(this,this.Self.success(null))}sepBy(t,e=!1){return this.Self.seq(this,this.Self.seq(t,this).map(g.#S).many()).map(g.#k)}skipSpace(){return this.Self.seq(this,this.Self.optWhitespace).map(g.#f)}map(t){return new this.Self(new o(this.#n,t))}chain(t){return new this.Self(new n(this.#n,t))}assert(t){return this.chain(((e,r,s)=>t(e,r,s)?this.Self.success(e):this.Self.failure()))}join(t=""){return this.map(g.#v)}toString(t=0,s=!1){return(s?"\n"+r.indentation.repeat(t):"")+this.#n.toString(e.makeContext(this,""),t)}}export{g as default}; diff --git a/src/Regexer.js b/src/Regexer.js index f0b1397..a616190 100644 --- a/src/Regexer.js +++ b/src/Regexer.js @@ -94,13 +94,17 @@ export default class Regexer { } /** - * @param {Regexer>} lhs - * @param {Regexer>} rhs + * @param {Regexer> | Parser} lhs + * @param {Regexer> | Parser} rhs */ static equals(lhs, rhs, strict = false) { - const a = lhs.getParser() - const b = rhs.getParser() - return a.equals(Reply.makeContext(lhs), b, strict) + const a = lhs instanceof Regexer ? lhs.getParser() : lhs + const b = rhs instanceof Regexer ? rhs.getParser() : rhs + return a.equals( + Reply.makeContext(lhs instanceof Regexer ? lhs : rhs instanceof Regexer ? rhs : null), + b, + strict + ) } getParser() { @@ -146,11 +150,11 @@ export default class Regexer { } static success(value = undefined) { - return new this(new SuccessParser(value)) + return new this(value === undefined ? SuccessParser.instance : new SuccessParser()) } static failure() { - return new this(new FailureParser()) + return new this(FailureParser.instance) } // Combinators @@ -265,7 +269,7 @@ export default class Regexer { */ assert(fn) { return this.chain((v, input, position) => fn(v, input, position) - ? this.Self.success(v) + ? this.Self.success().map(() => v) : this.Self.failure() ) } @@ -275,6 +279,7 @@ export default class Regexer { } toString(indent = 0, newline = false) { - return (newline ? "\n" + Parser.indentation.repeat(indent) : "") + this.#parser.toString(indent) + return (newline ? "\n" + Parser.indentation.repeat(indent) : "") + + this.#parser.toString(Reply.makeContext(this, ""), indent) } } diff --git a/src/Reply.js b/src/Reply.js index ba3aef4..f595c5b 100644 --- a/src/Reply.js +++ b/src/Reply.js @@ -41,7 +41,8 @@ export default class Reply { return /** @type {Context} */({ regexer: regexer, input: input, - visited: new PairMap() + equals: new PairMap(), + visited: new Set(), }) } } diff --git a/src/parser/AlternativeParser.js b/src/parser/AlternativeParser.js index 509fd2f..1a25166 100644 --- a/src/parser/AlternativeParser.js +++ b/src/parser/AlternativeParser.js @@ -1,5 +1,7 @@ import Parser from "./Parser.js" import Reply from "../Reply.js" +import StringParser from "./StringParser.js" +import SuccessParser from "./SuccessParser.js" /** * @template {Parser[]} T @@ -21,6 +23,27 @@ export default class AlternativeParser extends Parser { constructor(...parsers) { super() this.#parsers = parsers + if (this.#parsers.length === 1) { + this.isActualParser = false + } + } + + /** @protected */ + doMatchesEmpty() { + return this.#parsers.some(p => p.matchesEmpty()) + } + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return this.#parsers + .flatMap(p => p.starterList(context)) + .reduce( + (acc, cur) => acc.some(p => p.equals(context, cur, true)) ? acc : (acc.push(cur), acc), + /** @type {Parser[]} */([]) + ) } unwrap() { @@ -61,6 +84,7 @@ export default class AlternativeParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -81,13 +105,25 @@ export default class AlternativeParser extends Parser { return true } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { const indentation = Parser.indentation.repeat(indent) const deeperIndentation = Parser.indentation.repeat(indent + 1) + if (this.#parsers.length === 2 && this.#parsers[1] instanceof SuccessParser) { + let result = this.#parsers[0].toString(context, indent) + if (!(this.#parsers[0] instanceof StringParser) && !context.visited.has(this.#parsers[0])) { + result = "<" + result + ">" + } + result += "?" + return result + } return "ALT<\n" - + this.#parsers - .map(p => deeperIndentation + p.toString(indent + 1)) - .join("\n" + deeperIndentation + "|\n") + + deeperIndentation + this.#parsers + .map(p => p.toString(context, indent + 1)) + .join("\n" + deeperIndentation + "| ") + "\n" + indentation + ">" } } diff --git a/src/parser/AnchorParser.js b/src/parser/AnchorParser.js index 4662396..7c5fd0a 100644 --- a/src/parser/AnchorParser.js +++ b/src/parser/AnchorParser.js @@ -17,6 +17,8 @@ export const AnchorType = { */ export default class AnchorParser extends Parser { + static isTerminal = true + #type /** @param {T} type */ @@ -25,6 +27,19 @@ export default class AnchorParser extends Parser { this.#type = type } + /** @protected */ + doMatchesEmpty() { + return true + } + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return [this] + } + /** * @param {Context} context * @param {Number} position @@ -43,6 +58,7 @@ export default class AnchorParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -51,7 +67,11 @@ export default class AnchorParser extends Parser { return other instanceof AnchorParser && this.#type === other.#type } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return this.#type } } diff --git a/src/parser/AnyCharParser.js b/src/parser/AnyCharParser.js index 72c85cd..7870c08 100644 --- a/src/parser/AnyCharParser.js +++ b/src/parser/AnyCharParser.js @@ -1,9 +1,10 @@ -import Reply from "../Reply.js" import RegExpParser from "./RegExpParser.js" /** @extends RegExpParser<0> */ export default class AnyCharParser extends RegExpParser { + static isTerminal = true + #dotAll /** @param {Boolean} dotAll */ @@ -13,14 +14,18 @@ export default class AnyCharParser extends RegExpParser { } /** - * In an alternative, this would always match parser could might - * @param {Parser} parser + * @protected + * @param {Context} context */ - dominates(parser) { - return this.#dotAll || !parser.actualParser().parse(Reply.makeContext("\n"), 0).status + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return [this] } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return "." } } diff --git a/src/parser/AtomicGroupParser.js b/src/parser/AtomicGroupParser.js index 13eadc4..799f84e 100644 --- a/src/parser/AtomicGroupParser.js +++ b/src/parser/AtomicGroupParser.js @@ -40,6 +40,7 @@ export default class AtomicGroupParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -48,7 +49,11 @@ export default class AtomicGroupParser extends Parser { return other instanceof AtomicGroupParser && this.#parser.equals(context, other.#parser, strict) } - toString(indent = 0) { - return "(?>" + this.#parser.toString(indent) + ")" + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return "(?>" + this.#parser.toString(context, indent) + ")" } } diff --git a/src/parser/CapturingGroupParser.js b/src/parser/CapturingGroupParser.js index 0685b16..aac6901 100644 --- a/src/parser/CapturingGroupParser.js +++ b/src/parser/CapturingGroupParser.js @@ -44,6 +44,7 @@ export default class CapturingGroupParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -54,7 +55,11 @@ export default class CapturingGroupParser extends Parser { && this.#parser.equals(context, other.#parser, strict) } - toString(indent = 0) { - return "(" + (this.#id !== "" ? `?<${this.#id}>` : "") + this.#parser.toString(indent) + ")" + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return "(" + (this.#id !== "" ? `?<${this.#id}>` : "") + this.#parser.toString(context, indent) + ")" } } diff --git a/src/parser/ChainedParser.js b/src/parser/ChainedParser.js index b9d2e5d..3370386 100644 --- a/src/parser/ChainedParser.js +++ b/src/parser/ChainedParser.js @@ -25,6 +25,11 @@ export default class ChainedParser extends Parser { this.#fn = chained } + /** @protected */ + doMatchesEmpty() { + return false + } + unwrap() { return [this.#parser] } @@ -52,6 +57,7 @@ export default class ChainedParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -62,7 +68,11 @@ export default class ChainedParser extends Parser { && this.#parser.equals(context, other.parser, strict) } - toString(indent = 0) { - return this.#parser.toString(indent) + " => chained" + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return this.#parser.toString(context, indent) + " => chained" } } diff --git a/src/parser/ClassParser.js b/src/parser/ClassParser.js index 0e94476..cebffd1 100644 --- a/src/parser/ClassParser.js +++ b/src/parser/ClassParser.js @@ -1,6 +1,6 @@ -import Reply from "../Reply.js" import AlternativeParser from "./AlternativeParser.js" import Parser from "./Parser.js" +import Reply from "../Reply.js" /** * @template {Parser[]} T @@ -45,6 +45,7 @@ export default class ClassParser extends AlternativeParser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -57,9 +58,13 @@ export default class ClassParser extends AlternativeParser { && super.doEquals(context, other, strict) } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return "[" - + this.parsers.map(p => p.toString(indent)).join("") + + this.parsers.map(p => p.toString(context, indent)).join("") + "]" } } diff --git a/src/parser/ClassShorthandParser.js b/src/parser/ClassShorthandParser.js index ff8886c..3e0715d 100644 --- a/src/parser/ClassShorthandParser.js +++ b/src/parser/ClassShorthandParser.js @@ -32,6 +32,7 @@ export default class ClassShorthandParser extends RegExpParser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -40,7 +41,11 @@ export default class ClassShorthandParser extends RegExpParser { return other instanceof ClassShorthandParser && this.#char == other.#char } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return "\\" + this.#char } } diff --git a/src/parser/EscapedCharParser.js b/src/parser/EscapedCharParser.js index ee17fed..7ddb4a1 100644 --- a/src/parser/EscapedCharParser.js +++ b/src/parser/EscapedCharParser.js @@ -43,6 +43,7 @@ export default class EscapedCharParser extends StringParser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -52,14 +53,18 @@ export default class EscapedCharParser extends StringParser { && super.doEquals(context, other, strict) } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { switch (this.#type) { case EscapedCharParser.Type.NORMAL: return "\\" + ( Object.entries(EscapedCharParser.specialEscapedCharacters) .find(([k, v]) => this.value === v) ?.[0] - ?? super.toString(indent) + ?? super.doToString(context, indent) ) case EscapedCharParser.Type.HEX: return "\\x" + this.value.codePointAt(0).toString(16) @@ -68,6 +73,6 @@ export default class EscapedCharParser extends StringParser { case EscapedCharParser.Type.UNICODE_FULL: return "\\u{" + this.value.codePointAt(0).toString(16) + "}" } - return super.toString(indent) + return super.doToString(context, indent) } } diff --git a/src/parser/FailureParser.js b/src/parser/FailureParser.js index 7337303..143e744 100644 --- a/src/parser/FailureParser.js +++ b/src/parser/FailureParser.js @@ -4,6 +4,17 @@ import Reply from "../Reply.js" /** @extends Parser */ export default class FailureParser extends Parser { + static isTerminal = true + static instance = new FailureParser() + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return [this] + } + /** * @param {Context} context * @param {Number} position @@ -13,6 +24,7 @@ export default class FailureParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -21,7 +33,11 @@ export default class FailureParser extends Parser { return other instanceof FailureParser } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return "" } } diff --git a/src/parser/LazyParser.js b/src/parser/LazyParser.js index 58f0156..b571dc3 100644 --- a/src/parser/LazyParser.js +++ b/src/parser/LazyParser.js @@ -7,7 +7,7 @@ import Parser from "./Parser.js" export default class LazyParser extends Parser { #parser - static isActualParser = false + isActualParser = false /** @type {T} */ #resolvedPraser @@ -50,6 +50,7 @@ export default class LazyParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -67,7 +68,11 @@ export default class LazyParser extends Parser { return this.#resolvedPraser.equals(context, other, strict) } - toString(indent = 0) { - return this.resolve().toString(indent) + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return this.resolve().toString(context, indent) } } diff --git a/src/parser/LookaroundParser.js b/src/parser/LookaroundParser.js index fa3a0c1..777a3bc 100644 --- a/src/parser/LookaroundParser.js +++ b/src/parser/LookaroundParser.js @@ -66,6 +66,7 @@ export default class LookaroundParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -77,7 +78,11 @@ export default class LookaroundParser extends Parser { && this.#parser.equals(context, other.#parser, strict) } - toString(indent = 0) { - return "(" + this.#type + this.#parser.toString(indent) + ")" + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return "(" + this.#type + this.#parser.toString(context, indent) + ")" } } diff --git a/src/parser/MapParser.js b/src/parser/MapParser.js index a7f760f..a0bea48 100644 --- a/src/parser/MapParser.js +++ b/src/parser/MapParser.js @@ -7,8 +7,6 @@ import Parser from "./Parser.js" */ export default class MapParser extends Parser { - static isActualParser = false - #parser get parser() { return this.#parser @@ -19,6 +17,8 @@ export default class MapParser extends Parser { return this.#mapper } + isActualParser = false + /** * @param {P} parser * @param {(v: ParserValue

) => R} mapper @@ -55,6 +55,7 @@ export default class MapParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -65,11 +66,15 @@ export default class MapParser extends Parser { && this.#parser.equals(context, other.#parser, strict) } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { let serializedMapper = this.#mapper.toString() if (serializedMapper.length > 80 || serializedMapper.includes("\n")) { serializedMapper = "( ... ) => { ... }" } - return this.#parser.toString(indent) + ` -> map<${serializedMapper}>` + return this.#parser.toString(context, indent) + ` -> map<${serializedMapper}>` } } diff --git a/src/parser/NonCapturingGroupParser.js b/src/parser/NonCapturingGroupParser.js index b0797c7..d72fa1a 100644 --- a/src/parser/NonCapturingGroupParser.js +++ b/src/parser/NonCapturingGroupParser.js @@ -16,10 +16,10 @@ export const GroupType = { */ export default class NonCapturingGroupParser extends Parser { - static isActualParser = false - #parser + isActualParser = false + /** @param {T} parser */ constructor(parser) { super() @@ -47,6 +47,7 @@ export default class NonCapturingGroupParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -55,8 +56,12 @@ export default class NonCapturingGroupParser extends Parser { return other instanceof NonCapturingGroupParser && this.#parser.equals(context, other.#parser, strict) } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { let group = "" - return "(?:" + this.#parser.toString(indent) + ")" + return "(?:" + this.#parser.toString(context, indent) + ")" } } diff --git a/src/parser/Parser.js b/src/parser/Parser.js index e2d07fc..15e06fb 100644 --- a/src/parser/Parser.js +++ b/src/parser/Parser.js @@ -1,10 +1,19 @@ +import Reply from "../Reply.js" + /** @template T */ export default class Parser { + static isTerminal = false static indentation = " " + /** @type {Boolean?} */ + #matchesEmptyFlag + + /** @type {Parser[]} */ + #starterList + /** Calling parse() can make it change the overall parsing outcome */ - static isActualParser = true + isActualParser = true /** * @param {Result} a @@ -21,6 +30,56 @@ export default class Parser { }) } + matchesEmpty() { + if (this.#matchesEmptyFlag === undefined) { + return this.#matchesEmptyFlag = this.doMatchesEmpty() + } + return this.#matchesEmptyFlag + } + + /** + * @protected + * @returns {Boolean} + */ + doMatchesEmpty() { + const children = this.unwrap() + if (children.length === 1) { + return children[0].doMatchesEmpty() + } + return false + } + + /** + * List of starting terminal parsers + * @param {Parser[]} additional Additional non terminal parsers that will be considered part of the starter list when encounter even though non terminals + */ + starterList(context = Reply.makeContext(null, ""), additional = []) { + if (!this.#starterList && !context.visited.has(this)) { + context.visited.add(this) + this.#starterList = this.doStarterList(context, additional) + if (additional.length) { + this.#starterList = this.#starterList + .filter(v => !/** @type {typeof Parser} */(v.constructor).isTerminal && additional.includes(v)) + } + } + let result = this.#starterList + if (!/** @type {typeof Parser} */(this.constructor).isTerminal && additional.includes(this)) { + result = [this, ...result] + } + return result + } + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + let unwrapped = this.unwrap() + return unwrapped?.length === 1 + ? unwrapped[0].starterList(context, additional) + : [] + } + /** * In an alternative, this would always match parser could might * @param {Parser} parser @@ -58,8 +117,7 @@ export default class Parser { * @returns {Parser} */ actualParser(traverse = [], opaque = []) { - const self = /** @type {typeof Parser} */(this.constructor) - let isTraversable = (!self.isActualParser || traverse.find(type => this instanceof type)) + let isTraversable = (!this.isActualParser || traverse.find(type => this instanceof type)) && !opaque.find(type => this instanceof type) let unwrapped = isTraversable ? this.unwrap() : undefined isTraversable &&= unwrapped?.length === 1 @@ -73,8 +131,7 @@ export default class Parser { * @returns {Parser} */ withActualParser(other, traverse = [], opaque = []) { - const self = /** @type {typeof Parser} */(this.constructor) - let isTraversable = (!self.isActualParser || traverse.some(type => this instanceof type)) + let isTraversable = (!this.isActualParser || traverse.some(type => this instanceof type)) && !opaque.some(type => this instanceof type) let unwrapped = isTraversable ? this.unwrap() : undefined isTraversable &&= unwrapped?.length === 1 @@ -107,18 +164,19 @@ export default class Parser { lhs = rhs rhs = temp } - let memoized = context.visited.get(lhs, rhs) + let memoized = context.equals.get(lhs, rhs) if (memoized !== undefined) { return memoized } else if (memoized === undefined) { - context.visited.set(lhs, rhs, true) + context.equals.set(lhs, rhs, true) memoized = lhs.doEquals(context, rhs, strict) - context.visited.set(lhs, rhs, memoized) + context.equals.set(lhs, rhs, memoized) } return memoized } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -127,7 +185,20 @@ export default class Parser { return false } - toString(indent = 0) { + /** @param {Context} context */ + toString(context, indent = 0) { + if (context.visited.has(this)) { + return "<...>" // Recursive parser + } + context.visited.add(this) + return this.doToString(context, indent) + } + + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return `${this.constructor.name} does not implement toString()` } } diff --git a/src/parser/RangeParser.js b/src/parser/RangeParser.js index e85f8f9..af5c8d0 100644 --- a/src/parser/RangeParser.js +++ b/src/parser/RangeParser.js @@ -9,6 +9,8 @@ import StringParser from "./StringParser.js" */ export default class RangeParser extends Parser { + static isTerminal = true + #from get from() { return this.#from @@ -29,6 +31,14 @@ export default class RangeParser extends Parser { this.#to = to } + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return [this] + } + /** * @param {Context} context * @param {Number} position @@ -41,6 +51,7 @@ export default class RangeParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -51,7 +62,11 @@ export default class RangeParser extends Parser { && this.#to.equals(context, other.#to, strict) } - toString(indent = 0) { - return this.#from.toString(indent) + "-" + this.#to.toString(indent) + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return this.#from.toString(context, indent) + "-" + this.#to.toString(context, indent) } } diff --git a/src/parser/RegExpParser.js b/src/parser/RegExpParser.js index 8840104..5f5cf19 100644 --- a/src/parser/RegExpParser.js +++ b/src/parser/RegExpParser.js @@ -7,6 +7,8 @@ import Reply from "../Reply.js" */ export default class RegExpParser extends Parser { + static isTerminal = true + /** @type {RegExp} */ #regexp get regexp() { @@ -40,6 +42,14 @@ export default class RegExpParser extends Parser { this.#group = group } + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return [this] + } + isFullyGenerated() { return this.regexpFullyGenerated } @@ -56,6 +66,7 @@ export default class RegExpParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -66,7 +77,11 @@ export default class RegExpParser extends Parser { && this.#regexp.source === other.#regexp.source } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return "/" + this.#regexp.source + "/" } } diff --git a/src/parser/SequenceParser.js b/src/parser/SequenceParser.js index d58bf05..1831cec 100644 --- a/src/parser/SequenceParser.js +++ b/src/parser/SequenceParser.js @@ -16,6 +16,29 @@ export default class SequenceParser extends Parser { constructor(...parsers) { super() this.#parsers = parsers + if (this.#parsers.length === 1) { + this.isActualParser = false + } + } + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + const result = this.#parsers[0].starterList(context) + for (let i = 1; i < this.#parsers.length && this.#parsers[i - 1].matchesEmpty(); ++i) { + this.#parsers[i].starterList(context).reduce( + (acc, cur) => acc.some(p => p.equals(context, cur, false)) ? acc : (acc.push(cur), acc), + result + ) + } + return result + } + + /** @protected */ + doMatchesEmpty() { + return this.#parsers.every(p => p.matchesEmpty()) } unwrap() { @@ -49,6 +72,7 @@ export default class SequenceParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -65,12 +89,16 @@ export default class SequenceParser extends Parser { return true } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { const indentation = Parser.indentation.repeat(indent) const deeperIndentation = Parser.indentation.repeat(indent + 1) return "SEQ<\n" + this.#parsers - .map(p => deeperIndentation + p.toString(indent + 1)) + .map(p => deeperIndentation + p.toString(context, indent + 1)) .join("\n") + "\n" + indentation + ">" } diff --git a/src/parser/StringParser.js b/src/parser/StringParser.js index fb15a92..7f47978 100644 --- a/src/parser/StringParser.js +++ b/src/parser/StringParser.js @@ -7,6 +7,8 @@ import Reply from "../Reply.js" */ export default class StringParser extends Parser { + static isTerminal = true + #value get value() { return this.#value @@ -18,6 +20,19 @@ export default class StringParser extends Parser { this.#value = value } + /** @protected */ + doMatchesEmpty() { + return this.#value === "" + } + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + return [this] + } + /** * In an alternative, this would always match parser could might * @param {Parser} parser @@ -43,6 +58,7 @@ export default class StringParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -51,7 +67,11 @@ export default class StringParser extends Parser { return other instanceof StringParser && this.#value === other.#value } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { const inlined = this.value.replaceAll("\n", "\\n") return this.value.length > 1 || this.value[0] === " " ? `"${inlined.replaceAll('"', '\\"')}"` diff --git a/src/parser/SuccessParser.js b/src/parser/SuccessParser.js index 7449c57..183c222 100644 --- a/src/parser/SuccessParser.js +++ b/src/parser/SuccessParser.js @@ -1,38 +1,31 @@ import Parser from "./Parser.js" import Reply from "../Reply.js" +import StringParser from "./StringParser.js" -/** - * @template T - * @extends Parser - */ -export default class SuccessParser extends Parser { +/** @extends StringParser<""> */ +export default class SuccessParser extends StringParser { - #value + static instance = new SuccessParser() - /** @param {T} value */ - constructor(value) { - super() - this.#value = value - } - - /** - * @param {Context} context - * @param {Number} position - */ - parse(context, position) { - return Reply.makeSuccess(position, this.#value) + constructor() { + super("") } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ doEquals(context, other, strict) { - return other instanceof SuccessParser + return strict ? other instanceof SuccessParser : super.doEquals(context, other, false) } - toString(indent = 0) { + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { return "" } } diff --git a/src/parser/TimesParser.js b/src/parser/TimesParser.js index 33a3646..c49c58b 100644 --- a/src/parser/TimesParser.js +++ b/src/parser/TimesParser.js @@ -1,5 +1,6 @@ import Parser from "./Parser.js" import Reply from "../Reply.js" +import SuccessParser from "./SuccessParser.js" /** * @template {Parser} T @@ -38,6 +39,23 @@ export default class TimesParser extends Parser { this.#max = max } + /** @protected */ + doMatchesEmpty() { + return this.#min === 0 + } + + /** + * @protected + * @param {Context} context + */ + doStarterList(context, additional = /** @type {Parser[]} */([])) { + const result = this.#parser.starterList(context) + if (this.matchesEmpty() && !result.some(p => SuccessParser.instance.equals(context, p, false))) { + result.push(SuccessParser.instance) + } + return result + } + unwrap() { return [this.#parser] } @@ -81,6 +99,7 @@ export default class TimesParser extends Parser { } /** + * @protected * @param {Context} context * @param {Parser} other * @param {Boolean} strict @@ -93,8 +112,12 @@ export default class TimesParser extends Parser { && this.#parser.equals(context, other.#parser, strict) } - toString(indent = 0) { - return this.parser.toString(indent) + /** + * @protected + * @param {Context} context + */ + doToString(context, indent = 0) { + return this.parser.toString(context, indent) + ( this.#min === 0 && this.#max === 1 ? "?" : this.#min === 0 && this.#max === Number.POSITIVE_INFINITY ? "*" diff --git a/src/transformers/RecursiveSequenceTransformer.js b/src/transformers/RecursiveSequenceTransformer.js new file mode 100644 index 0000000..88b4b1f --- /dev/null +++ b/src/transformers/RecursiveSequenceTransformer.js @@ -0,0 +1,46 @@ +import ParentChildTransformer from "./ParentChildTransformer.js" +import Parser from "../parser/Parser.js" +import RemoveEmptyTransformer from "./RemoveEmptyTransformer.js" +import Reply from "../Reply.js" +import SequenceParser from "../parser/SequenceParser.js" +import TimesParser from "../parser/TimesParser.js" + +/** @extends {ParentChildTransformer<[SequenceParser], [Parser]>} */ +export default class RecursiveSequenceTransformer extends ParentChildTransformer { + + static #removeEmpty = new RemoveEmptyTransformer() + static replaceBothChildren = true + + constructor() { + super([SequenceParser], [Parser]) + } + + /** + * @protected + * @param {SequenceParser[]>} parent + * @param {Parser} child + * @param {Number} index + * @param {Parser} previousChild + * @returns {Parser?} + */ + doTransformParent(parent, child, index, previousChild) { + const context = Reply.makeContext(null, "") + if (child.starterList(context, [parent]).some(starter => starter.equals(context, parent, false))) { + if (!parent.matchesEmpty()) { + console.error( + "The following parser expects an infinite string\n" + + parent.toString(context) + ) + throw new Error("The parser expects an infinite string") + } + return parent.wrap( + new TimesParser( + RecursiveSequenceTransformer.#removeEmpty.run(parent.wrap(...parent.parsers.slice(0, index))).getParser(), + 1 + ), + ...parent.parsers.slice(index + 1), + ) + } + return parent + } +} diff --git a/src/transformers/RemoveEmptyTransformer.js b/src/transformers/RemoveEmptyTransformer.js new file mode 100644 index 0000000..3ea4fc3 --- /dev/null +++ b/src/transformers/RemoveEmptyTransformer.js @@ -0,0 +1,25 @@ +import AlternativeParser from "../parser/AlternativeParser.js" +import ParentChildTransformer from "./ParentChildTransformer.js" +import Parser from "../parser/Parser.js" + +/** @extends {ParentChildTransformer<[AlternativeParser], [Parser]>} */ +export default class RemoveEmptyTransformer extends ParentChildTransformer { + + constructor() { + super([AlternativeParser], [Parser]) + } + + /** + * @protected + * @param {Parser} parent + * @param {Parser} child + * @param {Number} index + * @param {Parser} previousChild + * @returns {Parser[]?} + */ + doTransformChild(parent, child, index, previousChild) { + if (!parent.matchesEmpty() && child.matchesEmpty()) { + return [] + } + } +} diff --git a/src/transformers/RemoveTrivialParsersTransformer.js b/src/transformers/RemoveTrivialParsersTransformer.js index 4d14c03..50656a2 100644 --- a/src/transformers/RemoveTrivialParsersTransformer.js +++ b/src/transformers/RemoveTrivialParsersTransformer.js @@ -43,10 +43,12 @@ export default class RemoveTrivialParsersTransformer extends ParentChildTransfor if (parent instanceof AlternativeParser && child instanceof SuccessParser) { return parent.wrap(...parent.parsers.slice(0, index)) } - if (parent instanceof SequenceParser && child instanceof FailureParser) { - return child - } - if (parent instanceof TimesParser || parent instanceof CapturingGroupParser || parent instanceof NonCapturingGroupParser) { + if ( + parent instanceof SequenceParser && child instanceof FailureParser + || parent instanceof TimesParser + || parent instanceof CapturingGroupParser + || parent instanceof NonCapturingGroupParser + ) { return child } } diff --git a/src/types.js b/src/types.js index 4a8bba8..f8e9132 100644 --- a/src/types.js +++ b/src/types.js @@ -50,7 +50,8 @@ * @typedef {{ * regexer: Regexer, * input: String, - * visited: PairMap, + * equals: PairMap, + * visited: Set>, * }} Context */ diff --git a/tests/equality.spec.js b/tests/equality.spec.js index 2df23ff..a69f75b 100644 --- a/tests/equality.spec.js +++ b/tests/equality.spec.js @@ -112,6 +112,18 @@ test("Test 1", async ({ page }) => { true, ) ).toBeFalsy() + expect( + R.equals(R.str(""), R.success(),) + ).toBeTruthy() + expect( + R.equals(R.success(), R.str("")) + ).toBeTruthy() + expect( + R.equals(R.str(""), R.success(), true) + ).toBeFalsy() + expect( + R.equals(R.success(), R.str(""), true) + ).toBeFalsy() }) test("Test 2", async ({ page }) => { @@ -223,27 +235,27 @@ test("Test 9", async ({ page }) => { const lhs = R.seq( R.grp(R.class( R.escapedChar("\b"), - R.negClass(R.range(R.str("a"), R.escapedChar("\0"))) + R.range(R.str("a"), R.escapedChar("\0")) )), R.nonGrp(R.escapedChar("\x48", EscapedCharParser.Type.HEX)), ) const rhs = R.seq( R.grp(R.alt( R.str("\b"), - R.negClass(R.range(R.str("a"), R.str("\0"))) + R.range(R.str("a"), R.str("\0")) )), R.str("\x48"), ) const rhs2 = R.seq( - R.grp(R.alt( + R.grp(R.negClass( R.str("\b"), R.range(R.str("a"), R.str("\0")) )), R.str("\x48"), ) expect(R.equals(lhs, rhs)).toBeTruthy() - expect(R.equals(lhs, rhs2)).toBeFalsy() expect(R.equals(rhs, lhs)).toBeTruthy() + expect(R.equals(lhs, rhs2)).toBeFalsy() expect(R.equals(rhs2, lhs)).toBeFalsy() expect(R.equals(lhs, rhs, true)).toBeFalsy() expect(R.equals(rhs, lhs, true)).toBeFalsy() @@ -436,8 +448,8 @@ test("Test 16", async ({ page }) => { test("Test 17", async ({ page }) => { class Grammar { /** @type {Regexer>} */ - static a = R.seq(R.str("a"), R.lazy(() => this.a)) - static b = R.seq(R.str("a"), R.seq(R.str("a"), R.seq(R.str("a"), R.lazy(() => this.b)))) + static a = R.seq(R.str("a"), R.lazy(() => this.a), R.success()) + static b = R.seq(R.str("a"), R.seq(R.str("a"), R.seq(R.str("a"), R.lazy(() => this.b), R.str("")), R.success()), R.success()) } expect(R.equals(Grammar.a, Grammar.b)).toBeTruthy() expect(R.equals(Grammar.b, Grammar.a)).toBeTruthy() diff --git a/tests/features.spec.js b/tests/features.spec.js new file mode 100644 index 0000000..d549e23 --- /dev/null +++ b/tests/features.spec.js @@ -0,0 +1,196 @@ +import { R } from "../src/grammars/RegExpGrammar.js" +import { test, expect } from "@playwright/test" +import Reply from "../src/Reply.js" +import SequenceParser from "../src/parser/SequenceParser.js" + +/** + * @param {Parser[]} a + * @param {Parser[]} b + */ +const compareArrays = (a, b) => { + expect(a.length).toEqual(b.length) + for (let i = 0; i < a.length; ++i) { + expect(R.equals(a[i], b[i])).toBeTruthy() + } +} + +test("Test 1", async ({ page }) => { + let p = /** @type {Regexer>} */(R.str("a")) + expect(p.getParser().matchesEmpty()).toBeFalsy() + compareArrays(p.getParser().starterList(), [R.str("a").getParser()]) + p = p.opt() + expect(p.getParser().matchesEmpty()).toBeTruthy() + compareArrays(p.getParser().starterList(), [R.str("a").getParser(), R.success().getParser()]) +}) + +test("Test 2", async ({ page }) => { + const p = R.str("").getParser() + expect(p.matchesEmpty()).toBeTruthy() + compareArrays(p.starterList(), [R.success().getParser()]) +}) + +test("Test 3", async ({ page }) => { + const p = R.alt( + R.nonGrp(R.str("alpha")), + R.str("beta"), + R.lazy(() => R.grp(R.str(""))), + R.regexp(/gamma/), + ).getParser() + expect(p.matchesEmpty()).toBeTruthy() + compareArrays( + p.starterList(), + [ + R.str("alpha").getParser(), + R.str("beta").getParser(), + R.success().getParser(), + R.regexp(/gamma/).getParser(), + ] + ) +}) + +test("Test 4", async ({ page }) => { + const p = R.alt( + R.str("first"), + R.seq(R.str("second").map(() => ""), R.grp(R.lazy(() => R.str("third")))), + R.lazy(() => R.seq(R.str(""), R.success(), R.alt( + R.grp(R.str("").map(() => "hello")), + R.lazy(() => R.nonGrp(R.success())) + ))), + R.number.map(() => 123) + ) + .getParser() + expect(p.matchesEmpty()).toBeTruthy() + compareArrays( + p.starterList(), + [ + R.str("first").getParser(), + R.str("second").getParser(), + R.success().getParser(), + R.number.getParser(), + ] + ) +}) + +test("Test 5", async ({ page }) => { + const p = R.lazy(() => R.seq( + R.regexp(/a/).opt(), + R.lazy(() => R.str("").map(() => [1, 2, 3, 4])).map(() => ""), + )).map(() => "some string") + .getParser() + expect(p.matchesEmpty()).toBeTruthy() + compareArrays( + p.starterList(), + [ + R.regexp(/a/).getParser(), + R.success().getParser(), + ] + ) +}) + +test("Test 6", async ({ page }) => { + const p = R.lazy(() => R.seq( + R.lazy(() => R.str("").map(() => [1, 2, 3, 4])), + R.grp(R.str(" ").map(() => 987)), + )).map(() => "") + expect(p.getParser().matchesEmpty()).toBeFalsy() + compareArrays( + p.getParser().starterList(), + [ + R.success().getParser(), + R.str(" ").getParser(), + ] + ) + expect(p.many().getParser().matchesEmpty()).toBeTruthy() + compareArrays( + p.many().getParser().starterList(), + [ + R.success().getParser(), + R.str(" ").getParser(), + ] + ) + +}) + +test("Test 7", async ({ page }) => { + const p = R.seq( + R.str(""), + R.lookahead(R.alt( + R.str("apple"), + R.lazy(() => R.grp(R.str(""))) + )), + R.success().map(() => "xyz"), + R.nonGrp(R.lazy(() => R.str("abc").atMost(2)).map(() => [1])), + ) + .getParser() + expect(p.matchesEmpty()).toBeTruthy() + compareArrays( + p.starterList(), + [ + R.success().getParser(), + R.str("apple").getParser(), + R.str("abc").getParser(), + ] + ) +}) + +test("Test 8", async ({ page }) => { + class Grammar { + static a = R.str("a") + /** @type {Regexer, Parser, Parser]>>} */ + static r1 = R.seq(Grammar.a, Grammar.a, R.lazy(() => Grammar.r1).opt()) + /** @type {Regexer, Parser, Parser]>>} */ + static r2 = R.seq(Grammar.a, Grammar.a, R.lazy(() => Grammar.r2.opt())) + /** @type {Regexer, Parser, Parser]>>} */ + static r3 = R.seq(Grammar.a, Grammar.a, R.lazy(() => Grammar.r2.many())) + } + const r1 = Grammar.r1.getParser().parsers[2] + const r2 = Grammar.r2.getParser().parsers[2] + const r3 = Grammar.r3.getParser().parsers[2] + const context = Reply.makeContext(null, "") + expect(Grammar.r1.getParser().matchesEmpty()).toBeFalsy() + compareArrays( + r1.starterList(), + [ + R.str("a").getParser(), + R.success().getParser(), + ] + ) + compareArrays( + r2.starterList(), + [ + R.str("a").getParser(), + R.success().getParser(), + ] + ) + compareArrays( + r3.starterList(), + [ + R.str("a").getParser(), + R.success().getParser(), + ] + ) + compareArrays( + r1.starterList(context, [r1]), + [ + r1, + R.str("a").getParser(), + R.success().getParser(), + ] + ) + compareArrays( + r2.starterList(context, [r2]), + [ + r2, + R.str("a").getParser(), + R.success().getParser(), + ] + ) + compareArrays( + r3.starterList(context, [r3]), + [ + r3, + R.str("a").getParser(), + R.success().getParser(), + ] + ) +}) diff --git a/tests/serializing.spec.js b/tests/serializing.spec.js index 9b9c1a8..f742dff 100644 --- a/tests/serializing.spec.js +++ b/tests/serializing.spec.js @@ -35,22 +35,11 @@ test("Test 6", async ({ page }) => { ) }) -// test("Test 7", async ({ page }) => { -// expect(R.seq(R.str("a"), R.neg(R.str("b")), R.str("c")).toString(2, true)).toEqual(` -// SEQ< -// a -// NOT -// c -// >` -// ) -// }) - -test("Test 8", async ({ page }) => { +test("Test 7", async ({ page }) => { expect(R.alt(R.str("alpha"), R.seq(R.str("beta"), R.str("gamma").many()).atLeast(1)).toString(2, true)).toEqual(` ALT< "alpha" - | - SEQ< + | SEQ< "beta" "gamma"* >+ @@ -58,8 +47,41 @@ test("Test 8", async ({ page }) => { ) }) -test("Test 9", async ({ page }) => { +test("Test 8", async ({ page }) => { expect(RegExpGrammar.regexp.parse(/[\!@#$%^&*()\\[\]{}\-_+=~`|:;"'<>,./?]/.source).toString()).toEqual( /[\!@#$%^&*()\\[\]{}\-_+=~`|:;"'<>,./?]/.source ) }) + +test("Test 9", async ({ page }) => { + class Grammar { + /** @type {Regexer>} */ + static rule = R.seq(R.str("a").opt(), R.lazy(() => Grammar.rule)) + } + expect(Grammar.rule.toString(2, true)).toEqual(` + SEQ< + a? + <...> + >` + ) +}) + +test("Test 10", async ({ page }) => { + class Grammar { + /** @type {Regexer>} */ + static rule = R.grp( + R.alt( + R.str("a"), + R.str("b"), + R.lazy(() => Grammar.rule).opt().map(() => 123) + ) + ) + } + expect(Grammar.rule.toString(2, true)).toEqual(` + (ALT< + a + | b + | <...>? -> map<() => 123> + >)` + ) +}) diff --git a/tests/transformers.spec.js b/tests/transformers.spec.js index d1abddc..ed158e0 100644 --- a/tests/transformers.spec.js +++ b/tests/transformers.spec.js @@ -2,6 +2,7 @@ import { test, expect } from "@playwright/test" import InlineParsersTransformer from "../src/transformers/InlineParsersTransformer.js" import LookaroundParser from "../src/parser/LookaroundParser.js" import MergeStringsTransformer from "../src/transformers/MergeStringsTransformer.js" +import RecursiveSequenceTransformer from "../src/transformers/RecursiveSequenceTransformer.js" import RegExpGrammar, { R } from "../src/grammars/RegExpGrammar.js" import RemoveDiscardedMapTransformer from "../src/transformers/RemoveDiscardedMapTransformer.js" import RemoveLazyTransformer from "../src/transformers/RemoveLazyTransformer.js" @@ -492,7 +493,21 @@ test("Merge strings 2", ({ page }) => { R.str("c").map(v => c = `3${v}`), ) ), - R.seq(R.success(), R.str("abc")), + // @ts-expect-error + R.seq(R.str("abc")), + ) + ).toBeTruthy() +}) + +test("Recursive sequence", ({ page }) => { + const recursiveSequence = new RecursiveSequenceTransformer() + /** @type {Regexer} */ + const p = R.seq(R.str("a"), R.str("b"), R.lazy(() => p.opt())) + expect( + R.equals( + recursiveSequence.run(p), + R.seq(R.str("a"), R.str("b")).atLeast(1), + true ) ).toBeTruthy() })