Un semplice generatore di parser
Ho bisogno di analizzare semplici DSL in alcuni progetti. Dato che non conosco BNF o altre grammatiche, ho pensato che un'alternativa sarebbe stata quella di utilizzare un semplice generatore di parser.
Sto cercando miglioramenti al lexer / parser per poterlo utilizzare per analizzare linguaggi più complessi in progetti futuri mantenendo un'interfaccia relativamente semplice per definire una grammatica.
Un feedback per aumentare la qualità del codice sarebbe molto apprezzato.
Vorrei anche sapere se mi mancano funzionalità cruciali che un lexer / parser dovrebbe includere.
Se sto facendo qualcosa di intrinsecamente sbagliato o utilizzo tecniche inappropriate, sarebbe utile saperlo.
Includerò un semplice esempio di utilizzo all'inizio e inserirò il codice e lo snippet in fondo. Penso che in quest'ordine sia più facile seguire il codice.
Ecco un esempio di come tokenizzare un'espressione aritmetica di base come 1+2+3*4*5*6+3;
const tokenDefinitions = [
TokenFactory({type:'Whitespace', ignore: true}).while(/^\s+$/), TokenFactory({type:'Integer'}).start(/-|\d/).next(/^\d$/),
TokenFactory({type:'Paren'}).start(/^[()]$/), TokenFactory({type:'Addition'}, true).start(/^\+|-$/),
TokenFactory({type:'Multiplication'}, true).start(/^\*|\\$/),
];
const src = '1 + 2 + 3 * 4 * 5'
const lexer = Lexer(tokenDefinitions);
const tokens = lexer(src).filter(t => !t.ignore);
Ecco un esempio per analizzare i token in un AST.
const Any = new Driver('Any').match(_ => true);
const Number = new Driver('Number').match(type('Integer')).bind(0, 0);
const RParen = new Driver('RParen').match(value(')')).bind(100, 0);
const Expression = new Driver('Expression').match(value('(')).consumeRight().end(value(')')).bind(0, 99)
const MulOperator = new Driver('Operator').match(type('Multiplication')).consumeLeft(Any).consumeRight().bind(60,60)
const AddOperator = new Driver('Operator').match(type('Addition')).consumeLeft(Any).consumeRight().bind(50,50)
const nodeDefinitions = [
MulOperator,
AddOperator,
Number,
Expression,
RParen,
];
const parse = Parser(nodeDefinitions);
const ast = parse(tokens);
Questo esempio utilizza i poteri di associazione sinistra e destra per definire la precedenza della moltiplicazione sull'addizione. Puoi ottenere lo stesso risultato usando .until, ma sembra un po 'sbagliato.
const Any = new Driver('Any').match(_ => true);
const Number = new Driver('Number').match(type('Integer'));
const RParen = new Driver('RParen').match(value(')'));
const Expression = new Driver('Expression').match(value('(')).consumeRight().until(value(')')).end(value(')'))
const MulOperator = new Driver('Operator').match(type('Multiplication')).consumeLeft(Any).consumeRight().until(parentOr(type('Addition')))
const AddOperator = new Driver('Operator').match(type('Addition')).consumeLeft(Any).consumeRight().until(parent)
In questo esempio, l'operatore di moltiplicazione consuma i token finché non incontra un token di addizione o, se all'interno di un'espressione, una parentesi destra.
Entrambi gli esempi producono il seguente AST.
[
{
children: [
{ children: [], token: { value: '1' }, id: 'Number' },
{
children: [
{ children: [], token: { value: '2' }, id: 'Number' },
{
children: [
{
children: [
{ children: [], token: { value: '3' }, id: 'Number' },
{
children: [
{
children: [],
token: { value: '4' },
id: 'Number'
},
{
children: [
{
children: [],
token: { value: '5' },
id: 'Number'
},
{
children: [],
token: { value: '6' },
id: 'Number'
}
],
token: { type: 'Multiplication', value: '*' },
id: 'Operator'
}
],
token: { type: 'Multiplication', value: '*' },
id: 'Operator'
}
],
token: { type: 'Multiplication', value: '*' },
id: 'Operator'
},
{ children: [], token: { value: '3' }, id: 'Number' }
],
token: { type: 'Addition', value: '+' },
id: 'Operator'
}
],
token: { type: 'Addition', value: '+' },
id: 'Operator'
}
],
token: { type: 'Addition', value: '+' },
id: 'Operator'
}
]
È possibile appiattire la struttura ricorsiva dell'AST modificando la grammatica dei token di addizione e moltiplicazione per analizzare ripetutamente il suo RHS mentre la sua condizione corrisponde utilizzando .repeato utilizzando .unfoldquale ricorre per primo e appiattisce la struttura dopo aver analizzato il nodo. Ciò può ridurre notevolmente le dimensioni dell'AST.
[
{
children: [
{ children: [], token: { value: '1' }, id: 'Number' },
{ children: [], token: { value: '2' }, id: 'Number' },
{
children: [
{ children: [], token: { value: '3' }, id: 'Number' },
{ children: [], token: { value: '4' }, id: 'Number' },
{ children: [], token: { value: '5' }, id: 'Number' },
{ children: [], token: { value: '6' }, id: 'Number' }
],
token: { type: 'Multiplication', value: '*' },
id: 'Operator'
},
{ children: [], token: { value: '3' }, id: 'Number' }
],
token: { type: 'Addition', value: '+' },
id: 'Operator'
}
]
const AddOperator = new Driver('Operator').match(type('Addition')).consumeLeft(Any).consumeRight().until(parent).repeat()
Ecco un esempio di come interpretare l'AST.
Non importa se l'AST è appiattito o meno, tutte le versioni (bind / until, repeat / unfold) verranno interpretate correttamente poiché la semantica non cambia *
const operators = {
'+': (a,b) => a+b,
'-': (a,b) => a-b,
'*': (a,b) => a*b,
'/': (a,b) => a/b,
};
const hasId = id => token => token.id === id;
const tokenValue = node => node.token.value;
const NrBh = new Behaviour(hasId('Number'), n => +tokenValue(n))
const OpBh = new Behaviour(hasId('Operator'), (node, _eval) => node.children.map(c => _eval(c)).reduce(operators[tokenValue(node)]));
const ExprBh = new Behaviour(hasId('Expression'), (node, _eval) => _eval(node.rhs));
const behaviours = [NrBh, OpBh, ExprBh];
const res = Behaviour.eval(ast[0], behaviours); // 63
Ecco il codice per il lexer.
//Matcher.js
const setInstanceProp = (instance, key, value) => (instance[key] = value, instance);
/**
* The Matcher defines multiple regular expressions or functions that are matched against a single character at different positions.
*/
class Matcher {
constructor (transform) {
/** Can be given a transform function that transforms the token value */
if (typeof transform === 'function')
this._transform = transform
}
/** Consumes a character once at the beginning.*/
start (regExp) {return setInstanceProp(this, '_start', regExp)}
/** Consumes a character each step*/
next (regExp) {return setInstanceProp(this, '_next', regExp)}
/** Consumes a character and terminates the current token*/
end (regExp) {return setInstanceProp(this, '_end', regExp)}
/** Consumes characters as long as the regExp matches */
while (regExp) {return setInstanceProp(this, '_while', regExp)}
/** Tests a regex or function against a character */
_test (obj, char) {
if (typeof obj === 'function')
return obj(char);
if (obj instanceof RegExp)
return obj.test(char);
return false;
}
/** Tests a character and token against the defined regexes/functions. Can be given a hint to test a specific regex/fn */
test (char, token = '', hint) {
if (hint === null) return false;
if (hint) return this._test(hint, char)
if (this._start && !token) return this._test(this._start, char);
if (this._next) return this._test(this._next, char);
if (this._while) return this._test(this._while, token + char);
return false;
}
/** Default transform behaviour. Returns the primitive token value */
_transform (token) {
return token;
}
/** Called by the tokenizer to transform the primitive token value to an object*/
transform (token) {
return this._transform(token);
}
}
/** Creates a matcher that transforms the matched token into an object with a prototype that shares common information*/
const TokenFactory = (proto, assign) => new Matcher((value) => {
if (typeof value === 'object') return value
if (assign)
return Object.assign({}, proto, {value})
return Object.assign(Object.create(proto), {value})
});
module.exports = {Matcher, TokenFactory};
//Lexer.js
const {Matcher} = require('./Matcher');
const Lexer = (def) => (src) => {
return src.split('').reduce((acc, char, i, arr) => {
let [token, lastMatcher, tokens] = acc;
const {_end = null} = lastMatcher; let ret;
if (lastMatcher.test(char, token, _end)) {
ret = [lastMatcher.transform(token+char), new Matcher, tokens];
} else if (lastMatcher.test(char, token)) {
ret = [token+char, lastMatcher,tokens];
} else {
const matcher = def.find(matcher => matcher.test(char));
if (!matcher) throw new Error(`No matcher found for character '${char}'.`);
token && tokens.push(lastMatcher.transform(token));
ret = [char, matcher, tokens];
lastMatcher = matcher;
}
if (i === arr.length - 1) {
tokens.push(lastMatcher.transform(ret[0]));
ret = tokens;
}
return ret;
}, ['', new Matcher, []]);
}
module.exports = {Lexer};
Ecco il codice del parser.
//Driver.js
class Driver {
constructor (id, transform) {
this.id = id;
this._transform = transform;
this.bind();
};
match (token) {
this._match = token;
return this;
}
consumeLeft (token) {
this._consumeLeft = token;
return this;
}
consumeRight (token = true, n = Infinity) {
this._consumeRight = token;
this.n = n;
return this;
}
end (token) {
this._end = token;
return this;
}
unfold () {
this._unfold = true;
return this;
}
until (token, lookAhead = 0) {
this._until = token;
this._lookAhead = lookAhead;
return this;
}
repeat (token) {
this._repeat = true;
return this;
}
test (token, nodes = []) {
let ret;
if (typeof this._match === 'function')
ret = this._match(token);
else if (this._match) {
ret = token.type === this._match || token.value === this._match;
}
if (this._consumeLeft) {
const lhs = nodes.slice().pop();
ret = ret && lhs && (lhs.id === this._consumeLeft.id || this._consumeLeft.test(lhs.token));
}
return ret;
}
transform (node) {
if (typeof this._transform === 'function')
return {...this._transform(node), id: this.id};
return {...node, id: this.id};
}
bind (l = 0, r = 0) {
this.lbp = l;
this.rbp = r;
return this;
}
}
module.exports = {Driver};
//Parser.js
const Parser = nodeDefinitions => {
const nodes = [];
return function parse (tokens, parents = []) {
if (tokens.length === 0)return [];
const [parent, ...rest] = parents;
let i=0;
do {
const token = tokens.shift();
const node = {children:[]};
const cur = nodeDefinitions.find (d => d.test(token, nodes));
if (!cur) {
throw new Error(`Unexpected token ${JSON.stringify(token)}`); } let next = tokens[0] const nextDriver = next && nodeDefinitions.find (d => d.test(next, nodes)); if (parent && nextDriver && parent.rbp < nextDriver.lbp) { tokens.unshift(token); break; } next = parent && (parent._lookAhead==0?token:tokens[parent._lookAhead - 1]); if (parent && parent._until && next && parent._until(next, parents, nodes)) { tokens.unshift(token); break; } if (cur._consumeLeft) { const lhs = nodes.pop(); if (!cur.test(token, [lhs])) throw new Error(`Expected token ${cur._consumeLeft._match} but found ${lhs.token.type} instead. ${cur.name}`)
node.children.push(lhs);
}
if (cur._consumeRight) {
let repeat = false;
do {
parse(tokens, [cur, ...parents]);
const rhs = nodes.shift();
node.children.push(rhs);
if (tokens[0] && cur.test(tokens[0], [node.children[0]])) {
tokens.shift();
repeat = true;
} else {
repeat = false;
}
} while (repeat);
}
node.token = token;
if (cur._unfold) {
const rhs = node.children.slice(-1)[0];
const un = rhs.children;
if (node.token.value === rhs.token.value) {
node.children = [node.children[0], ...un];
}
}
if (cur._end && cur._end(tokens[0] || {}, cur, nodes)) {
node.end = tokens.shift();
}
nodes.push(cur.transform(node));
if (parent && ++i === parent.n) break;
} while (tokens.length);
return nodes;
}
}
module.exports = {Parser};
Ecco il codice per l'interprete.
//Behaviour.js
class Behaviour {
static eval (ast, behaviours) {
const node = ast;
const beh = behaviours.find(b => b.testFn(ast));
if (!beh)
throw new Error(`No behaviour found for node ${JSON.stringify(node)}`)
return beh.evalFn(node, (node, _behaviours = behaviours) => {
const val = Behaviour.eval(node, _behaviours)
return val;
});
}
constructor (testFn, evalFn) {
this.testFn = testFn;
this.evalFn = evalFn;
}
}
Ecco un violino per eseguire l'esempio.
const tokenDefinitions = [
TokenFactory({type:'Whitespace', ignore: true}).while(/^\s+$/),
TokenFactory({type:'Integer'}).start(/-|\d/).next(/^\d$/), TokenFactory({type:'Paren'}).start(/^[()]$/),
TokenFactory({type:'Addition'}, true).start(/^\+|-$/), TokenFactory({type:'Multiplication'}, true).start(/^\*|\\$/),
];
const src = '1 + 2 + 3 * 4 * 5 * 6 + 3'
console.log ('Source', src);
const lexer = Lexer(tokenDefinitions);
const tokens = lexer(src).filter(t => !t.ignore);
console.log("Tokens", tokens);
const type = type => token => token.type === type;
const value = value => token => token.value === value;
const parent = (token, parents, nodes) => parents[1] && parents[1]._until(token, parents.slice(1), nodes) ;
const or = (...fns) => (token, parents, nodes) => fns.reduce((a, fn) => a || fn(token, parents, nodes), false);
const and = (...fns) => (token, parents, nodes) => fns.reduce((a, fn) => a && fn(token, parents, nodes), true);
const parentOr = fn => or(parent, fn);
const keyword = token => type('Identifier')(token) && keywords.some(k => value(k)(token));
// const Any = new Driver('Any').match(_ => true);
// const Number = new Driver('Number').match(type('Integer')).bind(0, 0);
// const RParen = new Driver('RParen').match(value(')')).bind(100, 0);
// const Expression = new Driver('Expression').match(value('(')).consumeRight().end(value(')')).bind(0, 99)
// const MulOperator = new Driver('Operator').match(type('Multiplication')).consumeLeft(Any).consumeRight().bind(60,60)
// const AddOperator = new Driver('Operator').match(type('Addition')).consumeLeft(Any).consumeRight().bind(50,50)
const Any = new Driver('Any').match(_ => true);
const Number = new Driver('Number').match(type('Integer'));
const RParen = new Driver('RParen').match(value(')'));
const Expression = new Driver('Expression').match(value('(')).consumeRight().until(value(')')).end(value(')'))
const MulOperator = new Driver('Operator').match(type('Multiplication')).consumeLeft(Any).consumeRight().until(or(parent,type('Multiplication'),type('Addition'))).repeat()
const AddOperator = new Driver('Operator').match(type('Addition')).consumeLeft(Any).consumeRight().until(parentOr(type('Addition'))).repeat();
const nodeDefinitions = [
MulOperator,
AddOperator,
Number,
Expression,
RParen,
];
const parse = Parser(nodeDefinitions);
const ast = parse(tokens);
console.log("AST", ast);
const operators = {
'+': (a,b) => a+b,
'-': (a,b) => a-b,
'*': (a,b) => a*b,
'/': (a,b) => a/b,
};
const hasId = id => token => token.id === id;
const tokenValue = node => node.token.value;
const NrBh = new Behaviour(hasId('Number'), n => +tokenValue(n))
const OpBh = new Behaviour(hasId('Operator'), (node, _eval) => node.children.map(c => _eval(c)).reduce(operators[tokenValue(node)]));
const ExprBh = new Behaviour(hasId('Expression'), (node, _eval) => _eval(node.rhs));
const behaviours = [NrBh, OpBh, ExprBh];
const res = Behaviour.eval(ast[0], behaviours);
console.log ("Result", res)
<script>
const setInstanceProp = (instance, key, value) => (instance[key] = value, instance);
class Matcher {
constructor (transform) {
if (typeof transform === 'function')
this._transform = transform
}
start (r) {return setInstanceProp(this, '_start', r)}
next (r) {return setInstanceProp(this, '_next', r)}
end (r) {return setInstanceProp(this, '_end', r)}
while (r) {return setInstanceProp(this, '_while', r)}
_test (obj, char) {
if (typeof obj === 'function')
return obj(char);
if (obj instanceof RegExp)
return obj.test(char);
return false;
}
test (char, token = '', hint) {
if (hint === null) return false;
if (hint) return this._test(hint, char)
if (this._start && !token) return this._test(this._start, char);
if (this._next) return this._test(this._next, char);
if (this._while) return this._test(this._while, token + char);
return false;
}
_transform (token) {
return token;
}
transform (token) {
return this._transform(token);
}
}
const TokenFactory = (proto, assign) => new Matcher((value) => {
if (typeof value === 'object') return value
if (assign)
return Object.assign({}, proto, {value})
return Object.assign(Object.create(proto), {value})
});
const Lexer = (def) => (src) => {
return src.split('').reduce((acc, char, i, arr) => {
let [token, lastMatcher, tokens] = acc;
const {_end = null} = lastMatcher; let ret;
if (lastMatcher.test(char, token, _end)) {
ret = [lastMatcher.transform(token+char), new Matcher, tokens];
} else if (lastMatcher.test(char, token)) {
ret = [token+char, lastMatcher,tokens];
} else {
const matcher = def.find(matcher => matcher.test(char));
if (!matcher) throw new Error(`No matcher found for character '${char}'.`); token && tokens.push(lastMatcher.transform(token)); ret = [char, matcher, tokens]; lastMatcher = matcher; } if (i === arr.length - 1) { tokens.push(lastMatcher.transform(ret[0])); ret = tokens; } return ret; }, ['', new Matcher, []]); } class Driver { constructor (id, transform) { this.id = id; this._transform = transform; this.bind(); }; match (token) { this._match = token; return this; } consumeLeft (token) { this._consumeLeft = token; return this; } consumeRight (token = true, n = Infinity) { this._consumeRight = token; this.n = n; return this; } end (token) { this._end = token; return this; } unfold () { this._unfold = true; return this; } until (token, lookAhead = 0) { this._until = token; this._lookAhead = lookAhead; return this; } repeat (token) { this._repeat = true; return this; } test (token, nodes = []) { let ret; if (typeof this._match === 'function') ret = this._match(token); else if (this._match) { ret = token.type === this._match || token.value === this._match; } if (this._consumeLeft) { const lhs = nodes.slice().pop(); ret = ret && lhs && (lhs.id === this._consumeLeft.id || this._consumeLeft.test(lhs.token)); } return ret; } transform (node) { if (typeof this._transform === 'function') return {...this._transform(node), id: this.id}; return {...node, id: this.id}; } bind (l = 0, r = 0) { this.lbp = l; this.rbp = r; return this; } } const Parser = nodeDefinitions => { const nodes = []; return function parse (tokens, parents = []) { if (tokens.length === 0)return []; const [parent, ...rest] = parents; let i=0; do { const token = tokens.shift(); const node = {children:[]}; const cur = nodeDefinitions.find (d => d.test(token, nodes)); if (!cur) { throw new Error(`Unexpected token ${JSON.stringify(token)}`);
}
let next = tokens[0]
const nextDriver = next && nodeDefinitions.find (d => d.test(next, nodes));
if (parent && nextDriver && parent.rbp < nextDriver.lbp) {
tokens.unshift(token);
break;
}
next = parent && (parent._lookAhead==0?token:tokens[parent._lookAhead - 1]);
if (parent && parent._until && next && parent._until(next, parents, nodes)) {
tokens.unshift(token);
break;
}
if (cur._consumeLeft) {
const lhs = nodes.pop();
if (!cur.test(token, [lhs]))
throw new Error(`Expected token ${cur._consumeLeft._match} but found ${lhs.token.type} instead. ${cur.name}`) node.children.push(lhs); } if (cur._consumeRight) { let repeat = false; do { parse(tokens, [cur, ...parents]); const rhs = nodes.shift(); node.children.push(rhs); if (tokens[0] && cur.test(tokens[0], [node.children[0]])) { tokens.shift(); repeat = true; } else { repeat = false; } } while (repeat); } node.token = token; if (cur._unfold) { const rhs = node.children.slice(-1)[0]; const un = rhs.children; if (node.token.value === rhs.token.value) { node.children = [node.children[0], ...un]; } } if (cur._end && cur._end(tokens[0] || {}, cur, nodes)) { node.end = tokens.shift(); } nodes.push(cur.transform(node)); if (parent && ++i === parent.n) break; } while (tokens.length); return nodes; } } class Behaviour { static eval (ast, behaviours) { const node = ast; const beh = behaviours.find(b => b.testFn(ast)); if (!beh) throw new Error(`No behaviour found for node ${JSON.stringify(node)}`)
return beh.evalFn(node, (node, _behaviours = behaviours) => {
const val = Behaviour.eval(node, _behaviours)
return val;
});
}
constructor (testFn, evalFn) {
this.testFn = testFn;
this.evalFn = evalFn;
}
}
</script>
Modificare:
Alcuni pensieri da parte mia. Non mi piace molto aggiungere un prefisso a metodi o proprietà _. Penso di poter spostare l'espressione regolare in un proprio oggetto poiché non sono legate all'istanza. Penso di poter sbarazzarmi del _transformmetodo eseguendo transforml' override nel costruttore. Ho solo pensato che archiviare una funzione in una proprietà che viene chiamata da un metodo di classe sia conveniente poiché potresti usarla per convalidare l'input. Se c'è un modo più pulito per farlo, sarebbe carino. Potrei usare un Mapnegozio la funzione, quindi non dovrei esporre una _transformproprietà.
Penso che i poteri vincolanti dovrebbero essere modificati per confrontare il token corrente con il token successivo. Attualmente funzionano come segue. Dati i 1 + 2 * 3poteri sorgente e vincolante 50/50, 60/60 per i token + e *, il token + competerà con il token * sul token 2 . Ho pensato che fosse più facile da capire, ma risulta che non puoi usarlo per uscire dall'attuale fase di analisi, senza usare until. Quale è una probabile necessità. fe utilizzando )per designare la fine di un'espressione. Funziona solo se confronto i poteri di legame di due segnalini adiacenti.
Risposte
Dichiarazione di non responsabilità: non è un revisore del codice
Il tuo codice sembra abbastanza buono! Solo brevemente commentando:
La mia ipotesi è che forse potresti voler progettare alcuni algoritmi a bassa complessità per eseguire l'analisi (se non usando un parser già sviluppato - che sarebbe stata la mia prima scelta - sfogliando GitHub), piuttosto che usare manipolazioni di stringhe ad alta intensità operativa con alle prime espressioni regolari .
Ecco solo un esempio di utilizzo dello stack:
const parser = function(s) {
if (s.length === 0) {
return 0;
}
let stack = [];
let operation = "+";
for (let index = 0, num = 0; index <= s.length; ++index) {
if (s[index] === ' ') {
continue;
}
if (s[index] >= '0' && s[index] <= '9') {
num *= 10;
num += parseInt(s[index]);
continue;
}
if (operation === '+') {
stack.push(num);
} else if (operation === '-') {
stack.push(-num);
} else if (operation === '*') {
stack.push(stack.pop() * num);
} else if (operation === '/') {
stack.push(Math.trunc(stack.pop() / num));
}
operation = s[index];
num = 0;
}
return stack.reduce((a, b) => a + b, 0);
};
console.log(parser(" 1 + 2 + 3 * 4 * 5 * 6 + 3 "));
Codifica felice !! (ˆ_ˆ)
Riferimento
- Calcolatrice del prefisso utilizzando lo stack in javascript
Non ho pensato molto ai lexer da quando ero uno studente universitario 14 anni fa e frequentavo un corso di compilatori. Da allora lavoro con Javascript.
Nel complesso il codice sembra ben scritto. Le variabili sono dichiarate correttamente conste in modo letappropriato. Molte altre funzionalità di ES6 sembrano essere applicate in modo appropriato. Vengono utilizzati confronti rigorosi per evitare costrizioni di tipo inutili.
Sono d'accordo con la rimozione dei trattini bassi dai nomi di metodi e proprietà. Ciò seguirebbe le raccomandazioni delle guide di stile popolari, ad esempio AirBNB .
Vedo queste righe di codice nella Parserfunzioneparse()
let repeat = false; do { parse(tokens, [cur, ...parents]); const rhs = nodes.shift(); node.children.push(rhs); if (tokens[0] && cur.test(tokens[0], [node.children[0]])) { tokens.shift(); repeat = true; } else { repeat = false; } } while (repeat);
Il dociclo può essere modificato in un forciclo e la variabile rhsviene utilizzata solo una volta dopo l'assegnazione, quindi non è necessario memorizzarla.
for (let repeat = true; repeat; ) {
parse(tokens, [cur, ...parents]);
node.children.push(nodes.shift());
if (tokens[0] && cur.test(tokens[0], [node.children[0]])) {
tokens.shift();
repeat = true;
} else {
repeat = false;
}
}