@sap/cds-compiler 6.1.0 → 6.2.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 (53) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/bin/cdsc.js +6 -2
  3. package/bin/cdsse.js +1 -1
  4. package/bin/cdsv2m.js +1 -1
  5. package/lib/api/main.js +29 -7
  6. package/lib/base/builtins.js +9 -0
  7. package/lib/base/keywords.js +1 -1
  8. package/lib/base/message-registry.js +5 -3
  9. package/lib/base/messages.js +2 -2
  10. package/lib/base/model.js +1 -0
  11. package/lib/base/optionProcessorHelper.js +7 -2
  12. package/lib/checks/featureFlags.js +4 -1
  13. package/lib/compiler/assert-consistency.js +3 -1
  14. package/lib/compiler/base.js +1 -1
  15. package/lib/compiler/builtins.js +1 -1
  16. package/lib/compiler/checks.js +38 -21
  17. package/lib/compiler/define.js +24 -5
  18. package/lib/compiler/extend.js +1 -1
  19. package/lib/compiler/finalize-parse-cdl.js +9 -1
  20. package/lib/compiler/generate.js +4 -4
  21. package/lib/compiler/lsp-api.js +2 -0
  22. package/lib/compiler/populate.js +8 -8
  23. package/lib/compiler/propagator.js +1 -1
  24. package/lib/compiler/resolve.js +15 -14
  25. package/lib/compiler/shared.js +6 -6
  26. package/lib/compiler/tweak-assocs.js +6 -6
  27. package/lib/compiler/utils.js +9 -16
  28. package/lib/compiler/xpr-rewrite.js +2 -2
  29. package/lib/gen/BaseParser.js +35 -29
  30. package/lib/gen/CdlGrammar.checksum +1 -1
  31. package/lib/gen/CdlParser.js +1423 -1432
  32. package/lib/gen/Dictionary.json +1 -0
  33. package/lib/gen/cdlKeywords.json +26 -0
  34. package/lib/inspect/inspectPropagation.js +1 -1
  35. package/lib/json/from-csn.js +2 -2
  36. package/lib/json/to-csn.js +1 -1
  37. package/lib/language/multiLineStringParser.js +1 -1
  38. package/lib/model/cloneCsn.js +1 -0
  39. package/lib/optionProcessor.js +8 -7
  40. package/lib/parsers/AstBuildingParser.js +24 -21
  41. package/lib/parsers/identifiers.js +2 -30
  42. package/lib/render/toCdl.js +63 -9
  43. package/lib/render/toSql.js +127 -108
  44. package/lib/render/utils/sql.js +67 -0
  45. package/lib/transform/addTenantFields.js +4 -4
  46. package/lib/transform/db/killAnnotations.js +1 -0
  47. package/lib/transform/db/processSqlServices.js +20 -2
  48. package/lib/transform/forOdata.js +91 -2
  49. package/lib/transform/forRelationalDB.js +1 -1
  50. package/lib/transform/odata/flattening.js +1 -1
  51. package/lib/transform/translateAssocsToJoins.js +2 -26
  52. package/lib/utils/moduleResolve.js +1 -1
  53. package/package.json +2 -2
@@ -813,7 +813,7 @@ function populate( model ) {
813
813
  // invent a name for code completion in expression, see also #10596
814
814
  col.name = {
815
815
  id: '',
816
- location: col.value && col.value.location || col.location,
816
+ location: col.value?.location || col.location,
817
817
  $inferred: 'none',
818
818
  };
819
819
  return '';
@@ -979,14 +979,14 @@ function populate( model ) {
979
979
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
980
980
  info( 'wildcard-excluding-many', [ sibling.name.location, query ],
981
981
  { id, keyword: 'excluding' },
982
- // eslint-disable-next-line @stylistic/js/max-len
982
+ // eslint-disable-next-line @stylistic/max-len
983
983
  'This select item replaces $(ID) from two or more sources. Add $(ID) to $(KEYWORD) to silence this message' );
984
984
  }
985
985
  else {
986
986
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
987
987
  info( 'wildcard-excluding-one', [ sibling.name.location, query ],
988
988
  { id, alias: navElem._parent.name.id, keyword: 'excluding' },
989
- // eslint-disable-next-line @stylistic/js/max-len
989
+ // eslint-disable-next-line @stylistic/max-len
990
990
  'This select item replaces $(ID) from table alias $(ALIAS). Add $(ID) to $(KEYWORD) to silence this message' );
991
991
  }
992
992
  }
@@ -1112,9 +1112,9 @@ function populate( model ) {
1112
1112
  // art: definitionScope( target ), - TODO extra debug info in message
1113
1113
  sorted_arts: exposed,
1114
1114
  }, {
1115
- // eslint-disable-next-line @stylistic/js/max-len
1115
+ // eslint-disable-next-line @stylistic/max-len
1116
1116
  std: 'Replace target $(TARGET) by one of $(SORTED_ARTS); can\'t auto-redirect this association if multiple projections exist in this service',
1117
- // eslint-disable-next-line @stylistic/js/max-len
1117
+ // eslint-disable-next-line @stylistic/max-len
1118
1118
  two: 'Replace target $(TARGET) by $(SORTED_ARTS) or $(SECOND); can\'t auto-redirect this association if multiple projections exist in this service',
1119
1119
  } );
1120
1120
  // continuation semantics: no auto-redirection
@@ -1136,11 +1136,11 @@ function populate( model ) {
1136
1136
  anno: 'cds.redirection.target',
1137
1137
  sorted_arts: exposed,
1138
1138
  }, {
1139
- // eslint-disable-next-line @stylistic/js/max-len
1139
+ // eslint-disable-next-line @stylistic/max-len
1140
1140
  std: 'Add $(ANNO) to one of $(SORTED_ARTS) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
1141
- // eslint-disable-next-line @stylistic/js/max-len
1141
+ // eslint-disable-next-line @stylistic/max-len
1142
1142
  two: 'Add $(ANNO) to either $(SORTED_ARTS) or $(SECOND) to select the entity as redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
1143
- // eslint-disable-next-line @stylistic/js/max-len
1143
+ // eslint-disable-next-line @stylistic/max-len
1144
1144
  justOne: 'Remove $(ANNO) from all but one of $(SORTED_ARTS) to have a unique redirection target for $(TARGET) in this service; can\'t auto-redirect $(ART) otherwise',
1145
1145
  } );
1146
1146
  }
@@ -354,7 +354,7 @@ function propagate( model ) {
354
354
  const art = item && item._artifact;
355
355
  if (art?.virtual?.val) {
356
356
  message( 'def-missing-virtual', [ item.location, elem ], { art, keyword: 'virtual' },
357
- // eslint-disable-next-line @stylistic/js/max-len
357
+ // eslint-disable-next-line @stylistic/max-len
358
358
  'Prepend $(KEYWORD) to current select item - containing element $(ART) is virtual' );
359
359
  return;
360
360
  }
@@ -326,7 +326,7 @@ function resolve( model ) {
326
326
  propagateKeys = false;
327
327
  info( 'query-from-many', [ toMany.location, query ], { art: toMany }, {
328
328
  std: 'Key properties are not propagated because a to-many association $(ART) is selected',
329
- // eslint-disable-next-line @stylistic/js/max-len
329
+ // eslint-disable-next-line @stylistic/max-len
330
330
  element: 'Key properties are not propagated because a to-many association $(MEMBER) of $(ART) is selected',
331
331
  } );
332
332
  }
@@ -367,9 +367,9 @@ function resolve( model ) {
367
367
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
368
368
  info( 'query-navigate-many', [ art.location, user || query ], { art }, {
369
369
  std: 'Navigating along to-many association $(ART) - key properties are not propagated',
370
- // eslint-disable-next-line @stylistic/js/max-len
370
+ // eslint-disable-next-line @stylistic/max-len
371
371
  element: 'Navigating along to-many association $(MEMBER) of $(ART) - key properties are not propagated',
372
- // eslint-disable-next-line @stylistic/js/max-len
372
+ // eslint-disable-next-line @stylistic/max-len
373
373
  alias: 'Navigating along to-many mixin association $(MEMBER) - key properties are not propagated',
374
374
  } );
375
375
  }
@@ -490,9 +490,9 @@ function resolve( model ) {
490
490
  resolveTypeExpr( obj, art );
491
491
  // typeOf unmanaged assoc? TODO: is this the right place to check this?
492
492
  // (probably better in rewriteAssociations)
493
- const elemtype = obj.type._artifact;
494
- if (elemtype && effectiveType( elemtype )) {
495
- const assocType = getAssocSpec( elemtype ) || {};
493
+ const elemType = obj.type._artifact;
494
+ if (elemType && effectiveType( elemType )) {
495
+ const assocType = getAssocSpec( elemType ) || {};
496
496
  if ((assocType.on || assocType.$assocFilter) && !obj.on)
497
497
  obj.on = { $inferred: 'rewrite' }; // TODO: no extra rewrite here
498
498
  if (assocType.targetAspect) {
@@ -506,11 +506,10 @@ function resolve( model ) {
506
506
  }
507
507
 
508
508
  // Check if relational type is missing its target or if it's used directly.
509
- if (elemtype.category === 'relation' &&
510
- !obj.target && !obj.targetAspect) {
509
+ if (elemType.category === 'relation' && !obj.target && !obj.targetAspect) {
511
510
  const isCsn = (obj._block && obj._block.$frontend === 'json');
512
511
  error( 'type-missing-target', [ obj.type.location, obj ],
513
- { '#': isCsn ? 'csn' : 'std', type: elemtype }, {
512
+ { '#': isCsn ? 'csn' : 'std', type: elemType }, {
514
513
  // We don't say "use 'association to <target>" because the type could be used
515
514
  // in action parameters, etc. as well.
516
515
  std: 'The type $(TYPE) can\'t be used directly because it\'s compiler internal',
@@ -920,7 +919,7 @@ function resolve( model ) {
920
919
  } );
921
920
  }
922
921
  else {
923
- const noTruthyAllowed = [ 'localized', 'key', 'virtual' ];
922
+ const noTruthyAllowed = [ 'key', 'virtual' ];
924
923
  for (const prop of noTruthyAllowed) {
925
924
  if (art[prop]?.val) {
926
925
  // probably better than a parse error (which is good for DEFAULT vs calc),
@@ -1274,7 +1273,7 @@ function resolve( model ) {
1274
1273
  const text = (item !== last) ? 'sub' : 'std';
1275
1274
  error( 'duplicate-key-ref', [ item.location, key ], { '#': text, name }, {
1276
1275
  std: 'Foreign key $(NAME) already refers to the same target element',
1277
- // eslint-disable-next-line @stylistic/js/max-len
1276
+ // eslint-disable-next-line @stylistic/max-len
1278
1277
  sub: 'Foreign key $(NAME) already refers to the target element whose sub element is again referred to here',
1279
1278
  // TODO: please add ideas for a better text, e.g. to (closed) PR #11325
1280
1279
  } );
@@ -1594,10 +1593,12 @@ function resolve( model ) {
1594
1593
  const art = type && (type.kind === 'entity' ? type : type.target?._artifact);
1595
1594
  if (!art)
1596
1595
  return ref; // error already reported via resolvePathItem()
1597
- const unexpectedFilter = expected !== 'column' && expected !== 'calc' && 'std' ||
1598
- isQuasiVirtualAssociation( type ) && 'model-only';
1599
- if (last.args || last.where || last.cardinality)
1596
+ if (last.args || last.where || last.cardinality) {
1597
+ const unexpectedFilter = (expected !== 'annotation' && expected !== 'column' &&
1598
+ expected !== 'calc' && 'std') ||
1599
+ isQuasiVirtualAssociation( type ) && 'model-only';
1600
1600
  reportUnexpectedArgsAndFilter( last, expected, user, art, unexpectedFilter );
1601
+ }
1601
1602
  // TODO: we should have different message-ids for the "last" stuff: adding
1602
1603
  // `.item` likely corrects the ref, probably with location at end of ref
1603
1604
  return ref;
@@ -991,7 +991,7 @@ function fns( model ) {
991
991
  }
992
992
  else if ($extended && art.elements) {
993
993
  warning( 'ref-deprecated-in-extend', [ head.location, user ], { id: head.id },
994
- // eslint-disable-next-line @stylistic/js/max-len
994
+ // eslint-disable-next-line @stylistic/max-len
995
995
  'In an added column, do not use the table alias $(ID) to refer to source elements' );
996
996
  }
997
997
  }
@@ -1489,7 +1489,7 @@ function fns( model ) {
1489
1489
  // of invisible table aliases; at least one stakeholder uses this,
1490
1490
  // so it can't be an error (yet).
1491
1491
  message( 'ref-deprecated-self-element', [ ref.path[0].location, user._user ], {},
1492
- // eslint-disable-next-line @stylistic/js/max-len
1492
+ // eslint-disable-next-line @stylistic/max-len
1493
1493
  'Referring to the query\'s own elements here might lead to invalid SQL references; use source elements only' );
1494
1494
  return false;
1495
1495
  default:
@@ -1898,7 +1898,7 @@ function fns( model ) {
1898
1898
  else if (target._artifact && target._artifact !== user._main && user._main.kind === 'entity') {
1899
1899
  const last = path[path.length - 1];
1900
1900
  warning( 'ref-invalid-backlink', [ last.location, user ], { art: target, id: '$self' },
1901
- // eslint-disable-next-line @stylistic/js/max-len
1901
+ // eslint-disable-next-line @stylistic/max-len
1902
1902
  'The target $(ART) of the association is not the current entity represented by $(ID)' );
1903
1903
  }
1904
1904
  }
@@ -1988,11 +1988,11 @@ function fns( model ) {
1988
1988
  std: 'Can follow association $(ART) only to its foreign key references, not to $(NAME)',
1989
1989
  keys: 'Can follow managed association $(ART) only to the keys of its target, not to $(NAME)',
1990
1990
  complete: 'The reference must cover a full foreign key reference of association $(ART)',
1991
- // eslint-disable-next-line @stylistic/js/max-len
1991
+ // eslint-disable-next-line @stylistic/max-len
1992
1992
  'self-std': 'In column ref starting with $(ALIAS), we can follow association $(ART) only to its foreign key references, not to $(NAME)',
1993
- // eslint-disable-next-line @stylistic/js/max-len
1993
+ // eslint-disable-next-line @stylistic/max-len
1994
1994
  'self-keys': 'In column ref starting with $(ALIAS), we can follow managed association $(ART) only to the keys of its target, not to $(NAME)',
1995
- // eslint-disable-next-line @stylistic/js/max-len
1995
+ // eslint-disable-next-line @stylistic/max-len
1996
1996
  'self-complete': 'The column reference starting with $(ALIAS) must cover a full foreign key reference of association $(ART)',
1997
1997
  } );
1998
1998
  // TODO later: mention allowed ones
@@ -129,7 +129,7 @@ function tweakAssocs( model ) {
129
129
  // ID published! Used in stakeholder project; if renamed, add to oldMessageIds
130
130
  info( 'assoc-outside-service', loc, { '#': text, target, service: main._service }, {
131
131
  std: 'Association target $(TARGET) is outside any service',
132
- // eslint-disable-next-line @stylistic/js/max-len
132
+ // eslint-disable-next-line @stylistic/max-len
133
133
  exposed: 'If association is published in service $(SERVICE), its target $(TARGET) is outside any service',
134
134
  } );
135
135
  }
@@ -146,7 +146,7 @@ function tweakAssocs( model ) {
146
146
  if (assoc && assoc.foreignKeys) {
147
147
  error( 'rewrite-key-for-unmanaged', [ elem.on.location, elem ],
148
148
  { keyword: 'on', art: assocWithExplicitSpec( assoc ) },
149
- // eslint-disable-next-line @stylistic/js/max-len
149
+ // eslint-disable-next-line @stylistic/max-len
150
150
  'Do not specify an $(KEYWORD) condition when redirecting the managed association $(ART)' );
151
151
  }
152
152
  checkIgnoredFilter( elem );
@@ -714,7 +714,7 @@ function tweakAssocs( model ) {
714
714
  if (assoc.$errorReported !== 'assoc-unexpected-scope') {
715
715
  error( 'assoc-unexpected-scope', [ assoc.value.location, assoc ],
716
716
  { id: assoc.value._artifact.name.id },
717
- // eslint-disable-next-line @stylistic/js/max-len
717
+ // eslint-disable-next-line @stylistic/max-len
718
718
  'Association $(ID) can\'t be projected because its ON-condition refers to a parameter' );
719
719
  assoc.$errorReported = 'assoc-unexpected-scope';
720
720
  }
@@ -747,11 +747,11 @@ function tweakAssocs( model ) {
747
747
  if (!(elem === item._artifact || // redirection for explicit def
748
748
  elem._origin === item._artifact)) {
749
749
  const art = assoc._origin;
750
- // eslint-disable-next-line @stylistic/js/max-len
750
+ // eslint-disable-next-line @stylistic/max-len
751
751
  warning( 'rewrite-shadowed', [ elem.name.location, elem ], { art: art && effectiveType( art ) }, {
752
- // eslint-disable-next-line @stylistic/js/max-len
752
+ // eslint-disable-next-line @stylistic/max-len
753
753
  std: 'This element is not originally referred to in the ON-condition of association $(ART)',
754
- // eslint-disable-next-line @stylistic/js/max-len
754
+ // eslint-disable-next-line @stylistic/max-len
755
755
  element: 'This element is not originally referred to in the ON-condition of association $(MEMBER) of $(ART)',
756
756
  } );
757
757
  }
@@ -150,7 +150,7 @@ function setMemberParent( elem, name, parent, prop ) {
150
150
  p[prop] = Object.create( null );
151
151
  dictAdd( p[prop], name, elem );
152
152
  }
153
- if (parent._outer && parent._outer.items) // TODO: remove for items, too
153
+ if (parent._outer?.items) // TODO: remove for items, too
154
154
  parent = parent._outer;
155
155
  setLink( elem, '_parent', parent );
156
156
  setLink( elem, '_main', parent._main || parent );
@@ -484,11 +484,13 @@ function traverseQueryPost( query, simpleOnly, callback ) {
484
484
  // else: with parse error (`select from <EOF>`, `select distinct from;`)
485
485
  }
486
486
 
487
- // Call callback on all queries in dependency order, i.e. starting with query Q
488
- // 1. sub queries in FROM sources of Q
489
- // 2. Q itself, except if non-referred query, but with right UNION parts
490
- // 3. sub queries in ON in FROM of Q
491
- // 4. sub queries in columns, WHERE, HAVING
487
+ /**
488
+ * Call callback on all queries in dependency order, i.e. starting with query Q
489
+ * 1. sub queries in FROM sources of Q
490
+ * 2. Q itself, ALSO if non-referred query
491
+ * 3. sub queries in ON in FROM of Q
492
+ * 4. sub queries in columns, WHERE, HAVING
493
+ */
492
494
  function traverseQueryExtra( main, callback ) {
493
495
  if (!main.$queries)
494
496
  return;
@@ -501,16 +503,7 @@ function traverseQueryExtra( main, callback ) {
501
503
  if (query._status === 'extra' || query._parent.kind === '$tableAlias')
502
504
  continue; // if parent is alias, query is FROM source -> run by traverseQueryPost
503
505
  // we are now in the top-level (parent is entity) or a non-referred query (parent is query)
504
- setLink( query, '_status', 'extra' ); // do not call callback() in non-referred query
505
- // console.log( 'A:', query.name,query._status)
506
- traverseQueryPost( query, null, (q) => {
507
- if (q._status !== 'extra') {
508
- // console.log( 'T:', q.name)
509
- setLink( q, '_status', 'extra' );
510
- callback( q );
511
- }
512
- // else console.log( 'E:', q.name)
513
- } );
506
+ traverseQueryPost( query, null, callback );
514
507
  }
515
508
  }
516
509
 
@@ -266,7 +266,7 @@ function xprRewriteFns( model ) {
266
266
  config.tokenExpr = expr;
267
267
  return traverseExpr.STOP === traverseExpr(
268
268
  expr, 'annoRewrite', config.target,
269
- // eslint-disable-next-line @stylistic/js/max-len, @stylistic/js/function-paren-newline
269
+ // eslint-disable-next-line @stylistic/max-len, @stylistic/function-paren-newline
270
270
  (e, refCtx) => (rewriteAnnoExpr( e, config, refCtx ) ? traverseExpr.STOP : traverseExpr.SKIP) );
271
271
  }
272
272
  return false;
@@ -323,7 +323,7 @@ function xprRewriteFns( model ) {
323
323
  const filterConfig = { ...config, target: assocTarget, isInFilter: true };
324
324
  if (traverseExpr.STOP === traverseExpr(
325
325
  step.where, 'filter', step,
326
- // eslint-disable-next-line @stylistic/js/max-len
326
+ // eslint-disable-next-line @stylistic/max-len
327
327
  (e, ctx) => expr.path && (rewriteGenericAnnoPath( e, filterConfig, ctx ) ? traverseExpr.STOP : traverseExpr.SKIP)
328
328
  ))
329
329
  return true;
@@ -1,4 +1,4 @@
1
- // Base class for generated parser, for redepage v0.2.5
1
+ // Base class for generated parser, for redepage v0.2.7
2
2
 
3
3
  'use strict';
4
4
 
@@ -17,7 +17,6 @@ class BaseParser {
17
17
  lexer;
18
18
 
19
19
  tokens = undefined;
20
- eofIndex = undefined;
21
20
  tokenIdx = 0;
22
21
  recoverTokenIdx = -1;
23
22
  conditionTokenIdx = -1; // TODO: can we use recoverTokenIdx ?
@@ -44,7 +43,6 @@ class BaseParser {
44
43
 
45
44
  init() {
46
45
  this.lexer.tokenize( this );
47
- this.eofIndex = this.tokens.length - 1;
48
46
  return this;
49
47
  }
50
48
 
@@ -53,7 +51,7 @@ class BaseParser {
53
51
  s: this.s,
54
52
  stack: this.stack,
55
53
  dynamic_: this.dynamic_,
56
- prec_: this.prec_ // TODO: necessary?
54
+ prec_: this.prec_, // TODO: necessary?
57
55
  };
58
56
  }
59
57
 
@@ -85,8 +83,8 @@ class BaseParser {
85
83
  la() { // lookahead: complete token
86
84
  return this.tokens[this.tokenIdx];
87
85
  }
88
- lb() { // look back: complete token
89
- return this.tokens[this.tokenIdx - 1];
86
+ lb( k = 1 ) { // look back: complete token
87
+ return this.tokens[this.tokenIdx - k];
90
88
  }
91
89
  lr() { // return the first token matched by current rule
92
90
  return this.tokens[this.stack[this.stack.length - 1].tokenIdx];
@@ -95,12 +93,12 @@ class BaseParser {
95
93
  // lookahead, error: ----------------------------------------------------------
96
94
 
97
95
  l() { // lookahead: token type
98
- return this.tokens[this.tokenIdx].type;
96
+ return this.la().type;
99
97
  }
100
98
 
101
99
  // instead of l() if keyword (reserved and/or unreserved) is in one of the cases
102
100
  lk() { // keyword lookahead
103
- const la = this.tokens[this.tokenIdx];
101
+ const la = this.la();
104
102
  if (!this.nextTokenAsId)
105
103
  return la.keyword || la.type;
106
104
  // return la.keyword && this.table[this.s][la.keyword] && la.keyword || la.type;
@@ -109,7 +107,7 @@ class BaseParser {
109
107
  }
110
108
 
111
109
  e() { // error: report and recover
112
- const la = this.tokens[this.tokenIdx];
110
+ const la = this.la();
113
111
  this._trace( 'detect parsing error' );
114
112
  if (this.errorTokenIdx === this.tokenIdx)
115
113
  throw Error( `Already reported error for ${ tokenFullName( la ) } at ${ la.location }`);
@@ -141,7 +139,7 @@ class BaseParser {
141
139
 
142
140
  // instead of e() in default if lk() had been used and 'Id' is in a non-default case
143
141
  ei() { // error (after trying to test again as identifier)
144
- if (!this.tokens[this.tokenIdx].keyword) // lk() had directly returned the type
142
+ if (!this.la().keyword) // lk() had directly returned the type
145
143
  return this.e();
146
144
  this.nextTokenAsId = true;
147
145
  return false; // do not execute action after it
@@ -200,18 +198,6 @@ class BaseParser {
200
198
  return false; // do not execute action after it
201
199
  }
202
200
 
203
- // instead of gi() at rule end (RuleEnd_ in follow-set) for `Id<weak>`, TODO: delete
204
- giR( state, follow ) { // go to state (after trying to test again as identifier)
205
- const { keyword } = this.tokens[this.tokenIdx];
206
- if (!keyword || this.keywords[keyword])
207
- return this.g( state, follow );
208
- this._tracePush( [ 'R', 0 ] );
209
- if (this._matchesInFollow( 'Id', keyword, 'R' ))
210
- return this.g( state, follow );
211
- this.nextTokenAsId = true;
212
- return false; // do not execute action after it
213
- }
214
-
215
201
  // instead of g() in a non-default case if there is a LL1 conflict
216
202
  gP( state, follow ) { // goto state with standard weak-conflict prediction
217
203
  return this.lP( follow ) && this.g( state );
@@ -303,7 +289,8 @@ class BaseParser {
303
289
  // “go if user condition fails”
304
290
  gc( state, cond, arg ) {
305
291
  if (this.conditionTokenIdx === this.tokenIdx && // tested on same
306
- this.conditionStackLength == null) { // after error recovery
292
+ this.conditionStackLength == null && // after error recovery
293
+ !this[cond].afterError) {
307
294
  this._tracePush( [ 'C' ] );
308
295
  return true;
309
296
  }
@@ -314,12 +301,11 @@ class BaseParser {
314
301
  }
315
302
  // calling the condition might have side effects (precendence conditions have)
316
303
  // → call tracing “name” before
317
- const fail = this[cond]( arg, true ); // TODO: use single-letter for run?
304
+ const fail = this[cond]( arg, true ); // TODO: use single-letter for run! → 'X'
318
305
  if (this.constructor.tracingParser)
319
306
  this._traceSubPush( !fail );
320
307
  // The default case must not have actions. If written in grammar with action,
321
- // the default must have <default=fallback>
322
-
308
+ // the default must currently have <default=true>
323
309
 
324
310
  if (fail) { // TODO: extra gcK() method instead of check below
325
311
  // TODO: probably remove the following (and `conditionStackLength` tests)
@@ -346,6 +332,23 @@ class BaseParser {
346
332
  return this.gc( null, cond, arg );
347
333
  }
348
334
 
335
+ // predefined guard:
336
+ isNoKeywordInRuleFollow( _arg, mode ) {
337
+ const { keyword } = this.la();
338
+ if (this.constructor.tracingParser && (mode === true || mode === 'M')) {
339
+ // TODO: mode === 'X' || mode === 'M'
340
+ --this.trace.at(-1).length; // do not show guard name in trace
341
+ if (!keyword || this.keywords[keyword] == null)
342
+ return false; // ok
343
+ const r = this._matchesInFollow( 'Id', keyword, 'R' );
344
+ --this.trace.at(-1).length; // this.gc() also traces result
345
+ return r;
346
+ }
347
+ if (!keyword || this.keywords[keyword] == null)
348
+ return false; // ok
349
+ return this._matchesInFollow( 'Id', keyword, 'R' );
350
+ }
351
+
349
352
  // rule start, end and call: --------------------------------------------------
350
353
 
351
354
  rule_( state, followState = -1 ) { // start rule
@@ -762,12 +765,13 @@ class BaseParser {
762
765
  const { keyword, type } = token;
763
766
  if (keyword && set[keyword] === true)
764
767
  delete set[keyword];
765
- else if (set[type] === true && !(keyword && this.keywords[keyword]))
766
- delete set[type]; // delete Id if Id token or non-reserved keyword
768
+ else if (set[type] === true && !(keyword && this.keywords[keyword] != null))
769
+ delete set[type]; // delete if not keyword
767
770
 
768
771
  this._trace( 'collect tokens for message' );
769
772
  const { trace } = this;
770
773
  const saved = this._saveForWalk();
774
+ saved.fixKeywordTokenIdx = this.fixKeywordTokenIdx; // changed by confirmExpected
771
775
  const expecting = Object.keys( set )
772
776
  .filter( tok => this._confirmExpected( tok, saved ) );
773
777
  token.type = type; // overwritten by _confirmExpected
@@ -780,7 +784,7 @@ class BaseParser {
780
784
  _findSyncToken( syncSet ) {
781
785
  const rewindDepth = this.stack.length
782
786
  this.recoverTokenIdx = this.tokenIdx;
783
- while (this.recoverTokenIdx <= this.eofIndex) {
787
+ while (this.recoverTokenIdx < this.tokens.length) {
784
788
  const { keyword, type } = this.tokens[this.recoverTokenIdx];
785
789
  let recoverDepth = keyword ? syncSet[keyword] : null;
786
790
  if (recoverDepth != null)
@@ -971,6 +975,8 @@ class BaseParser {
971
975
  const members = BaseParser.prototype;
972
976
  // functions below are to be called with `call` to set `this`
973
977
 
978
+ members.isNoKeywordInRuleFollow.afterError = true;
979
+
974
980
  members.precLeft_.traceName = function( prec ) {
975
981
  const parentPrec = this.stack.at( -1 ).prec;
976
982
  return `${ parentPrec ?? '-∞' }<${ prec }`;
@@ -1 +1 @@
1
- 7cfe4871a952fbc999cd1e8b06300f7f
1
+ 7489417512fa82b33fb6799686ce2917