@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/bin/cdsc.js +5 -0
  3. package/bin/cdshi.js +8 -8
  4. package/doc/CHANGELOG_BETA.md +9 -4
  5. package/lib/api/validate.js +5 -0
  6. package/lib/base/message-registry.js +25 -1
  7. package/lib/base/messages.js +1 -1
  8. package/lib/base/model.js +0 -1
  9. package/lib/compiler/assert-consistency.js +2 -2
  10. package/lib/compiler/builtins.js +1 -1
  11. package/lib/compiler/checks.js +25 -6
  12. package/lib/compiler/define.js +24 -28
  13. package/lib/compiler/extend.js +11 -13
  14. package/lib/compiler/generate.js +3 -3
  15. package/lib/compiler/populate.js +13 -7
  16. package/lib/compiler/propagator.js +2 -2
  17. package/lib/compiler/resolve.js +58 -60
  18. package/lib/compiler/shared.js +5 -5
  19. package/lib/compiler/tweak-assocs.js +247 -34
  20. package/lib/compiler/utils.js +40 -32
  21. package/lib/compiler/xpr-rewrite.js +44 -58
  22. package/lib/edm/annotations/genericTranslation.js +4 -4
  23. package/lib/edm/csn2edm.js +2 -2
  24. package/lib/edm/edm.js +46 -21
  25. package/lib/edm/edmInboundChecks.js +0 -1
  26. package/lib/edm/edmPreprocessor.js +40 -27
  27. package/lib/edm/edmUtils.js +1 -1
  28. package/lib/gen/BaseParser.js +180 -122
  29. package/lib/gen/CdlParser.js +2226 -2170
  30. package/lib/gen/language.checksum +1 -1
  31. package/lib/gen/language.interp +1 -1
  32. package/lib/gen/languageParser.js +3820 -3777
  33. package/lib/inspect/inspectPropagation.js +1 -1
  34. package/lib/json/from-csn.js +5 -3
  35. package/lib/json/to-csn.js +7 -10
  36. package/lib/language/antlrParser.js +38 -4
  37. package/lib/language/errorStrategy.js +1 -1
  38. package/lib/language/genericAntlrParser.js +4 -4
  39. package/lib/language/multiLineStringParser.js +1 -1
  40. package/lib/main.d.ts +23 -0
  41. package/lib/model/cloneCsn.js +22 -13
  42. package/lib/optionProcessor.js +7 -7
  43. package/lib/parsers/AstBuildingParser.js +155 -37
  44. package/lib/parsers/CdlGrammar.g4 +154 -81
  45. package/lib/parsers/Lexer.js +20 -10
  46. package/lib/render/toCdl.js +23 -18
  47. package/lib/transform/addTenantFields.js +4 -4
  48. package/lib/transform/db/rewriteCalculatedElements.js +11 -5
  49. package/lib/transform/db/transformExists.js +43 -18
  50. package/lib/transform/effective/main.js +1 -1
  51. package/lib/transform/forRelationalDB.js +8 -7
  52. package/lib/utils/moduleResolve.js +1 -1
  53. package/package.json +1 -1
  54. package/share/messages/redirected-to-complex.md +6 -3
@@ -1,16 +1,17 @@
1
- // Base class for generated parser, for redepage v0.1.7
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.parsed = 0;
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
- if (++this.tokenIdx > this.eofIndex)
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.conditionTokenIdx !== this.tokenIdx ||
108
- follow?.includes( lk || lt ))
109
- return true;
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
- return this.e();
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, parsed = 'token' ) { // consume token
202
+ c( state, parsedAs = 'token' ) { // consume token
203
203
  const la = this.tokens[this.tokenIdx];
204
- la.parsed = parsed;
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().parsed` yourself
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( `${ fail ? '¬' : '✓' } ${ cond }` );
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( `(${ this._prec })!` );
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' && ` ≤ ${ tp }` || mode === 'none' && ` < ${ tp }`;
286
- this._tracePush( `${ fail ? '¬' : '✓' }(${ pp } < ${ prec }${ suffix || '' })` );
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 (typeof cmd === 'string')
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
- this._traceIdOrPred( '-P1' );
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
- if (this._pred( nextState, lt2, lk2 ))
354
- return true;
355
- if (lt2 === 'IllegalToken') // TODO: keep?
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, set tokenIndex+nextState for extra
358
- // expected calculation if parser fails after Id - we would then also add the
359
- // expected tokens after keyword-interpretation
360
- this._traceIdOrPred( '-Id' );
361
- this.nextTokenAsId = true;
362
- return false; // do not execute action after it
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 next state for first token as keyword, for lP()
397
+ // returns state after matching the first token as keyword, for lP()
388
398
  while (state) {
389
- this._tracePush( `${ state }-P1` );
399
+ this._traceSubPush( state );
390
400
  let cmd = this.table[state];
391
- if (!Array.isArray( cmd )) {
392
- const alt = cmd[keyword] || cmd.Id; // Id to cover optimized rule call
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._tracePush( `${ state }-P2` );
421
+ this._traceSubPush( state );
415
422
  let cmd = this.table[state];
416
423
  if (!Array.isArray( cmd )) {
417
- const alt = keyword && cmd[keyword] || cmd[type];
418
- cmd = (typeof alt === 'string')
419
- ? cmd[alt]
420
- : typeof alt === 'number' && [ 'g', alt ] || alt || [ 'default', cmd[''] ];
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': case 'ci':
428
- return type === 'Id' && (!keyword || this.keywords[keyword] !== false);
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
- this._traceIdOrPred( 'f' );
438
- this._tracePush( this.s );
439
- // TODO: really false, not true?
440
- // `false` means that la1 is not considered an unreserved keyword. This is
441
- // correct (consider `e: Association @Anno`), but probably not optimal for
442
- // error reporting (consider `e: Association +`). Improving that is more
443
- // costly, as we really need to consider rule exits → stack.
444
- return false;
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[(typeof cmd[prop] === 'string') ? cmd[prop] : prop];
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 ) && this._exp_collect( expecting, 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 ) || [ 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_( _token ) {
528
- return null;
557
+ translateParserToken_( token ) {
558
+ return [ token ];
529
559
  }
530
560
 
531
561
  // Error recovery -------------------------------------------------------------
532
562
 
533
563
  _recoverInline( expecting ) {
534
- // Inline error recovery - single token deletion (TODO later: also try more !)
535
- // token position has been advanced before calling this function
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 (other than the one skipped one) in rule: found rule
544
- if (this.tokenIdx - 1 > caller.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
- ++this.tokenIdx;
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
- _traceIdOrPred( suffix ) {
696
+ _traceSubPush( state ) {
667
697
  if (this.constructor.tracingParser)
668
- this.trace[this.trace.length - 1] += suffix;
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.parsed || type.parsed === 'keyword') && type.keyword || 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.parsed && token.parsed !== 'keyword' && token.parsed !== '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;