I have a parser which consumes an ordered list of tokens (based on Vim's command grammar) and returns either:
- an error object, or
- an object that can be directly
eval
-ed by a separate function (not shown)
The code that calls parse
handles both of these cases. I am looking for help with the structure/architecture of the parser function below.
This cascading if-else chain works, and I personally find it easy to read.
However, it seems like encoding the grammar precedence rules into an if-else chain is very brittle. For example, it seems like it will be difficult to add the visual-mode rules to this structure.
/* Given an array of tokens, return a multiplier (mult),
and the command to perform (cmd),
otherwise return an error message.
NOTE: The order of the `has('foo')` statements reflects the
precedence rules of the grammar.
*/
var parse=(tokens)=>{
var cmd={verb:'g',mult:1,original:tokens.map(x=>x[1]).reduce((x,y)=>x.concat(y)),},
err={original:tokens,error:'PARSE ERROR'},// default error message
has=(str)=>tokens.map(x=>x.includes(str)).some(x=>x);
// error
if(has('UNKNOWN TOKEN')){err.error='TOKENIZER ERROR';return err;}
// rule: [count] operator [count] modifier object
else if(has('modifier')){
var t=tokens.shift();
if(t[0]==='count'){cmd.mult*=parseInt(t[1],10);t=tokens.shift();}
if(t[0]==='operator'){cmd.verb=t[1];t=tokens.shift();}else{return err;}
if(t[0]==='count'){cmd.mult*=parseInt(t[1],10);t=tokens.shift();}
if(t[0]==='modifier'){cmd.mod=t[1];t=tokens.shift();}else{return err;}
if(t[0]==='object'){cmd.noun=t[1];if(t=tokens.shift()){return err;}}else{return err;}
return cmd;
}
// rule: [count] operator [count] (motion|object)
else if(has('operator')){
var t=tokens.shift();
if(t[0]==='count'){cmd.mult*=parseInt(t[1],10);t=tokens.shift();}
if(t[0]==='operator'){cmd.verb=t[1];t=tokens.shift();}else{return err;}
if(t[0]==='count'){cmd.mult*=parseInt(t[1],10);t=tokens.shift();}
if(t[0]==='motion'){cmd.noun=t[1];if(t=tokens.shift()){return err;};}
else if(t[0]==='object'){cmd.noun=t[1];if(t=tokens.shift()){return err;}}else{return err;}
return cmd;
}
// rule: [count] motion
else if(has('motion')){
var t=tokens.shift();
if(t[0]==='count'){cmd.mult*=parseInt(t[1],10);t=tokens.shift();}
if(t[0]==='motion'){cmd.noun=t[1];if(t=tokens.shift()){return err;}}else{return err;}
return cmd;
}
// rule: [count] object
else if(has('object')){
var t=tokens.shift();
if(t[0]==='count'){cmd.mult*=parseInt(t[1],10);t=tokens.shift();}
if(t[0]==='object'){cmd.noun=t[1];if(t=tokens.shift()){return err;}}else{return err;}
return cmd;
}
else{return err;}
},