@sap/cds-compiler 5.2.0 → 5.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/bin/cdsc.js +5 -0
- package/bin/cdshi.js +8 -8
- package/doc/CHANGELOG_BETA.md +9 -4
- package/lib/api/validate.js +5 -0
- package/lib/base/message-registry.js +25 -1
- package/lib/base/messages.js +1 -1
- package/lib/base/model.js +0 -1
- package/lib/compiler/assert-consistency.js +2 -2
- package/lib/compiler/builtins.js +1 -1
- package/lib/compiler/checks.js +25 -6
- package/lib/compiler/define.js +24 -28
- package/lib/compiler/extend.js +11 -13
- package/lib/compiler/generate.js +3 -3
- package/lib/compiler/populate.js +13 -7
- package/lib/compiler/propagator.js +2 -2
- package/lib/compiler/resolve.js +58 -60
- package/lib/compiler/shared.js +5 -5
- package/lib/compiler/tweak-assocs.js +247 -34
- package/lib/compiler/utils.js +40 -32
- package/lib/compiler/xpr-rewrite.js +44 -58
- package/lib/edm/annotations/genericTranslation.js +4 -4
- package/lib/edm/csn2edm.js +2 -2
- package/lib/edm/edm.js +46 -21
- package/lib/edm/edmInboundChecks.js +0 -1
- package/lib/edm/edmPreprocessor.js +40 -27
- package/lib/edm/edmUtils.js +1 -1
- package/lib/gen/BaseParser.js +180 -122
- package/lib/gen/CdlParser.js +2226 -2170
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageParser.js +3820 -3777
- package/lib/inspect/inspectPropagation.js +1 -1
- package/lib/json/from-csn.js +5 -3
- package/lib/json/to-csn.js +7 -10
- package/lib/language/antlrParser.js +38 -4
- package/lib/language/errorStrategy.js +1 -1
- package/lib/language/genericAntlrParser.js +4 -4
- package/lib/language/multiLineStringParser.js +1 -1
- package/lib/main.d.ts +23 -0
- package/lib/model/cloneCsn.js +22 -13
- package/lib/optionProcessor.js +7 -7
- package/lib/parsers/AstBuildingParser.js +155 -37
- package/lib/parsers/CdlGrammar.g4 +154 -81
- package/lib/parsers/Lexer.js +20 -10
- package/lib/render/toCdl.js +23 -18
- package/lib/transform/addTenantFields.js +4 -4
- package/lib/transform/db/rewriteCalculatedElements.js +11 -5
- package/lib/transform/db/transformExists.js +43 -18
- package/lib/transform/effective/main.js +1 -1
- package/lib/transform/forRelationalDB.js +8 -7
- package/lib/utils/moduleResolve.js +1 -1
- package/package.json +1 -1
- package/share/messages/redirected-to-complex.md +6 -3
package/lib/gen/BaseParser.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
// Base class for generated parser, for redepage v0.1.
|
|
1
|
+
// Base class for generated parser, for redepage v0.1.12
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
class BaseParser {
|
|
6
6
|
constructor( lexer, keywords, table ) {
|
|
7
7
|
this.keywords = keywords;
|
|
8
|
-
this.table = table;
|
|
8
|
+
this.table = compileTable( table );
|
|
9
9
|
this.lexer = lexer;
|
|
10
10
|
this.tokens = undefined;
|
|
11
11
|
this.eofIndex = undefined;
|
|
12
12
|
this.tokenIdx = 0;
|
|
13
13
|
this.conditionTokenIdx = -1;
|
|
14
|
+
this.fixKeywordTokenIdx = -1;
|
|
14
15
|
this.conditionStackLength = -1;
|
|
15
16
|
this.nextTokenAsId = false;
|
|
16
17
|
|
|
@@ -63,7 +64,7 @@ class BaseParser {
|
|
|
63
64
|
if (this.trace.length > 1)
|
|
64
65
|
this._trace( 'detected parsing error,' );
|
|
65
66
|
this.reportUnexpectedToken_( la );
|
|
66
|
-
la.
|
|
67
|
+
la.parsedAs = 0;
|
|
67
68
|
|
|
68
69
|
if (this.conditionTokenIdx === this.tokenIdx &&
|
|
69
70
|
this.conditionStackLength === this.stack.length &&
|
|
@@ -73,7 +74,8 @@ class BaseParser {
|
|
|
73
74
|
this.s = (this.tokenIdx > tokenIdx) ? this.errorState : ruleState;
|
|
74
75
|
return false; // error recovery: ignore condition/precedence
|
|
75
76
|
}
|
|
76
|
-
|
|
77
|
+
|
|
78
|
+
if (this.tokenIdx >= this.eofIndex)
|
|
77
79
|
return this._stopParsing( this.stack.length );
|
|
78
80
|
// TODO: also sync to what comes next in current rule, at least after rule call,
|
|
79
81
|
// this way we do not have to do the check of g(0) in re() as we did before 2023-12-07
|
|
@@ -87,7 +89,6 @@ class BaseParser {
|
|
|
87
89
|
ei() { // error (after trying to test again as identifier)
|
|
88
90
|
if (!this.tokens[this.tokenIdx].keyword) // lk() had directly returned the type
|
|
89
91
|
return this.e();
|
|
90
|
-
this._traceIdOrPred( '-Id' );
|
|
91
92
|
this.nextTokenAsId = true;
|
|
92
93
|
return false; // do not execute action after it
|
|
93
94
|
}
|
|
@@ -104,25 +105,27 @@ class BaseParser {
|
|
|
104
105
|
const { type: lt, keyword: lk } = this.tokens[this.tokenIdx];
|
|
105
106
|
if (lk && // Id also for unreserved, except after condition failure
|
|
106
107
|
follow?.[0] === 'Id' && this.keywords[lk] !== false &&
|
|
107
|
-
this.
|
|
108
|
-
follow?.includes( lk || lt ))
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// Do we have possibilities to stay in rule with error recovery?
|
|
112
|
-
const expecting = this._expecting( 0 ); // dynamic follow-set
|
|
113
|
-
// TODO: improve performance: no check needed for a rule-end directly after
|
|
114
|
-
// a rule end: the second is definitely successful if the first was.
|
|
115
|
-
// TODO: do not calculate the complete dynamic follow-set, provide dedicated
|
|
116
|
-
// function to test whether the next token is valid
|
|
117
|
-
// we might also cache the result in the stack
|
|
118
|
-
// ok: lk or lt -> lk=e or (lt=e && (not cond || not keyw)
|
|
119
|
-
if (expecting[lk] ||
|
|
120
|
-
// if at failed condition, do not make Id in follow end the rule
|
|
121
|
-
// (assuming that there is no condition for `Id` at optional rule end):
|
|
122
|
-
expecting[lt] && !(lk && this.conditionTokenIdx === this.tokenIdx))
|
|
108
|
+
this.fixKeywordTokenIdx !== this.tokenIdx ||
|
|
109
|
+
follow?.includes( lk || lt )) {
|
|
110
|
+
this._tracePush( [ 'E', true ] );
|
|
123
111
|
return true;
|
|
124
|
-
|
|
125
|
-
|
|
112
|
+
}
|
|
113
|
+
this._tracePush( [ 'E', 0 ] );
|
|
114
|
+
// TODO: caching
|
|
115
|
+
const { dynamic_ } = this;
|
|
116
|
+
let match;
|
|
117
|
+
let depth = this.stack.length;
|
|
118
|
+
while (match == null && --depth) {
|
|
119
|
+
this.dynamic_ = Object.getPrototypeOf( this.dynamic_ );
|
|
120
|
+
const { followState } = this.stack[depth];
|
|
121
|
+
match = this._pred_next( followState, lt, lk, 'E' );
|
|
122
|
+
this._traceSubPush( match ?? 0 );
|
|
123
|
+
}
|
|
124
|
+
this.dynamic_ = dynamic_;
|
|
125
|
+
// If the parser reaches this point with match = null, even the top-level rule
|
|
126
|
+
// does not have a required token (typically `EOF`) at the end → the parser
|
|
127
|
+
// must accept any token → rule exit possible (but no output '✔' in trace).
|
|
128
|
+
return (match ?? true) || this.e();
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
// go to state; non-tracing parser: `this.s=‹state›` or `this.gr()`
|
|
@@ -138,7 +141,6 @@ class BaseParser {
|
|
|
138
141
|
giA( state, follow ) { // go to state (after trying to test again as identifier)
|
|
139
142
|
if (!this.tokens[this.tokenIdx].keyword) // lk() had directly returned the type
|
|
140
143
|
return this.g( state, follow );
|
|
141
|
-
this._traceIdOrPred( '-Id' );
|
|
142
144
|
this.nextTokenAsId = true;
|
|
143
145
|
return false; // do not execute action after it
|
|
144
146
|
}
|
|
@@ -148,9 +150,8 @@ class BaseParser {
|
|
|
148
150
|
const lk = this.tokens[this.tokenIdx].keyword;
|
|
149
151
|
// As opposed to ei(), we also check for reserved keywords here; this way, we
|
|
150
152
|
// do not have to add reserved keywords from the follow-set to the `switch`.
|
|
151
|
-
if (!lk || this.keywords[lk] === false)
|
|
153
|
+
if (!lk || this.keywords[lk] === false) // TODO: consider fixKeywordTokenIdx ?
|
|
152
154
|
return this.g( state, follow );
|
|
153
|
-
this._traceIdOrPred( '-Id' );
|
|
154
155
|
this.nextTokenAsId = true;
|
|
155
156
|
return false; // do not execute action after it
|
|
156
157
|
}
|
|
@@ -160,7 +161,6 @@ class BaseParser {
|
|
|
160
161
|
const lk = this.tokens[this.tokenIdx].keyword;
|
|
161
162
|
if (!lk || this.keywords[lk] === false || this._keyword_after_rule( lk ))
|
|
162
163
|
return this.g( state, follow );
|
|
163
|
-
this._traceIdOrPred( '-Id' );
|
|
164
164
|
this.nextTokenAsId = true;
|
|
165
165
|
return false; // do not execute action after it
|
|
166
166
|
}
|
|
@@ -199,9 +199,9 @@ class BaseParser {
|
|
|
199
199
|
: this.e();
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
c( state,
|
|
202
|
+
c( state, parsedAs = 'token' ) { // consume token
|
|
203
203
|
const la = this.tokens[this.tokenIdx];
|
|
204
|
-
la.
|
|
204
|
+
la.parsedAs = parsedAs;
|
|
205
205
|
if (this.tokenIdx < this.eofIndex) ++this.tokenIdx;
|
|
206
206
|
// TODO: handle identifier-including-reserved-words later (e.g. for id after a `.`)
|
|
207
207
|
this.s = state;
|
|
@@ -235,12 +235,16 @@ class BaseParser {
|
|
|
235
235
|
return this.lP( first2 ) && this.ck( state );
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
// for parser token
|
|
238
|
+
// for parser token or token set via `/`
|
|
239
239
|
ckA( state ) {
|
|
240
|
-
// if it really should be considered an Id, `set this.la().
|
|
240
|
+
// if it really should be considered an Id, `set this.la().parsedAs` yourself
|
|
241
241
|
return this.c( state, (this.l() === 'Id' ? 'keyword' : 'token') );
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
skipToken_() {
|
|
245
|
+
++this.tokenIdx;
|
|
246
|
+
}
|
|
247
|
+
|
|
244
248
|
// condition and precedence handling ------------------------------------------
|
|
245
249
|
|
|
246
250
|
// state must match the goto-state of the default (there must be no default
|
|
@@ -250,14 +254,25 @@ class BaseParser {
|
|
|
250
254
|
// “go if user condition fails”
|
|
251
255
|
gc( state, cond ) {
|
|
252
256
|
if (this.conditionTokenIdx === this.tokenIdx &&
|
|
253
|
-
this.conditionStackLength === this.stack.length)
|
|
257
|
+
this.conditionStackLength === this.stack.length) {
|
|
258
|
+
this._tracePush( [ 'C' ] );
|
|
254
259
|
return true; // error recovery: ignore condition
|
|
260
|
+
}
|
|
255
261
|
this.conditionTokenIdx = this.tokenIdx;
|
|
256
262
|
this.conditionStackLength = this.stack.length;
|
|
257
263
|
// TODO: let this[cond]( true ) return recovery badness in error case
|
|
258
264
|
const fail = !this[cond]( true );
|
|
259
265
|
if (this.constructor.tracingParser)
|
|
260
|
-
this._tracePush(
|
|
266
|
+
this._tracePush( [ 'C', cond, !fail ] );
|
|
267
|
+
// TODO TOOL: in this case, the default case must not have actions (tool must
|
|
268
|
+
// add state if it does)
|
|
269
|
+
if (fail) { // TODO: extra gcK() method instead of check below
|
|
270
|
+
// TODO: extra method necessary for academic case
|
|
271
|
+
// ( 'unreserved' 'foo' | <cond> Id 'bar' )` with input `unreserved bar`
|
|
272
|
+
const { keyword } = this.la();
|
|
273
|
+
if (keyword && this.table[keyword])
|
|
274
|
+
this.fixKeywordTokenIdx = this.tokenIdx;
|
|
275
|
+
}
|
|
261
276
|
return !fail || this.g( state ) && false;
|
|
262
277
|
}
|
|
263
278
|
|
|
@@ -269,7 +284,7 @@ class BaseParser {
|
|
|
269
284
|
gp( state, prec, mode ) {
|
|
270
285
|
if (this.conditionTokenIdx === this.tokenIdx &&
|
|
271
286
|
this.conditionStackLength === this.stack.length) {
|
|
272
|
-
this._tracePush(
|
|
287
|
+
this._tracePush( [ 'C' ] );
|
|
273
288
|
return true; // error recovery: ignore condition
|
|
274
289
|
}
|
|
275
290
|
this.conditionTokenIdx = this.tokenIdx;
|
|
@@ -282,11 +297,17 @@ class BaseParser {
|
|
|
282
297
|
if (this.constructor.tracingParser) {
|
|
283
298
|
const pp = (parentPrec === -Infinity) ? '-∞' : parentPrec;
|
|
284
299
|
const tp = (this.prec_ == null) ? '∞' : this.prec_;
|
|
285
|
-
const suffix = mode === 'post' &&
|
|
286
|
-
this._tracePush(
|
|
300
|
+
const suffix = mode === 'post' && `≤${ tp }` || mode === 'none' && `<${ tp }`;
|
|
301
|
+
this._tracePush( [ 'C', `${ pp }<${ prec }${ suffix || '' }`, !fail ] );
|
|
287
302
|
}
|
|
288
|
-
if (fail)
|
|
303
|
+
if (fail) { // TODO: extra gcK() method instead of check below
|
|
304
|
+
// TODO: extra method necessary for academic case
|
|
305
|
+
// ( 'unreserved' 'foo' | <cond> Id 'bar' )` with input `unreserved bar`
|
|
306
|
+
const { keyword } = this.la();
|
|
307
|
+
if (keyword && this.table[this.s][keyword])
|
|
308
|
+
this.fixKeywordTokenIdx = this.tokenIdx;
|
|
289
309
|
return this.g( state ) && false; // TODO: reset this.prec_ ?
|
|
310
|
+
}
|
|
290
311
|
this.prec_ = (mode === 'right') ? prec - 1 : prec; // -1: <…,assoc=right>, <…,prefix>
|
|
291
312
|
return true;
|
|
292
313
|
}
|
|
@@ -329,75 +350,60 @@ class BaseParser {
|
|
|
329
350
|
|
|
330
351
|
// predicate used before rule call if with LL(1) conflict, 'Id' in other case
|
|
331
352
|
lP( first2 ) { // only start rule if this predicate returns true
|
|
332
|
-
const { type: lt2, keyword: lk2 } = this.tokens[this.tokenIdx + 1];
|
|
333
|
-
// Argument first2 is just a performance hint with ckP():
|
|
334
|
-
if (lk2 && first2?.[0] === 'Id' && this.keywords[lk2] !== false ||
|
|
335
|
-
first2?.includes( lk2 || lt2 ))
|
|
336
|
-
return true;
|
|
337
|
-
|
|
338
353
|
// nothing to check if not a non-reserved keyword:
|
|
339
354
|
const { keyword: lk1 } = this.tokens[this.tokenIdx];
|
|
340
355
|
if (!lk1 || !this.keywords[lk1])
|
|
341
356
|
return true;
|
|
342
357
|
|
|
358
|
+
const { type: lt2, keyword: lk2 } = this.tokens[this.tokenIdx + 1];
|
|
359
|
+
// Argument first2 is just a performance hint with ckP():
|
|
360
|
+
if (lk2 && first2?.[0] === 'Id' && this.keywords[lk2] !== false ||
|
|
361
|
+
first2?.includes( lk2 || lt2 )) {
|
|
362
|
+
this._tracePush( [ 'P', true ] );
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
this._tracePush( [ 'P' ] );
|
|
343
366
|
// now check it dynamically:
|
|
344
367
|
let cmd = this.table[this.s][lk1];
|
|
345
|
-
if (
|
|
346
|
-
cmd = this.table[this.s][cmd];
|
|
347
|
-
if (!Array.isArray( cmd ) || cmd[2] !== 1)
|
|
368
|
+
if (cmd[2] !== 1)
|
|
348
369
|
throw Error( `Unexpected command '${ cmd?.[0] }' without prediction at state ${ this.s } for ‘${ lk1 }’` );
|
|
349
370
|
|
|
350
|
-
|
|
371
|
+
// if not the keyword match, the command is “goto” or “rule call”
|
|
351
372
|
const nextState = (cmd[0] === 'ck') ? cmd[1] : this._pred_keyword( cmd[1], lk1 );
|
|
352
373
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
374
|
+
++this.tokenIdx; // for user lookahead fns and conditions
|
|
375
|
+
const match = this._pred_next( nextState, lt2, lk2, 'P' );
|
|
376
|
+
--this.tokenIdx;
|
|
377
|
+
|
|
378
|
+
const r = match ?? true;
|
|
379
|
+
if (match == null)
|
|
380
|
+
this._traceSubPush( 0 );
|
|
381
|
+
if (lt2 === 'IllegalToken')
|
|
356
382
|
return true
|
|
357
|
-
// TODO: instead of this IllegalToken test,
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
this.
|
|
361
|
-
|
|
362
|
-
|
|
383
|
+
// TODO: instead of this IllegalToken test, implement a “confirm unreserved
|
|
384
|
+
// keyword as Id” prediction which tests whether the token after the then-Id
|
|
385
|
+
// matches.
|
|
386
|
+
this._traceSubPush( r );
|
|
387
|
+
if (!r)
|
|
388
|
+
this.nextTokenAsId = true;
|
|
389
|
+
return r;
|
|
363
390
|
}
|
|
364
391
|
|
|
365
392
|
// Now the helper methods =====================================================
|
|
366
393
|
|
|
367
394
|
// Standard weak-conflict predicate -------------------------------------------
|
|
368
|
-
// Weak (and fast) single-step walk and test (no rule exit, start is fine): for
|
|
369
|
-
// pg(), pr(). The main point is that we do not (again) consider predicates.
|
|
370
|
-
// Currently just tests against the token _type_ of the next token, not its
|
|
371
|
-
// specific keyword; see comments below for details.
|
|
372
|
-
|
|
373
|
-
_pred( nextState, lt2, lk2 ) {
|
|
374
|
-
if (nextState) {
|
|
375
|
-
// return this._pred_test( nextState, lt2 );
|
|
376
|
-
const r = this._pred_next( nextState, lt2, lk2 );
|
|
377
|
-
this._tracePush( this.s );
|
|
378
|
-
return r;
|
|
379
|
-
}
|
|
380
|
-
// dubious weak conflict at end of rule:
|
|
381
|
-
this._traceIdOrPred( '-P0' );
|
|
382
|
-
this._tracePush( this.s );
|
|
383
|
-
return true; // dubious
|
|
384
|
-
}
|
|
385
395
|
|
|
386
396
|
_pred_keyword( state, keyword ) {
|
|
387
|
-
// returns
|
|
397
|
+
// returns state after matching the first token as keyword, for lP()
|
|
388
398
|
while (state) {
|
|
389
|
-
this.
|
|
399
|
+
this._traceSubPush( state );
|
|
390
400
|
let cmd = this.table[state];
|
|
391
|
-
if (!Array.isArray( cmd ))
|
|
392
|
-
|
|
393
|
-
cmd = (typeof alt === 'string')
|
|
394
|
-
? cmd[alt]
|
|
395
|
-
: typeof alt === 'number' && [ 'g', alt ] || alt || [ 'g', cmd[''] ];
|
|
396
|
-
}
|
|
401
|
+
if (!Array.isArray( cmd ))
|
|
402
|
+
cmd = cmd[keyword] || cmd.Id || cmd[''];
|
|
397
403
|
switch (cmd[0]) {
|
|
398
404
|
case 'ck': case 'mk':
|
|
399
405
|
return cmd[1]; // state after token consumption
|
|
400
|
-
case 'g':
|
|
406
|
+
case 'g': // TODO: another rule call?
|
|
401
407
|
break;
|
|
402
408
|
default:
|
|
403
409
|
if (typeof cmd[0] !== 'number')
|
|
@@ -409,43 +415,70 @@ class BaseParser {
|
|
|
409
415
|
throw Error( 'Not supported: option for unreserved keywords in follow set' );
|
|
410
416
|
}
|
|
411
417
|
|
|
412
|
-
_pred_next( state, type, keyword ) {
|
|
418
|
+
_pred_next( state, type, keyword, mode ) {
|
|
419
|
+
let hasEnteredRule = false;
|
|
413
420
|
while (state) {
|
|
414
|
-
this.
|
|
421
|
+
this._traceSubPush( state );
|
|
415
422
|
let cmd = this.table[state];
|
|
416
423
|
if (!Array.isArray( cmd )) {
|
|
417
|
-
const
|
|
418
|
-
cmd =
|
|
419
|
-
? cmd[
|
|
420
|
-
:
|
|
424
|
+
const lookahead = cmd[' lookahead'];
|
|
425
|
+
cmd = lookahead
|
|
426
|
+
? cmd[this[lookahead]( mode )] || cmd['']
|
|
427
|
+
: keyword && cmd[keyword] || cmd[type] || cmd[''];
|
|
421
428
|
}
|
|
422
429
|
switch (cmd[0]) {
|
|
423
|
-
case 'c': case 'ck': case 'ciA':
|
|
430
|
+
case 'c': case 'ck': case 'ciA': case 'ckA': // TODO: re-check ckA
|
|
424
431
|
return true;
|
|
432
|
+
case 'ci':
|
|
433
|
+
if (!keyword ||
|
|
434
|
+
this.keywords[keyword] !== false && this.fixKeywordTokenIdx !== this.tokenIdx)
|
|
435
|
+
return true;
|
|
436
|
+
cmd = this.table[state]['']; // is currently always 'g' or 'e'
|
|
437
|
+
break;
|
|
425
438
|
case 'm':
|
|
426
439
|
return type === cmd[2];
|
|
427
|
-
case 'mi':
|
|
428
|
-
return type === 'Id' &&
|
|
440
|
+
case 'mi':
|
|
441
|
+
return type === 'Id' &&
|
|
442
|
+
(!keyword ||
|
|
443
|
+
this.keywords[keyword] !== false && this.fixKeywordTokenIdx !== this.tokenIdx);
|
|
429
444
|
case 'miA':
|
|
430
445
|
return type === 'Id';
|
|
431
446
|
case 'mk':
|
|
432
447
|
return keyword === cmd[2];
|
|
448
|
+
case 'g': case 'e':
|
|
449
|
+
break;
|
|
450
|
+
default:
|
|
451
|
+
if (typeof cmd[0] !== 'number')
|
|
452
|
+
throw Error( `Unexpected command ${ cmd[0] } at state ${ this.s }` );
|
|
453
|
+
// If the parser enters a rule, reaching the rule end (can happen with
|
|
454
|
+
// option `minTokensMatched`) means "no match".
|
|
455
|
+
hasEnteredRule = true;
|
|
456
|
+
// If we want to support conditions before matching the first token in a
|
|
457
|
+
// rule, we would have to handle `this.stack` and `this.dynamically_`.
|
|
433
458
|
}
|
|
434
459
|
// We could optimize with rule call - only 'Id' must be further investigated
|
|
435
460
|
state = cmd[1];
|
|
436
461
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
462
|
+
// If invalid state, the second token does not match, e.g. for `VIRTUAL +`
|
|
463
|
+
// or `VIRTUAL §` (with IllegalToken):
|
|
464
|
+
if (state == null)
|
|
465
|
+
return false;
|
|
466
|
+
|
|
467
|
+
// Otherwise, the parser could end the rule after having matched the keyword
|
|
468
|
+
// with prediction. TODO: as we do not look behind the current rule for the
|
|
469
|
+
// prediction, the tool can normally omit the prediction (and output a
|
|
470
|
+
// message), no so with `ruleStartingWithUnreserved`. We will rather look
|
|
471
|
+
// behind the current rule _after_ having decided that the token is to be
|
|
472
|
+
// matched as identifier.
|
|
473
|
+
return !hasEnteredRule && null; // let caller decide how to interpret this
|
|
445
474
|
}
|
|
446
475
|
|
|
447
476
|
_keyword_after_rule( keyword ) {
|
|
448
477
|
// TODO: this is a slow implementation - do dedicated traversal later
|
|
478
|
+
// It is used in giR() only and this is currently used just once.
|
|
479
|
+
// TODO: using mode = 'R' and tracing R(…)
|
|
480
|
+
// TODO: investigate why this was not written before adding
|
|
481
|
+
// `<default=fallback>` in rule `fromRefWithOptAlias`.
|
|
449
482
|
return this._expecting()[keyword];
|
|
450
483
|
}
|
|
451
484
|
|
|
@@ -477,16 +510,13 @@ class BaseParser {
|
|
|
477
510
|
// tokens at the follow state of the current rule. Argument `prop` is the token
|
|
478
511
|
// name for `cmd` in a decision.
|
|
479
512
|
_exp_collect( expecting, cmd, prop ) {
|
|
480
|
-
if (prop != null)
|
|
481
|
-
cmd = cmd[
|
|
482
|
-
}
|
|
483
|
-
if (typeof cmd === 'number') // ‹followState› = short form for this.g(‹followState›)
|
|
484
|
-
cmd = [ 'g', cmd ];
|
|
485
|
-
|
|
513
|
+
if (prop != null)
|
|
514
|
+
cmd = cmd[prop];
|
|
486
515
|
if (!Array.isArray( cmd )) {
|
|
487
516
|
let reachedRuleEnd = false;
|
|
488
517
|
for (const tok in cmd) {
|
|
489
|
-
if (Object.hasOwn( cmd, tok ) &&
|
|
518
|
+
if (Object.hasOwn( cmd, tok ) && tok.charAt(0) !== ' ' &&
|
|
519
|
+
this._exp_collect( expecting, cmd, tok ))
|
|
490
520
|
reachedRuleEnd = true;
|
|
491
521
|
}
|
|
492
522
|
return reachedRuleEnd;
|
|
@@ -496,7 +526,7 @@ class BaseParser {
|
|
|
496
526
|
expecting[prop] = true;
|
|
497
527
|
return false;
|
|
498
528
|
case 'ckA':
|
|
499
|
-
for (const tok of this.translateParserToken_( prop )
|
|
529
|
+
for (const tok of this.translateParserToken_( prop ))
|
|
500
530
|
expecting[tok] = true;
|
|
501
531
|
return false;
|
|
502
532
|
case 'm': case 'mk':
|
|
@@ -524,24 +554,23 @@ class BaseParser {
|
|
|
524
554
|
}
|
|
525
555
|
}
|
|
526
556
|
|
|
527
|
-
translateParserToken_(
|
|
528
|
-
return
|
|
557
|
+
translateParserToken_( token ) {
|
|
558
|
+
return [ token ];
|
|
529
559
|
}
|
|
530
560
|
|
|
531
561
|
// Error recovery -------------------------------------------------------------
|
|
532
562
|
|
|
533
563
|
_recoverInline( expecting ) {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (!expecting[this.lk()] && !expecting[this.l()])
|
|
564
|
+
const { type: lt2, keyword: lk2 } = this.tokens[this.tokenIdx + 1];
|
|
565
|
+
if (!(lk2 && expecting[lk2] || expecting[lt2]))
|
|
537
566
|
return false;
|
|
538
567
|
|
|
539
568
|
// Immediately exit rules (except start) when no tokens have yet been consumed:
|
|
540
569
|
let { length } = this.stack;
|
|
541
570
|
while (--length > 0) {
|
|
542
571
|
const caller = this.stack[length];
|
|
543
|
-
// matched tokens
|
|
544
|
-
if (this.tokenIdx
|
|
572
|
+
// matched tokens in rule: found rule
|
|
573
|
+
if (this.tokenIdx > caller.tokenIdx)
|
|
545
574
|
break;
|
|
546
575
|
caller.followState = null;
|
|
547
576
|
}
|
|
@@ -557,13 +586,13 @@ class BaseParser {
|
|
|
557
586
|
this.s = this.errorState;
|
|
558
587
|
}
|
|
559
588
|
|
|
589
|
+
this.skipToken_();
|
|
560
590
|
if (this.constructor.tracingParser)
|
|
561
591
|
this._trace( [ this.stack[length - 1].ruleState, 'recover inside rule' ] );
|
|
562
592
|
return true; // to be re-checked with actions
|
|
563
593
|
}
|
|
564
594
|
|
|
565
595
|
_recoverPanicMode() {
|
|
566
|
-
--this.tokenIdx
|
|
567
596
|
const { length } = this.stack;
|
|
568
597
|
// Panic mode: resume at token in then-expecting set:
|
|
569
598
|
const followSets = { EOF: 0 };
|
|
@@ -584,7 +613,7 @@ class BaseParser {
|
|
|
584
613
|
// TODO: handle Id here
|
|
585
614
|
if (depth != null)
|
|
586
615
|
return this._error_panic( depth, length, tokenIdx );
|
|
587
|
-
|
|
616
|
+
this.skipToken_();
|
|
588
617
|
}
|
|
589
618
|
throw Error( 'EOF was added...' );
|
|
590
619
|
}
|
|
@@ -614,11 +643,12 @@ class BaseParser {
|
|
|
614
643
|
}
|
|
615
644
|
|
|
616
645
|
_stopParsing( idx ) {
|
|
617
|
-
--this.tokenIdx;
|
|
618
646
|
if (this.constructor.tracingParser) {
|
|
619
647
|
this.log( this.la().location.toString() + ':', 'Info:',
|
|
620
648
|
`leave all active ${ idx } rules prematurely, stop parsing` );
|
|
621
649
|
}
|
|
650
|
+
// TODO: run this.skipToken_() on all remaining tokens? Does ANTLR consumes
|
|
651
|
+
// those in error recovery mode? Probably not.
|
|
622
652
|
for (const c of this.stack)
|
|
623
653
|
c.followState = null;
|
|
624
654
|
this.errorState = null;
|
|
@@ -663,9 +693,9 @@ class BaseParser {
|
|
|
663
693
|
if (this.constructor.tracingParser)
|
|
664
694
|
this.trace.push( state ?? '⚠' );
|
|
665
695
|
}
|
|
666
|
-
|
|
696
|
+
_traceSubPush( state ) {
|
|
667
697
|
if (this.constructor.tracingParser)
|
|
668
|
-
this.trace
|
|
698
|
+
this.trace.at(-1).push( state );
|
|
669
699
|
}
|
|
670
700
|
traceAction( location ) { // will be put into tracing parser
|
|
671
701
|
this._trace( 'execute action,', { location } );
|
|
@@ -678,7 +708,7 @@ class BaseParser {
|
|
|
678
708
|
msg = this._rule( ...msg );
|
|
679
709
|
this.trace.push( this.s ?? '⚠' );
|
|
680
710
|
this.log( (la || this.la()).location.toString() + ':',
|
|
681
|
-
'Info:', msg, 'states:', this.trace.join( ' → ' ) );
|
|
711
|
+
'Info:', msg, 'states:', this.trace.map( traceStep ).join( ' → ' ) );
|
|
682
712
|
this.trace = [ this.s ?? '⚠' ];
|
|
683
713
|
}
|
|
684
714
|
|
|
@@ -704,17 +734,45 @@ class BaseParser {
|
|
|
704
734
|
|
|
705
735
|
}
|
|
706
736
|
|
|
737
|
+
function traceStep( step ) {
|
|
738
|
+
if (!Array.isArray( step ))
|
|
739
|
+
return step;
|
|
740
|
+
const result = { true: '✔', false: '✖' }[step.at( -1 )] ?? '';
|
|
741
|
+
const intro = (typeof step[1] === 'number') ? '→' : '';
|
|
742
|
+
const arg = step.slice( 1, result ? -1 : undefined ).join( '→' );
|
|
743
|
+
return `${ step[0] }(${ intro }${ arg })${ result }`;
|
|
744
|
+
}
|
|
745
|
+
|
|
707
746
|
function tokenName( type ) {
|
|
708
747
|
if (typeof type !== 'string')
|
|
709
|
-
type = (!type.
|
|
748
|
+
type = (!type.parsedAs || type.parsedAs === 'keyword') && type.keyword || type.type;
|
|
710
749
|
return (/^[A-Z]+/.test( type )) ? `‹${ type }›` : `‘${ type }’`;
|
|
711
750
|
}
|
|
712
751
|
|
|
713
752
|
function tokenFullName( token, sep ) {
|
|
714
|
-
return (token.
|
|
753
|
+
return (token.parsedAs && token.parsedAs !== 'keyword' && token.parsedAs !== 'token' ||
|
|
715
754
|
token.type !== 'Id' && token.type !== token.text && token.text)
|
|
716
755
|
? `‘${ token.text }’${ sep }${ tokenName( token ) }`
|
|
717
756
|
: tokenName( token );
|
|
718
757
|
}
|
|
719
758
|
|
|
759
|
+
function compileTable( table ) {
|
|
760
|
+
if (table.$compiled)
|
|
761
|
+
return table;
|
|
762
|
+
for (const line of table) {
|
|
763
|
+
if (typeof line !== 'object' || Array.isArray( line ))
|
|
764
|
+
continue;
|
|
765
|
+
const cache = Object.create( null ); // very sparse array
|
|
766
|
+
for (const prop of Object.keys( line )) {
|
|
767
|
+
const alt = line[prop];
|
|
768
|
+
if (!Array.isArray( alt ) && prop.charAt(0) !== ' ') // string or number
|
|
769
|
+
line[prop] = (typeof alt === 'string') ? line[alt] : (cache[alt] ??= [ 'g', alt ]);
|
|
770
|
+
}
|
|
771
|
+
if (!line[''])
|
|
772
|
+
line[''] = [ 'e' ];
|
|
773
|
+
}
|
|
774
|
+
table.$compiled = true;
|
|
775
|
+
return table;
|
|
776
|
+
}
|
|
777
|
+
|
|
720
778
|
module.exports = BaseParser;
|