@sap/cds-compiler 5.5.0 → 5.6.0
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 +17 -1
- package/bin/cdsse.js +3 -0
- package/lib/base/message-registry.js +2 -0
- package/lib/checks/cdsMap.js +27 -0
- package/lib/checks/validator.js +3 -1
- package/lib/compiler/builtins.js +2 -0
- package/lib/compiler/define.js +1 -0
- package/lib/edm/annotations/vocabularyDefinitions.js +6 -0
- package/lib/gen/BaseParser.js +115 -76
- package/lib/gen/CdlParser.js +1106 -1104
- package/lib/gen/Dictionary.json +185 -6
- package/lib/model/cloneCsn.js +1 -5
- package/lib/parsers/AstBuildingParser.js +94 -50
- package/lib/parsers/CdlGrammar.g4 +55 -29
- package/lib/transform/db/expansion.js +3 -0
- package/lib/transform/forOdata.js +1 -1
- package/lib/transform/odata/createForeignKeys.js +92 -9
- package/lib/utils/objectUtils.js +13 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@
|
|
|
7
7
|
Note: `beta` fixes, changes and features are usually not listed in this ChangeLog but [here](doc/CHANGELOG_BETA.md).
|
|
8
8
|
The compiler behavior concerning `beta` features can change at any time without notice.
|
|
9
9
|
|
|
10
|
+
## Version 5.6.0 - 2024-12-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Allow to refer to draft state element `HasActiveEntity` and `HasDraftEntity` via variable `$draft` in annotation path expressions.
|
|
15
|
+
- for.odata|to.edm(x): Introduce annotating the generated foreign keys
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Update OData vocabularies: 'Common', 'EntityRelationship', 'UI'
|
|
20
|
+
|
|
21
|
+
## Version 5.5.2 - 2024-12-02
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- to.hdi|sql|edm[x]: Correctly handle `cds.Map`, ensure that it does not have `.elements` yet.
|
|
10
26
|
|
|
11
27
|
## Version 5.5.0 - 2024-11-22
|
|
12
28
|
|
|
@@ -126,7 +142,7 @@ The compiler behavior concerning `beta` features can change at any time without
|
|
|
126
142
|
### Added
|
|
127
143
|
|
|
128
144
|
- cdsc: Option `--stdin` was added to support input via standard input, e.g. `cat file.cds | cdsc --stdin`
|
|
129
|
-
- Allow to refer to draft state element `IsActiveEntity` via
|
|
145
|
+
- Allow to refer to draft state element `IsActiveEntity` via variable `$draft.IsActiveEntity` in annotation path expressions.
|
|
130
146
|
+ for.odata: During draft augmentation `$draft.IsActiveEntity` is rewritten to `$self.IsActiveEntity` for all draft enabled
|
|
131
147
|
entities (root and sub nodes but not for named types or entity parameters).
|
|
132
148
|
+ to.edm(x): (V4 only) Allow to refer to an entity element in a bound action via `$self` and not only via explicit binding parameter
|
package/bin/cdsse.js
CHANGED
|
@@ -158,6 +158,9 @@ function tokensAt( buf, _offset, col, symbol ) {
|
|
|
158
158
|
else if (/^[A-Z_]+$/.test( n )) {
|
|
159
159
|
console.log( n.toLowerCase(), 'keyword' );
|
|
160
160
|
}
|
|
161
|
+
else if (n === 'Boolean') {
|
|
162
|
+
console.log( 'true keyword\nfalse keyword' );
|
|
163
|
+
}
|
|
161
164
|
else if (n !== 'Identifier') {
|
|
162
165
|
console.log( n, 'unknown' );
|
|
163
166
|
}
|
|
@@ -1271,6 +1271,8 @@ const centralMessageTexts = {
|
|
|
1271
1271
|
'magic': 'Unexpected magic variable $(ELEMREF) in $(ANNO)',
|
|
1272
1272
|
'bparam_v2_expl': 'Unexpected explicit binding parameter path $(ELEMREF) for OData $(VERSION) in $(ANNO)',
|
|
1273
1273
|
'bparam_v2_impl': 'Unexpected implicit binding parameter path $(ELEMREF) for OData $(VERSION) in $(ANNO)',
|
|
1274
|
+
// forOdata/generateForeignKeys
|
|
1275
|
+
'fk_substitution': 'Expected foreign key path $(ELEMREF) in $(ANNO) to end in a scalar typed leaf element',
|
|
1274
1276
|
},
|
|
1275
1277
|
// -----------------------------------------------------------------------------------
|
|
1276
1278
|
// OData Message section ends here, no messages below this line
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { hasNonEnumerable } = require('../utils/objectUtils');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* We don't support cds.Map in conjunction with .elements yet. To ensure that no one uses it and accidentally creates an
|
|
7
|
+
* empty structured type, we check for it and forbid it.
|
|
8
|
+
*
|
|
9
|
+
* Non-enumerable .elements are added by cds.linked - we silently remove them and proceed as usual.
|
|
10
|
+
*
|
|
11
|
+
* @param {*} parent
|
|
12
|
+
* @param {*} prop
|
|
13
|
+
* @param {*} type
|
|
14
|
+
* @param {*} path
|
|
15
|
+
*/
|
|
16
|
+
function checkCdsMap( parent, prop, type, path ) {
|
|
17
|
+
if (type === 'cds.Map' && parent.elements) {
|
|
18
|
+
if (hasNonEnumerable(parent, 'elements'))
|
|
19
|
+
delete parent.elements; // linked CSN sets a non-enumerable empty elements on cds.Map
|
|
20
|
+
else
|
|
21
|
+
this.error('type-unexpected-elements-for-map', path, { id: path.at(-1), type: 'cds.Map' }, 'Unexpected .elements for element $(ID) of type $(TYPE)');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
type: checkCdsMap,
|
|
27
|
+
};
|
package/lib/checks/validator.js
CHANGED
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
checkTemporalAnnotationsAssignment,
|
|
27
27
|
} = require('./annotationsOData');
|
|
28
28
|
// both
|
|
29
|
+
const checkCdsMap = require('./cdsMap');
|
|
29
30
|
const { validateOnCondition, validateMixinOnCondition } = require('./onConditions');
|
|
30
31
|
const validateForeignKeys = require('./foreignKeys');
|
|
31
32
|
const {
|
|
@@ -80,6 +81,7 @@ const forRelationalDBArtifactValidators = [
|
|
|
80
81
|
];
|
|
81
82
|
|
|
82
83
|
const forRelationalDBCsnValidators = [
|
|
84
|
+
checkCdsMap,
|
|
83
85
|
existsMustEndInAssoc,
|
|
84
86
|
forbidAssocInExists,
|
|
85
87
|
nonexpandableStructuredInExpression,
|
|
@@ -118,7 +120,7 @@ const forOdataArtifactValidators
|
|
|
118
120
|
checkReadOnlyAndInsertOnly,
|
|
119
121
|
];
|
|
120
122
|
|
|
121
|
-
const forOdataCsnValidators = [ nonexpandableStructuredInExpression ];
|
|
123
|
+
const forOdataCsnValidators = [ checkCdsMap, nonexpandableStructuredInExpression ];
|
|
122
124
|
|
|
123
125
|
const forOdataQueryValidators = [];
|
|
124
126
|
|
package/lib/compiler/builtins.js
CHANGED
package/lib/compiler/define.js
CHANGED
|
@@ -917,6 +917,7 @@ function define( model ) {
|
|
|
917
917
|
else {
|
|
918
918
|
// a late syntax error (this code also runs with parse-cdl), i.e.
|
|
919
919
|
// no semantic loc (wouldn't be available for expand/inline anyway)
|
|
920
|
+
// TODO: why here and not in parser?
|
|
920
921
|
error( 'syntax-duplicate-wildcard', [ col.location, null ], {
|
|
921
922
|
'#': (wildcard.location.col ? 'col' : 'std'),
|
|
922
923
|
prop: '*',
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
Common (published)
|
|
19
19
|
Communication (published)
|
|
20
20
|
DataIntegration (published)
|
|
21
|
+
EntityRelationship (experimental)
|
|
21
22
|
Graph (published, experimental)
|
|
22
23
|
Hierarchy (published, experimental)
|
|
23
24
|
HTML5 (published, experimental)
|
|
@@ -75,6 +76,11 @@ const vocabularyDefinitions = {
|
|
|
75
76
|
inc: { Alias: 'DataIntegration', Namespace: 'com.sap.vocabularies.DataIntegration.v1' },
|
|
76
77
|
int: { filename: 'DataIntegration.xml' },
|
|
77
78
|
},
|
|
79
|
+
EntityRelationship: {
|
|
80
|
+
ref: { Uri: 'https://sap.github.io/odata-vocabularies/vocabularies/EntityRelationship.xml' },
|
|
81
|
+
inc: { Alias: 'EntityRelationship', Namespace: 'com.sap.vocabularies.EntityRelationship.v1' },
|
|
82
|
+
int: { filename: 'EntityRelationship.xml' },
|
|
83
|
+
},
|
|
78
84
|
Graph: {
|
|
79
85
|
ref: { Uri: 'https://sap.github.io/odata-vocabularies/vocabularies/Graph.xml' },
|
|
80
86
|
inc: { Alias: 'Graph', Namespace: 'com.sap.vocabularies.Graph.v1' },
|
package/lib/gen/BaseParser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Base class for generated parser, for redepage v0.1.
|
|
1
|
+
// Base class for generated parser, for redepage v0.1.19
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
@@ -32,6 +32,7 @@ class BaseParser {
|
|
|
32
32
|
dynamic_ = {}; // TODO: extra class
|
|
33
33
|
prec_ = null;
|
|
34
34
|
$hasErrors = null;
|
|
35
|
+
leanConditions = {};
|
|
35
36
|
// trace:
|
|
36
37
|
trace = [];
|
|
37
38
|
|
|
@@ -163,6 +164,7 @@ class BaseParser {
|
|
|
163
164
|
this._tracePush( [ 'E', true ] );
|
|
164
165
|
return true;
|
|
165
166
|
}
|
|
167
|
+
this._tracePush( [ 'E', 0 ] );
|
|
166
168
|
const match = this._matchesInFollow( type, keyword, 'E' );
|
|
167
169
|
// If the parser reaches this point with match = null, even the top-level rule
|
|
168
170
|
// does not have a required token (typically `EOF`) at the end → the parser
|
|
@@ -203,8 +205,10 @@ class BaseParser {
|
|
|
203
205
|
// cases
|
|
204
206
|
giR( state, follow ) { // go to state (after trying to test again as identifier)
|
|
205
207
|
const { keyword } = this.tokens[this.tokenIdx];
|
|
206
|
-
if (!keyword || this.keywords[keyword]
|
|
207
|
-
|
|
208
|
+
if (!keyword || this.keywords[keyword])
|
|
209
|
+
return this.g( state, follow );
|
|
210
|
+
this._tracePush( [ 'R', 0 ] );
|
|
211
|
+
if (this._matchesInFollow( 'Id', keyword, 'R' ))
|
|
208
212
|
return this.g( state, follow );
|
|
209
213
|
this.nextTokenAsId = true;
|
|
210
214
|
return false; // do not execute action after it
|
|
@@ -256,6 +260,8 @@ class BaseParser {
|
|
|
256
260
|
|
|
257
261
|
// instead of c() for identifiers, used both with l() and lk()
|
|
258
262
|
ci( state, ident = 'ident' ) { // consume identifier token
|
|
263
|
+
if (this.tokenIdx === this.fixKeywordTokenIdx)
|
|
264
|
+
return this.e();
|
|
259
265
|
const la = this.tokens[this.tokenIdx];
|
|
260
266
|
if (this.keywords[la.keyword])
|
|
261
267
|
this.reportReservedWord_();
|
|
@@ -385,95 +391,104 @@ class BaseParser {
|
|
|
385
391
|
return true;
|
|
386
392
|
}
|
|
387
393
|
|
|
388
|
-
// predicate used before rule call
|
|
389
|
-
|
|
394
|
+
// predicate used before rule call (and called by `ckP` and `gP`) on keyword
|
|
395
|
+
// branch if with weak LL(1) conflict, i.e. there is an 'Id' branch or the
|
|
396
|
+
// default branch has `Id` in its first-set (TODO: or rule end, and `Id` is in
|
|
397
|
+
// follow-union)
|
|
398
|
+
lP( first2 ) {
|
|
390
399
|
// nothing to check if not a non-reserved keyword:
|
|
391
400
|
const { keyword: lk1 } = this.tokens[this.tokenIdx];
|
|
392
|
-
if (!lk1 || this.keywords[lk1] !== 0)
|
|
401
|
+
if (!lk1 || this.keywords[lk1] !== 0 || this.fixKeywordTokenIdx === this.tokenIdx)
|
|
393
402
|
return true;
|
|
394
403
|
|
|
404
|
+
this._tracePush( [ 'K' ] );
|
|
395
405
|
const { type: lt2, keyword: lk2 } = this.tokens[this.tokenIdx + 1];
|
|
396
|
-
|
|
406
|
+
if (lt2 === 'IllegalToken')
|
|
407
|
+
return true
|
|
408
|
+
// Argument first2 is just a performance hint:
|
|
397
409
|
if (lk2 && first2?.[0] === 'Id' && !this.keywords[lk2] ||
|
|
398
410
|
first2?.includes( lk2 || lt2 )) {
|
|
399
|
-
this.
|
|
411
|
+
this._traceSubPush( true );
|
|
400
412
|
return true;
|
|
401
413
|
}
|
|
402
|
-
this._tracePush( [ 'K' ] );
|
|
403
414
|
// now check it dynamically:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
415
|
+
if (this._walkPred( this.table[this.s][lk1], lk1, lt2, lk2 ))
|
|
416
|
+
return true;
|
|
417
|
+
this._tracePush( [ 'I' ] );
|
|
418
|
+
const choice = this.table[this.s];
|
|
419
|
+
if (!this._walkPred( choice.Id || choice[''], null, lt2, lk2 ))
|
|
420
|
+
return true;
|
|
421
|
+
this.nextTokenAsId = true;
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
407
424
|
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
425
|
+
_walkPred( cmd, lk1, lt2, lk2 ) {
|
|
426
|
+
const saved = this._saveForWalk();
|
|
427
|
+
const { length } = this.stack;
|
|
428
|
+
if (typeof cmd[0] !== 'number') // don't skip push to state with rule call
|
|
429
|
+
this.s = cmd[1];
|
|
430
|
+
if (cmd[0] !== (lk1 ? 'ck' : 'ci')) { // make the std case fast
|
|
431
|
+
let match1 = this._pred_next( 'Id', lk1, 'P' ); // TODO: really P for I?
|
|
432
|
+
if (!match1) {
|
|
433
|
+
if (lk1 || match1 === false) // assert for correct code generation
|
|
434
|
+
throw Error( `Cannot match first prediction token in rule at state ${ saved.s }` );
|
|
435
|
+
if (match1 == null) {
|
|
436
|
+
this._traceSubPush( 0 ); // TODO: make _pred_next push this
|
|
437
|
+
match1 = this._matchesInFollow( 'Id', lk1, 'I' );
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
this._traceSubPush( false );
|
|
441
|
+
}
|
|
442
|
+
Object.assign( this, saved );
|
|
443
|
+
this.stack.length = length;
|
|
444
|
+
return !!match1;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
411
447
|
|
|
448
|
+
this._traceSubPush( '' ); // between the two tokens
|
|
412
449
|
++this.tokenIdx; // for user lookahead fns and conditions
|
|
413
|
-
|
|
450
|
+
let match2 = this._pred_next( lt2, lk2, (lk1 ? 'K' : 'I') );
|
|
451
|
+
if (match2 == null) {
|
|
452
|
+
this._traceSubPush( 0 ); // TODO: make _pred_next push this
|
|
453
|
+
match2 = !!this._matchesInFollow( lt2, lk2, (lk1 ? 'K' : 'I') );
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
this._traceSubPush( match2 );
|
|
457
|
+
}
|
|
458
|
+
Object.assign( this, saved );
|
|
459
|
+
this.stack.length = length;
|
|
414
460
|
--this.tokenIdx;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const r = match ?? true;
|
|
418
|
-
if (match == null)
|
|
419
|
-
this._traceSubPush( 0 );
|
|
420
|
-
if (lt2 === 'IllegalToken')
|
|
421
|
-
return true
|
|
422
|
-
// TODO: instead of this IllegalToken test, implement a “confirm unreserved
|
|
423
|
-
// keyword as Id” prediction which tests whether the token after the then-Id
|
|
424
|
-
// matches.
|
|
425
|
-
this._traceSubPush( r );
|
|
426
|
-
if (!r)
|
|
427
|
-
this.nextTokenAsId = true;
|
|
428
|
-
return r;
|
|
461
|
+
return match2;
|
|
429
462
|
}
|
|
430
463
|
|
|
431
464
|
// Now the helper methods =====================================================
|
|
432
465
|
|
|
433
466
|
// Standard weak-conflict predicate -------------------------------------------
|
|
434
467
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (!Array.isArray( cmd ))
|
|
441
|
-
cmd = cmd[keyword] || cmd.Id || cmd[''];
|
|
442
|
-
switch (cmd[0]) {
|
|
443
|
-
case 'ck': case 'mk':
|
|
444
|
-
return cmd[1]; // state after token consumption
|
|
445
|
-
case 'g': // TODO: another rule call?
|
|
446
|
-
break;
|
|
447
|
-
default:
|
|
448
|
-
if (typeof cmd[0] !== 'number')
|
|
449
|
-
throw Error( `Unexpected command ${ cmd[0] } at state ${ this.s }` );
|
|
450
|
-
}
|
|
451
|
-
state = cmd[1];
|
|
452
|
-
}
|
|
453
|
-
// reached end of rule without having consumed a token
|
|
454
|
-
throw Error( 'Not supported: option for unreserved keywords in follow set' );
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
_pred_next( type, keyword, mode ) { // mode = K | E | R | M
|
|
458
|
-
const useConditions = (mode === 'M'); // TODO: extra method with conditions ?
|
|
459
|
-
let hasEnteredRule = false;
|
|
468
|
+
_pred_next( type, keyword, mode ) { // mode = P | K | I | E | R | M
|
|
469
|
+
const properCall = (mode === 'P');
|
|
470
|
+
const lean = (mode !== 'M'); // TODO: extra method with conditions ?
|
|
471
|
+
// TODO: if false, use condition in this.leanConditions
|
|
472
|
+
let hasMatchedToken = null; // undecided yet → calculate on demand
|
|
460
473
|
while (this.s) {
|
|
461
|
-
if (
|
|
462
|
-
this._tracePush( this.s );
|
|
463
|
-
else
|
|
474
|
+
if (lean)
|
|
464
475
|
this._traceSubPush( this.s );
|
|
476
|
+
else
|
|
477
|
+
this._tracePush( this.s ); // TODO: push new state instead
|
|
465
478
|
let cmd = this.table[this.s];
|
|
466
479
|
if (!Array.isArray( cmd )) {
|
|
467
480
|
const lookahead = cmd[' lookahead'];
|
|
468
481
|
const c = lookahead // TODO: call with { keyword, type } ?
|
|
469
482
|
? cmd[this[lookahead]( mode )]
|
|
470
483
|
: keyword && cmd[keyword] || cmd[type];
|
|
471
|
-
cmd = !(c &&
|
|
484
|
+
cmd = !(c && this._rejectCondition( c, mode, lean )) && c || cmd[''];
|
|
472
485
|
}
|
|
486
|
+
const state = this.s;
|
|
487
|
+
this.s = cmd[1];
|
|
473
488
|
switch (cmd[0]) {
|
|
474
489
|
case 'c': case 'ck': case 'ckA': // TODO: re-check ckA
|
|
475
490
|
return true;
|
|
476
|
-
case 'ciA':
|
|
491
|
+
case 'ciA': // TODO: fixKeywordTokenIdx ?
|
|
477
492
|
return mode !== 'R';
|
|
478
493
|
// in the R prediction for optional `Id<reserved>` at rule end, only
|
|
479
494
|
// alternative keyword matches are preferred, not identifier matches
|
|
@@ -481,7 +496,8 @@ class BaseParser {
|
|
|
481
496
|
if (!keyword ||
|
|
482
497
|
!this.keywords[keyword] && this.fixKeywordTokenIdx !== this.tokenIdx)
|
|
483
498
|
return mode !== 'R';
|
|
484
|
-
cmd = this.table[
|
|
499
|
+
cmd = this.table[state]['']; // is currently always 'g' or 'e'
|
|
500
|
+
this.s = cmd[1];
|
|
485
501
|
break;
|
|
486
502
|
case 'm':
|
|
487
503
|
return type === cmd[2];
|
|
@@ -497,17 +513,27 @@ class BaseParser {
|
|
|
497
513
|
break;
|
|
498
514
|
default:
|
|
499
515
|
if (typeof cmd[0] !== 'number')
|
|
500
|
-
throw Error( `Unexpected command ${ cmd[0] } at state ${
|
|
516
|
+
throw Error( `Unexpected command ${ cmd[0] } at state ${ state }` );
|
|
501
517
|
// If the parser enters a rule, reaching the rule end (can happen with
|
|
502
518
|
// option `minTokensMatched`) means "no match".
|
|
503
|
-
|
|
519
|
+
hasMatchedToken = false;
|
|
504
520
|
// If we want to support conditions before matching the first token in a
|
|
505
521
|
// rule, we would have to handle `this.stack` and `this.dynamically_`.
|
|
522
|
+
if (properCall) {
|
|
523
|
+
// rule_() - TODO: also w/ conditions before matching first token
|
|
524
|
+
this.stack.push( {
|
|
525
|
+
ruleState: cmd[1],
|
|
526
|
+
followState: cmd[0],
|
|
527
|
+
tokenIdx: this.tokenIdx,
|
|
528
|
+
prec: this.prec_,
|
|
529
|
+
} );
|
|
530
|
+
this.dynamic_ = Object.create( this.dynamic_ );
|
|
531
|
+
this.prec_ = null;
|
|
532
|
+
}
|
|
506
533
|
}
|
|
507
534
|
// We could optimize with rule call - only 'Id' must be further investigated
|
|
508
535
|
// TODO: actually also with `g`
|
|
509
536
|
// in both cases if no condition is evaluated
|
|
510
|
-
this.s = cmd[1];
|
|
511
537
|
// TODO <prepare=…, arg=…> for real trial run also before all returns
|
|
512
538
|
// if (cmd[5])
|
|
513
539
|
// this.cmd[5]( cmd[4], mode );
|
|
@@ -522,28 +548,31 @@ class BaseParser {
|
|
|
522
548
|
// prediction, the tool can normally omit the prediction (and output a
|
|
523
549
|
// message), no so with `ruleStartingWithUnreserved`. We will rather look
|
|
524
550
|
// behind the current rule _after_ having decided that the token is to be
|
|
525
|
-
|
|
526
|
-
return
|
|
551
|
+
// matched as identifier.
|
|
552
|
+
return (hasMatchedToken ?? this.tokenIdx > this.stack.at( -1 ).tokenIdx)
|
|
553
|
+
&& null; // let caller decide how to interpret this
|
|
527
554
|
}
|
|
528
555
|
|
|
529
|
-
_rejectCondition( cmd, mode ) {
|
|
556
|
+
_rejectCondition( cmd, mode, lean ) {
|
|
530
557
|
const cond = cmd[3];
|
|
531
|
-
if (!cond)
|
|
558
|
+
if (!cond || lean && !this.leanConditions[cond])
|
|
532
559
|
return false;
|
|
533
560
|
if (!this.constructor.tracingParser)
|
|
534
561
|
return !this[cond]( mode, cmd[4] );
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
562
|
+
// TODO: let this[cond]( true ) return recovery badness in error case
|
|
563
|
+
if (!lean) {
|
|
564
|
+
const { traceName } = this[cond];
|
|
565
|
+
this._tracePush( [ 'C', traceName?.call( this, cmd[4] ) ?? cond ] );
|
|
566
|
+
// calling the condition might have side effects (precendence conditions have)
|
|
567
|
+
// → call tracing “name” before
|
|
568
|
+
}
|
|
540
569
|
const fail = !this[cond]( mode, cmd[4] );
|
|
541
|
-
this._traceSubPush( !fail );
|
|
570
|
+
this._traceSubPush( lean ? { true: 'C✔', false: 'C✖' }[!fail] : !fail );
|
|
542
571
|
return fail;
|
|
543
572
|
}
|
|
544
573
|
|
|
545
574
|
_matchesInFollow( type, keyword, mode ) { // mode = E | R
|
|
546
|
-
|
|
575
|
+
// TODO: now also set stack!
|
|
547
576
|
const savedState = this.s;
|
|
548
577
|
// TODO: caching
|
|
549
578
|
const { dynamic_ } = this;
|
|
@@ -564,9 +593,11 @@ class BaseParser {
|
|
|
564
593
|
}
|
|
565
594
|
|
|
566
595
|
_confirmExpected( token, saved ) { // mode = M
|
|
567
|
-
const
|
|
596
|
+
const fix = /^[_a-z]/.test( token );
|
|
597
|
+
const [ type, keyword ] = (fix) ? [ 'Id', token ] : [ token ];
|
|
568
598
|
Object.assign( this.la(), { type, keyword } );
|
|
569
599
|
this._cloneFromSaved( saved );
|
|
600
|
+
this.fixKeywordTokenIdx = fix && this.tokenIdx;
|
|
570
601
|
this.trace = [];
|
|
571
602
|
let match;
|
|
572
603
|
while (this.stack.length) {
|
|
@@ -639,6 +670,8 @@ class BaseParser {
|
|
|
639
670
|
for (const prop in dict) {
|
|
640
671
|
if (prop && Object.hasOwn( dict, prop ) && prop !== 'Id' &&
|
|
641
672
|
!Object.hasOwn( expecting, prop ) && prop.charAt(0) !== ' ') {
|
|
673
|
+
// TODO: or call this.translateParserToken_ always?
|
|
674
|
+
// it should then directly set the dictionary -> setTokenInSet_()
|
|
642
675
|
if (lookahead) { // yes, independently from ckA()
|
|
643
676
|
for (const p of this.translateParserToken_( prop, lookahead ))
|
|
644
677
|
expecting[p] = val;
|
|
@@ -715,12 +748,14 @@ class BaseParser {
|
|
|
715
748
|
delete set[type]; // delete Id if Id token or non-reserved keyword
|
|
716
749
|
|
|
717
750
|
this._trace( 'collect tokens for message,' );
|
|
751
|
+
const { trace } = this;
|
|
718
752
|
const saved = this._saveForWalk();
|
|
719
753
|
const expecting = Object.keys( set )
|
|
720
754
|
.filter( tok => this._confirmExpected( tok, saved ) );
|
|
721
755
|
token.type = type; // overwritten by _confirmExpected
|
|
722
756
|
token.keyword = keyword;
|
|
723
757
|
Object.assign( this, saved );
|
|
758
|
+
this.trace = trace;
|
|
724
759
|
// TODO: also trace M(…) collection, extra line for each token, with condition
|
|
725
760
|
return expecting;
|
|
726
761
|
}
|
|
@@ -761,6 +796,7 @@ class BaseParser {
|
|
|
761
796
|
// Continue parsing: ignore next predicate (TODO: except some specified ones?)
|
|
762
797
|
this.conditionTokenIdx = this.tokenIdx;
|
|
763
798
|
this.conditionStackLength = null;
|
|
799
|
+
this.fixKeywordTokenIdx = -1; // was set when collecting expecting-set
|
|
764
800
|
|
|
765
801
|
// TODO: re-check for rule calls which are at the optional rule end:
|
|
766
802
|
// x: 'x not'; b: 'b'? x {console.log('x→b')} 'b'?; a: b {console.log('b→a')} 'a'
|
|
@@ -887,6 +923,9 @@ class BaseParser {
|
|
|
887
923
|
|
|
888
924
|
// Predefined conditions with extra option names:
|
|
889
925
|
|
|
926
|
+
hide_( mode ) {
|
|
927
|
+
return mode !== 'M';
|
|
928
|
+
}
|
|
890
929
|
precLeft_( _test, prec ) { // <prec=…>, <…,assoc=left>, <…,prefix=once>
|
|
891
930
|
const parentPrec = this.stack.at( -1 ).prec;
|
|
892
931
|
if (parentPrec != null && parentPrec >= prec)
|