1553 lines
34 KiB
JavaScript
1553 lines
34 KiB
JavaScript
function cons(a, b) {
|
|
return [a, b];
|
|
}
|
|
|
|
function *listIter(l) {
|
|
while (l) {
|
|
yield l[0];
|
|
l = l[1];
|
|
}
|
|
}
|
|
|
|
let _elemDebugInfo = null;
|
|
function _currentElem() {
|
|
if (_elemDebugInfo)
|
|
return _elemDebugInfo.currentElem();
|
|
}
|
|
|
|
class AstNode {
|
|
__elemId;__pure;__createdAt=new (class AstNodeCreation extends Error{})('AstNode');
|
|
next = null; prev = null;
|
|
compile(ctx) {}
|
|
|
|
*_getNexts() {}
|
|
|
|
*getNexts() {
|
|
yield *this._getNexts();
|
|
if (this.next)
|
|
yield this.next;
|
|
}
|
|
|
|
*recurseTree() {
|
|
yield this;
|
|
let iters = [this.getNexts()];
|
|
while (iters.length) {
|
|
let citers = iters;
|
|
iters = [];
|
|
for (let iter of citers) {
|
|
for (let o of iter) {
|
|
iters.push(o.getNexts());
|
|
yield o;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_isPure() { return true; }
|
|
|
|
isPure() {
|
|
for (let o of this.recurseTree()) {
|
|
if (!o._isPure()) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
_clone() {
|
|
return new this.constructor;
|
|
}
|
|
|
|
clone() {
|
|
let oldN = this, newN = this._clone();
|
|
let ret = newN;
|
|
while (1) {
|
|
oldN = this.next;
|
|
if (!oldN)
|
|
break;
|
|
let nextNewN = oldN.clone();
|
|
nextNewN.attachTo(newN);
|
|
newN = newN.chain(nextNewN);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
attachTo(parent) {
|
|
if (this.prev) {
|
|
let e = this.clone();
|
|
e.attachTo(parent);
|
|
return e;
|
|
}
|
|
this.prev = parent;
|
|
return this;
|
|
}
|
|
|
|
chain(other) {
|
|
let current = this;
|
|
while (current.next)
|
|
current = current.next;
|
|
other = other.attachTo(current);
|
|
current.next = other;
|
|
return other;
|
|
}
|
|
}
|
|
|
|
class AstString extends AstNode {
|
|
str;
|
|
constructor(str) {
|
|
if (str.length == 0) {
|
|
return new AstNode;
|
|
}
|
|
if (str.length == 1) {
|
|
return new AstChars(new CharsPredicate(false, [str]));
|
|
}
|
|
super();this.str = str;
|
|
}
|
|
_clone() { return new AstString(this.str); }
|
|
|
|
compile(ctx) {
|
|
let sc = ctx.scope();
|
|
let s = ctx.newId();
|
|
let i = ctx.newId();
|
|
let b = ctx.newId();
|
|
let c = ctx.newId();
|
|
let ava = ctx.newId();
|
|
let str = ctx.bind(this.str);
|
|
ctx.write(
|
|
`{let ${s}=${str};for(let ${i}=0;${i}<${this.str.length};){`+
|
|
`while(${ctx.s}.wait())yield;`+
|
|
`if(${ctx.s}.eof())`+ctx.exit+
|
|
`let ${ava}=${ctx.s}.avail();`+
|
|
`if (${ava}>${this.str.length}-${i})${ava}=${this.str.length}-${i};`+
|
|
`let ${b}=${s}.slice(${i},${i}+${ava});`+
|
|
`let ${c}=${ctx.s}.peek(${ava});`+
|
|
`if(${b}!=${c})`+ctx.exit+
|
|
`${ctx.s}.consume(${ava});`+
|
|
`${i}+=${ava};`+
|
|
`}}`
|
|
)
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
class XFunction extends Function {
|
|
constructor(fn) {
|
|
return Object.setPrototypeOf(fn, new.target.prototype);
|
|
}
|
|
}
|
|
|
|
class CharsPredicate extends XFunction {
|
|
ops; invert;
|
|
|
|
constructor(invert, ...ops) {
|
|
super((c) => {
|
|
if (typeof c == 'string') {
|
|
c = c.charCodeAt(0);
|
|
}
|
|
let ok = false;
|
|
for (let [op,...b] of this.ops) {
|
|
if (op == '==')
|
|
ok ||= c == b[0];
|
|
else if (op == '<=>')
|
|
ok ||= c >= b[0] && c <= b[1];
|
|
if (ok) break;
|
|
}
|
|
if (this.invert) ok = !ok;
|
|
return ok;
|
|
});
|
|
function normChar(c) {
|
|
if (typeof c == 'string') {
|
|
if (c.length != 1)
|
|
throw new TypeError('suspiciously long character')
|
|
c = c.charCodeAt(0);
|
|
}
|
|
if (typeof c != 'number') {
|
|
throw new Error('not a char or charcode');
|
|
}
|
|
return c;
|
|
}
|
|
this.ops = ops.flatMap(o => {
|
|
if (o instanceof CharsPredicate && o.invert == invert) {
|
|
return o.ops;
|
|
}
|
|
if (!(Array.isArray(o) && (o.length == 2 || o.length == 1)))
|
|
throw new TypeError('must be array with arity 1 (chars: `[str]`) or 2 (range: `[char,char]`)');
|
|
if (o.length == 1) {
|
|
o = o[0]
|
|
if (typeof o == 'string') {
|
|
o = o.split('');
|
|
} else {
|
|
o = o.map(normChar);
|
|
}
|
|
return o.map(e => ['==',e.charCodeAt(0)]);
|
|
} else if (o.length == 2) {
|
|
o = o.map(normChar);
|
|
if (o[0] > o[1])
|
|
o = [o[1],o[0]];
|
|
return [
|
|
['<=>', o[0],o[1]],
|
|
];
|
|
}
|
|
});
|
|
this.invert = invert;
|
|
}
|
|
|
|
compile(ctx, c) {
|
|
let ops;
|
|
if (this.ops.length) {
|
|
ops = this.ops.map(e => ({
|
|
['<=>']: (a,b) => `${c}>=${ctx.bind(a)}&&${c}<=${ctx.bind(b)}`,
|
|
['==']: (a) => `${c}==${ctx.bind(a)}`
|
|
})[e[0]](...e.slice(1).map(e=>String.fromCharCode(e)))).join('||')
|
|
} else {
|
|
ops = 'true';
|
|
}
|
|
ctx.write(this.invert ? `!(${ops})` : ops);
|
|
}
|
|
}
|
|
|
|
class AstChars extends AstNode {
|
|
pred;
|
|
constructor(pred) {
|
|
super();
|
|
this.pred = pred;
|
|
}
|
|
_clone() { return new AstChars(this.pred); }
|
|
|
|
compilePred(ctx, c) {
|
|
if (this.pred instanceof CharsPredicate) {
|
|
ctx.write('if(!(');
|
|
this.pred.compile(ctx, c);
|
|
ctx.write('))'+ctx.exit);
|
|
} else {
|
|
let pred = ctx.bind(this.pred);
|
|
ctx.write(
|
|
'if(!'+pred+'('+c+'))'+ctx.exit
|
|
)
|
|
}
|
|
}
|
|
|
|
compile(ctx) {
|
|
let sc = ctx.scope();
|
|
let c = ctx.newId();
|
|
ctx.write('{'+
|
|
'while ('+ctx.s+'.wait())yield;'+
|
|
'if('+ctx.s+'.eof())'+ctx.exit+
|
|
'let '+c+'='+ctx.s+'.peek();');
|
|
this.compilePred(ctx, c);
|
|
ctx.write(
|
|
''+ctx.s+'.consume();'+
|
|
'}')
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
class AstCond extends AstNode {
|
|
conds;
|
|
constructor(conds) {
|
|
super();
|
|
this.conds = conds.map(([p,c]) => [p.attachTo(this), c.attachTo(this)]);
|
|
}
|
|
_clone() { return new AstCond(this.conds); }
|
|
*_getNexts() { yield *this.conds.flatMap(e => e); }
|
|
|
|
compile(ctx) {
|
|
let sc = ctx.scope();
|
|
ctx.write('{')
|
|
for (let [cond, ops] of this.conds) {
|
|
let res = ctx.newId();
|
|
let sv = ctx.newId();
|
|
ctx.write(`let ${sv}=${ctx.s}.save();`)
|
|
ctx.compileEnclosed(cond, res);
|
|
ctx.write(`${ctx.s}.restore(${sv});${sv}.destroy();` +
|
|
`if(${res}){`);
|
|
ctx.compile(ops);
|
|
ctx.write('}else{');
|
|
}
|
|
ctx.write('}'.repeat(1 + this.conds.length))
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
let breakSymbol = Symbol('break');
|
|
|
|
class AstRepeat extends AstNode {
|
|
min; max; inner; strict; label; canBreak=false;
|
|
constructor(lim, inner, strict, label=null) {
|
|
super();
|
|
if (typeof lim == 'string' || typeof lim == 'symbol' || typeof lim == 'number' || lim == null)
|
|
lim = [lim,lim];
|
|
let [min,max] = lim;
|
|
[min, max] = [min, max].map(e => {
|
|
if (e == null) return e;
|
|
if (!(({number: 1, string: 1, symbol: 1})[typeof e]))
|
|
throw new TypeError('expected number, string or symbol');
|
|
return e;
|
|
});
|
|
min ??= 0;
|
|
max ??= Infinity;
|
|
this.min = min;
|
|
this.max = max;
|
|
this.inner = inner.attachTo(this);
|
|
this.strict = strict;
|
|
this.label = label;
|
|
}
|
|
_clone() { return new AstRepeat([this.min,this.max],this.inner,this.strict,this.label); }
|
|
*_getNexts() { yield this.inner; }
|
|
|
|
compile(ctx) {
|
|
let sc = ctx.scope();
|
|
ctx.write('{');
|
|
let min, max;
|
|
let dynamic = false;
|
|
[min, max] = [this.min, this.max].map(e => {
|
|
if (typeof e == 'string' || typeof e == 'symbol') {
|
|
dynamic = true;
|
|
let v = ctx.newId();
|
|
ctx.write(`let ${v}=${ctx.result}.get(${ctx.bind(e)});`)
|
|
return v;
|
|
}
|
|
return ctx.bind(e);
|
|
});
|
|
let i = ctx.newId();
|
|
if (this.inner instanceof AstChars && this.inner.next == null) {
|
|
let ava = ctx.newId();
|
|
let c = ctx.newId();
|
|
let j = ctx.newId();
|
|
let noop = this.inner.pred instanceof CharsPredicate &&
|
|
(this.inner.pred.invert) &&
|
|
(!this.inner.pred.ops.length);
|
|
ctx.write(
|
|
`for(let ${i}=0;${i}<${max};){`+
|
|
`while(${ctx.s}.wait())yield;`+
|
|
`if(${ctx.s}.eof()){if(${i}<${min})${ctx.exit}else{break}}`+
|
|
`let ${ava}=${ctx.s}.avail();`+
|
|
`if (${ava}>${max}-${i})${ava}=${max}-${i};`
|
|
);
|
|
if (!noop) {
|
|
ctx.write(
|
|
`let ${c}=${ctx.s}.peek(${ava});`+
|
|
`let ${j}=0;`+
|
|
`for (;${j}<${ava};${j}++){`
|
|
);
|
|
let oldExit = ctx.exit;
|
|
ctx.exit = 'break;'
|
|
this.inner.compilePred(ctx, `${c}[${j}]`);
|
|
ctx.exit = oldExit;
|
|
ctx.write(
|
|
`}`
|
|
);
|
|
ctx.write(
|
|
`if(${j}<${ava}){`+
|
|
((!this.strict) ? (
|
|
`if(${i}+${j}<${min}){`+
|
|
`${ctx.exit}`+
|
|
`}else{`+
|
|
`if(${j}>0){`+
|
|
`${ctx.s}.consume(${j})`+
|
|
`}`+
|
|
`break`+
|
|
`}`)
|
|
: (ctx.exit)
|
|
)+
|
|
`}`
|
|
);
|
|
}
|
|
ctx.write(
|
|
`${ctx.s}.consume(${noop?ava:j});`+
|
|
`${i}+=${noop?ava:j};`+
|
|
`}`
|
|
)
|
|
} else {
|
|
let sv = ctx.newId();
|
|
let res = ctx.newId();
|
|
ctx.write(
|
|
`for(let ${i}=0;${i}<${max};${i}++){`+
|
|
`let ${sv}=(${i}>=${min})?${this.strict?'true':`${ctx.s}.save()`}:null;`
|
|
);
|
|
if (this.strict) {
|
|
ctx.compile(this.inner);
|
|
} else {
|
|
ctx.compileEnclosed(this.inner, res);
|
|
if (!this.inner.isPure())
|
|
ctx.mergeResult(res);
|
|
ctx.write(
|
|
`if(!${res}){`+
|
|
`if(${sv}){`+
|
|
`${ctx.s}.restore(${sv});`+
|
|
`${sv}.destroy();`+
|
|
`break`+
|
|
`}else{`+
|
|
`${ctx.exit}`+
|
|
`}`+
|
|
`}`+
|
|
`if(${sv})${sv}.destroy()`
|
|
)
|
|
};
|
|
if (this.canBreak) {
|
|
let brk = ctx.newId();
|
|
let brkSym = ctx.globalBind('breakSymbol', breakSymbol);
|
|
ctx.write(
|
|
`let ${brk}=${ctx.result}.get(${brkSym});`+
|
|
`if(${brk}===true||${brk}===${ctx.bind(this.label)}){if(${sv}){break}else{${ctx.exit}}}`
|
|
)
|
|
}
|
|
ctx.write(
|
|
`}`
|
|
);
|
|
}
|
|
ctx.write('}');
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
class AstDisjunction extends AstNode {
|
|
branches;
|
|
constructor(...branches) {
|
|
if (branches.every(e => (
|
|
(e instanceof AstChars) &&
|
|
(e.next == null) &&
|
|
(e.pred instanceof CharsPredicate) &&
|
|
(!e.pred.invert)
|
|
))) {
|
|
return new AstChars(new CharsPredicate(false, ...branches.map(e => e.pred)))
|
|
}
|
|
super();
|
|
this.branches = branches.map(e => e.attachTo(this));
|
|
}
|
|
_clone() { return new AstDisjunction(...this.branches); }
|
|
*_getNexts() { yield*this.branches; }
|
|
|
|
compile(ctx) {
|
|
let sc=ctx.scope();
|
|
let sv=ctx.newId();
|
|
let finished=ctx.newId();
|
|
ctx.write(
|
|
'do{'+
|
|
`let ${sv}=${ctx.s}.save();`+
|
|
`let ${finished}=null;`
|
|
);
|
|
|
|
let cleaned = false;
|
|
for (let [i,o] of this.branches.map((v,i)=>[i,v])) {
|
|
let sc = ctx.scope();
|
|
let oldResult = ctx.scope();
|
|
ctx.write(
|
|
'{'
|
|
);
|
|
if (i)
|
|
ctx.write(
|
|
`${ctx.s}.restore(${sv});`
|
|
)
|
|
if (i == this.branches.length-1) {
|
|
cleaned = true;
|
|
ctx.write(
|
|
`${sv}=(${sv}.destroy(),null);`
|
|
);
|
|
}
|
|
let res = ctx.newId();
|
|
ctx.compileEnclosed(o, res);
|
|
let sve=ctx.newId();
|
|
ctx.write(
|
|
`if(${res}){`+
|
|
`let ${sve}=${ctx.s}.save();`+
|
|
`if((!${finished})||(${finished}.p.max(${sve})==${sve})){`+
|
|
`if(${finished}){${finished}.p.destroy()}`+
|
|
`${finished}={p:${sve},v:${res}}`+
|
|
`}`+
|
|
'}'+
|
|
'}'
|
|
);
|
|
ctx.closeScope(sc);
|
|
}
|
|
if (!cleaned) {
|
|
ctx.write(`${sv}=(${sv}.destroy(),null);`);
|
|
}
|
|
ctx.write(
|
|
`if(${finished}){`+
|
|
`${ctx.s}.restore(${finished}.p);`+
|
|
`${finished}.p.destroy();`
|
|
);
|
|
ctx.mergeResult(finished+'.v');
|
|
ctx.write(
|
|
`}else{`+
|
|
ctx.exit+
|
|
`}`+
|
|
`}while(0)`
|
|
);
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
class AstAbort extends AstNode {
|
|
compile(ctx) {
|
|
ctx.write(ctx.exit);
|
|
}
|
|
}
|
|
|
|
class AstResultEdit extends AstNode {
|
|
cback;
|
|
constructor(cback) {
|
|
super();
|
|
this.cback = cback;
|
|
}
|
|
_clone() { return new AstResultEdit(this.cback); }
|
|
_isPure() { return false; }
|
|
|
|
static wrapCall(cback) {
|
|
return (result, ...arg) => {
|
|
let ret = cback({
|
|
set(k, v) { result.set(k, v); },
|
|
get(k) { return result.get(k); },
|
|
add(k, v) { result.add(k, v); },
|
|
}, ...arg);
|
|
return !((!ret) && (ret != null))
|
|
}
|
|
}
|
|
|
|
compile(ctx) {
|
|
let sc = ctx.scope();
|
|
let cback = ctx.bind(this.constructor.wrapCall(this.cback));
|
|
ctx.write(`if(!${cback}(${ctx.result}))${ctx.exit}`)
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
class AstMark extends AstNode {
|
|
key; value; multi;
|
|
constructor(key, value, multi) {
|
|
super();
|
|
this.value = value;
|
|
this.key = key;
|
|
this.multi = multi;
|
|
}
|
|
_clone() { return new AstMark(this.key, this.value, this.multi); }
|
|
_isPure() { return false; }
|
|
|
|
compile(ctx) {
|
|
if (this.key == breakSymbol) {
|
|
let obj = this.prev;
|
|
while (1) {
|
|
if (obj instanceof AstRepeat && (this.value === true || obj.label === this.value)) {
|
|
obj.canBreak = true;
|
|
break;
|
|
}
|
|
obj = obj.prev;
|
|
if (!obj) {
|
|
throw new Error('misplaced break operator');
|
|
}
|
|
}
|
|
}
|
|
|
|
let sc = ctx.scope();
|
|
let key = (this.key == breakSymbol) ? ctx.globalBind('breakSymbol', this.key) : ctx.bind(this.key);
|
|
let value = ctx.bind(this.value);
|
|
ctx.write(`${ctx.result}.${this.multi?'add':'set'}(${key}, ${value});`)
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
class AstCapture extends AstNode {
|
|
key; inner; multi;
|
|
constructor(key, inner, multi) {
|
|
super();
|
|
this.key = key;
|
|
this.inner = inner.attachTo(this);
|
|
this.multi = multi;
|
|
}
|
|
_clone() { return new AstCapture(this.key, this.inner, this.multi); }
|
|
*_getNexts() { yield this.inner; }
|
|
_isPure() { return false; }
|
|
|
|
compile(ctx) {
|
|
let sc = ctx.scope();
|
|
let key = ctx.bind(this.key);
|
|
let sv1 = ctx.newId();
|
|
let sv2 = ctx.newId();
|
|
ctx.write(`{let ${sv1}=${ctx.s}.save();`);
|
|
let oldExit = ctx.exit;
|
|
ctx.exit = `{${sv1}.destroy();${ctx.exit}}`;
|
|
ctx.compile(this.inner);
|
|
ctx.exit=oldExit;
|
|
ctx.write(
|
|
`let ${sv2}=${ctx.s}.save();`+
|
|
`${ctx.result}.${this.multi?'add':'set'}(${key},${sv1}.slice(${sv2}));`+
|
|
`${sv1}.destroy();${sv2}.destroy()}`
|
|
);
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
class AstLimit extends AstNode {
|
|
limit; inner;
|
|
constructor(limit, inner) {
|
|
super();
|
|
this.limit = limit;
|
|
this.inner = inner.attachTo(this);
|
|
}
|
|
_clone() { return new AstLimit(this.limit, this.inner); }
|
|
*_getNexts() { yield this.inner; }
|
|
|
|
compile(ctx) {
|
|
let sc = ctx.scope();
|
|
let limc = ctx.bind(this.limit);
|
|
let limk = ctx.newId();
|
|
ctx.write(`{let ${limk}=${ctx.s}.setLimit(${limc});`);
|
|
let oldExit = ctx.exit;
|
|
ctx.exit = `{${ctx.s}.unsetLimit(${limk});${ctx.exit}}`;
|
|
ctx.compile(this.inner);
|
|
ctx.exit=oldExit;
|
|
ctx.write(
|
|
`${ctx.s}.unsetLimit(${limk})}`
|
|
);
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
class AstCaptureYield extends AstNode {
|
|
key; inner;
|
|
constructor(key, inner) {
|
|
super();
|
|
this.key = key;
|
|
this.inner = inner.attachTo(this);
|
|
}
|
|
_clone() { return new AstCaptureYield(this.key, this.inner); }
|
|
*_getNexts() { yield this.inner; }
|
|
|
|
compile(ctx) {
|
|
let sc = ctx.scope();
|
|
let key = ctx.bind(this.key);
|
|
let yld = ctx.newId();
|
|
ctx.write(`{let ${yld}=${ctx.s}.addYielder(${key});`);
|
|
let oldExit = ctx.exit;
|
|
ctx.exit = `{${ctx.s}.removeYielder(${yld});${ctx.exit}}`;
|
|
ctx.compile(this.inner);
|
|
ctx.exit=oldExit;
|
|
ctx.write(
|
|
`${ctx.s}.removeYielder(${yld})}`
|
|
);
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
class AstYield extends AstNode {
|
|
key; value;
|
|
constructor(key, value) {
|
|
super();
|
|
this.value = value ?? key;
|
|
this.key = key;
|
|
}
|
|
_clone() { return new AstYield(this.key, this.value); }
|
|
_isPure() { return false; }
|
|
|
|
compile(ctx) {
|
|
let sc = ctx.scope();
|
|
let key = ctx.bind(this.key);
|
|
let value = ctx.bind(this.value);
|
|
ctx.write(`${ctx.s}.yield(${key},${ctx.result}.get(${value},true));`)
|
|
ctx.closeScope(sc);
|
|
}
|
|
}
|
|
|
|
function compileAst(elem) {
|
|
let ctx = {
|
|
s: 'D',
|
|
result: 'R',
|
|
vars: 0,
|
|
binds: [],
|
|
globals: [],
|
|
code: [],
|
|
debugInfo: [],
|
|
codeLength: 0,
|
|
|
|
exit: 'return false;',
|
|
exitTrapLoop(loop, vname, vval='false') {
|
|
let old = this.exit;
|
|
this.exit = '{'+(vname?(vname+'='+vval+';'):'')+'break '+loop+'}'
|
|
return old;
|
|
},
|
|
exitTrapFunc() {
|
|
let old = this.exit;
|
|
this.exit = 'return false;';
|
|
return old;
|
|
},
|
|
exitUntrap(old) {
|
|
this.exit = old;
|
|
},
|
|
|
|
scope() {
|
|
return this.vars;
|
|
},
|
|
|
|
closeScope(sc) {
|
|
this.vars = sc
|
|
},
|
|
|
|
newId() {
|
|
let n = '$'+this.vars
|
|
this.vars++;
|
|
return n;
|
|
},
|
|
|
|
bind(v, force=false) {
|
|
if (!force) {
|
|
if (typeof v == 'string')
|
|
return JSON.stringify(v);
|
|
else if (typeof v == 'bigint')
|
|
return v+'n';
|
|
else if (
|
|
typeof v == 'number' ||
|
|
typeof v == 'boolean' ||
|
|
v == null
|
|
)
|
|
return v+'';
|
|
}
|
|
let n = '$_'+this.binds.length;
|
|
this.binds.push([n,v]);
|
|
return n;
|
|
},
|
|
|
|
globalBind(k, ...v) {
|
|
let i = this.globals[k];
|
|
if (i != null) {
|
|
if (v.length)
|
|
this.binds[i][1] = v[0];
|
|
return this.binds[i][0]
|
|
}
|
|
if (!v.length)
|
|
return;
|
|
i = this.binds.length;
|
|
this.bind(v[0], true);
|
|
this.globals[k] = i;
|
|
return this.binds[i][0];
|
|
},
|
|
|
|
write(...str) {
|
|
this.code.push(...str)
|
|
this.codeLength += str.map(e=>e.length).reduce((a,b)=>a+b);
|
|
},
|
|
|
|
elemId: 0,
|
|
|
|
compile(elem) {
|
|
while (elem) {
|
|
if (elem.elemId == null)
|
|
elem.__elemId = this.elemId++;
|
|
if (elem.__pure == null)
|
|
elem.__pure = elem.isPure();
|
|
let name = elem.constructor.name+'#'+elem.__elemId
|
|
this.write('/*'+name+'{*/');
|
|
this.debugInfo.push({at: this.codeLength, type:'begin', name, createdAt: elem.__createdAt })
|
|
elem.compile(this);
|
|
this.debugInfo.push({at: this.codeLength, type:'end'});
|
|
this.write('/*}*/');
|
|
elem = elem.next;
|
|
}
|
|
},
|
|
|
|
compileEnclosed(elem, res, pure=elem.isPure()) {
|
|
let oldResult;
|
|
if(!pure) {
|
|
oldResult = this.newId();
|
|
this.write(
|
|
`let ${oldResult}=${this.result};`+
|
|
`${this.result}=${this.result}.clone();`
|
|
);
|
|
}
|
|
this.write(`let ${res}=${this.result};`);
|
|
let loop = this.newId();
|
|
this.write(''+loop+':do{');
|
|
let rcode = this.exitTrapLoop(loop, res);
|
|
this.compile(elem);
|
|
this.exitUntrap(rcode);
|
|
this.write('}while(0);')
|
|
if (!pure) {
|
|
this.write(
|
|
`${this.result}=${oldResult};`
|
|
);
|
|
}
|
|
},
|
|
|
|
mergeResult(res) {
|
|
this.write(
|
|
`{if(${res}&&(${this.result}!=${res}))`+
|
|
`{${this.result}=${res}.mergeDown()}}`
|
|
)
|
|
},
|
|
};
|
|
|
|
let resultClass = ctx.globalBind('ResultSet', ResultSet);
|
|
|
|
ctx.compile(elem);
|
|
|
|
let code =
|
|
[`"use strict";(${ctx.binds.map(e=>e[0]).join(',')})=>function*(${ctx.s}){`,
|
|
`let ${ctx.result}=new ${resultClass};`,
|
|
`${ctx.code.join('')}`,
|
|
`${ctx.s}.checkBarrier();`,
|
|
`return ${ctx.result}.build()}`,
|
|
];
|
|
|
|
ctx.debugInfo.forEach(e => e.at += code[0].length);
|
|
code = code.join('');
|
|
|
|
let func = (0,eval)(code)(...ctx.binds.map(e=>e[1]));
|
|
|
|
let Debug = {
|
|
debugInfo: ctx.debugInfo,
|
|
currentElem() {
|
|
let err = (new Error).stack.split('\n')
|
|
let s = err.find(e => e.match('compileAst'));
|
|
if (!s) return;
|
|
let i = s.match(/:([0-9]*)[^0-9]*$/)[1];
|
|
i = +i;
|
|
let stk = [];
|
|
for (let {at,type,...o} of this.debugInfo) {
|
|
if (at > i) break;
|
|
if (type == 'begin')
|
|
stk.push(o.name);
|
|
else if (type == 'end')
|
|
stk.pop();
|
|
}
|
|
return stk.pop();
|
|
}
|
|
}
|
|
|
|
return (...arg) => {
|
|
let gen = func(...arg);
|
|
gen.next = ((next) => function(...arg){
|
|
let dbg = _elemDebugInfo;
|
|
_elemDebugInfo = Debug;
|
|
try {
|
|
return next.apply(this, arg);
|
|
} finally {
|
|
_elemDebugInfo = dbg;
|
|
}
|
|
})(gen.next);
|
|
return gen;
|
|
}
|
|
}
|
|
|
|
class ResultSet {
|
|
data = Object.create(null); dirty=false; parent;
|
|
|
|
clone() {
|
|
let o = new ResultSet;
|
|
o.data = Object.create(this.data);
|
|
o.parent = this;
|
|
return o;
|
|
}
|
|
|
|
mergeDown() {
|
|
if (!this.dirty)
|
|
return this.parent;
|
|
for (let k of [...Object.getOwnPropertyNames(this.data), ...Object.getOwnPropertySymbols(this.data)]) {
|
|
this.parent._set(k, this.data[k]);
|
|
}
|
|
return this.parent;
|
|
}
|
|
|
|
build() {
|
|
return Object.fromEntries(
|
|
[...(function*(){ for(let i in this.data) yield [i,this.data[i]]; }).call(this)]
|
|
.filter(e => e[1] != null)
|
|
.map(([k,v]) => ([k,({
|
|
'value': v => v.data,
|
|
'list': v => [...listIter(v.data)].reverse(),
|
|
})[v.type](v)]))
|
|
.filter(e => e[1] != null)
|
|
)
|
|
}
|
|
|
|
_set(k, v) {
|
|
this.dirty = true;
|
|
if (v == null) v = undefined;
|
|
if (v == null && !(this.parent && (k in this.parent.data))) {
|
|
delete this.data[k];
|
|
return;
|
|
}
|
|
this.data[k] = v;
|
|
}
|
|
_get(k) { return this.data[k]; }
|
|
|
|
set(k, v) {
|
|
if (v != null)
|
|
v = { type: 'value', data: v };
|
|
this._set(k, v);
|
|
}
|
|
|
|
add(k, v) {
|
|
let o = this._get(k);
|
|
if (o && o.type == 'value') {
|
|
o = { type: 'list', data: cons(o.data, undefined) };
|
|
} else if (!o) {
|
|
o = { type: 'list', data: undefined };
|
|
}
|
|
this._set(k, { type: 'list', data: cons(v, o.data) });
|
|
}
|
|
|
|
get(k, fin=false) {
|
|
let o = this._get(k);
|
|
if (o && o.type == 'list') {
|
|
o = listIter(o.data);
|
|
if (fin)
|
|
o = [...o].reverse();
|
|
} else if (o && o.type == 'value')
|
|
o = o.data;
|
|
else
|
|
o = null;
|
|
return o;
|
|
}
|
|
}
|
|
|
|
let StreamoidPriv;
|
|
|
|
export class Streamoid {
|
|
static Pin = class StreamoidPin {
|
|
target; bufi; i;
|
|
|
|
constructor(target, bufi, i) {
|
|
this.target = target;
|
|
this.bufi = bufi;
|
|
this.i = i;
|
|
}
|
|
|
|
destroy() {
|
|
let n = this.target.#pins[this.bufi]-1;
|
|
if (!n)
|
|
delete this.target.#pins[this.bufi];
|
|
else
|
|
this.target.#pins[this.bufi] = n;
|
|
this.target.cleanup();
|
|
this.target = null;
|
|
}
|
|
|
|
span(dst, cb) {
|
|
let target = this.target;
|
|
if (target != dst.target)
|
|
throw new Error('non-matching targets');
|
|
let src = this;
|
|
if (src.bufi > dst.bufi || (src.bufi == dst.bufi && src.i > dst.i))
|
|
throw new Error('span go from start to end, not end to start');
|
|
if (src.bufi == dst.bufi) {
|
|
cb({ bufi: src.bufi, start: src.i, end: dst.i })
|
|
return;
|
|
}
|
|
if (!target.#bufs[src.bufi]) {
|
|
return;
|
|
}
|
|
cb({ bufi: src.bufi, start: src.i, end: target.#bufs[src.bufi].length });
|
|
for (let i=src.bufi+1; i<dst.bufi; i++) {
|
|
cb({ bufi: i, start: 0, end: target.#bufs[i].length });
|
|
}
|
|
cb({ bufi: dst.bufi, start: 0, end: dst.i });
|
|
}
|
|
|
|
length(other) {
|
|
let l = 0;
|
|
this.span(other, ({ start, end }) => {
|
|
l += end - start;
|
|
})
|
|
return l;
|
|
}
|
|
|
|
static max(...a) {
|
|
let maxBufi = -Infinity, maxI = -Infinity, maxP = null;
|
|
for (let p of a) {
|
|
if (p.bufi > maxBufi || (p.bufi == maxBufi && p.i > maxI)) {
|
|
[maxBufi, maxI, maxP] = [p.bufi, p.i, p];
|
|
}
|
|
}
|
|
return maxP;
|
|
}
|
|
|
|
max(...other) {
|
|
return StreamoidPin.max(this, ...other);
|
|
}
|
|
|
|
slice(other, cut=false) {
|
|
let a = [];
|
|
this.span(other, ({ bufi, start, end }) => {
|
|
if (end > 0)
|
|
a.push(this.target.#bufs[bufi].slice(start, end));
|
|
});
|
|
return (cut) ? a : (a.length ? (a.length == 1 ? a[0] : a.join('')) : '');
|
|
}
|
|
};
|
|
|
|
static Limit = class StreamoidLimit {
|
|
total; remain; constructor(total) { this.remain = this.total = total; }
|
|
};
|
|
|
|
#i=0; #bufi=0; #buf0=0; #bufs=[]; #pins=[]; #limits=[]; #yielders=[];
|
|
#minBufi = 0; #minI = 0; #broken=false;
|
|
#output = [];
|
|
|
|
static {
|
|
StreamoidPriv = class {
|
|
#target;
|
|
constructor(target) {
|
|
this.#target = target
|
|
}
|
|
// s/[^#]*#\([a-zA-Z0-9_]*\)[^#]*/\t\t\tget \1() { return this.#target.#\1 } set \1 (v) { return this.#target.#\1 = v }\r/g
|
|
get i() { return this.#target.#i } set i (v) { return this.#target.#i = v }
|
|
get bufi() { return this.#target.#bufi } set bufi (v) { return this.#target.#bufi = v }
|
|
get buf0() { return this.#target.#buf0 } set buf0 (v) { return this.#target.#buf0 = v }
|
|
get bufs() { return this.#target.#bufs } set bufs (v) { return this.#target.#bufs = v }
|
|
get pins() { return this.#target.#pins } set pins (v) { return this.#target.#pins = v }
|
|
get limits() { return this.#target.#limits } set limits (v) { return this.#target.#limits = v }
|
|
get yielders() { return this.#target.#yielders } set yielders (v) { return this.#target.#yielders = v }
|
|
|
|
get minBufi() { return this.#target.#minBufi } set minBufi (v) { return this.#target.#minBufi = v }
|
|
get minI() { return this.#target.#minI } set minI (v) { return this.#target.#minI = v }
|
|
get broken() { return this.#target.#broken } set broken (v) { return this.#target.#broken = v }
|
|
|
|
get output() { return this.#target.#output } set output (v) { return this.#target.#output = v }
|
|
}
|
|
}
|
|
|
|
wait() {
|
|
if (!(this.#bufi in this.#bufs))
|
|
return true;
|
|
}
|
|
|
|
checkBarrier() {
|
|
if (this.#broken)
|
|
throw new Error('crossed a no-backtrack barrier');
|
|
}
|
|
|
|
save() {
|
|
this.#pins[this.#bufi] ??= 0;
|
|
this.#pins[this.#bufi]++;
|
|
let p = new StreamoidPin(this, this.#bufi, this.#i);
|
|
return p;
|
|
}
|
|
|
|
barrier() {
|
|
this.#minBufi = this.#bufi;
|
|
this.#minI = this.#minI;
|
|
}
|
|
|
|
hasOutput() {
|
|
return this.#output.length;
|
|
}
|
|
|
|
readOutput() {
|
|
let out = this.#output;
|
|
this.#output = [];
|
|
return out;
|
|
}
|
|
|
|
yield(k,v) {
|
|
this.checkBarrier();
|
|
this.#output.push([k,v]);
|
|
this.barrier();
|
|
}
|
|
|
|
addYielder(k) {
|
|
let o = {name: k}
|
|
this.#yielders.push(o);
|
|
return o;
|
|
}
|
|
|
|
removeYielder(o) {
|
|
if (this.#yielders[this.#yielders.length-1] != o)
|
|
throw new Error('remove yielder across multiple yielders');
|
|
this.#yielders.pop();
|
|
}
|
|
|
|
saveEnd() {
|
|
let bufi = this.#bufi;
|
|
let i = this.#i;
|
|
this.#bufi = this.#bufs.length;
|
|
while (this.#bufi>0 && this.#bufs[this.#bufi-1]==null) this.#bufi--;
|
|
this.#i = 0;
|
|
let p = this.save();
|
|
this.#bufi = bufi;
|
|
this.#i = i;
|
|
return p;
|
|
}
|
|
|
|
saveStart() {
|
|
let bufi = this.#bufi;
|
|
let i = this.#i;
|
|
this.#bufi = this.#buf0;
|
|
this.#i = 0;
|
|
let p = this.save();
|
|
this.#bufi = bufi;
|
|
this.#i = i;
|
|
return p;
|
|
}
|
|
|
|
#bufSpan(mode, f) {
|
|
let a,b;
|
|
if (mode == 'back') {
|
|
a=this.saveStart();
|
|
b=this.save();
|
|
} else if (mode == 'front') {
|
|
a=this.save();
|
|
b=this.saveEnd();
|
|
}
|
|
try {
|
|
return f(a,b)
|
|
} finally {
|
|
a.destroy();
|
|
b.destroy();
|
|
}
|
|
}
|
|
|
|
get backBufferLength() { return this.#bufSpan('back', (a,b) => a.length(b)); }
|
|
get backBuffer() { return this.#bufSpan('back', (a,b) => a.slice(b)); }
|
|
get backBufferSlices() { return this.#bufSpan('back', (a,b) => a.slice(b, true)); }
|
|
|
|
get frontBufferLength() { return this.#bufSpan('front', (a,b) => a.length(b)); }
|
|
get frontBuffer() { return this.#bufSpan('front', (a,b) => a.slice(b)); }
|
|
get frontBufferSlices() { return this.#bufSpan('front', (a,b) => a.slice(b, true)); }
|
|
|
|
restore(p) {
|
|
if (p.target != this)
|
|
throw new Error('invalid pin');
|
|
if (this.#limits.length > 0 && (p.bufi != this.#bufi || p.i != this.#i)) {
|
|
let np = this.save();
|
|
this.unconsumeLimit(p.length(np));
|
|
np.destroy();
|
|
}
|
|
if (p.bufi < this.#minBufi || (p.bufi == this.#minBufi && p.i < this.#minI)) {
|
|
this.#broken = true;
|
|
}
|
|
this.#bufi = p.bufi;
|
|
this.#i = p.i;
|
|
}
|
|
|
|
cleanup() {
|
|
for (let i=this.#buf0;;i++) {
|
|
if (i >= this.#bufi) break;
|
|
if (this.#pins[i]) break;
|
|
delete this.#bufs[i];
|
|
this.#buf0++;
|
|
}
|
|
if (!(this.#buf0 in this.#bufs || this.#pins[this.#buf0])) {
|
|
this.#bufs = [];
|
|
this.#pins = [];
|
|
this.#i = 0;
|
|
this.#bufi = 0;
|
|
this.#buf0 = 0;
|
|
this.#minBufi = 0;
|
|
this.#minI = 0;
|
|
}
|
|
}
|
|
|
|
getLimit() {
|
|
if (this.#limits.length == 0)
|
|
return Infinity
|
|
return this.#limits[this.#limits.length-1].remain;
|
|
}
|
|
|
|
setLimit(limit) {
|
|
if (this.getLimit() <= limit) return null;
|
|
limit = new StreamoidLimit(limit);
|
|
this.#limits.push(limit);
|
|
return limit;
|
|
}
|
|
|
|
unsetLimit(limit) {
|
|
if (limit == null) return;
|
|
if (this.#limits[this.#limits.length-1] != limit)
|
|
throw new Error('unset jump over multiple limits');
|
|
this.#limits.pop();
|
|
}
|
|
|
|
consumeLimit(count) {
|
|
if (this.#limits.length == 0)
|
|
return true;
|
|
if (count > this.getLimit())
|
|
throw new Error('limit overflowed');
|
|
this.#limits.forEach(e => {
|
|
e.remain -= count;
|
|
});
|
|
return true;
|
|
}
|
|
|
|
unconsumeLimit(count) {
|
|
this.#limits.forEach(e => {
|
|
e.remain += count;
|
|
if (e.remain > e.total)
|
|
throw new Error('Backtrack beyond a limit');
|
|
})
|
|
}
|
|
|
|
avail() {
|
|
let avail = this.#bufs[this.#bufi].length - this.#i;
|
|
let limit = this.getLimit();
|
|
if (limit < avail) avail = limit;
|
|
return avail;
|
|
}
|
|
|
|
peek() {
|
|
if (arguments.length >= 1) {
|
|
return this.#bufs[this.#bufi].slice(this.#i, arguments[0] == null ? undefined : (this.#i + arguments[0]))
|
|
}
|
|
return this.#bufs[this.#bufi][this.#i];
|
|
}
|
|
|
|
consume(count) {
|
|
count ??= 1;
|
|
this.consumeLimit(count);
|
|
let v;
|
|
if (this.#yielders.length > 0) {
|
|
v = this.peek(count);
|
|
}
|
|
this.#i += count;
|
|
if (this.#i >= this.#bufs[this.#bufi].length) {
|
|
this.#bufi++;
|
|
this.#i = 0;
|
|
}
|
|
this.cleanup();
|
|
if (this.#yielders.length > 0) {
|
|
this.#yielders.forEach(y => {
|
|
this.yield(y.name, v);
|
|
});
|
|
}
|
|
}
|
|
|
|
eof() {
|
|
return this.#bufs[this.#bufi] == null || (this.getLimit() == 0);
|
|
}
|
|
|
|
supply(s) {
|
|
if (!s) s = null;
|
|
if (s == null || s.length > 0)
|
|
this.#bufs.push(s);
|
|
}
|
|
|
|
run(f) {
|
|
while (1) {
|
|
let v = f.next();
|
|
if (v.done || this.wait()) return v;
|
|
}
|
|
}
|
|
}
|
|
|
|
let StreamoidPin = Streamoid.Pin;
|
|
let StreamoidLimit = Streamoid.Limit;
|
|
|
|
export const _tests = (() => {
|
|
function die(msg) { throw new Error(msg??'bad') }
|
|
function matchObject(a, b) {
|
|
if (typeof a != 'object' || typeof b != 'object')
|
|
return a == b;
|
|
function keySet(s) {
|
|
return new Set((function*(s){for(let i in s) yield i; if(Array.isArray(s)) yield 'length';})(s))
|
|
}
|
|
let ak = keySet(a), bk = keySet(b);
|
|
if (ak.symmetricDifference(bk).size) return false;
|
|
for (let k of ak)
|
|
if (!matchObject(a[k],b[k])) return (console.log('uneq',a,b),false);
|
|
return true;
|
|
}
|
|
|
|
return {
|
|
streamoid() {
|
|
let s = new Streamoid;
|
|
let sp = new StreamoidPriv(s);
|
|
|
|
function eatStr(str) {
|
|
for (let c of str) {
|
|
if (s.wait()) die('wait');
|
|
if (s.eof()) die('eof');
|
|
if (s.peek() != c) die('peek');
|
|
s.consume();
|
|
}
|
|
}
|
|
|
|
s.supply('Hello: sigma');
|
|
eatStr('Hello: ');
|
|
let a = s.save();
|
|
eatStr('sigma');
|
|
if (!s.wait()) die('nowait')
|
|
s.supply(' ligma;');
|
|
eatStr(' ligma');
|
|
let b = s.save();
|
|
if (a.slice(b) != 'sigma ligma') die('noligma');
|
|
if (a.length(b) != 'sigma ligma'.length) die('liglen');
|
|
s.restore(a);
|
|
eatStr('sigma ligma');
|
|
a.destroy();
|
|
b.destroy();
|
|
eatStr(';');
|
|
if (sp.i != 0 || sp.bufi != 0 || sp.bufs.length != 0 || sp.pins.length != 0) die('badform')
|
|
|
|
s.supply()
|
|
if (sp.i != 0 || sp.bufi != 0 || sp.bufs.length != 1 || sp.bufs[0]!=null || sp.pins.length != 0) die('badform')
|
|
if (s.wait()) die('wait')
|
|
if (!s.eof()) die('noeof')
|
|
},
|
|
|
|
async sussy() {
|
|
let { test } = await import('./test/sussy.mjs');
|
|
|
|
let res = test(regexoid);
|
|
|
|
if (!matchObject(
|
|
res,
|
|
[
|
|
{ done: false },
|
|
{ done: false },
|
|
{ done: false },
|
|
{
|
|
done: true,
|
|
match: {
|
|
meow: [ 'sussy' ],
|
|
meower: [ true, '2', '3' ],
|
|
unknownFields: [ 'Unknownus(hmm?)', 'meower(BAD)' ],
|
|
sigma: [ 'meower' ]
|
|
},
|
|
leftoverCount: 0,
|
|
leftover: ''
|
|
}
|
|
]
|
|
)) die('unmatch');
|
|
},
|
|
|
|
async http() {
|
|
let httpParser = (await import('./test/http.mjs')).httpParser(regexoid);
|
|
let req = (await import('node:fs')).readFileSync(new URL('./test/req/req.txt', import.meta.url))+'';
|
|
let dbg=false;
|
|
|
|
for (let cs=1; cs<req.length; cs++) {
|
|
if(dbg)console.log('\n=== CHUNKSIZE: ',cs,'===\n');
|
|
let result = {};
|
|
let proc = httpParser();
|
|
for (let [k, v] of (function*(){
|
|
for (let i=0; i<=req.length;) {
|
|
let chunksize = cs;
|
|
let chunk = req.slice(i, i+chunksize);
|
|
let res = proc(chunk);
|
|
if(dbg)console.dir({
|
|
length: chunk.length,
|
|
chunk,
|
|
result: res,
|
|
backBuffer: proc.stream.backBuffer,
|
|
frontBuffer: proc.stream.frontBuffer,
|
|
}, {depth: null});
|
|
if (res.output)
|
|
yield *res.output;
|
|
if (res.done) {
|
|
if (!res.match)
|
|
die('Bad');
|
|
break;
|
|
}
|
|
i += chunk.length;
|
|
if (chunk.length == 0 && !res.done)
|
|
die('Infinity')
|
|
}
|
|
})()) {
|
|
if(dbg)console.log({k,v});
|
|
if (!(k in result))
|
|
result[k] = v
|
|
else
|
|
result[k] += v;
|
|
}
|
|
|
|
if (!matchObject(result,
|
|
{
|
|
type: 'h1',
|
|
method: 'PUT',
|
|
path: '/',
|
|
fields: {
|
|
host: [ 'localhost:4534' ],
|
|
'user-agent': [ 'curl/8.6.0' ],
|
|
accept: [ '*/*' ],
|
|
'transfer-encoding': [ 'chunked' ],
|
|
connection: [ 'Upgrade, HTTP2-Settings' ],
|
|
upgrade: [ 'h2c' ],
|
|
'http2-settings': [ 'AAMAAABkAAQAoAAAAAIAAAAA' ]
|
|
},
|
|
body: 'Hello, world!\n',
|
|
trailers: {}
|
|
}
|
|
))
|
|
die('unmatch');
|
|
}
|
|
}
|
|
}
|
|
})();
|
|
|
|
|
|
let r;r = {
|
|
seq(...elem) {
|
|
elem = elem.map(e => {
|
|
if (e instanceof AstNode) {
|
|
return e;
|
|
} else if (e.length != null) {
|
|
return new AstString(e);
|
|
} else {
|
|
throw new Error('bad element: '+e);
|
|
}
|
|
})
|
|
elem.reduce((a,b) => a.chain(b));
|
|
return elem[0];
|
|
},
|
|
|
|
noop() {
|
|
return new AstNode;
|
|
},
|
|
|
|
any(...elem) {
|
|
elem = elem.map(e => r.seq(e));
|
|
return new AstDisjunction(...elem);
|
|
},
|
|
|
|
rep: new (class extends XFunction {
|
|
#strict = false; #label = null;
|
|
constructor(strict, label) {
|
|
super((lim, ...elem) => {
|
|
elem = r.seq(...elem);
|
|
return new AstRepeat(lim, elem, this.#strict, this.#label);
|
|
});
|
|
this.#strict = strict;
|
|
this.#label = label;
|
|
}
|
|
|
|
get strict() {
|
|
return new this.constructor(
|
|
true,
|
|
this.#label
|
|
);
|
|
}
|
|
|
|
label(label) {
|
|
label = label+'';
|
|
return new this.constructor(
|
|
this.#strict,
|
|
label
|
|
);
|
|
}
|
|
}),
|
|
|
|
markBreak(label) {
|
|
if (label == null)
|
|
label = true;
|
|
else
|
|
label = label+'';
|
|
return new AstMark(breakSymbol, label, false);
|
|
},
|
|
|
|
limit(lim, ...elem) {
|
|
elem = r.seq(...elem);
|
|
return new AstLimit(lim, elem);
|
|
},
|
|
|
|
captureOutput(key, ...elem) {
|
|
elem = r.seq(...elem);
|
|
return new AstCaptureYield(key, elem);
|
|
},
|
|
|
|
cond(...conds) {
|
|
conds = conds.map(([b, ...elem]) => [r.seq(b), r.seq(...elem)]);
|
|
return new AstCond(conds);
|
|
},
|
|
|
|
output(key, value) {
|
|
return new AstYield(key, value);
|
|
},
|
|
|
|
abort() { return new AstAbort; },
|
|
map(f) { return new AstResultEdit(f); },
|
|
|
|
chars: new (class extends XFunction {
|
|
#mode;
|
|
|
|
constructor(mode) {
|
|
super((...arg) => {
|
|
return ({
|
|
func: this.#newFunc,
|
|
any: this.#newAny,
|
|
none: this.#newNone,
|
|
})[this.#mode??'func'].call(this,...arg);
|
|
});
|
|
this.#mode = mode;
|
|
}
|
|
|
|
#newFunc(pred) { return new AstChars(pred); }
|
|
#newAny(...ops) { return new AstChars(new CharsPredicate(false, ...ops)); }
|
|
#newNone(...ops) { return new AstChars(new CharsPredicate(true, ...ops)); }
|
|
|
|
any(...arg) { return (new this.constructor('any'))(...arg) }
|
|
none(...arg) { return (new this.constructor('none'))(...arg) }
|
|
}),
|
|
|
|
...((o) => Object.fromEntries(
|
|
Object.entries(o)
|
|
.flatMap(([k,v]) => [
|
|
[k, function(...arg) { return v.call(this, false, ...arg); }],
|
|
[k+'Multi', function(...arg) { return v.call(this, true, ...arg); }],
|
|
])
|
|
))({
|
|
mark(multi, key, value=true) {
|
|
return new AstMark(key, value, multi);
|
|
},
|
|
|
|
capture(multi, key, ...elem) {
|
|
elem = r.seq(...elem);
|
|
return new AstCapture(key, elem, multi);
|
|
},
|
|
}),
|
|
};
|
|
|
|
export function regexoid(f) {
|
|
let code = compileAst(r.seq(f(r)));
|
|
|
|
return () => {
|
|
let stream = new Streamoid;
|
|
let regex = code(stream);
|
|
let done = null;
|
|
let fn = s => {
|
|
if (done && (s == null || s == '')) {
|
|
return done;
|
|
} else if (done && s != null) {
|
|
return { done: true, match: false, leftoverCount: s.length, leftover: s };
|
|
}
|
|
stream.supply(s);
|
|
let v = stream.run(regex);
|
|
let extra = {};
|
|
if (stream.hasOutput()) {
|
|
extra.output = stream.readOutput();
|
|
}
|
|
if (v.done) {
|
|
if (!v.value) {
|
|
done = null;
|
|
return {
|
|
done: true,
|
|
match: false,
|
|
...extra,
|
|
};
|
|
}
|
|
let sv1 = stream.save();
|
|
let sv2 = stream.saveEnd();
|
|
let leftover = null;
|
|
done = {
|
|
done: true,
|
|
match: v.value,
|
|
leftoverCount: sv1.length(sv2),
|
|
get leftover() {
|
|
if (leftover != null)
|
|
return leftover;
|
|
leftover = sv1.slice(sv2);
|
|
sv1.destroy();
|
|
sv2.destroy();
|
|
return leftover;
|
|
},
|
|
...extra
|
|
}
|
|
let ret = done;
|
|
if (done.leftoverCount) {
|
|
done = null;
|
|
}
|
|
return ret;
|
|
}
|
|
return { done: false, ...extra };
|
|
}
|
|
fn.stream = stream;
|
|
return fn;
|
|
}
|
|
}
|