@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
893 lines (640 loc) • 17.8 kB
JavaScript
// Base class for generated parser, for redepage v0.4.0
'use strict';
class BaseParser {
keywords;
table;
lexer;
tokens = undefined;
tokenIdx = 0;
conditionTokenIdx = -1;
errorTokenIdx = -1;
recoverTokenIdx = -1;
reuseErrorTokenIdx = null;
fixKeywordTokenIdx = -1;
conditionStackLength = -1;
s = null;
errorState = null;
stack = [];
dynamic_ = {};
prec_ = null;
$hasErrors = null;
hardGuards = {};
_trace = [];
constructor( lexer, keywords, table ) {
this.keywords = { __proto__: null, ...keywords };
this.table = compileTable( table );
this.lexer = lexer;
}
init() {
this.lexer.tokenize( this );
return this;
}
_runTransparently( callback ) {
const { tokenIdx } = this;
const saved = this._saveForWalk();
const { length } = this.stack;
const r = callback();
this.stack.length = length;
Object.assign( this, saved );
this.tokenIdx = tokenIdx;
return r;
}
_saveForWalk() {
return {
s: this.s,
stack: this.stack,
dynamic_: this.dynamic_,
prec_: this.prec_,
};
}
_cloneFromSaved( saved ) {
this.s = saved.s;
this.stack = saved.stack.map( obj => ({ ...obj }) );
this.dynamic_ = this._cloneDynamic( saved.dynamic_ );
this.prec_ = saved.prec_;
}
_cloneDynamic( dynamic_ ) {
let chain = [];
while (dynamic_ !== Object.prototype) {
const obj = {};
for (const [ prop, val ] of Object.entries( dynamic_ ))
obj[prop] = Array.isArray( val ) ? [ ...val ] : val;
chain.push( obj );
dynamic_ = Object.getPrototypeOf( dynamic_ );
}
let copy = Object.prototype;
let { length } = chain;
while (--length >= 0)
copy = { __proto__: copy, ...chain[length] };
return copy;
}
la() {
return this.tokens[this.tokenIdx];
}
lb( k = 1 ) {
return this.tokens[this.tokenIdx - k];
}
lr() {
return this.tokens[this.stack[this.stack.length - 1].tokenIdx];
}
lt() {
return this.la().type;
}
lk() {
const la = this.la();
return la.keyword || la.type;
}
l() {
let cmd = this.table[this.s];
let { keyword, type } = this.la();
const kb = keyword && cmd[keyword];
if (Array.isArray( kb ) &&
(!kb[2] || this.fixKeywordTokenIdx === this.tokenIdx || this._predictKeyword( kb[2] )))
return (kb[3] && this._rejectCond( kb[3], kb[4] )) ? '' : keyword;
const tb = cmd[type];
return (!tb || tb[3] && this._rejectCond( tb[3], tb[4] )) ? '' : type;
}
e() {
const la = this.la();
this.s = null;
if (this.errorTokenIdx === this.tokenIdx &&
(this.reuseErrorTokenIdx == null && this.reuseErrorTokenIdx < this.tokenIdx))
throw Error( `Already reported error for ${ tokenFullName( la ) } at ${ la.location }`);
la.parsedAs = '';
this.errorTokenIdx = this.tokenIdx;
this.conditionStackLength = null;
let { length } = this.stack;
while (--length && this.tokenIdx === this.stack[length].tokenIdx)
this.stack[length].followState = null;
if (++length === this.stack.length)
return this._reportAndRecover();
this.stack[length].followState = this.errorState;
this.s = null;
return false;
}
gr( follow ) {
if (!this.g( 0 ))
return false;
const { type, keyword } = this.tokens[this.tokenIdx];
if (keyword &&
follow?.[0] === 'Id' && !this.keywords[keyword] &&
this.fixKeywordTokenIdx !== this.tokenIdx ||
follow?.includes( keyword || type ))
return true
const match = this._matchesInFollow( type, keyword, 'E' );
return (match ?? true) || this.e();
}
g( state ) {
this.s = state;
return (state || this.stack[this.stack.length - 1].tokenIdx < this.tokenIdx)
? true
: this.e();
}
m( state, token ) {
return (this.tokens[this.tokenIdx].type === token)
? this.c( state )
: this.e();
}
mi( state, ident = true ) {
return (this.tokens[this.tokenIdx].type === 'Id')
? this.ci( state, ident )
: this.e();
}
my( state, ident = true ) {
return (this.tokens[this.tokenIdx].type === 'Id')
? this.cy( state, ident )
: this.e();
}
mk( state, token ) {
return (this.tokens[this.tokenIdx].keyword === token)
? this.ck( state )
: this.e();
}
c( state, parsedAs = 'token' ) {
const la = this.tokens[this.tokenIdx++];
la.parsedAs = parsedAs;
this.s = state;
this.errorState = state;
return true
}
ci( state, ident = 'ident' ) {
if (this.tokenIdx === this.fixKeywordTokenIdx)
return this.e();
const la = this.tokens[this.tokenIdx];
if (this.keywords[la.keyword] && this.errorTokenIdx !== this.tokenIdx) {
if (this._runTransparently( () => {
++this.tokenIdx;
this.s = state;
const { type, keyword } = this.la();
return !(this._pred_next( type, keyword, 'R' ) ??
this._matchesInFollow( type, keyword, 'R' ));
} ))
return this.e();
this.reportReservedWord_();
}
return this.c( state, ident )
}
cy( state, ident = 'ident' ) {
return this.c( state, ident )
}
ck( state ) {
return this.c( state, 'keyword' )
}
cx( state ) {
return this.c( state, (this.l() === 'Id' ? 'keyword' : 'token') );
}
skipToken_() {
++this.tokenIdx;
}
reuseToken_() {
--this.tokenIdx;
}
_rejectCond( cond, arg ) {
if (this.conditionTokenIdx === this.tokenIdx &&
this.conditionStackLength == null &&
!this[cond].afterError)
return false
const fail = this[cond]( arg, true );
if (!fail)
return false;
const { keyword } = this.la();
if (keyword && this.table[this.s][keyword])
this.fixKeywordTokenIdx = this.tokenIdx;
this.conditionTokenIdx = this.tokenIdx;
this.conditionStackLength = this.stack.length;
this.conditionName = cond;
this.conditionFailure = fail;
return true;
}
isNoKeywordInRuleFollow() {
const { keyword } = this.la();
if (!keyword || this.keywords[keyword] == null)
return false;
return this._matchesInFollow( 'Id', keyword, 'F' );
}
rule_( state, followState = -1 ) {
this.s = state;
this.stack.push( {
ruleState: state,
followState,
tokenIdx: this.tokenIdx,
prec: this.prec_,
} );
this.dynamic_ = Object.create( this.dynamic_ );
this.prec_ = null;
this.errorState ??= state;
}
exit_() {
if (this.s)
throw Error( `this.s === ${ this.s }; illegally set by action, or runtime/generator bug` );
this.dynamic_ = Object.getPrototypeOf( this.dynamic_ );
const caller = this.stack.pop();
const immediately = this.tokenIdx === caller.tokenIdx && this.tokenIdx >= this.errorTokenIdx;
this.prec_ = caller.prec;
this.s = caller.followState;
if (immediately)
return this.s != null && this._reportAndRecover();
if (this.s != null)
this.errorState = this.s;
return true;
}
_predictKeyword( first2 ) {
const { keyword: lk1 } = this.la();
if (this.keywords[lk1] !== 0)
return true;
const { type: lt2, keyword: lk2 } = this.tokens[this.tokenIdx + 1];
if (lt2 === 'IllegalToken')
return true
if (lk2 && first2?.[0] === 'Id' && !this.keywords[lk2] ||
first2?.includes( lk2 || lt2 ))
return true
if (this._walkPred( this.table[this.s][lk1], lk1, lt2, lk2 ))
return true;
const choice = this.table[this.s];
return !this._walkPred( choice.Id || choice[''], null, lt2, lk2 );
}
_walkPred( cmd, lk1, lt2, lk2 ) {
const saved = this._saveForWalk();
const { length } = this.stack;
if (typeof cmd[0] !== 'number')
this.s = cmd[1];
if (cmd[0] !== (lk1 ? 'ck' : 'ci')) {
let match1 = this._pred_next( 'Id', lk1, 'P' );
if (!match1) {
if (lk1) {
const { location } = this.la();
throw Error( `Cannot match first prediction token at ${ location.line }:${ location.col } in rule at state ${ saved.s }` );
}
if (match1 == null)
match1 = this._matchesInFollow( 'Id', lk1, 'I' );
Object.assign( this, saved );
this.stack.length = length;
return !!match1;
}
}
++this.tokenIdx;
const mode = lk1 ? 'K' : 'I';
let match2 = this._pred_next( lt2, lk2, mode );
if (match2 == null)
match2 = !!this._matchesInFollow( lt2, lk2, mode );
Object.assign( this, saved );
this.stack.length = length;
--this.tokenIdx;
return match2;
}
_pred_next( type, keyword, mode ) {
const properCall = (mode === 'P');
const lean = (mode !== 'M');
let hasMatchedToken = null;
while (this.s) {
let cmd = this.table[this.s];
if (!Array.isArray( cmd )) {
const lookahead = cmd[' lookahead'];
const c = lookahead
? cmd[this[lookahead]( mode )]
: keyword && cmd[keyword] || cmd[type];
cmd = !(c && this._rejectCondition( c, mode, lean )) && c || cmd[''];
}
const state = this.s;
this.s = cmd[1];
switch (cmd[0]) {
case 'c': case 'ck': case 'cx':
return true;
case 'cy':
return mode !== 'F';
case 'ci':
if (!keyword ||
!this.keywords[keyword] && this.fixKeywordTokenIdx !== this.tokenIdx)
return mode !== 'F';
cmd = this.table[state][''];
this.s = cmd[1];
break;
case 'm':
return type === cmd[2];
case 'mi':
return type === 'Id' && mode !== 'F' &&
(!keyword ||
!this.keywords[keyword] && this.fixKeywordTokenIdx !== this.tokenIdx);
case 'my':
return type === 'Id' && mode !== 'F';
case 'mk':
return keyword === cmd[2];
case 'g': case 'e':
break;
default:
if (typeof cmd[0] !== 'number')
throw Error( `Unexpected command ${ cmd[0] } at state ${ state }` );
hasMatchedToken = false;
if (properCall) {
this.stack.push( {
ruleState: cmd[1],
followState: cmd[0],
tokenIdx: this.tokenIdx,
prec: this.prec_,
} );
this.dynamic_ = Object.create( this.dynamic_ );
this.prec_ = null;
}
}
}
if (this.s == null)
return false;
return (hasMatchedToken ?? this.tokenIdx > this.stack.at( -1 ).tokenIdx)
&& null;
}
_rejectCondition( cmd, mode, lean ) {
const cond = cmd[3];
if (!cond || lean && !this.hardGuards[cond])
return false;
if (!this.constructor.tracingParser)
return !!this[cond]( cmd[4], mode );
const succeed = !this[cond]( cmd[4], mode );
return !succeed;
}
_matchesInFollow( type, keyword, mode ) {
const savedState = this.s;
const { dynamic_ } = this;
let match;
let depth = this.stack.length;
while (match == null && --depth) {
this.dynamic_ = Object.getPrototypeOf( this.dynamic_ );
this.s = this.stack[depth].followState;
match = this._pred_next( type, keyword, mode );
}
this.dynamic_ = dynamic_;
this.s = savedState;
return match;
}
_confirmExpected( token, saved ) {
const fix = /^[_a-z]/.test( token );
const [ type, keyword ] = (fix) ? [ 'Id', token ] : [ token ];
Object.assign( this.la(), { type, keyword } );
this._cloneFromSaved( saved );
this.fixKeywordTokenIdx = fix && this.tokenIdx;
let match;
while (this.stack.length) {
match = this._pred_next( type, keyword, 'M' );
if (match != null)
break;
this.dynamic_ = Object.getPrototypeOf( this.dynamic_ );
this.s = this.stack.pop().followState;
}
return match ?? true;
}
_calculateTokenSet( mode ) {
const savedState = this.s;
const savedDynamic = this.dynamic_;
const savedStack = this.stack;
this.stack = [ ...savedStack ];
this.s = this.errorState;
const set = Object.create(null);
if (mode === 'M') {
while (this.stack.length && this._tokenSetInRule( set, true )) {
this.dynamic_ = Object.getPrototypeOf( this.dynamic_ );
this.s = this.stack.pop().followState;
}
}
else {
let val = this.stack.length + 1;
while (this.stack.length) {
this._tokenSetInRule( set, val );
val = this.stack.length;
this.dynamic_ = Object.getPrototypeOf( this.dynamic_ );
this.s = this.stack.pop().followState;
}
set.EOF ??= 0;
}
this.stack = savedStack;
this.s = savedState;
this.dynamic_ = savedDynamic;
return set;
}
_tokenSetInRule( expecting, val, cmd, collectKeywordsAndIdOnly = false ) {
const savedDynamic = this.dynamic_;
const savedState = this.s;
let enteredRules = 0;
loop: while (this.s) {
cmd ??= this.table[this.s];
if (!Array.isArray( cmd )) {
const lookahead = cmd[' lookahead'];
const dict = cmd;
cmd = dict[''];
for (const prop in dict) {
if (prop && Object.hasOwn( dict, prop ) && prop !== 'Id' &&
!Object.hasOwn( expecting, prop ) && prop.charAt(0) !== ' ') {
const ad = dict[prop];
if (ad[1] !== cmd[1] || ad[0] !== 'g')
this.addTokenToSet_( expecting, prop, val, collectKeywordsAndIdOnly, lookahead );
}
}
if (dict.Id) {
if (cmd[0] === 'e') {
collectKeywordsAndIdOnly = true;
cmd = dict.Id;
}
else {
this._tokenSetInRule( expecting, val, dict.Id, true );
}
}
}
switch (cmd[0]) {
case 'm': case 'mk':
this.addTokenToSet_( expecting, cmd[2], val, collectKeywordsAndIdOnly );
break loop;
case 'ci': case 'cy': case 'mi': case 'my':
this.addTokenToSet_( expecting, 'Id', val, false );
break loop;
case 'g': case 'e':
break;
default:
if (typeof cmd[0] !== 'number')
throw Error( `Unexpected command ${ cmd[0] } at state ${ this.s }` );
++enteredRules;
this.stack.push( {
ruleState: cmd[1],
followState: cmd[0],
tokenIdx: this.tokenIdx,
prec: this.prec_,
} );
this.dynamic_ = Object.create( this.dynamic_ );
this.prec_ = null;
}
this.s = cmd[1];
cmd = null;
}
const inspectOuterRules = (this.s === 0 && !enteredRules);
this.s = savedState;
this.dynamic_ = savedDynamic;
this.stack.length -= enteredRules;
return inspectOuterRules;
}
addTokenToSet_( set, token, val, collectKeywordsOnly, _lookahead ) {
if (!collectKeywordsOnly || /^[_a-z]/.test( token ))
set[token] ??= val;
}
expectingArray_() {
const token = this.la();
const set = this._calculateTokenSet( 'M' );
const { keyword, type } = token;
if (keyword && set[keyword] === true)
delete set[keyword];
else if (set[type] === true && !(keyword && this.keywords[keyword] != null))
delete set[type];
const saved = this._saveForWalk();
saved._trace = this._trace;
saved.fixKeywordTokenIdx = this.fixKeywordTokenIdx;
const expecting = Object.keys( set )
.filter( tok => this._confirmExpected( tok, saved ) );
token.type = type;
token.keyword = keyword;
Object.assign( this, saved );
return expecting;
}
_findSyncToken( syncSet ) {
const rewindDepth = this.stack.length
this.recoverTokenIdx = this.tokenIdx;
while (this.recoverTokenIdx < this.tokens.length) {
const { keyword, type } = this.tokens[this.recoverTokenIdx];
let recoverDepth = keyword ? syncSet[keyword] : null;
if (recoverDepth != null)
return recoverDepth;
recoverDepth = syncSet[type];
if (recoverDepth != null &&
(type !== 'Id' || (!keyword || !this.keywords[keyword]) &&
(recoverDepth > rewindDepth ||
[ ';', '}' ].includes( this.tokens[this.recoverTokenIdx - 1].type ))))
return recoverDepth;
++this.recoverTokenIdx;
}
throw Error( 'EOF must be last in `tokens`' );
}
_reportAndRecover() {
this.s = this.errorState;
const syncSet = this._calculateTokenSet( 'Y' );
this.recoverTokenIdx = this.tokenIdx;;
const prev = this.lb();
const reuseRecoverDepth = this.reuseErrorTokenIdx != null &&
prev?.keyword && prev.parsedAs !== 'keyword' &&
this.reuseErrorTokenIdx < this.tokenIdx && syncSet[prev.keyword];
this.reportUnexpectedToken_( reuseRecoverDepth ? 'reuse' : null );
if (this.reuseErrorTokenIdx != null)
this.reuseErrorTokenIdx = (reuseRecoverDepth) ? this.tokenIdx : -1;
this.fixKeywordTokenIdx = (reuseRecoverDepth) ? --this.recoverTokenIdx : -1;
const recoverDepth = reuseRecoverDepth || this._findSyncToken( syncSet );
this.s = null;
let depth = this.stack.length;
if (recoverDepth > depth)
this.s = this.errorState;
while (depth > recoverDepth)
this.stack[--depth].followState = null;
if (this.tokenIdx > this.recoverTokenIdx)
this.reuseToken_();
else while (this.tokenIdx < this.recoverTokenIdx)
this.skipToken_();
this.conditionTokenIdx = this.tokenIdx;
this.conditionStackLength = null;
return false;
}
log( ...args ) {
console.log( ...args );
}
reportError_( location, text ) {
this.$hasErrors = true;
this.log( `${ location }:`, text );
}
reportUnexpectedToken_( msg ) {
const token = (msg === 'reuse') ? this.lb() : this.la();
msg ??= `Unexpected token ${ tokenFullName( token, ': ' ) }`;
msg = (msg === 'reuse')
? `Missing input before keyword ${ tokenFullName( token, ': ' ) }`
: msg + ' - expecting: ' + this.expectingArray_().map( tokenName ).sort().join( ', ' );
this.reportError_( token.location, msg );
}
reportReservedWord_() {
this.reportUnexpectedToken_( `Unexpected reserved word ‘${ this.la().text }’` );
}
errorAndRecoverOutside( token, text ) {
this.reportError_( token.location, text );
while (this.l() !== ';')
this.skipToken_();
this.s = null;
return false;
}
inSameRule_( lowState = this.s, highState = this.stack.at(-1).followState ) {
if (lowState > highState)
[ lowState, highState ] = [ highState, lowState ];
while (lowState < highState) {
if (typeof this.table[++lowState] === 'string')
return false;
}
return true;
}
hide_( _arg, mode ) {
return mode === 'M';
}
precLeft_( prec ) {
const parentPrec = this.stack.at( -1 ).prec;
if (parentPrec != null && parentPrec >= prec)
return true;
this.prec_ = prec;
return false;
}
precRight_( prec ) {
const parentPrec = this.stack.at( -1 ).prec;
if (parentPrec != null && parentPrec >= prec)
return true;
this.prec_ = prec - 1;
return false;
}
precNone_( prec ) {
const parentPrec = this.stack.at( -1 ).prec;
if (parentPrec != null && parentPrec >= prec ||
this.prec_ != null && this.prec_ <= prec)
return true;
this.prec_ = prec;
return false;
}
precPost_( prec ) {
const parentPrec = this.stack.at( -1 ).prec;
if (parentPrec != null && parentPrec >= prec ||
this.prec_ != null && this.prec_ < prec)
return true;
this.prec_ = prec;
return false;
}
}
function tokenName( type ) {
if (typeof type !== 'string')
type = (!type.parsedAs || type.parsedAs === 'keyword') && type.keyword || type.type;
return (/^[A-Z]+/.test( type )) ? `‹${ type }›` : `‘${ type }’`;
}
function tokenFullName( token, sep ) {
return (token.parsedAs && token.parsedAs !== 'keyword' && token.parsedAs !== 'token' ||
token.type !== 'Id' && token.type !== token.text && token.text)
? `‘${ token.text }’${ sep }${ tokenName( token ) }`
: tokenName( token );
}
function compileTable( table ) {
if (table.$compiled)
return table;
for (const line of table) {
if (typeof line !== 'object' || Array.isArray( line ))
continue;
for (const prop of Object.keys( line ))
compileTableItem( line, prop );
if (!line[''])
line[''] = [ 'e' ];
}
table.$compiled = true;
return table;
}
function compileTableItem( line, prop ) {
const alt = line[prop];
if (typeof alt === 'number')
return line[prop] = [ 'g', alt ];
else if (typeof alt === 'string' && prop.charAt(0) !== ' ')
return line[prop] = compileTableItem( line, alt );
return alt;
}
BaseParser.prototype.isNoKeywordInRuleFollow.afterError = true;
module.exports = BaseParser;