@sap/cds-compiler 6.3.6 → 6.4.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 (55) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +32 -0
  3. package/README.md +14 -2
  4. package/bin/cdsse.js +0 -3
  5. package/doc/CHANGELOG_BETA.md +1 -1
  6. package/doc/CHANGELOG_DEPRECATED.md +1 -1
  7. package/lib/base/message-registry.js +7 -0
  8. package/lib/base/messages.js +1 -1
  9. package/lib/base/model.js +2 -0
  10. package/lib/compiler/assert-consistency.js +1 -0
  11. package/lib/compiler/checks.js +37 -26
  12. package/lib/compiler/define.js +1 -1
  13. package/lib/compiler/extend.js +39 -50
  14. package/lib/compiler/finalize-parse-cdl.js +1 -1
  15. package/lib/compiler/lsp-api.js +1 -1
  16. package/lib/compiler/populate.js +2 -2
  17. package/lib/compiler/propagator.js +29 -6
  18. package/lib/compiler/resolve.js +13 -3
  19. package/lib/compiler/shared.js +31 -25
  20. package/lib/compiler/tweak-assocs.js +86 -28
  21. package/lib/compiler/xpr-rewrite.js +70 -38
  22. package/lib/edm/annotations/edmJson.js +206 -37
  23. package/lib/edm/csn2edm.js +13 -0
  24. package/lib/edm/edmUtils.js +2 -2
  25. package/lib/gen/BaseParser.js +106 -72
  26. package/lib/gen/CdlGrammar.checksum +1 -1
  27. package/lib/gen/CdlParser.js +1500 -1509
  28. package/lib/json/to-csn.js +8 -5
  29. package/lib/language/genericAntlrParser.js +0 -0
  30. package/lib/main.js +19 -16
  31. package/lib/model/csnRefs.js +589 -521
  32. package/lib/model/csnUtils.js +8 -5
  33. package/lib/model/enrichCsn.js +1 -0
  34. package/lib/parsers/AstBuildingParser.js +72 -27
  35. package/lib/render/toCdl.js +2 -1
  36. package/lib/render/toHdbcds.js +6 -3
  37. package/lib/render/toSql.js +5 -0
  38. package/lib/transform/db/applyTransformations.js +1 -1
  39. package/lib/transform/db/assertUnique.js +4 -1
  40. package/lib/transform/db/cdsPersistence.js +17 -18
  41. package/lib/transform/db/expansion.js +179 -3
  42. package/lib/transform/db/flattening.js +16 -5
  43. package/lib/transform/db/rewriteCalculatedElements.js +79 -283
  44. package/lib/transform/effective/main.js +8 -1
  45. package/lib/transform/forOdata.js +1 -1
  46. package/lib/transform/forRelationalDB.js +21 -80
  47. package/lib/transform/localized.js +65 -110
  48. package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +89 -63
  49. package/lib/transform/transformUtils.js +23 -21
  50. package/lib/transform/translateAssocsToJoins.js +7 -5
  51. package/lib/transform/tupleExpansion.js +16 -3
  52. package/package.json +1 -1
  53. package/doc/DeprecatedOptions_v2.md +0 -150
  54. package/doc/NameResolution.md +0 -837
  55. package/lib/transform/parseExpr.js +0 -415
@@ -1,4 +1,4 @@
1
- // Base class for generated parser, for redepage v0.3.0
1
+ // Base class for generated parser, for redepage v0.3.1
2
2
 
3
3
  'use strict';
4
4
 
@@ -18,9 +18,10 @@ class BaseParser {
18
18
 
19
19
  tokens = undefined;
20
20
  tokenIdx = 0;
21
- recoverTokenIdx = -1;
22
- conditionTokenIdx = -1; // TODO: can we use recoverTokenIdx ?
23
- errorTokenIdx = -1;
21
+ conditionTokenIdx = -1;
22
+ errorTokenIdx = -1; // token index where error is detected
23
+ recoverTokenIdx = -1; // token index where error recovery resumes
24
+ reuseErrorTokenIdx = null; // tmp special for error recovery “token-reuse”
24
25
  fixKeywordTokenIdx = -1;
25
26
  conditionStackLength = -1;
26
27
  nextTokenAsId = false;
@@ -46,6 +47,17 @@ class BaseParser {
46
47
  return this;
47
48
  }
48
49
 
50
+ _runTransparently( callback ) {
51
+ const { tokenIdx } = this;
52
+ const saved = this._saveForWalk();
53
+ const { length } = this.stack;
54
+ const r = callback();
55
+ this.stack.length = length;
56
+ Object.assign( this, saved );
57
+ this.tokenIdx = tokenIdx;
58
+ return r;
59
+ }
60
+
49
61
  _saveForWalk() {
50
62
  return {
51
63
  s: this.s,
@@ -108,8 +120,9 @@ class BaseParser {
108
120
 
109
121
  e() { // error: report and recover
110
122
  const la = this.la();
111
- this._trace( 'detect parsing error' );
112
- if (this.errorTokenIdx === this.tokenIdx)
123
+ this._trace( 'detect parsing error' ); // TODO: write reason (la, no tokens matched, …)
124
+ if (this.errorTokenIdx === this.tokenIdx &&
125
+ (this.reuseErrorTokenIdx == null && this.reuseErrorTokenIdx < this.tokenIdx))
113
126
  throw Error( `Already reported error for ${ tokenFullName( la ) } at ${ la.location }`);
114
127
 
115
128
  la.parsedAs = ''; // current token is erroneous
@@ -120,23 +133,13 @@ class BaseParser {
120
133
  while (--length && this.tokenIdx === this.stack[length].tokenIdx)
121
134
  this.stack[length].followState = null;
122
135
  if (++length === this.stack.length) // last good state in current rule
123
- return this._reportAndRecover();
136
+ return this._reportAndRecover(); // otherwise report+recovery after unwind in exit_()
124
137
 
125
138
  this.stack[length].followState = this.errorState;
126
139
  this.s = null;
127
140
  return false;
128
141
  }
129
142
 
130
- _reportAndRecover() {
131
- this.s = this.errorState;
132
- this.reportUnexpectedToken_();
133
- const syncSet = this._calculateTokenSet( 'Y' );
134
- const recoverDepth = this._findSyncToken( syncSet );
135
- this._trace( 'recover from error' );
136
- this._recoverFromError( recoverDepth );
137
- return false;
138
- }
139
-
140
143
  // instead of e() in default if lk() had been used and 'Id' is in a non-default case
141
144
  ei() { // error (after trying to test again as identifier)
142
145
  if (!this.la().keyword) // lk() had directly returned the type
@@ -150,6 +153,7 @@ class BaseParser {
150
153
  // go to end of the rule, in tracing parser: g(0)
151
154
  gr( follow ) { // intersection follow set for fast exit
152
155
  if (this.stack[this.stack.length - 1].tokenIdx === this.tokenIdx)
156
+ //this.tokenIdx >= this.errorTokenIdx) not necessary
153
157
  return this.e(); // match at least one token
154
158
  this.s = 0;
155
159
  // TODO: also have recursive flag in stack: was rule was called recursively?
@@ -249,9 +253,19 @@ class BaseParser {
249
253
  const la = this.tokens[this.tokenIdx];
250
254
  // TODO: consider this like a failed condition? Will be relevant if we try
251
255
  // different error recovery possibilities.
252
- if (this.keywords[la.keyword])
256
+ if (this.keywords[la.keyword]) {
257
+ this._tracePush( [ 'R' ] );
258
+ if (this._runTransparently( () => {
259
+ ++this.tokenIdx;
260
+ this.s = state;
261
+ const { type, keyword } = this.la();
262
+ return !(this._pred_next( type, keyword, 'R' ) ??
263
+ this._matchesInFollow( type, keyword, 'R' ));
264
+ } ))
265
+ return this.e();
253
266
  this.reportReservedWord_();
254
- // with error recovery: use that (consider this having a good score)
267
+ // with error recovery: use that (consider this having a good score)
268
+ }
255
269
  return this.c( state, ident )
256
270
  }
257
271
 
@@ -280,6 +294,10 @@ class BaseParser {
280
294
  ++this.tokenIdx;
281
295
  }
282
296
 
297
+ reuseToken_() {
298
+ --this.tokenIdx;
299
+ }
300
+
283
301
  // condition and precedence handling ------------------------------------------
284
302
 
285
303
  // state must match the goto-state of the default (there must be no default
@@ -340,13 +358,14 @@ class BaseParser {
340
358
  --this.trace.at(-1).length; // do not show guard name in trace
341
359
  if (!keyword || this.keywords[keyword] == null)
342
360
  return false; // ok
343
- const r = this._matchesInFollow( 'Id', keyword, 'R' );
361
+ const r = this._matchesInFollow( 'Id', keyword, 'F' );
344
362
  --this.trace.at(-1).length; // this.gc() also traces result
345
363
  return r;
346
364
  }
347
365
  if (!keyword || this.keywords[keyword] == null)
348
366
  return false; // ok
349
- return this._matchesInFollow( 'Id', keyword, 'R' );
367
+ return this._matchesInFollow( 'Id', keyword, 'F' );
368
+ // TODO: still extra tests for 'F' in _pred_next()?
350
369
  }
351
370
 
352
371
  // rule start, end and call: --------------------------------------------------
@@ -370,32 +389,28 @@ class BaseParser {
370
389
  throw Error( `this.s === ${ this.s } // illegally set by action, or runtime/generator bug` );
371
390
  this.dynamic_ = Object.getPrototypeOf( this.dynamic_ );
372
391
  const caller = this.stack.pop();
373
- const immediately = this.tokenIdx === caller.tokenIdx;
392
+ const immediately = this.tokenIdx === caller.tokenIdx && this.tokenIdx >= this.errorTokenIdx;
374
393
  if (this.constructor.tracingParser) {
375
394
  const post = this.s == null &&
376
395
  (immediately
377
- ? ' immediately'
396
+ ? ' immediately' // no token matched (further unwind or to last good state)
378
397
  : caller.followState == null
379
- ? ' unsuccessfully'
380
- : ' prematurely');
398
+ ? ' unsuccessfully' // further error rewind
399
+ : ' prematurely'); // continue at sync state after exit
381
400
  const text = immediately ? '⚠ exit rule' : '⏎ exit rule';
382
401
  this.s = caller.followState; // for trace
383
402
  this._trace( [ text, caller.ruleState, post, 'back to' ] )
384
403
  if (immediately && this.stack.at(-1)?.followState != null)
385
404
  this.trace = [ this.errorState ]; // show last good state in trace
386
405
  }
406
+ this.prec_ = caller.prec;
387
407
  this.s = caller.followState;
388
- if (immediately)
408
+ if (immediately) // this.gr() has already called this.e() if no token is matched
389
409
  return this.s != null && this._reportAndRecover();
390
410
 
391
- this.prec_ = caller.prec;
392
- if (this.s)
393
- this._skipErrorTokens(); // TODO: re-think - directly with _reportAndRecover() ?
394
- else if (this.s == null)
395
- return true; // attached actions are executed even with "unsuccessful exit"
396
-
397
- this.errorState = this.s;
398
- return true;
411
+ if (this.s != null)
412
+ this.errorState = this.s; // last good state is now after rule call
413
+ return true; // attached actions are executed even with "unsuccessful exit"
399
414
  }
400
415
 
401
416
  // predicate used before rule call (and called by `ckP` and `gP`) on keyword
@@ -438,8 +453,11 @@ class BaseParser {
438
453
  // TODO: also not with lean condition
439
454
  let match1 = this._pred_next( 'Id', lk1, 'P' ); // first step of `K`/`I` prediction
440
455
  if (!match1) {
441
- if (lk1 || match1 === false) // assert for correct code generation
442
- throw Error( `Cannot match first prediction token in rule at state ${ saved.s }` );
456
+ if (lk1) { // assert for correct code generation
457
+ // Remark: this._pred_next() returns false also if rule has not matched any token
458
+ const { location } = this.la();
459
+ throw Error( `Cannot match first prediction token at ${ location.line }:${ location.col } in rule at state ${ saved.s }` );
460
+ }
443
461
  if (match1 == null) { // TODO: just return true, rule exit prediction will do it
444
462
  this._traceSubPush( 0 ); // TODO: make _pred_next push this
445
463
  match1 = this._matchesInFollow( 'Id', lk1, 'I' );
@@ -513,25 +531,25 @@ class BaseParser {
513
531
  case 'c': case 'ck': case 'ckA': // TODO: re-check ckA
514
532
  return true;
515
533
  case 'ciA': // TODO: fixKeywordTokenIdx ?
516
- return mode !== 'R';
534
+ return mode !== 'F';
517
535
  // in the R prediction for optional `Id<weak>` at rule end, only
518
536
  // alternative keyword matches are preferred, not identifier matches
519
537
  // TODO: delete this prediction
520
538
  case 'ci':
521
539
  if (!keyword ||
522
540
  !this.keywords[keyword] && this.fixKeywordTokenIdx !== this.tokenIdx)
523
- return mode !== 'R';
541
+ return mode !== 'F';
524
542
  cmd = this.table[state]['']; // is currently always 'g' or 'e'
525
543
  this.s = cmd[1];
526
544
  break;
527
545
  case 'm':
528
546
  return type === cmd[2];
529
547
  case 'mi':
530
- return type === 'Id' && mode !== 'R' &&
548
+ return type === 'Id' && mode !== 'F' &&
531
549
  (!keyword ||
532
550
  !this.keywords[keyword] && this.fixKeywordTokenIdx !== this.tokenIdx);
533
551
  case 'miA':
534
- return type === 'Id' && mode !== 'R';
552
+ return type === 'Id' && mode !== 'F';
535
553
  case 'mk':
536
554
  return keyword === cmd[2];
537
555
  case 'g': case 'e':
@@ -573,7 +591,7 @@ class BaseParser {
573
591
  // prediction, the tool can normally omit the prediction (and output a
574
592
  // message), no so with `ruleStartingWithUnreserved`. We will rather look
575
593
  // behind the current rule _after_ having decided that the token is to be
576
- // matched as identifier.
594
+ // matched as identifier.
577
595
  return (hasMatchedToken ?? this.tokenIdx > this.stack.at( -1 ).tokenIdx)
578
596
  && null; // let caller decide how to interpret this
579
597
  }
@@ -609,7 +627,7 @@ class BaseParser {
609
627
  this.s = this.stack[depth].followState;
610
628
  // TODO: this.prec_ ?
611
629
  match = this._pred_next( type, keyword, mode );
612
- this._traceSubPush( match == null ? 0 : match === (mode !== 'R') );
630
+ this._traceSubPush( match == null ? 0 : match === (mode !== 'F') );
613
631
  // successfully matching a keyword in giR() means unsuccessful match as
614
632
  // reserved identifer
615
633
  // TODO: this.stack ?
@@ -667,13 +685,13 @@ class BaseParser {
667
685
  else { // or always when calculating the sync-set
668
686
  let val = this.stack.length + 1;
669
687
  while (this.stack.length) {
670
- if (!this._tokenSetInRule( set, val ))
671
- val = this.stack.length;
688
+ this._tokenSetInRule( set, val ); // TODO: use if Y-M unification
689
+ val = this.stack.length;
672
690
  // TODO: use new _tracePush if `val` changes, probably also use Y‹val›(…)
673
691
  this.dynamic_ = Object.getPrototypeOf( this.dynamic_ );
674
692
  this.s = this.stack.pop().followState;
675
693
  }
676
- set.EOF ??= 0;
694
+ set.EOF ??= 0; // TODO: really necessary, see also _findSyncToken()
677
695
  }
678
696
  this.stack = savedStack;
679
697
  this.s = savedState; // should be the errorState anyway - TODO: confirm
@@ -706,7 +724,7 @@ class BaseParser {
706
724
  collectKeywordsAndIdOnly = true;
707
725
  cmd = dict.Id;
708
726
  }
709
- else { // Id branch never leads to rule exit:
727
+ else { // Id branch never leads to rule exit (really?):
710
728
  this._tracePush( [ '[' ] );
711
729
  this._tokenSetInRule( expecting, val, dict.Id, true );
712
730
  this._tracePush( [ ']' ] );
@@ -722,7 +740,7 @@ class BaseParser {
722
740
  this.addTokenToSet_( expecting, 'Id', val, false );
723
741
  // TODO: should we do s/th special, such that a reserved word is a sync
724
742
  // token for Id<greedy>? Probably not, see also comment in
725
- // _findSyncToken()
743
+ // _findSyncToken()
726
744
  break loop;
727
745
  case 'g': case 'gi': case 'e':
728
746
  break;
@@ -781,62 +799,77 @@ class BaseParser {
781
799
  return expecting;
782
800
  }
783
801
 
784
- _findSyncToken( syncSet ) {
802
+ _findSyncToken( syncSet ) { // only called from _reportAndRecover()
785
803
  const rewindDepth = this.stack.length
786
- this.recoverTokenIdx = this.tokenIdx;
804
+ this.recoverTokenIdx = this.tokenIdx; // TODO: make it part of the return value?
787
805
  while (this.recoverTokenIdx < this.tokens.length) {
788
806
  const { keyword, type } = this.tokens[this.recoverTokenIdx];
789
807
  let recoverDepth = keyword ? syncSet[keyword] : null;
790
808
  if (recoverDepth != null)
791
809
  return recoverDepth;
792
810
  recoverDepth = syncSet[type];
793
- // sync to Id only if in expected set of last good state or if after ';'
811
+ // sync to Id only if in intra-rule expected set of last good state or if after ';'/`}`
794
812
  if (recoverDepth != null &&
795
813
  (type !== 'Id' || (!keyword || !this.keywords[keyword]) &&
796
- // reserved words do not match Id in expected-set
797
- (recoverDepth > rewindDepth || this.tokens[this.recoverTokenIdx - 1].type === ';')))
814
+ // reserved words do not match Id in expected-set → as method
815
+ (recoverDepth > rewindDepth ||
816
+ [ ';', '}' ].includes( this.tokens[this.recoverTokenIdx - 1].type ))))
798
817
  // if (recoverDepth != null &&
799
818
  // (this.recoverTokenIdx > this.tokenIdx ||
800
819
  return recoverDepth;
801
820
  ++this.recoverTokenIdx;
802
821
  }
803
- throw Error( 'EOF must be last in `tokens`' );
822
+ throw Error( 'EOF must be last in `tokens`' ); // TODO: really necessary?
804
823
  }
805
824
 
806
- _recoverFromError( recoverDepth ) {
825
+ _reportAndRecover() { // called from this.e() and this.exit_() after rewind
826
+ this.s = this.errorState;
827
+ const syncSet = this._calculateTokenSet( 'Y' );
828
+ // TODO: use (extended) syncSet also for “expecting” in messages
829
+ this._trace( 'investigate how to recover' );
830
+ this.recoverTokenIdx = this.tokenIdx;;
831
+ const prev = this.lb();
832
+ const reuseRecoverDepth = this.reuseErrorTokenIdx != null && // only if specially switched on
833
+ prev?.keyword && prev.parsedAs !== 'keyword' &&
834
+ this.reuseErrorTokenIdx < this.tokenIdx && syncSet[prev.keyword];
835
+ // TODO: this reuse-token will only properly work if we can check that the token at
836
+ // this.la() will definitely match after having re-used this.lb()
837
+ // → if we have done that, we can again remove this.reuseErrorTokenIdx
838
+ this.reportUnexpectedToken_( reuseRecoverDepth ? 'reuse' : null );
839
+
840
+ if (this.reuseErrorTokenIdx != null)
841
+ this.reuseErrorTokenIdx = (reuseRecoverDepth) ? this.tokenIdx : -1;
842
+ this.fixKeywordTokenIdx = (reuseRecoverDepth) ? --this.recoverTokenIdx : -1;
843
+
844
+ const recoverDepth = reuseRecoverDepth || this._findSyncToken( syncSet );
807
845
  this.s = null;
808
846
  let depth = this.stack.length;
809
847
  if (recoverDepth > depth) { // no rewind, no rule exit
810
848
  this.trace = [ this.errorState ]; // show last good state in trace
811
849
  this.s = this.errorState;
812
- if (this.s)
813
- this._skipErrorTokens();
814
850
  }
815
851
  while (depth > recoverDepth)
816
852
  this.stack[--depth].followState = null;
817
853
  // TODO: when the error is due to failed rule exit prediction, try to keep
818
854
  // existing followState (if that reaches RuleEnd_)
819
855
  // Continue parsing: ignore next predicate (TODO: except some specified ones?)
820
- this.conditionTokenIdx = this.tokenIdx;
821
- this.conditionStackLength = null;
822
- this.fixKeywordTokenIdx = -1; // was set when collecting expecting-set
823
856
 
824
857
  // TODO: re-check for rule calls which are at the optional rule end:
825
858
  // x: 'x not'; b: 'b'? x {console.log('x→b')} 'b'?; a: b {console.log('b→a')} 'a'
826
859
  // with start rule `a` and input `x a`: output should be x→b + b→a
827
860
  // with start rule `a` and input `b a`: output should be b→a
828
- //
829
- // → the rule is: if a rule can continue at the specified state and has
830
- // matched at least one token, then its action is executed, otherwise not
831
- }
832
861
 
833
- _skipErrorTokens() {
834
- if (this.constructor.tracingParser && this.tokenIdx <= this.recoverTokenIdx) {
835
- this._trace( `skip ${ this.recoverTokenIdx - this.tokenIdx } tokens to recover from error`,
862
+ if (this.constructor.tracingParser) {
863
+ this._trace( `skipped ${ this.recoverTokenIdx - this.tokenIdx } tokens to recover from error`,
836
864
  this.tokens[this.recoverTokenIdx] );
837
- }
838
- while (this.tokenIdx < this.recoverTokenIdx)
865
+ }
866
+ if (this.tokenIdx > this.recoverTokenIdx)
867
+ this.reuseToken_();
868
+ else while (this.tokenIdx < this.recoverTokenIdx)
839
869
  this.skipToken_();
870
+ this.conditionTokenIdx = this.tokenIdx;
871
+ this.conditionStackLength = null;
872
+ return false;
840
873
  }
841
874
 
842
875
  // small methods --------------------------------------------------------------
@@ -851,11 +884,12 @@ class BaseParser {
851
884
  }
852
885
 
853
886
  reportUnexpectedToken_( msg ) {
854
- const token = this.la();
887
+ const token = (msg === 'reuse') ? this.lb() : this.la();
855
888
  msg ??= `Unexpected token ${ tokenFullName( token, ': ' ) }`;
856
- this.reportError_(
857
- token.location, msg + ' - expecting: ' +
858
- this.expectingArray_().map( tokenName ).sort().join( ', ' ) );
889
+ msg = (msg === 'reuse')
890
+ ? `Missing input before keyword ${ tokenFullName( token, ': ' ) }`
891
+ : msg + ' - expecting: ' + this.expectingArray_().map( tokenName ).sort().join( ', ' );
892
+ this.reportError_( token.location, msg );
859
893
  }
860
894
 
861
895
  reportReservedWord_() {
@@ -1 +1 @@
1
- bffacbf5acb61179807cb657191c5114
1
+ 2f488eae5a0a809b97737edac6e2a42d