@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
270 lines (242 loc) • 7.62 kB
JavaScript
// Create a hierarchical expression tree from an `xpr` array
// See ./CdlGrammar.js for the operator precedences.
// CSN representation of window functions: unfortunately not Option 4 in #7632
'use strict';
const prefixOperators = { // see <prec=…,prefix> in `expression` of CdlGrammar
__proto__: null,
new: 39, // special in CDL (only before ref)
exists: 33, // special in CDL
'+': 30, // note: binary `.` and `over` have higher precedence!
'-': 30,
not: 8,
};
const binaryOperators = {
__proto__: null,
'.': 37, // nary
over: 35, // TODO: only after ref with arg?
'*': 24, // nary
'/': 24,
'+': 22,
'-': 22,
'||': 20,
'=': 10, // with ANY/SOME/ALL
'<>': 10,
'>': 10,
'>=': 10,
'<': 10,
'<=': 10,
'!=': 10,
'==': 10,
and: 4,
or: 2,
// with second token or ternary (in the grammar, these ops have prec=10, but
// also assoc=none, i.e. could not be used without parens together):
is: 11, // is binary op here, not postfix
in: 13,
between: 13,
like: 13,
not: 15, // specially handled
};
const secondTokens = {
// the value is the precedence of the first token listed in `binaryOperators`
__proto__: null,
any: 10, // for `=` etc
some: 10,
all: 10,
not: 11, // for `is`
between: 15, // for `not`
in: 15,
like: 15,
};
const naryOperators = {
// all strings on the right must not have a precedence mentioned in secondTokens
// true must not be used for operator (precedence) with secondTokens
__proto__: null,
'.': true, // CSN-tree not as left-assoc binary?
'*': true,
'/': true,
'+': true,
'-': true,
'||': true,
and: true,
or: true,
between: 'and',
like: 'escape',
};
class XprTree {
nodes; // array value of CSN property `xpr`/`where`/…
nodeIdx = 0;
args; // corresponding XSN array, with already tree-like sub expressions
location; // true → CSN input: direct array, no n-ary
constructor( nodes, args, location ) {
this.nodes = nodes;
this.args = args;
this.location = location;
}
splitClauses() {
const { length } = this.args;
if (length < 3)
return this.args;
const args = [];
let idx = 0;
while (idx < length) {
if (this.isToken( idx + 1, 'by' ) &&
this.isToken( idx, 'partition' ) || this.isToken( idx, 'order' )) {
this.pushSection( args, idx );
args.push( this.args[idx++] );
args.push( this.args[idx++] );
this.nodeIdx = idx;
}
else if (this.isToken( idx, 'rows' )) {
this.pushSection( args, idx );
args.push( this.args[idx++] );
this.nodeIdx = idx;
}
else {
++idx;
}
}
if (!args.length)
return this.args;
this.pushSection( args, idx );
return args;
}
isToken( idx, keyword ) {
const tok = this.args[idx];
return tok && (this.location === true)
? keyword === tok
: keyword === tok?.val && tok.literal === 'token';
}
pushSection( args, idx ) {
if (idx > this.nodeIdx) {
args.push( (idx > this.nodeIdx + 1)
? this.create( this.args.slice( this.nodeIdx, idx ) )
: this.args[this.nodeIdx] );
this.nodeIdx = idx;
}
}
tree() {
const args = [];
const { length } = this.args;
while (this.nodeIdx < length) {
const expr = this.expression( -1 );
if (expr)
args.push( expr );
}
if (args.length === 1) {
// For CSN, keep xpr arrays as arrays
return (this.location === true) ? args : args[0];
}
return this.create( args );
}
expression( parentPrec ) {
// console.log('B:',this.nodeIdx,parentPrec)
let append;
let naryOp;
let args;
// Term = ref/val or unary operator with expression as operand
let expr = this.args[this.nodeIdx];
if (!expr)
return expr;
let node = this.nodes[this.nodeIdx++];
if (typeof node === 'string') {
const prec = prefixOperators[node]; // <prec=…,prefix> in CdlGrammar
if (prec) {
const right = this.expression( prec - 1 );
if (!right)
return expr;
expr = this.create( [ expr, right ] );
}
else if (node === 'case') {
expr = this.caseWhen( [ expr ] );
}
else { // unknown token (keyword in CDL):
return expr; // …from fns with irregular syntax?
// also handles `null` as right side of `is` in `is null`
// TODO: `(` from CSN v0.x ?
// It is important not to handle binary ops after this, because otherwise
// we would not properly parse functions with irregular syntax
}
}
node = this.nodes[this.nodeIdx];
while (typeof node === 'string') {
const prec = binaryOperators[node]; // <prec=…> in CdlGrammar
if (!prec || parentPrec >= prec)
return expr;
// handle n-ary extensions of binary operators
if (node === append && this.location !== true) { // not for CSN input
args.push( this.args[this.nodeIdx++] );
}
else {
naryOp = naryOperators[node];
append = naryOp === true && node;
args = [ expr, this.args[this.nodeIdx++] ];
expr = this.create( args, naryOp === true );
}
// handle second token of operator (there must be none for naryOp=true):
const second = this.nodes[this.nodeIdx];
if (typeof second === 'string' && secondTokens[second] === prec) {
args.push( this.args[this.nodeIdx++] );
if (node === 'not')
naryOp = naryOperators[second];
}
// the right side
const right = this.expression( prec );
if (!right) // incomplete
return expr;
args.push( right );
node = this.nodes[this.nodeIdx];
if (node === naryOp && typeof node === 'string')
node = this.pushTokenAndExpression( args, prec );
}
// console.log('E:',this.nodeIdx)
return expr;
}
caseWhen( args ) {
const expr = this.create( args );
let node = this.nodes[this.nodeIdx];
if (node !== 'when') {
const value = this.expression( -1 );
if (value)
args.push( value );
node = this.nodes[this.nodeIdx];
}
while (node === 'when') {
node = this.pushTokenAndExpression( args );
if (node === 'then')
node = this.pushTokenAndExpression( args );
}
if (node === 'else')
node = this.pushTokenAndExpression( args );
if (node === 'end')
args.push( this.args[this.nodeIdx++] );
return expr;
}
pushTokenAndExpression( args, prec = -1 ) {
args.push( this.args[this.nodeIdx++] );
const value = this.expression( prec );
if (value)
args.push( value );
return this.nodes[this.nodeIdx];
}
create( args, isNary = false ) {
if (this.location === true) // for CSN
return args;
return {
op: { val: (isNary ? 'nary' : 'ixpr'), location: this.location },
location: this.location,
args,
};
}
}
function xprAsTree( nodes, args, location ) {
return (new XprTree( nodes, args, location )).tree();
}
function splitClauses( tree, isCsn ) {
return (new XprTree( null, tree, !!isCsn || tree.location )).splitClauses();
}
module.exports = {
xsnAsTree: xprAsTree,
csnAsTree: nodes => xprAsTree( nodes, nodes, true ),
splitClauses,
};