diff --git a/dist/regexer.js b/dist/regexer.js index 8a8b93f..61f4a34 100644 --- a/dist/regexer.js +++ b/dist/regexer.js @@ -1,7 +1,8 @@ -/** @template Value */ +/** @template T */ class Parser { static indentation = " " + /** Calling parse() can make it change the overall parsing outcome */ static isActualParser = true @@ -25,62 +26,142 @@ class Parser { * @param {Parser} parser */ dominates(parser) { - return this.equals(parser, false) + //return this.equals(context, parser, false) } - /** @returns {Parser} */ + /** @returns {Parser[]} */ unwrap() { - return null + return [] } /** - * @template {Parser} T - * @param {T} parser - * @returns {Parser} + * @template {Parser[]} T + * @param {T} parsers + * @returns {Parser} */ - wrap(...parser) { + wrap(...parsers) { return null } /** * @param {Context} context * @param {Number} position - * @returns {Result} + * @returns {Result} */ parse(context, position) { return null } /** - * @param {(new (...args: any) => Parser)[]} traverse List of types to ignore and traverse to find the actual parser + * @param {(new (...args: any) => Parser)[]} traverse List of types to ignore and traverse even though they have isActualParser = true + * @param {(new (...args: any) => Parser)[]} opaque List of types to consider actual parser even though they have isActualParser = false * @returns {Parser} */ - actualParser(...traverse) { + actualParser(traverse = [], opaque = []) { const self = /** @type {typeof Parser} */(this.constructor); - return !self.isActualParser || traverse.find(type => this instanceof type) - ? this.unwrap().actualParser() - : this + let isTraversable = (!self.isActualParser || traverse.find(type => this instanceof type)) + && !opaque.find(type => this instanceof type); + let unwrapped = isTraversable ? this.unwrap() : undefined; + isTraversable &&= unwrapped?.length === 1; + return isTraversable ? unwrapped[0].actualParser(traverse, opaque) : this } - /** @returns {Parser} */ - withActualParser(other) { + /** + * @param {Parser} other + * @param {(new (...args: any) => Parser)[]} traverse List of types to ignore and traverse even though they have isActualParser = true + * @param {(new (...args: any) => Parser)[]} opaque List of types to consider actual parser even though they have isActualParser = false + * @returns {Parser} + */ + withActualParser(other, traverse = [], opaque = []) { const self = /** @type {typeof Parser} */(this.constructor); - return self.isActualParser ? other : this.wrap(this.unwrap().withActualParser(other)) + let isTraversable = (!self.isActualParser || traverse.some(type => this instanceof type)) + && !opaque.some(type => this instanceof type); + let unwrapped = isTraversable ? this.unwrap() : undefined; + isTraversable &&= unwrapped?.length === 1; + return isTraversable + ? this.wrap(unwrapped[0].withActualParser(other, traverse, opaque)) + : other + } + + /** + * @param {Context} context + * @param {Parser} other + * @param {Boolean} strict + */ + equals(context, other, strict) { + let self = /** @type {Parser} */(this); + if (self === other) { + return true + } + if (!strict) { + self = this.actualParser(); + other = other.actualParser(); + } + let memoized = context.visited.get(self, other); + if (memoized !== undefined) { + return memoized + } else if (memoized === undefined) { + context.visited.set(self, other, true); + memoized = self.doEquals(context, other, strict); + context.visited.set(self, other, memoized); + } + return memoized } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - return strict ? this.actualParser() === other.actualParser() : this === other + doEquals(context, other, strict) { + return false } - toString(indent) { + toString(indent = 0) { return `${this.constructor.name} does not implement toString()` } } +/** @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 {{ @@ -90,6 +171,7 @@ class Parser { * }} Result */ + class Reply { /** @@ -117,18 +199,18 @@ class Reply { }) } - /** @param {String} input */ - static makeContext(input) { - return /** @type {Context} */( - { - input: input - } - ) + /** @param {Regexer>} regexer */ + static makeContext(regexer, input = "") { + return /** @type {Context} */({ + regexer: regexer, + input: input, + visited: new PairMap() + }) } } /** - * @template {[Parser, ...Parser[]]} T + * @template {Parser[]} T * @extends Parser> */ class AlternativeParser extends Parser { @@ -144,6 +226,18 @@ class AlternativeParser extends Parser { this.#parsers = parsers; } + unwrap() { + return [...this.#parsers] + } + + /** + * @template {Parser[]} T + * @param {T} parsers + */ + wrap(...parsers) { + return new AlternativeParser(...parsers) + } + /** * @param {Context} context * @param {Number} position @@ -160,21 +254,16 @@ class AlternativeParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser(); - } - if (this === other) { - return true - } + doEquals(context, other, strict) { if (!(other instanceof AlternativeParser) || this.#parsers.length != other.#parsers.length) { return false } for (let i = 0; i < this.#parsers.length; ++i) { - if (!this.#parsers[i].equals(other.#parsers[i], strict)) { + if (!this.#parsers[i].equals(context, other.#parsers[i], strict)) { return false } } @@ -217,15 +306,15 @@ class ChainedParser extends Parser { } unwrap() { - return this.#parser + return [this.#parser] } /** - * @template {Parser>} P - * @param {P} parser + * @template {Parser[]} T + * @param {T} parsers */ - wrap(...parser) { - return new ChainedParser(parser, this.#fn) + wrap(...parsers) { + return new ChainedParser(parsers[0], this.#fn) } /** @@ -242,6 +331,17 @@ class ChainedParser extends Parser { return result } + /** + * @param {Context} context + * @param {Parser} other + * @param {Boolean} strict + */ + doEquals(context, other, strict) { + return other instanceof ChainedParser + && this.#fn === other.#fn + && this.#parser.equals(context, other.parser, strict) + } + toString(indent = 0) { return this.#parser.toString(indent) + " => chained" } @@ -259,13 +359,11 @@ class FailureParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser(); - } + doEquals(context, other, strict) { return other instanceof FailureParser } @@ -275,18 +373,18 @@ class FailureParser extends Parser { } /** - * @template {Parser} P - * @extends Parser> + * @template {Parser} T + * @extends Parser> */ class LazyParser extends Parser { #parser static isActualParser = false - /** @type {P} */ + /** @type {T} */ #resolvedPraser - /** @param {() => Regexer

} parser */ + /** @param {() => Regexer} parser */ constructor(parser) { super(); this.#parser = parser; @@ -300,12 +398,18 @@ class LazyParser extends Parser { } unwrap() { - return this.resolve() + return [this.resolve()] } - wrap(parser) { - const regexerConstructor = /** @type {new (...args: any) => Regexer

} */(this.#parser().constructor); - return new LazyParser(() => new regexerConstructor(parser)) + /** + * @template {Parser[]} P + * @param {P} parsers + */ + wrap(...parsers) { + const regexerConstructor = /** @type {new (...args: any) => Regexer} */( + this.#parser().constructor + ); + return new LazyParser(() => new regexerConstructor(parsers[0])) } /** @@ -318,20 +422,19 @@ class LazyParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (other instanceof LazyParser && this.#parser === other.#parser) { - return true - } - this.resolve(); - if (!strict) { - other = other.actualParser(); - } else if (other instanceof LazyParser) { + doEquals(context, other, strict) { + if (other instanceof LazyParser) { + if (this.#parser === other.#parser) { + return true + } other = other.resolve(); } - return this.#resolvedPraser === other || this.#resolvedPraser.equals(other, strict) + this.resolve(); + return this.#resolvedPraser.equals(context, other, strict) } toString(indent = 0) { @@ -373,6 +476,18 @@ class LookaroundParser extends Parser { this.#type = type; } + unwrap() { + return [this.#parser] + } + + /** + * @template {Parser[]} P + * @param {P} parsers + */ + wrap(...parsers) { + return new LookaroundParser(parsers[0], this.#type) + } + /** * @param {Context} context * @param {Number} position @@ -392,17 +507,15 @@ class LookaroundParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser(); - } + doEquals(context, other, strict) { return this === other || other instanceof LookaroundParser && this.#type === other.#type - && this.#parser.equals(other.#parser, strict) + && this.#parser.equals(context, other.#parser, strict) } toString(indent = 0) { @@ -440,13 +553,17 @@ class MapParser extends Parser { } unwrap() { - return this.#parser + return [this.#parser] } - /** @param {Parser} parser */ - wrap(parser) { - return new MapParser(parser, this.#mapper) + /** + * @template {Parser[]} T + * @param {T} parsers + */ + wrap(...parsers) { + return new MapParser(parsers[0], this.#mapper) } + /** * @param {Context} context * @param {Number} position @@ -461,20 +578,14 @@ class MapParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser(); - } - return this === other || ( - strict - ? other instanceof MapParser - && this.#mapper === other.#mapper - && this.#parser.equals(other.#parser, strict) - : this.actualParser().equals(other, strict) - ) + doEquals(context, other, strict) { + return other instanceof MapParser + && this.#mapper === other.#mapper + && this.#parser.equals(context, other.#parser, strict) } toString(indent = 0) { @@ -541,14 +652,18 @@ class RegExpParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser(); - } - return other instanceof RegExpParser && this.#regexp.source === other.#regexp.source + doEquals(context, other, strict) { + return other instanceof RegExpParser + && (!strict || this.#group === other.#group) + && this.#regexp.source === other.#regexp.source + } + + toString(indent = 0) { + return "/" + this.#regexp.source + "/" } } @@ -569,6 +684,18 @@ class SequenceParser extends Parser { this.#parsers = parsers; } + unwrap() { + return [...this.#parsers] + } + + /** + * @template {Parser[]} P + * @param {P} parsers + */ + wrap(...parsers) { + return new SequenceParser(...parsers) + } + /** * @param {Context} context * @param {Number} position @@ -588,21 +715,16 @@ class SequenceParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser(); - } - if (this === other) { - return true - } + doEquals(context, other, strict) { if (!(other instanceof SequenceParser) || this.#parsers.length != other.#parsers.length) { return false } for (let i = 0; i < this.#parsers.length; ++i) { - if (!this.#parsers[i].equals(other.#parsers[i], strict)) { + if (!this.#parsers[i].equals(context, other.#parsers[i], strict)) { return false } } @@ -662,13 +784,11 @@ class StringParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser(); - } + doEquals(context, other, strict) { return other instanceof StringParser && this.#value === other.#value } @@ -703,13 +823,11 @@ class SuccessParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser(); - } + doEquals(context, other, strict) { return other instanceof SuccessParser } @@ -719,8 +837,8 @@ class SuccessParser extends Parser { } /** - * @template {Parser} P - * @extends Parser[]> + * @template {Parser} T + * @extends {Parser[]>} */ class TimesParser extends Parser { @@ -739,7 +857,7 @@ class TimesParser extends Parser { return this.#max } - /** @param {P} parser */ + /** @param {T} parser */ constructor(parser, min = 0, max = Number.POSITIVE_INFINITY) { super(); if (min > max) { @@ -751,16 +869,15 @@ class TimesParser extends Parser { } unwrap() { - return this.#parser + return [this.#parser] } /** - * @template {Parser} T - * @param {T} parser - * @returns {TimesParser} + * @template {Parser[]} P + * @param {P} parsers */ - wrap(parser) { - return new TimesParser(parser, this.#min, this.#max) + wrap(...parsers) { + return new TimesParser(parsers[0], this.#min, this.#max) } /** @@ -769,7 +886,7 @@ class TimesParser extends Parser { */ parse(context, position) { const value = []; - const result = /** @type {Result[]>} */( + const result = /** @type {Result[]>} */( Reply.makeSuccess(position, value) ); for (let i = 0; i < this.#max; ++i) { @@ -784,18 +901,15 @@ class TimesParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser(); - } - return this === other - || other instanceof TimesParser + doEquals(context, other, strict) { + return other instanceof TimesParser && this.#min === other.#min && this.#max === other.#max - && this.#parser.equals(other.#parser, strict) + && this.#parser.equals(context, other.#parser, strict) } toString(indent = 0) { @@ -900,9 +1014,9 @@ class Regexer { const b = rhs.getParser(); if (b instanceof a.constructor && !(a instanceof b.constructor)) { // typeof b extends typeof a, invert to take advantage of polymorphism - return b.equals(a, strict) + return b.equals(Reply.makeContext(rhs), a, strict) } - return a.equals(b, strict) + return a.equals(Reply.makeContext(lhs), b, strict) } getParser() { @@ -914,7 +1028,7 @@ class Regexer { * @returns {Result>} */ run(input) { - const result = this.#parser.parse(Reply.makeContext(input), 0); + const result = this.#parser.parse(Reply.makeContext(this, input), 0); return result.status && result.position === input.length ? result : Reply.makeFailure(result.position) } @@ -958,7 +1072,7 @@ class Regexer { // Combinators /** - * @template {Regexer[]} P + * @template {[Regexer, Regexer, ...Regexer[]]} P * @param {P} parsers * @returns {Regexer>>} */ @@ -969,7 +1083,7 @@ class Regexer { } /** - * @template {Regexer[]} P + * @template {[Regexer, Regexer, ...Regexer[]]} P * @param {P} parsers * @returns {Regexer>>} */ @@ -987,12 +1101,11 @@ class Regexer { } /** - * @template {Regexer>} P + * @template {Regexer} P * @param {() => P} parser * @returns {Regexer>>} */ static lazy(parser) { - // @ts-expect-error return new Regexer(new LazyParser(parser)) } @@ -1015,12 +1128,12 @@ class Regexer { return this.times(0, n) } - /** @returns {Regexer} */ + /** @returns {Regexer} */ opt() { // @ts-expect-error return Regexer.alt( this, - Regexer.success(undefined) + Regexer.success(null) ) } @@ -1031,7 +1144,6 @@ class Regexer { sepBy(separator, allowTrailing = false) { const results = Regexer.seq( this, - // @ts-expect-error Regexer.seq(separator, this).map(Regexer.#secondElementGetter).many() ) .map(Regexer.#arrayFlatter); diff --git a/dist/regexer.min.js b/dist/regexer.min.js index b80c67f..5bcaa00 100644 --- a/dist/regexer.min.js +++ b/dist/regexer.min.js @@ -1 +1 @@ -class e{static indentation=" ";static isActualParser=!0;static mergeResults(e,r){return r?{status:e.status,position:e.position,value:e.value}:e}dominates(e){return this.equals(e,!1)}unwrap(){return null}wrap(e){return null}parse(e,r){return null}actualParser(...e){return!this.constructor.isActualParser||e.find((e=>this instanceof e))?this.unwrap().actualParser():this}withActualParser(e){return this.constructor.isActualParser?e:this.wrap(this.unwrap().withActualParser(e))}equals(e,r){return r?this.actualParser()===e.actualParser():this===e}toString(e){return`${this.constructor.name} does not implement toString()`}}class r{static makeSuccess(e,r){return{status:!0,value:r,position:e}}static makeFailure(e){return{status:!1,value:null,position:e}}static makeContext(e){return{input:e}}}class t extends e{#e;get parsers(){return this.#e}constructor(...e){super(),this.#e=e}parse(e,t){let s;for(let r=0;rs+e.toString(r+1))).join("\n"+s+"|\n")+"\n"+t+">"}}class s extends e{#r;get parser(){return this.#r}#t;constructor(e,r){super(),this.#r=e,this.#t=r}unwrap(){return this.#r}wrap(e){return new s(e,this.#t)}parse(e,t){let s=this.#r.parse(e,t);return s.status?(s=this.#t(s.value,e.input,s.position)?.getParser().parse(e,s.position)??r.makeFailure(s.position),s):s}toString(e=0){return this.#r.toString(e)+" => chained"}}class a extends e{parse(e,t){return r.makeFailure(t)}equals(e,r){return r||(e=e.actualParser()),e instanceof a}toString(e=0){return""}}class n extends e{#r;static isActualParser=!1;#s;constructor(e){super(),this.#r=e}resolve(){return this.#s||(this.#s=this.#r().getParser()),this.#s}unwrap(){return this.resolve()}wrap(e){const r=this.#r().constructor;return new n((()=>new r(e)))}parse(e,r){return this.resolve(),this.#s.parse(e,r)}equals(e,r){return e instanceof n&&this.#r===e.#r||(this.resolve(),r?e instanceof n&&(e=e.resolve()):e=e.actualParser(),this.#s===e||this.#s.equals(e,r))}toString(e=0){return this.resolve().toString(e)}}class i extends e{#r;get parser(){return this.#r}#a;get type(){return this.#a}static Type={NEGATIVE_AHEAD:"?!",NEGATIVE_BEHIND:"?80||r.includes("\n"))&&(r="( ... ) => { ... }"),this.#r.toString(e)+` -> map<${r}>`}}class p extends e{#i;get regexp(){return this.#i}#u;#p;regexpGenerated=!1;regexpFullyGenerated=!0;cyclomaticComplexity=1;constructor(e,r){super(),e instanceof RegExp?(this.#i=e,this.#u=new RegExp(`^(?:${e.source})`,e.flags)):e instanceof p&&(this.#i=e.#i,this.#u=e.#u,this.regexpGenerated=e.regexpGenerated,this.regexpFullyGenerated=e.regexpFullyGenerated,this.cyclomaticComplexity=e.cyclomaticComplexity),this.#p=r}isFullyGenerated(){return this.regexpFullyGenerated}parse(e,t){const s=this.#u.exec(e.input.substring(t));return s?r.makeSuccess(t+s[0].length,this.#p>=0?s[this.#p]:s):r.makeFailure(t)}equals(e,r){return r||(e=e.actualParser()),e instanceof p&&this.#i.source===e.#i.source}}class o extends e{#e;get parsers(){return this.#e}constructor(...e){super(),this.#e=e}parse(e,t){const s=new Array(this.#e.length),a=r.makeSuccess(t,s);for(let r=0;rs+e.toString(r+1))).join("\n")+"\n"+t+">"}}class c extends e{#o;get value(){return this.#o}constructor(e){super(),this.#o=e}dominates(e){if((e=e.actualParser())instanceof c){return e.#o.startsWith(this.#o)}}parse(e,t){const s=t+this.#o.length,a=e.input.substring(t,s);return this.#o===a?r.makeSuccess(s,this.#o):r.makeFailure(t)}equals(e,r){return r||(e=e.actualParser()),e instanceof c&&this.#o===e.#o}toString(e=0){const r=this.value.replaceAll("\n","\\n");return this.value.length>1||" "===this.value[0]?`"${r.replaceAll('"','\\"')}"`:r}}class l extends e{#o;constructor(e){super(),this.#o=e}parse(e,t){return r.makeSuccess(t,this.#o)}equals(e,r){return r||(e=e.actualParser()),e instanceof l}toString(e=0){return""}}class h extends e{#r;get parser(){return this.#r}#c;get min(){return this.#c}#l;get max(){return this.#l}constructor(e,r=0,t=Number.POSITIVE_INFINITY){if(super(),r>t)throw new Error("Min is more than max");this.#r=e,this.#c=r,this.#l=t}unwrap(){return this.#r}wrap(e){return new h(e,this.#c,this.#l)}parse(e,t){const s=r.makeSuccess(t,[]);for(let r=0;r=this.#c?s:t;s.value.push(t.value),s.position=t.position}return s}equals(e,r){return r||(e=e.actualParser()),this===e||e instanceof h&&this.#c===e.#c&&this.#l===e.#l&&this.#r.equals(e.#r,r)}toString(e=0){return this.parser.toString(e)+(0===this.#c&&1===this.#l?"?":0===this.#c&&this.#l===Number.POSITIVE_INFINITY?"*":1===this.#c&&this.#l===Number.POSITIVE_INFINITY?"+":"{"+this.#c+(this.#c!==this.#l?",":this.#l!==Number.POSITIVE_INFINITY?this.#l:"")+"}")}}class m{#r;#h;#m=new Map;static#g=e=>Number(e);static#x=([e,r])=>e;static#d=([e,r])=>r;static#w=([e,r])=>[e,...r];static#f=e=>e instanceof Array?e.join(""):e;static#S=e=>String.raw`[^${e}\\]*(?:\\.[^${e}\\]*)*`;static#E=/[-\+]?(?:\d*\.)?\d+/;static number=m.regexp(new RegExp(m.#E.source+String.raw`(?!\.)`)).map(m.#g);static numberNatural=m.regexp(/\d+/).map(m.#g);static numberExponential=m.regexp(new RegExp(m.#E.source+String.raw`(?:[eE][\+\-]?\d+)?(?!\.)`)).map(m.#g);static numberUnit=m.regexp(/\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/).map(m.#g);static whitespace=m.regexp(/\s+/);static whitespaceInline=m.regexp(/[^\S\n]+/);static whitespaceMultiline=m.regexp(/\s*?\n\s*/);static optWhitespace=m.regexp(/\s*/);static doubleQuotedString=m.regexpGroups(new RegExp(`"(${m.#S('"')})"`)).map(m.#d);static singleQuotedString=m.regexpGroups(new RegExp(`'(${m.#S("'")})'`)).map(m.#d);static backtickQuotedString=m.regexpGroups(new RegExp(`\`(${m.#S("`")})\``)).map(m.#d);constructor(e,r=!1){this.#r=e,this.#h=r}static optimize(e){}static equals(e,r,t=!1){const s=e.getParser(),a=r.getParser();return a instanceof s.constructor&&!(s instanceof a.constructor)?a.equals(s,t):s.equals(a,t)}getParser(){return this.#r}run(e){const t=this.#r.parse(r.makeContext(e),0);return t.status&&t.position===e.length?t:r.makeFailure(t.position)}parse(e){const r=this.run(e);if(!r.status)throw new Error("Parsing error");return r.value}static str(e){return new m(new c(e))}static regexp(e,r=0){return new m(new p(e,r))}static regexpGroups(e){return new m(new p(e,-1))}static success(e=void 0){return new m(new l(e))}static failure(){return new m(new a)}static seq(...e){return new m(new o(...e.map((e=>e.getParser()))))}static alt(...e){return new m(new t(...e.map((e=>e.getParser()))))}static lookahead(e){return new m(new i(e.getParser(),i.Type.POSITIVE_AHEAD))}static lazy(e){return new m(new n(e))}times(e,r=e){return new m(new h(this.#r,e,r))}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 m.alt(this,m.success(void 0))}sepBy(e,r=!1){return m.seq(this,m.seq(e,this).map(m.#d).many()).map(m.#w)}skipSpace(){return m.seq(this,m.optWhitespace).map(m.#x)}map(e){return new m(new u(this.#r,e))}chain(e){return new m(new s(this.#r,e))}assert(e){return this.chain(((r,t,s)=>e(r,t,s)?m.success(r):m.failure()))}join(e=""){return this.map(m.#f)}toString(r=0,t=!1){return(t?"\n"+e.indentation.repeat(r):"")+this.#r.toString(r)}}export{m as default}; +class e{static indentation=" ";static isActualParser=!0;static mergeResults(e,r){return r?{status:e.status,position:e.position,value:e.value}:e}dominates(e){}unwrap(){return[]}wrap(...e){return null}parse(e,r){return null}actualParser(e=[],r=[]){let t=(!this.constructor.isActualParser||e.find((e=>this instanceof e)))&&!r.find((e=>this instanceof e)),s=t?this.unwrap():void 0;return t&&=1===s?.length,t?s[0].actualParser(e,r):this}withActualParser(e,r=[],t=[]){let s=(!this.constructor.isActualParser||r.some((e=>this instanceof e)))&&!t.some((e=>this instanceof e)),a=s?this.unwrap():void 0;return s&&=1===a?.length,s?this.wrap(a[0].withActualParser(e,r,t)):e}equals(e,r,t){let s=this;if(s===r)return!0;t||(s=this.actualParser(),r=r.actualParser());let a=e.visited.get(s,r);return void 0!==a||void 0===a&&(e.visited.set(s,r,!0),a=s.doEquals(e,r,t),e.visited.set(s,r,a)),a}doEquals(e,r,t){return!1}toString(e=0){return`${this.constructor.name} does not implement toString()`}}class r{#e=new Map;get(e,r){return this.#e.get(e)?.get(r)}set(e,r,t){let s=this.#e.get(e);return s||(s=new Map,this.#e.set(e,s)),s.set(r,t),this}setGet(e,r,t){return this.set(e,r,t),t}}class t{static makeSuccess(e,r){return{status:!0,value:r,position:e}}static makeFailure(e){return{status:!1,value:null,position:e}}static makeContext(e,t=""){return{regexer:e,input:t,visited:new r}}}class s extends e{#r;get parsers(){return this.#r}constructor(...e){super(),this.#r=e}unwrap(){return[...this.#r]}wrap(...e){return new s(...e)}parse(e,r){let s;for(let t=0;ts+e.toString(r+1))).join("\n"+s+"|\n")+"\n"+t+">"}}class a extends e{#t;get parser(){return this.#t}#s;constructor(e,r){super(),this.#t=e,this.#s=r}unwrap(){return[this.#t]}wrap(...e){return new a(e[0],this.#s)}parse(e,r){let s=this.#t.parse(e,r);return s.status?(s=this.#s(s.value,e.input,s.position)?.getParser().parse(e,s.position)??t.makeFailure(s.position),s):s}doEquals(e,r,t){return r instanceof a&&this.#s===r.#s&&this.#t.equals(e,r.parser,t)}toString(e=0){return this.#t.toString(e)+" => chained"}}class n extends e{parse(e,r){return t.makeFailure(r)}doEquals(e,r,t){return r instanceof n}toString(e=0){return""}}class i extends e{#t;static isActualParser=!1;#a;constructor(e){super(),this.#t=e}resolve(){return this.#a||(this.#a=this.#t().getParser()),this.#a}unwrap(){return[this.resolve()]}wrap(...e){const r=this.#t().constructor;return new i((()=>new r(e[0])))}parse(e,r){return this.resolve(),this.#a.parse(e,r)}doEquals(e,r,t){if(r instanceof i){if(this.#t===r.#t)return!0;r=r.resolve()}return this.resolve(),this.#a.equals(e,r,t)}toString(e=0){return this.resolve().toString(e)}}class u extends e{#t;get parser(){return this.#t}#n;get type(){return this.#n}static Type={NEGATIVE_AHEAD:"?!",NEGATIVE_BEHIND:"?80||r.includes("\n"))&&(r="( ... ) => { ... }"),this.#t.toString(e)+` -> map<${r}>`}}class o extends e{#u;get regexp(){return this.#u}#p;#o;regexpGenerated=!1;regexpFullyGenerated=!0;cyclomaticComplexity=1;constructor(e,r){super(),e instanceof RegExp?(this.#u=e,this.#p=new RegExp(`^(?:${e.source})`,e.flags)):e instanceof o&&(this.#u=e.#u,this.#p=e.#p,this.regexpGenerated=e.regexpGenerated,this.regexpFullyGenerated=e.regexpFullyGenerated,this.cyclomaticComplexity=e.cyclomaticComplexity),this.#o=r}isFullyGenerated(){return this.regexpFullyGenerated}parse(e,r){const s=this.#p.exec(e.input.substring(r));return s?t.makeSuccess(r+s[0].length,this.#o>=0?s[this.#o]:s):t.makeFailure(r)}doEquals(e,r,t){return r instanceof o&&(!t||this.#o===r.#o)&&this.#u.source===r.#u.source}toString(e=0){return"/"+this.#u.source+"/"}}class c extends e{#r;get parsers(){return this.#r}constructor(...e){super(),this.#r=e}unwrap(){return[...this.#r]}wrap(...e){return new c(...e)}parse(e,r){const s=new Array(this.#r.length),a=t.makeSuccess(r,s);for(let r=0;rs+e.toString(r+1))).join("\n")+"\n"+t+">"}}class h extends e{#c;get value(){return this.#c}constructor(e){super(),this.#c=e}dominates(e){if((e=e.actualParser())instanceof h){return e.#c.startsWith(this.#c)}}parse(e,r){const s=r+this.#c.length,a=e.input.substring(r,s);return this.#c===a?t.makeSuccess(s,this.#c):t.makeFailure(r)}doEquals(e,r,t){return r instanceof h&&this.#c===r.#c}toString(e=0){const r=this.value.replaceAll("\n","\\n");return this.value.length>1||" "===this.value[0]?`"${r.replaceAll('"','\\"')}"`:r}}class l extends e{#c;constructor(e){super(),this.#c=e}parse(e,r){return t.makeSuccess(r,this.#c)}doEquals(e,r,t){return r instanceof l}toString(e=0){return""}}class m extends e{#t;get parser(){return this.#t}#h;get min(){return this.#h}#l;get max(){return this.#l}constructor(e,r=0,t=Number.POSITIVE_INFINITY){if(super(),r>t)throw new Error("Min is more than max");this.#t=e,this.#h=r,this.#l=t}unwrap(){return[this.#t]}wrap(...e){return new m(e[0],this.#h,this.#l)}parse(e,r){const s=t.makeSuccess(r,[]);for(let r=0;r=this.#h?s:t;s.value.push(t.value),s.position=t.position}return s}doEquals(e,r,t){return r instanceof m&&this.#h===r.#h&&this.#l===r.#l&&this.#t.equals(e,r.#t,t)}toString(e=0){return this.parser.toString(e)+(0===this.#h&&1===this.#l?"?":0===this.#h&&this.#l===Number.POSITIVE_INFINITY?"*":1===this.#h&&this.#l===Number.POSITIVE_INFINITY?"+":"{"+this.#h+(this.#h!==this.#l?",":this.#l!==Number.POSITIVE_INFINITY?this.#l:"")+"}")}}class g{#t;#m;#g=new Map;static#d=e=>Number(e);static#x=([e,r])=>e;static#w=([e,r])=>r;static#E=([e,r])=>[e,...r];static#f=e=>e instanceof Array?e.join(""):e;static#S=e=>String.raw`[^${e}\\]*(?:\\.[^${e}\\]*)*`;static#v=/[-\+]?(?:\d*\.)?\d+/;static number=g.regexp(new RegExp(g.#v.source+String.raw`(?!\.)`)).map(g.#d);static numberNatural=g.regexp(/\d+/).map(g.#d);static numberExponential=g.regexp(new RegExp(g.#v.source+String.raw`(?:[eE][\+\-]?\d+)?(?!\.)`)).map(g.#d);static numberUnit=g.regexp(/\+?(?:0(?:\.\d+)?|1(?:\.0+)?)(?![\.\d])/).map(g.#d);static whitespace=g.regexp(/\s+/);static whitespaceInline=g.regexp(/[^\S\n]+/);static whitespaceMultiline=g.regexp(/\s*?\n\s*/);static optWhitespace=g.regexp(/\s*/);static doubleQuotedString=g.regexpGroups(new RegExp(`"(${g.#S('"')})"`)).map(g.#w);static singleQuotedString=g.regexpGroups(new RegExp(`'(${g.#S("'")})'`)).map(g.#w);static backtickQuotedString=g.regexpGroups(new RegExp(`\`(${g.#S("`")})\``)).map(g.#w);constructor(e,r=!1){this.#t=e,this.#m=r}static optimize(e){}static equals(e,r,s=!1){const a=e.getParser(),n=r.getParser();return n instanceof a.constructor&&!(a instanceof n.constructor)?n.equals(t.makeContext(r),a,s):a.equals(t.makeContext(e),n,s)}getParser(){return this.#t}run(e){const r=this.#t.parse(t.makeContext(this,e),0);return r.status&&r.position===e.length?r:t.makeFailure(r.position)}parse(e){const r=this.run(e);if(!r.status)throw new Error("Parsing error");return r.value}static str(e){return new g(new h(e))}static regexp(e,r=0){return new g(new o(e,r))}static regexpGroups(e){return new g(new o(e,-1))}static success(e=void 0){return new g(new l(e))}static failure(){return new g(new n)}static seq(...e){return new g(new c(...e.map((e=>e.getParser()))))}static alt(...e){return new g(new s(...e.map((e=>e.getParser()))))}static lookahead(e){return new g(new u(e.getParser(),u.Type.POSITIVE_AHEAD))}static lazy(e){return new g(new i(e))}times(e,r=e){return new g(new m(this.#t,e,r))}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 g.alt(this,g.success(null))}sepBy(e,r=!1){return g.seq(this,g.seq(e,this).map(g.#w).many()).map(g.#E)}skipSpace(){return g.seq(this,g.optWhitespace).map(g.#x)}map(e){return new g(new p(this.#t,e))}chain(e){return new g(new a(this.#t,e))}assert(e){return this.chain(((r,t,s)=>e(r,t,s)?g.success(r):g.failure()))}join(e=""){return this.map(g.#f)}toString(r=0,t=!1){return(t?"\n"+e.indentation.repeat(r):"")+this.#t.toString(r)}}export{g as default}; diff --git a/src/Regexer.js b/src/Regexer.js index 2e72b63..95c1690 100644 --- a/src/Regexer.js +++ b/src/Regexer.js @@ -100,9 +100,9 @@ export default class Regexer { const b = rhs.getParser() if (b instanceof a.constructor && !(a instanceof b.constructor)) { // typeof b extends typeof a, invert to take advantage of polymorphism - return b.equals(a, strict) + return b.equals(Reply.makeContext(rhs), a, strict) } - return a.equals(b, strict) + return a.equals(Reply.makeContext(lhs), b, strict) } getParser() { @@ -114,7 +114,7 @@ export default class Regexer { * @returns {Result>} */ run(input) { - const result = this.#parser.parse(Reply.makeContext(input), 0) + const result = this.#parser.parse(Reply.makeContext(this, input), 0) return result.status && result.position === input.length ? result : Reply.makeFailure(result.position) } @@ -187,12 +187,11 @@ export default class Regexer { } /** - * @template {Regexer>} P + * @template {Regexer} P * @param {() => P} parser * @returns {Regexer>>} */ static lazy(parser) { - // @ts-expect-error return new Regexer(new LazyParser(parser)) } @@ -231,7 +230,6 @@ export default class Regexer { sepBy(separator, allowTrailing = false) { const results = Regexer.seq( this, - // @ts-expect-error Regexer.seq(separator, this).map(Regexer.#secondElementGetter).many() ) .map(Regexer.#arrayFlatter) diff --git a/src/Reply.js b/src/Reply.js index 45aad85..ba3aef4 100644 --- a/src/Reply.js +++ b/src/Reply.js @@ -7,6 +7,8 @@ * }} Result */ +import PairMap from "./utility/PairMap.js" + export default class Reply { /** @@ -34,11 +36,12 @@ export default class Reply { }) } - /** @param {String} input */ - static makeContext(input) { + /** @param {Regexer>} regexer */ + static makeContext(regexer, input = "") { return /** @type {Context} */({ + regexer: regexer, input: input, - visited: new Set(), + visited: new PairMap() }) } } diff --git a/src/grammars/RegExpGrammar.js b/src/grammars/RegExpGrammar.js index b1994fb..8b25e2e 100644 --- a/src/grammars/RegExpGrammar.js +++ b/src/grammars/RegExpGrammar.js @@ -59,8 +59,10 @@ export const R = class extends Regexer { * @template {Regexer>} T * @param {T} parser * @param {String | Symbol} id + * @returns {Regexer>>} */ static grp(parser, id = "") { + // @ts-expect-error return new Regexer(new CapturingGroupParser(parser.getParser(), id)) } diff --git a/src/parser/AlternativeParser.js b/src/parser/AlternativeParser.js index 85ab38a..b139fa7 100644 --- a/src/parser/AlternativeParser.js +++ b/src/parser/AlternativeParser.js @@ -46,21 +46,16 @@ export default class AlternativeParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } - if (this === other) { - return true - } + doEquals(context, other, strict) { if (!(other instanceof AlternativeParser) || this.#parsers.length != other.#parsers.length) { return false } for (let i = 0; i < this.#parsers.length; ++i) { - if (!this.#parsers[i].equals(other.#parsers[i], strict)) { + if (!this.#parsers[i].equals(context, other.#parsers[i], strict)) { return false } } diff --git a/src/parser/AnchorParser.js b/src/parser/AnchorParser.js index c796012..4662396 100644 --- a/src/parser/AnchorParser.js +++ b/src/parser/AnchorParser.js @@ -43,13 +43,11 @@ export default class AnchorParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return other instanceof AnchorParser && this.#type === other.#type } diff --git a/src/parser/AtomicGroupParser.js b/src/parser/AtomicGroupParser.js index 65f9090..13eadc4 100644 --- a/src/parser/AtomicGroupParser.js +++ b/src/parser/AtomicGroupParser.js @@ -40,14 +40,12 @@ export default class AtomicGroupParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } - return other instanceof AtomicGroupParser && this.#parser.equals(other.#parser, strict) + doEquals(context, other, strict) { + return other instanceof AtomicGroupParser && this.#parser.equals(context, other.#parser, strict) } toString(indent = 0) { diff --git a/src/parser/CapturingGroupParser.js b/src/parser/CapturingGroupParser.js index 67231d6..0685b16 100644 --- a/src/parser/CapturingGroupParser.js +++ b/src/parser/CapturingGroupParser.js @@ -44,16 +44,14 @@ export default class CapturingGroupParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return other instanceof CapturingGroupParser && this.#id == other.#id - && this.#parser.equals(other.#parser, strict) + && this.#parser.equals(context, other.#parser, strict) } toString(indent = 0) { diff --git a/src/parser/ChainedParser.js b/src/parser/ChainedParser.js index 061e33b..b9d2e5d 100644 --- a/src/parser/ChainedParser.js +++ b/src/parser/ChainedParser.js @@ -52,16 +52,14 @@ export default class ChainedParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return other instanceof ChainedParser && this.#fn === other.#fn - && this.#parser.equals(other.parser, strict) + && this.#parser.equals(context, other.parser, strict) } toString(indent = 0) { diff --git a/src/parser/ClassParser.js b/src/parser/ClassParser.js index 56bf611..ae54c0a 100644 --- a/src/parser/ClassParser.js +++ b/src/parser/ClassParser.js @@ -37,18 +37,16 @@ export default class ClassParser extends AlternativeParser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return ( !strict && !this.#negative && !(other instanceof ClassParser && other.#negative) || other instanceof ClassParser && this.negative === other.#negative ) - && super.equals(other, strict) + && super.doEquals(context, other, strict) } toString(indent = 0) { diff --git a/src/parser/ClassShorthandParser.js b/src/parser/ClassShorthandParser.js index 7a75579..ff8886c 100644 --- a/src/parser/ClassShorthandParser.js +++ b/src/parser/ClassShorthandParser.js @@ -32,13 +32,11 @@ export default class ClassShorthandParser extends RegExpParser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return other instanceof ClassShorthandParser && this.#char == other.#char } diff --git a/src/parser/EscapedCharParser.js b/src/parser/EscapedCharParser.js index 6f4b2bc..ee17fed 100644 --- a/src/parser/EscapedCharParser.js +++ b/src/parser/EscapedCharParser.js @@ -43,15 +43,13 @@ export default class EscapedCharParser extends StringParser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return (!strict || other instanceof EscapedCharParser && this.#type === other.#type) - && super.equals(other, strict) + && super.doEquals(context, other, strict) } toString(indent = 0) { diff --git a/src/parser/FailureParser.js b/src/parser/FailureParser.js index 5c55231..7337303 100644 --- a/src/parser/FailureParser.js +++ b/src/parser/FailureParser.js @@ -13,13 +13,11 @@ export default class FailureParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return other instanceof FailureParser } diff --git a/src/parser/LazyParser.js b/src/parser/LazyParser.js index 42afafe..1ed2fb3 100644 --- a/src/parser/LazyParser.js +++ b/src/parser/LazyParser.js @@ -50,20 +50,19 @@ export default class LazyParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (other instanceof LazyParser && this.#parser === other.#parser) { - return true - } - this.resolve() - if (!strict) { - other = other.actualParser() - } else if (other instanceof LazyParser) { + doEquals(context, other, strict) { + if (other instanceof LazyParser) { + if (this.#parser === other.#parser) { + return true + } other = other.resolve() } - return this.#resolvedPraser === other || this.#resolvedPraser.equals(other, strict) + this.resolve() + return this.#resolvedPraser.equals(context, other, strict) } toString(indent = 0) { diff --git a/src/parser/LookaroundParser.js b/src/parser/LookaroundParser.js index ac6a71a..fa3a0c1 100644 --- a/src/parser/LookaroundParser.js +++ b/src/parser/LookaroundParser.js @@ -66,17 +66,15 @@ export default class LookaroundParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return this === other || other instanceof LookaroundParser && this.#type === other.#type - && this.#parser.equals(other.#parser, strict) + && this.#parser.equals(context, other.#parser, strict) } toString(indent = 0) { diff --git a/src/parser/MapParser.js b/src/parser/MapParser.js index b5be302..a7f760f 100644 --- a/src/parser/MapParser.js +++ b/src/parser/MapParser.js @@ -55,21 +55,14 @@ export default class MapParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - let self = /** @type {Parser} */(this) - if (!strict) { - other = other.actualParser() - } - return this === other || ( - strict - ? other instanceof MapParser - && this.#mapper === other.#mapper - && this.#parser.equals(other.#parser, strict) - : this.actualParser().equals(other, strict) - ) + doEquals(context, other, strict) { + return other instanceof MapParser + && this.#mapper === other.#mapper + && this.#parser.equals(context, other.#parser, strict) } toString(indent = 0) { diff --git a/src/parser/NonCapturingGroupParser.js b/src/parser/NonCapturingGroupParser.js index a92e04f..b0797c7 100644 --- a/src/parser/NonCapturingGroupParser.js +++ b/src/parser/NonCapturingGroupParser.js @@ -47,16 +47,12 @@ export default class NonCapturingGroupParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } - return strict - ? other instanceof NonCapturingGroupParser && this.#parser.equals(other.#parser, strict) - : this.actualParser().equals(other, strict) + doEquals(context, other, strict) { + return other instanceof NonCapturingGroupParser && this.#parser.equals(context, other.#parser, strict) } toString(indent = 0) { diff --git a/src/parser/Parser.js b/src/parser/Parser.js index b054f6b..3eb4c2c 100644 --- a/src/parser/Parser.js +++ b/src/parser/Parser.js @@ -26,7 +26,7 @@ export default class Parser { * @param {Parser} parser */ dominates(parser) { - return this.equals(parser, false) + //return this.equals(context, parser, false) } /** @returns {Parser[]} */ @@ -84,11 +84,37 @@ export default class Parser { } /** + * @param {Context} context + * @param {Parser} other + * @param {Boolean} strict + */ + equals(context, other, strict) { + let self = /** @type {Parser} */(this) + if (self === other) { + return true + } + if (!strict) { + self = this.actualParser() + other = other.actualParser() + } + let memoized = context.visited.get(self, other) + if (memoized !== undefined) { + return memoized + } else if (memoized === undefined) { + context.visited.set(self, other, true) + memoized = self.doEquals(context, other, strict) + context.visited.set(self, other, memoized) + } + return memoized + } + + /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - return strict ? this.actualParser() === other.actualParser() : this === other + doEquals(context, other, strict) { + return false } toString(indent = 0) { diff --git a/src/parser/RangeParser.js b/src/parser/RangeParser.js index 1dcb2b3..e85f8f9 100644 --- a/src/parser/RangeParser.js +++ b/src/parser/RangeParser.js @@ -41,16 +41,14 @@ export default class RangeParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return other instanceof RangeParser - && this.#from.equals(other.#from, strict) - && this.#to.equals(other.#to, strict) + && this.#from.equals(context, other.#from, strict) + && this.#to.equals(context, other.#to, strict) } toString(indent = 0) { diff --git a/src/parser/RegExpParser.js b/src/parser/RegExpParser.js index 4ad67f0..8840104 100644 --- a/src/parser/RegExpParser.js +++ b/src/parser/RegExpParser.js @@ -56,13 +56,11 @@ export default class RegExpParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return other instanceof RegExpParser && (!strict || this.#group === other.#group) && this.#regexp.source === other.#regexp.source diff --git a/src/parser/SequenceParser.js b/src/parser/SequenceParser.js index b95ec52..d58bf05 100644 --- a/src/parser/SequenceParser.js +++ b/src/parser/SequenceParser.js @@ -49,21 +49,16 @@ export default class SequenceParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } - if (this === other) { - return true - } + doEquals(context, other, strict) { if (!(other instanceof SequenceParser) || this.#parsers.length != other.#parsers.length) { return false } for (let i = 0; i < this.#parsers.length; ++i) { - if (!this.#parsers[i].equals(other.#parsers[i], strict)) { + if (!this.#parsers[i].equals(context, other.#parsers[i], strict)) { return false } } diff --git a/src/parser/StringParser.js b/src/parser/StringParser.js index c64e80b..fb15a92 100644 --- a/src/parser/StringParser.js +++ b/src/parser/StringParser.js @@ -43,13 +43,11 @@ export default class StringParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return other instanceof StringParser && this.#value === other.#value } diff --git a/src/parser/SuccessParser.js b/src/parser/SuccessParser.js index 75687ce..7449c57 100644 --- a/src/parser/SuccessParser.js +++ b/src/parser/SuccessParser.js @@ -24,13 +24,11 @@ export default class SuccessParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } + doEquals(context, other, strict) { return other instanceof SuccessParser } diff --git a/src/parser/TimesParser.js b/src/parser/TimesParser.js index 755b3df..5e12325 100644 --- a/src/parser/TimesParser.js +++ b/src/parser/TimesParser.js @@ -66,18 +66,15 @@ export default class TimesParser extends Parser { } /** + * @param {Context} context * @param {Parser} other * @param {Boolean} strict */ - equals(other, strict) { - if (!strict) { - other = other.actualParser() - } - return this === other - || other instanceof TimesParser + doEquals(context, other, strict) { + return other instanceof TimesParser && this.#min === other.#min && this.#max === other.#max - && this.#parser.equals(other.#parser, strict) + && this.#parser.equals(context, other.#parser, strict) } toString(indent = 0) { diff --git a/src/types.js b/src/types.js index e9aa9d8..2ba5c54 100644 --- a/src/types.js +++ b/src/types.js @@ -5,6 +5,11 @@ * @typedef {import("./Reply.js").Result} Result */ +/** + * @template T + * @typedef {import("./utility/PairMap.js").default} PairMap + */ + /** * @template V * @typedef {import("./parser/Parser.js").default} Parser @@ -36,11 +41,15 @@ * } ConstructorsFromArrayTypes **/ +/** + * + */ + /** * @typedef {{ + * regexer: Regexer, * input: String, - * parser: Regexer, - * visited: Set>, + * visited: PairMap, * }} Context */ @@ -65,9 +74,9 @@ /** * @template T * @typedef {T extends [] ? [] - * : T extends [import("./Regexer.js").default] ? [P] - * : T extends [import("./Regexer.js").default, ...infer Rest] ? [P, ...UnwrapParser] + * : T extends [infer R] ? [UnwrapParser] + * : T extends [infer R, ...infer Rest] ? [UnwrapParser, ...UnwrapParser] * : T extends import("./Regexer.js").default ? P - * : any + * : Parser * } UnwrapParser */ diff --git a/src/utility/PairMap.js b/src/utility/PairMap.js new file mode 100644 index 0000000..e7bda72 --- /dev/null +++ b/src/utility/PairMap.js @@ -0,0 +1,39 @@ +/** @template T */ +export default 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 + } +} diff --git a/tests/equality.spec.js b/tests/equality.spec.js index f256935..707282b 100644 --- a/tests/equality.spec.js +++ b/tests/equality.spec.js @@ -154,7 +154,7 @@ test("Test 4", async ({ page }) => { test("Test 5", async ({ page }) => { const lhs = R.seq(R.number, R.nonGrp(R.whitespaceInline), R.alt(R.str("a").map(v => 123), R.str("b"))) - const rhs = R.seq(R.number, /*******/ R.whitespaceInline, R.alt(R.str("a"), /***********/ R.str("b"))) + const rhs = R.seq(R.number, /******/ R.whitespaceInline, R.alt(R.str("a"), /************/ R.str("b"))) expect(R.equals(lhs, rhs)).toBeTruthy() expect(R.equals(rhs, lhs)).toBeTruthy() expect(R.equals(lhs, rhs, true)).toBeFalsy() @@ -363,5 +363,66 @@ test("Test 12", async ({ page }) => { }) test("Test 13", async ({ page }) => { + class Grammar { + /** @type {Regexer>} */ + static a = R.seq(R.str("a"), R.str("a"), R.lazy(() => this.a)) + static root = this.a + } + expect(R.equals( + Grammar.a, + R.seq(R.str("a"), R.str("a"), R.seq(R.str("a"), R.str("a"), R.seq(R.str("a"), R.str("a"), Grammar.a))), + true + )).toBeTruthy() +}) + +test("Test 14", async ({ page }) => { + class Grammar { + static a = R.regexp(/a/).map(f3) + static b = R.grp(R.str("b")) + static c = R.alt(Grammar.a, Grammar.b, R.lazy(() => Grammar.d)) + /** @type {Regexer>} */ + static d = R.seq(Grammar.c, R.str("d")) + static root = this.d + } + expect(R.equals( + Grammar.root, + R.seq(R.alt(R.regexp(/a/).map(f3), Grammar.b, R.seq(Grammar.c, R.str("d"))), R.str("d")), + true + )).toBeTruthy() +}) +test("Test 15", async ({ page }) => { + class Grammar { + /** @type {Regexer>} */ + static a = R.seq(R.str("a"), R.str("a"), R.lazy(() => this.a)) + static b = R.seq(R.str("a"), R.str("a"), R.lazy(() => this.b)) + } + expect(R.equals( + Grammar.a, + Grammar.b, + )).toBeTruthy() +}) + +test("Test 16", async ({ page }) => { + class Grammar { + /** @type {Regexer>} */ + static a = R.seq(R.str("a"), R.str("a"), R.lazy(() => this.a)) + static b = R.seq(R.str("a"), R.lazy(() => this.b)) + } + expect(R.equals( + Grammar.a, + Grammar.b, + )).toBeFalsy() +}) + +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)))) + } + expect(R.equals( + Grammar.a, + Grammar.b, + )).toBeTruthy() })