UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

893 lines (640 loc) 17.8 kB
// 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;