@ldclabs/kip-lang 0.3.0 → 0.4.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/README.md +69 -5
- package/dist/ast.d.ts +10 -1
- package/dist/ast.d.ts.map +1 -1
- package/dist/budget.d.ts +37 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +105 -0
- package/dist/budget.js.map +1 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +7 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +27 -0
- package/dist/errors.js.map +1 -0
- package/dist/exec-ast.d.ts +314 -0
- package/dist/exec-ast.d.ts.map +1 -0
- package/dist/exec-ast.js +23 -0
- package/dist/exec-ast.js.map +1 -0
- package/dist/formatter.d.ts +5 -7
- package/dist/formatter.d.ts.map +1 -1
- package/dist/formatter.js +71 -143
- package/dist/formatter.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/lexer.d.ts.map +1 -1
- package/dist/lexer.js +42 -11
- package/dist/lexer.js.map +1 -1
- package/dist/lower.d.ts +18 -0
- package/dist/lower.d.ts.map +1 -0
- package/dist/lower.js +877 -0
- package/dist/lower.js.map +1 -0
- package/dist/parser.js +311 -82
- package/dist/parser.js.map +1 -1
- package/dist/semantics.d.ts +13 -0
- package/dist/semantics.d.ts.map +1 -0
- package/dist/semantics.js +228 -0
- package/dist/semantics.js.map +1 -0
- package/dist/token.d.ts +4 -4
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +5 -10
- package/dist/token.js.map +1 -1
- package/dist/version.d.ts +13 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +13 -0
- package/dist/version.js.map +1 -0
- package/package.json +9 -4
package/dist/parser.js
CHANGED
|
@@ -20,6 +20,7 @@ class Parser {
|
|
|
20
20
|
const start = this.currentPos();
|
|
21
21
|
this.skipComments();
|
|
22
22
|
while (!this.isAtEnd()) {
|
|
23
|
+
const before = this.pos;
|
|
23
24
|
this.skipComments();
|
|
24
25
|
if (this.isAtEnd())
|
|
25
26
|
break;
|
|
@@ -32,6 +33,11 @@ class Parser {
|
|
|
32
33
|
// Error recovery: skip to next statement-level keyword
|
|
33
34
|
this.recoverToNextStatement();
|
|
34
35
|
}
|
|
36
|
+
// A sub-parser that rejects its first token reports and returns
|
|
37
|
+
// without consuming it, so a loop keyed on that token would spin
|
|
38
|
+
// forever building diagnostics. Stop as soon as nothing moved.
|
|
39
|
+
if (this.pos === before)
|
|
40
|
+
break;
|
|
35
41
|
}
|
|
36
42
|
const end = this.currentPos();
|
|
37
43
|
return {
|
|
@@ -74,6 +80,7 @@ class Parser {
|
|
|
74
80
|
const start = this.currentPos();
|
|
75
81
|
const comments = this.collectLeadingComments();
|
|
76
82
|
this.expect(TokenType.Find);
|
|
83
|
+
const lparen = this.current();
|
|
77
84
|
this.expect(TokenType.LParen);
|
|
78
85
|
const projections = [];
|
|
79
86
|
if (!this.check(TokenType.RParen)) {
|
|
@@ -82,6 +89,9 @@ class Parser {
|
|
|
82
89
|
projections.push(this.parseExpression());
|
|
83
90
|
}
|
|
84
91
|
}
|
|
92
|
+
if (projections.length === 0) {
|
|
93
|
+
this.error(`FIND must declare at least one output expression, e.g. FIND(?var)`, lparen);
|
|
94
|
+
}
|
|
85
95
|
this.expect(TokenType.RParen);
|
|
86
96
|
let where;
|
|
87
97
|
let orderBy;
|
|
@@ -116,11 +126,12 @@ class Parser {
|
|
|
116
126
|
parseUpsertStatement() {
|
|
117
127
|
const start = this.currentPos();
|
|
118
128
|
const comments = this.collectLeadingComments();
|
|
119
|
-
this.
|
|
129
|
+
this.expectKeywordWithSpace(TokenType.Upsert);
|
|
120
130
|
this.expect(TokenType.LBrace);
|
|
121
131
|
const blocks = [];
|
|
122
132
|
this.skipComments();
|
|
123
133
|
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
134
|
+
const before = this.pos;
|
|
124
135
|
this.skipComments();
|
|
125
136
|
if (this.check(TokenType.Concept)) {
|
|
126
137
|
blocks.push(this.parseConceptBlock());
|
|
@@ -136,6 +147,11 @@ class Parser {
|
|
|
136
147
|
this.advance();
|
|
137
148
|
}
|
|
138
149
|
this.skipComments();
|
|
150
|
+
// A sub-parser that rejects its first token reports and returns
|
|
151
|
+
// without consuming it, so a loop keyed on that token would spin
|
|
152
|
+
// forever building diagnostics. Stop as soon as nothing moved.
|
|
153
|
+
if (this.pos === before)
|
|
154
|
+
break;
|
|
139
155
|
}
|
|
140
156
|
this.expect(TokenType.RBrace);
|
|
141
157
|
let metadata;
|
|
@@ -153,8 +169,12 @@ class Parser {
|
|
|
153
169
|
parseConceptBlock() {
|
|
154
170
|
const start = this.currentPos();
|
|
155
171
|
const comments = this.collectLeadingComments();
|
|
156
|
-
this.
|
|
157
|
-
|
|
172
|
+
this.expectKeywordWithSpace(TokenType.Concept);
|
|
173
|
+
// The handle is optional: a block nothing else refers to needs no name.
|
|
174
|
+
let handle;
|
|
175
|
+
if (this.check(TokenType.Variable)) {
|
|
176
|
+
handle = this.expectVariable();
|
|
177
|
+
}
|
|
158
178
|
this.expect(TokenType.LBrace);
|
|
159
179
|
const matcher = this.parseConceptMatcher();
|
|
160
180
|
let expectVersion;
|
|
@@ -166,16 +186,17 @@ class Parser {
|
|
|
166
186
|
let metadata;
|
|
167
187
|
this.skipComments();
|
|
168
188
|
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
189
|
+
const before = this.pos;
|
|
169
190
|
this.skipComments();
|
|
170
191
|
if (this.check(TokenType.Set)) {
|
|
171
192
|
const setStart = this.currentPos();
|
|
172
|
-
this.advance(); // skip SET
|
|
193
|
+
const setTok = this.advance(); // skip SET
|
|
173
194
|
if (this.check(TokenType.Attributes)) {
|
|
174
|
-
this.
|
|
195
|
+
this.expectSecondWord(TokenType.Attributes, setTok);
|
|
175
196
|
setAttributes = this.parseSetAttributesBody(setStart);
|
|
176
197
|
}
|
|
177
198
|
else if (this.check(TokenType.Propositions)) {
|
|
178
|
-
this.
|
|
199
|
+
this.expectSecondWord(TokenType.Propositions, setTok);
|
|
179
200
|
setPropositions = this.parseSetPropositionsBody(setStart);
|
|
180
201
|
}
|
|
181
202
|
else {
|
|
@@ -197,6 +218,11 @@ class Parser {
|
|
|
197
218
|
this.advance();
|
|
198
219
|
}
|
|
199
220
|
this.skipComments();
|
|
221
|
+
// A sub-parser that rejects its first token reports and returns
|
|
222
|
+
// without consuming it, so a loop keyed on that token would spin
|
|
223
|
+
// forever building diagnostics. Stop as soon as nothing moved.
|
|
224
|
+
if (this.pos === before)
|
|
225
|
+
break;
|
|
200
226
|
}
|
|
201
227
|
this.expect(TokenType.RBrace);
|
|
202
228
|
// Concept-level WITH METADATA (outside the CONCEPT braces)
|
|
@@ -218,7 +244,7 @@ class Parser {
|
|
|
218
244
|
parsePropositionBlock() {
|
|
219
245
|
const start = this.currentPos();
|
|
220
246
|
const comments = this.collectLeadingComments();
|
|
221
|
-
this.
|
|
247
|
+
this.expectKeywordWithSpace(TokenType.Proposition);
|
|
222
248
|
let handle;
|
|
223
249
|
if (this.check(TokenType.Variable)) {
|
|
224
250
|
handle = this.expectVariable();
|
|
@@ -233,12 +259,13 @@ class Parser {
|
|
|
233
259
|
let metadata;
|
|
234
260
|
this.skipComments();
|
|
235
261
|
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
262
|
+
const before = this.pos;
|
|
236
263
|
this.skipComments();
|
|
237
264
|
if (this.check(TokenType.Set)) {
|
|
238
265
|
const setStart = this.currentPos();
|
|
239
|
-
this.advance();
|
|
266
|
+
const setTok = this.advance();
|
|
240
267
|
if (this.check(TokenType.Attributes)) {
|
|
241
|
-
this.
|
|
268
|
+
this.expectSecondWord(TokenType.Attributes, setTok);
|
|
242
269
|
setAttributes = this.parseSetAttributesBody(setStart);
|
|
243
270
|
}
|
|
244
271
|
else {
|
|
@@ -250,9 +277,15 @@ class Parser {
|
|
|
250
277
|
break;
|
|
251
278
|
}
|
|
252
279
|
else {
|
|
280
|
+
this.error(`Unexpected token '${this.current().value}' in PROPOSITION block`, this.current());
|
|
253
281
|
this.advance();
|
|
254
282
|
}
|
|
255
283
|
this.skipComments();
|
|
284
|
+
// A sub-parser that rejects its first token reports and returns
|
|
285
|
+
// without consuming it, so a loop keyed on that token would spin
|
|
286
|
+
// forever building diagnostics. Stop as soon as nothing moved.
|
|
287
|
+
if (this.pos === before)
|
|
288
|
+
break;
|
|
256
289
|
}
|
|
257
290
|
this.expect(TokenType.RBrace);
|
|
258
291
|
if (this.check(TokenType.With)) {
|
|
@@ -278,20 +311,21 @@ class Parser {
|
|
|
278
311
|
parseUpdateStatement() {
|
|
279
312
|
const start = this.currentPos();
|
|
280
313
|
const comments = this.collectLeadingComments();
|
|
281
|
-
this.
|
|
314
|
+
this.expectKeywordWithSpace(TokenType.Update);
|
|
282
315
|
const target = this.expectVariable();
|
|
283
316
|
let setAttributes;
|
|
284
317
|
let setMetadata;
|
|
285
318
|
this.skipComments();
|
|
286
319
|
while (this.check(TokenType.Set) && !this.isAtEnd()) {
|
|
320
|
+
const before = this.pos;
|
|
287
321
|
const setStart = this.currentPos();
|
|
288
|
-
this.advance();
|
|
322
|
+
const setTok = this.advance();
|
|
289
323
|
if (this.check(TokenType.Attributes)) {
|
|
290
|
-
this.
|
|
324
|
+
this.expectSecondWord(TokenType.Attributes, setTok);
|
|
291
325
|
setAttributes = this.parseSetAttributesBody(setStart);
|
|
292
326
|
}
|
|
293
327
|
else if (this.check(TokenType.Metadata)) {
|
|
294
|
-
this.
|
|
328
|
+
this.expectSecondWord(TokenType.Metadata, setTok);
|
|
295
329
|
setMetadata = this.parseSetMetadataBody(setStart);
|
|
296
330
|
}
|
|
297
331
|
else {
|
|
@@ -299,6 +333,11 @@ class Parser {
|
|
|
299
333
|
this.advance();
|
|
300
334
|
}
|
|
301
335
|
this.skipComments();
|
|
336
|
+
// A sub-parser that rejects its first token reports and returns
|
|
337
|
+
// without consuming it, so a loop keyed on that token would spin
|
|
338
|
+
// forever building diagnostics. Stop as soon as nothing moved.
|
|
339
|
+
if (this.pos === before)
|
|
340
|
+
break;
|
|
302
341
|
}
|
|
303
342
|
if (!setAttributes && !setMetadata) {
|
|
304
343
|
this.error(`Expected SET ATTRIBUTES or SET METADATA in UPDATE statement`, this.current());
|
|
@@ -325,8 +364,8 @@ class Parser {
|
|
|
325
364
|
parseMergeStatement() {
|
|
326
365
|
const start = this.currentPos();
|
|
327
366
|
const comments = this.collectLeadingComments();
|
|
328
|
-
this.expect(TokenType.Merge);
|
|
329
|
-
this.
|
|
367
|
+
const mergeTok = this.expect(TokenType.Merge);
|
|
368
|
+
this.expectSecondWord(TokenType.Concept, mergeTok);
|
|
330
369
|
const source = this.expectVariable();
|
|
331
370
|
this.expect(TokenType.Into);
|
|
332
371
|
const target = this.expectVariable();
|
|
@@ -346,7 +385,7 @@ class Parser {
|
|
|
346
385
|
parseDeleteStatement() {
|
|
347
386
|
const start = this.currentPos();
|
|
348
387
|
const comments = this.collectLeadingComments();
|
|
349
|
-
this.
|
|
388
|
+
this.expectKeywordWithSpace(TokenType.Delete);
|
|
350
389
|
let deleteType;
|
|
351
390
|
let keys;
|
|
352
391
|
let target;
|
|
@@ -419,7 +458,7 @@ class Parser {
|
|
|
419
458
|
parseDescribeStatement() {
|
|
420
459
|
const start = this.currentPos();
|
|
421
460
|
const comments = this.collectLeadingComments();
|
|
422
|
-
this.
|
|
461
|
+
this.expectKeywordWithSpace(TokenType.Describe);
|
|
423
462
|
let describeType;
|
|
424
463
|
let typeName;
|
|
425
464
|
let typeNameValue;
|
|
@@ -434,14 +473,14 @@ class Parser {
|
|
|
434
473
|
this.advance();
|
|
435
474
|
}
|
|
436
475
|
else if (this.check(TokenType.Concept)) {
|
|
437
|
-
this.advance();
|
|
476
|
+
const headTok = this.advance();
|
|
438
477
|
if (this.check(TokenType.Types)) {
|
|
439
478
|
describeType = 'CONCEPT_TYPES';
|
|
440
|
-
this.
|
|
479
|
+
this.expectSecondWord(TokenType.Types, headTok);
|
|
441
480
|
}
|
|
442
481
|
else if (this.check(TokenType.Type)) {
|
|
443
482
|
describeType = 'CONCEPT_TYPE';
|
|
444
|
-
this.
|
|
483
|
+
this.expectSecondWord(TokenType.Type, headTok);
|
|
445
484
|
typeNameValue = this.parseStringOrParameterValue('DESCRIBE CONCEPT TYPE');
|
|
446
485
|
typeName =
|
|
447
486
|
typeNameValue.kind === 'StringLiteral'
|
|
@@ -454,14 +493,14 @@ class Parser {
|
|
|
454
493
|
}
|
|
455
494
|
}
|
|
456
495
|
else if (this.check(TokenType.Proposition)) {
|
|
457
|
-
this.advance();
|
|
496
|
+
const headTok = this.advance();
|
|
458
497
|
if (this.check(TokenType.Types)) {
|
|
459
498
|
describeType = 'PROPOSITION_TYPES';
|
|
460
|
-
this.
|
|
499
|
+
this.expectSecondWord(TokenType.Types, headTok);
|
|
461
500
|
}
|
|
462
501
|
else if (this.check(TokenType.Type)) {
|
|
463
502
|
describeType = 'PROPOSITION_TYPE';
|
|
464
|
-
this.
|
|
503
|
+
this.expectSecondWord(TokenType.Type, headTok);
|
|
465
504
|
typeNameValue = this.parseStringOrParameterValue('DESCRIBE PROPOSITION TYPE');
|
|
466
505
|
typeName =
|
|
467
506
|
typeNameValue.kind === 'StringLiteral'
|
|
@@ -477,10 +516,18 @@ class Parser {
|
|
|
477
516
|
this.error(`Expected PRIMER, DOMAINS, CONCEPT, or PROPOSITION after DESCRIBE`, this.current());
|
|
478
517
|
describeType = 'PRIMER';
|
|
479
518
|
}
|
|
519
|
+
// Only the plural `... TYPES` forms are paginated (§5.1.3 / §5.1.5).
|
|
520
|
+
const paginable = describeType === 'CONCEPT_TYPES' || describeType === 'PROPOSITION_TYPES';
|
|
480
521
|
if (this.check(TokenType.Limit)) {
|
|
522
|
+
if (!paginable) {
|
|
523
|
+
this.error(`LIMIT is only valid on DESCRIBE CONCEPT TYPES / PROPOSITION TYPES`, this.current());
|
|
524
|
+
}
|
|
481
525
|
limit = this.parseLimitClause();
|
|
482
526
|
}
|
|
483
527
|
if (this.check(TokenType.Cursor)) {
|
|
528
|
+
if (!paginable) {
|
|
529
|
+
this.error(`CURSOR is only valid on DESCRIBE CONCEPT TYPES / PROPOSITION TYPES`, this.current());
|
|
530
|
+
}
|
|
484
531
|
cursor = this.parseCursorClause();
|
|
485
532
|
}
|
|
486
533
|
return {
|
|
@@ -500,15 +547,15 @@ class Parser {
|
|
|
500
547
|
parseSearchStatement() {
|
|
501
548
|
const start = this.currentPos();
|
|
502
549
|
const comments = this.collectLeadingComments();
|
|
503
|
-
this.
|
|
550
|
+
this.expectKeywordWithSpace(TokenType.Search);
|
|
504
551
|
let searchTarget;
|
|
505
552
|
if (this.check(TokenType.Concept)) {
|
|
506
553
|
searchTarget = 'CONCEPT';
|
|
507
|
-
this.
|
|
554
|
+
this.expectKeywordWithSpace(TokenType.Concept);
|
|
508
555
|
}
|
|
509
556
|
else if (this.check(TokenType.Proposition)) {
|
|
510
557
|
searchTarget = 'PROPOSITION';
|
|
511
|
-
this.
|
|
558
|
+
this.expectKeywordWithSpace(TokenType.Proposition);
|
|
512
559
|
}
|
|
513
560
|
else {
|
|
514
561
|
this.error(`Expected CONCEPT or PROPOSITION after SEARCH`, this.current());
|
|
@@ -522,10 +569,15 @@ class Parser {
|
|
|
522
569
|
let modeValue;
|
|
523
570
|
let threshold;
|
|
524
571
|
let limit;
|
|
572
|
+
// Clauses may appear in any order but each at most once — a second
|
|
573
|
+
// `LIMIT` is trailing input, not an override.
|
|
525
574
|
while (!this.isAtEnd()) {
|
|
575
|
+
const before = this.pos;
|
|
576
|
+
const clause = this.current();
|
|
526
577
|
if (this.check(TokenType.With)) {
|
|
527
|
-
this.
|
|
528
|
-
this.
|
|
578
|
+
this.rejectRepeat(withTypeValue, 'WITH TYPE', clause);
|
|
579
|
+
const withTok = this.advance();
|
|
580
|
+
this.expectSecondWord(TokenType.Type, withTok);
|
|
529
581
|
withTypeValue = this.parseStringOrParameterValue('SEARCH WITH TYPE');
|
|
530
582
|
withType =
|
|
531
583
|
withTypeValue.kind === 'StringLiteral'
|
|
@@ -533,20 +585,28 @@ class Parser {
|
|
|
533
585
|
: withTypeValue.name;
|
|
534
586
|
}
|
|
535
587
|
else if (this.check(TokenType.Mode)) {
|
|
588
|
+
this.rejectRepeat(modeValue, 'MODE', clause);
|
|
536
589
|
this.advance();
|
|
537
590
|
modeValue = this.parseStringOrParameterValue('SEARCH MODE');
|
|
538
591
|
mode =
|
|
539
592
|
modeValue.kind === 'StringLiteral' ? modeValue.parsed : modeValue.name;
|
|
540
593
|
}
|
|
541
594
|
else if (this.check(TokenType.Threshold)) {
|
|
595
|
+
this.rejectRepeat(threshold, 'THRESHOLD', clause);
|
|
542
596
|
threshold = this.parseThresholdClause();
|
|
543
597
|
}
|
|
544
598
|
else if (this.check(TokenType.Limit)) {
|
|
599
|
+
this.rejectRepeat(limit, 'LIMIT', clause);
|
|
545
600
|
limit = this.parseLimitClause();
|
|
546
601
|
}
|
|
547
602
|
else {
|
|
548
603
|
break;
|
|
549
604
|
}
|
|
605
|
+
// A sub-parser that rejects its first token reports and returns
|
|
606
|
+
// without consuming it, so a loop keyed on that token would spin
|
|
607
|
+
// forever building diagnostics. Stop as soon as nothing moved.
|
|
608
|
+
if (this.pos === before)
|
|
609
|
+
break;
|
|
550
610
|
}
|
|
551
611
|
return {
|
|
552
612
|
kind: 'SearchStatement',
|
|
@@ -569,18 +629,23 @@ class Parser {
|
|
|
569
629
|
parseExportStatement() {
|
|
570
630
|
const start = this.currentPos();
|
|
571
631
|
const comments = this.collectLeadingComments();
|
|
572
|
-
this.
|
|
632
|
+
this.expectKeywordWithSpace(TokenType.Export);
|
|
573
633
|
const target = this.expectVariable();
|
|
574
634
|
const where = this.parseWhereClause();
|
|
575
635
|
let limit;
|
|
636
|
+
let cursor;
|
|
576
637
|
if (this.check(TokenType.Limit)) {
|
|
577
638
|
limit = this.parseLimitClause();
|
|
578
639
|
}
|
|
640
|
+
if (this.check(TokenType.Cursor)) {
|
|
641
|
+
cursor = this.parseCursorClause();
|
|
642
|
+
}
|
|
579
643
|
return {
|
|
580
644
|
kind: 'ExportStatement',
|
|
581
645
|
target,
|
|
582
646
|
where,
|
|
583
647
|
limit,
|
|
648
|
+
cursor,
|
|
584
649
|
range: { start, end: this.currentPos() },
|
|
585
650
|
leadingComments: comments.length > 0 ? comments : undefined
|
|
586
651
|
};
|
|
@@ -604,6 +669,7 @@ class Parser {
|
|
|
604
669
|
const patterns = [];
|
|
605
670
|
this.skipComments();
|
|
606
671
|
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
672
|
+
const before = this.pos;
|
|
607
673
|
this.skipComments();
|
|
608
674
|
if (this.check(TokenType.RBrace))
|
|
609
675
|
break;
|
|
@@ -611,6 +677,11 @@ class Parser {
|
|
|
611
677
|
if (pattern)
|
|
612
678
|
patterns.push(pattern);
|
|
613
679
|
this.skipComments();
|
|
680
|
+
// A sub-parser that rejects its first token reports and returns
|
|
681
|
+
// without consuming it, so a loop keyed on that token would spin
|
|
682
|
+
// forever building diagnostics. Stop as soon as nothing moved.
|
|
683
|
+
if (this.pos === before)
|
|
684
|
+
break;
|
|
614
685
|
}
|
|
615
686
|
return patterns;
|
|
616
687
|
}
|
|
@@ -678,8 +749,12 @@ class Parser {
|
|
|
678
749
|
}
|
|
679
750
|
parseConceptMatcher() {
|
|
680
751
|
const start = this.currentPos();
|
|
681
|
-
this.expect(TokenType.LBrace);
|
|
682
|
-
const
|
|
752
|
+
const brace = this.expect(TokenType.LBrace);
|
|
753
|
+
const seen = { trailingComma: false };
|
|
754
|
+
const entries = this.parseObjectEntries(seen);
|
|
755
|
+
if (seen.trailingComma) {
|
|
756
|
+
this.error(`A concept matcher takes no trailing comma`, brace);
|
|
757
|
+
}
|
|
683
758
|
this.expect(TokenType.RBrace);
|
|
684
759
|
return {
|
|
685
760
|
kind: 'ConceptMatcher',
|
|
@@ -820,17 +895,11 @@ class Parser {
|
|
|
820
895
|
parseHopRange() {
|
|
821
896
|
const start = this.currentPos();
|
|
822
897
|
this.expect(TokenType.LBrace);
|
|
823
|
-
const
|
|
824
|
-
if (minTok.type !== TokenType.Number) {
|
|
825
|
-
this.error(`Expected number in hop range`, minTok);
|
|
826
|
-
}
|
|
827
|
-
const min = Number(minTok.value);
|
|
828
|
-
this.advance();
|
|
898
|
+
const min = this.expectHopCount();
|
|
829
899
|
let max;
|
|
830
900
|
if (this.match(TokenType.Comma)) {
|
|
831
901
|
if (this.check(TokenType.Number)) {
|
|
832
|
-
max =
|
|
833
|
-
this.advance();
|
|
902
|
+
max = this.expectHopCount();
|
|
834
903
|
}
|
|
835
904
|
// else: {m,} means unbounded
|
|
836
905
|
}
|
|
@@ -845,6 +914,27 @@ class Parser {
|
|
|
845
914
|
range: { start, end: this.currentPos() }
|
|
846
915
|
};
|
|
847
916
|
}
|
|
917
|
+
/**
|
|
918
|
+
* Reads one bound of a `{m,n}` hop quantifier.
|
|
919
|
+
*
|
|
920
|
+
* A hop count is a plain 16-bit integer — no sign, no decimal point, no
|
|
921
|
+
* exponent. `"p"{1e9,}` is not an enormous traversal, it is a typo, and
|
|
922
|
+
* accepting it would hand the engine a bound it cannot honour.
|
|
923
|
+
*/
|
|
924
|
+
expectHopCount() {
|
|
925
|
+
const tok = this.current();
|
|
926
|
+
if (tok.type !== TokenType.Number || !/^[0-9]+$/.test(tok.value)) {
|
|
927
|
+
this.error(`Expected a whole number in a hop range`, tok);
|
|
928
|
+
this.advance();
|
|
929
|
+
return 0;
|
|
930
|
+
}
|
|
931
|
+
const value = Number(tok.value);
|
|
932
|
+
if (value > 0xffff) {
|
|
933
|
+
this.error(`Hop count ${tok.value} exceeds the maximum of 65535`, tok);
|
|
934
|
+
}
|
|
935
|
+
this.advance();
|
|
936
|
+
return value;
|
|
937
|
+
}
|
|
848
938
|
parseFilterClause() {
|
|
849
939
|
const start = this.currentPos();
|
|
850
940
|
this.expect(TokenType.Filter);
|
|
@@ -921,11 +1011,22 @@ class Parser {
|
|
|
921
1011
|
const items = [];
|
|
922
1012
|
this.skipComments();
|
|
923
1013
|
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1014
|
+
const before = this.pos;
|
|
924
1015
|
this.skipComments();
|
|
925
1016
|
if (this.check(TokenType.RBrace))
|
|
926
1017
|
break;
|
|
927
1018
|
items.push(this.parsePropositionItem());
|
|
928
1019
|
this.skipComments();
|
|
1020
|
+
// Items are juxtaposed, but a separating comma — including a trailing
|
|
1021
|
+
// one — is tolerated. Generated KML reaches for it constantly, and the
|
|
1022
|
+
// reference grammar accepts it.
|
|
1023
|
+
this.match(TokenType.Comma);
|
|
1024
|
+
this.skipComments();
|
|
1025
|
+
// A sub-parser that rejects its first token reports and returns
|
|
1026
|
+
// without consuming it, so a loop keyed on that token would spin
|
|
1027
|
+
// forever building diagnostics. Stop as soon as nothing moved.
|
|
1028
|
+
if (this.pos === before)
|
|
1029
|
+
break;
|
|
929
1030
|
}
|
|
930
1031
|
this.expect(TokenType.RBrace);
|
|
931
1032
|
return {
|
|
@@ -954,8 +1055,19 @@ class Parser {
|
|
|
954
1055
|
};
|
|
955
1056
|
}
|
|
956
1057
|
isIdMatcherStart() {
|
|
957
|
-
|
|
958
|
-
|
|
1058
|
+
if (!this.isIdKeyToken(this.current()))
|
|
1059
|
+
return false;
|
|
1060
|
+
const next = this.peekPast(this.pos + 1);
|
|
1061
|
+
// `(id: "...")` may be written with a comment between the key and the
|
|
1062
|
+
// colon; comments are trivia everywhere else, so they are here too.
|
|
1063
|
+
return (next?.type === TokenType.Colon ||
|
|
1064
|
+
(next?.type === TokenType.Parameter && next.value.startsWith(':')));
|
|
1065
|
+
}
|
|
1066
|
+
/** The first non-comment token at or after `i`. */
|
|
1067
|
+
peekPast(i) {
|
|
1068
|
+
while (this.tokens[i]?.type === TokenType.Comment)
|
|
1069
|
+
i++;
|
|
1070
|
+
return this.tokens[i];
|
|
959
1071
|
}
|
|
960
1072
|
parseIdMatcherValue() {
|
|
961
1073
|
const keyTok = this.current();
|
|
@@ -974,7 +1086,7 @@ class Parser {
|
|
|
974
1086
|
return {
|
|
975
1087
|
kind: 'StringLiteral',
|
|
976
1088
|
value: tok.value,
|
|
977
|
-
parsed: this.unescapeString(tok.value),
|
|
1089
|
+
parsed: this.unescapeString(tok.value, tok),
|
|
978
1090
|
range: { start, end: this.currentPos() }
|
|
979
1091
|
};
|
|
980
1092
|
}
|
|
@@ -1000,8 +1112,8 @@ class Parser {
|
|
|
1000
1112
|
}
|
|
1001
1113
|
parseWithMetadata() {
|
|
1002
1114
|
const start = this.currentPos();
|
|
1003
|
-
this.expect(TokenType.With);
|
|
1004
|
-
this.
|
|
1115
|
+
const withTok = this.expect(TokenType.With);
|
|
1116
|
+
this.expectSecondWord(TokenType.Metadata, withTok);
|
|
1005
1117
|
this.expect(TokenType.LBrace);
|
|
1006
1118
|
const entries = this.parseObjectEntries();
|
|
1007
1119
|
this.expect(TokenType.RBrace);
|
|
@@ -1013,8 +1125,8 @@ class Parser {
|
|
|
1013
1125
|
}
|
|
1014
1126
|
parseExpectVersion() {
|
|
1015
1127
|
const start = this.currentPos();
|
|
1016
|
-
this.expect(TokenType.Expect);
|
|
1017
|
-
this.
|
|
1128
|
+
const expectTok = this.expect(TokenType.Expect);
|
|
1129
|
+
this.expectSecondWord(TokenType.Version, expectTok);
|
|
1018
1130
|
const value = this.parseNumberOrParameterValue('EXPECT VERSION');
|
|
1019
1131
|
return {
|
|
1020
1132
|
kind: 'ExpectVersion',
|
|
@@ -1027,8 +1139,8 @@ class Parser {
|
|
|
1027
1139
|
// ────────────────────────────────────────────────────────────────────
|
|
1028
1140
|
parseOrderBy() {
|
|
1029
1141
|
const start = this.currentPos();
|
|
1030
|
-
this.expect(TokenType.Order);
|
|
1031
|
-
this.
|
|
1142
|
+
const orderTok = this.expect(TokenType.Order);
|
|
1143
|
+
this.expectSecondWord(TokenType.By, orderTok);
|
|
1032
1144
|
const keys = [];
|
|
1033
1145
|
keys.push(this.parseOrderByKey());
|
|
1034
1146
|
while (this.match(TokenType.Comma)) {
|
|
@@ -1064,7 +1176,7 @@ class Parser {
|
|
|
1064
1176
|
}
|
|
1065
1177
|
parseThresholdClause() {
|
|
1066
1178
|
const start = this.currentPos();
|
|
1067
|
-
this.
|
|
1179
|
+
this.expectKeywordWithSpace(TokenType.Threshold);
|
|
1068
1180
|
const value = this.parseNumberOrParameterValue('THRESHOLD');
|
|
1069
1181
|
return {
|
|
1070
1182
|
kind: 'ThresholdClause',
|
|
@@ -1107,7 +1219,7 @@ class Parser {
|
|
|
1107
1219
|
}
|
|
1108
1220
|
parseLimitClause() {
|
|
1109
1221
|
const start = this.currentPos();
|
|
1110
|
-
this.
|
|
1222
|
+
this.expectKeywordWithSpace(TokenType.Limit);
|
|
1111
1223
|
const value = this.parseNumberOrParameterValue('LIMIT');
|
|
1112
1224
|
return {
|
|
1113
1225
|
kind: 'LimitClause',
|
|
@@ -1117,14 +1229,14 @@ class Parser {
|
|
|
1117
1229
|
}
|
|
1118
1230
|
parseCursorClause() {
|
|
1119
1231
|
const start = this.currentPos();
|
|
1120
|
-
this.
|
|
1232
|
+
this.expectKeywordWithSpace(TokenType.Cursor);
|
|
1121
1233
|
const tok = this.current();
|
|
1122
1234
|
let value;
|
|
1123
1235
|
if (tok.type === TokenType.String) {
|
|
1124
1236
|
value = {
|
|
1125
1237
|
kind: 'StringLiteral',
|
|
1126
1238
|
value: tok.value,
|
|
1127
|
-
parsed: this.unescapeString(tok.value),
|
|
1239
|
+
parsed: this.unescapeString(tok.value, tok),
|
|
1128
1240
|
range: { start: this.currentPos(), end: this.currentPos() }
|
|
1129
1241
|
};
|
|
1130
1242
|
this.advance();
|
|
@@ -1247,13 +1359,27 @@ class Parser {
|
|
|
1247
1359
|
name,
|
|
1248
1360
|
range: { start, end: this.currentPos() }
|
|
1249
1361
|
};
|
|
1250
|
-
// Dot access chain
|
|
1362
|
+
// Dot access chain. A dot path is written with no whitespace anywhere
|
|
1363
|
+
// inside it: `?x.name` is a path, but `?x. name` is a path followed by
|
|
1364
|
+
// stray input, and reading them alike would let `ORDER BY ?x.name. ASC`
|
|
1365
|
+
// silently sort by a field named `ASC` with no direction.
|
|
1366
|
+
let prevEnd = tok.offset + tok.value.length;
|
|
1251
1367
|
while (this.check(TokenType.Dot)) {
|
|
1368
|
+
const dotTok = this.current();
|
|
1369
|
+
if (dotTok.offset !== prevEnd) {
|
|
1370
|
+
this.error(`Unexpected whitespace before '.' in a dot path`, dotTok);
|
|
1371
|
+
break;
|
|
1372
|
+
}
|
|
1252
1373
|
this.advance();
|
|
1253
1374
|
const propTok = this.current();
|
|
1375
|
+
if (propTok.offset !== dotTok.offset + 1) {
|
|
1376
|
+
this.error(`Expected property name after '.'`, propTok);
|
|
1377
|
+
break;
|
|
1378
|
+
}
|
|
1254
1379
|
if (propTok.type === TokenType.Identifier ||
|
|
1255
1380
|
this.isNonAmbiguousKeyword(propTok.type)) {
|
|
1256
1381
|
const prop = propTok.value;
|
|
1382
|
+
prevEnd = propTok.offset + propTok.value.length;
|
|
1257
1383
|
this.advance();
|
|
1258
1384
|
expr = {
|
|
1259
1385
|
kind: 'DotExpression',
|
|
@@ -1284,7 +1410,7 @@ class Parser {
|
|
|
1284
1410
|
return {
|
|
1285
1411
|
kind: 'StringLiteral',
|
|
1286
1412
|
value: tok.value,
|
|
1287
|
-
parsed: this.unescapeString(tok.value),
|
|
1413
|
+
parsed: this.unescapeString(tok.value, tok),
|
|
1288
1414
|
range: { start, end: this.currentPos() }
|
|
1289
1415
|
};
|
|
1290
1416
|
}
|
|
@@ -1329,6 +1455,7 @@ class Parser {
|
|
|
1329
1455
|
}
|
|
1330
1456
|
// System identifier as literal
|
|
1331
1457
|
if (tok.type === TokenType.SystemIdent) {
|
|
1458
|
+
this.error(`Unquoted value '${tok.value}': KIP values are JSON values, so write "${tok.value}"`, tok);
|
|
1332
1459
|
this.advance();
|
|
1333
1460
|
return {
|
|
1334
1461
|
kind: 'StringLiteral',
|
|
@@ -1337,8 +1464,12 @@ class Parser {
|
|
|
1337
1464
|
range: { start, end: this.currentPos() }
|
|
1338
1465
|
};
|
|
1339
1466
|
}
|
|
1340
|
-
//
|
|
1467
|
+
// A bare word is not a KIP value — only object *keys* may go unquoted, and
|
|
1468
|
+
// those never reach here. Recover as a string so the tree stays usable in
|
|
1469
|
+
// an editor, and report it: `lower` sees only the tree, so it is the
|
|
1470
|
+
// caller's error-diagnostic check that keeps this reading off the wire.
|
|
1341
1471
|
if (tok.type === TokenType.Identifier) {
|
|
1472
|
+
this.error(`Unquoted value '${tok.value}': KIP values are JSON values, so write "${tok.value}"`, tok);
|
|
1342
1473
|
this.advance();
|
|
1343
1474
|
return {
|
|
1344
1475
|
kind: 'StringLiteral',
|
|
@@ -1389,13 +1520,16 @@ class Parser {
|
|
|
1389
1520
|
const start = this.currentPos();
|
|
1390
1521
|
this.expect(TokenType.LBracket);
|
|
1391
1522
|
const elements = [];
|
|
1523
|
+
let trailingComma = false;
|
|
1392
1524
|
this.skipComments();
|
|
1393
1525
|
if (!this.check(TokenType.RBracket)) {
|
|
1394
1526
|
elements.push(this.parseExpression());
|
|
1395
1527
|
while (this.match(TokenType.Comma)) {
|
|
1396
1528
|
this.skipComments();
|
|
1397
|
-
if (this.check(TokenType.RBracket))
|
|
1529
|
+
if (this.check(TokenType.RBracket)) {
|
|
1530
|
+
trailingComma = true;
|
|
1398
1531
|
break;
|
|
1532
|
+
}
|
|
1399
1533
|
elements.push(this.parseExpression());
|
|
1400
1534
|
}
|
|
1401
1535
|
}
|
|
@@ -1404,30 +1538,34 @@ class Parser {
|
|
|
1404
1538
|
return {
|
|
1405
1539
|
kind: 'ArrayLiteral',
|
|
1406
1540
|
elements,
|
|
1541
|
+
trailingComma,
|
|
1407
1542
|
range: { start, end: this.currentPos() }
|
|
1408
1543
|
};
|
|
1409
1544
|
}
|
|
1410
1545
|
parseObjectLiteral() {
|
|
1411
1546
|
const start = this.currentPos();
|
|
1412
1547
|
this.expect(TokenType.LBrace);
|
|
1413
|
-
const
|
|
1548
|
+
const seen = { trailingComma: false };
|
|
1549
|
+
const entries = this.parseObjectEntries(seen);
|
|
1414
1550
|
this.expect(TokenType.RBrace);
|
|
1415
1551
|
return {
|
|
1416
1552
|
kind: 'ObjectLiteral',
|
|
1417
1553
|
entries,
|
|
1554
|
+
trailingComma: seen.trailingComma,
|
|
1418
1555
|
range: { start, end: this.currentPos() }
|
|
1419
1556
|
};
|
|
1420
1557
|
}
|
|
1421
|
-
parseObjectEntries() {
|
|
1558
|
+
parseObjectEntries(seen) {
|
|
1422
1559
|
const entries = [];
|
|
1423
1560
|
this.skipComments();
|
|
1424
1561
|
while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
|
|
1562
|
+
const before = this.pos;
|
|
1425
1563
|
this.skipComments();
|
|
1426
1564
|
if (this.check(TokenType.RBrace))
|
|
1427
1565
|
break;
|
|
1428
1566
|
const entryStart = this.currentPos();
|
|
1429
1567
|
const { key, isQuoted } = this.expectKeyWithQuoting();
|
|
1430
|
-
this.
|
|
1568
|
+
this.expectObjectColon(key);
|
|
1431
1569
|
const value = this.parseExpression();
|
|
1432
1570
|
entries.push({
|
|
1433
1571
|
kind: 'ObjectEntry',
|
|
@@ -1441,9 +1579,16 @@ class Parser {
|
|
|
1441
1579
|
break;
|
|
1442
1580
|
if (this.match(TokenType.Comma)) {
|
|
1443
1581
|
this.skipComments();
|
|
1582
|
+
if (seen && this.check(TokenType.RBrace))
|
|
1583
|
+
seen.trailingComma = true;
|
|
1444
1584
|
continue;
|
|
1445
1585
|
}
|
|
1446
1586
|
this.error(`Expected ',' or '}' after object entry`, this.current());
|
|
1587
|
+
// A sub-parser that rejects its first token reports and returns
|
|
1588
|
+
// without consuming it, so a loop keyed on that token would spin
|
|
1589
|
+
// forever building diagnostics. Stop as soon as nothing moved.
|
|
1590
|
+
if (this.pos === before)
|
|
1591
|
+
break;
|
|
1447
1592
|
}
|
|
1448
1593
|
return entries;
|
|
1449
1594
|
}
|
|
@@ -1491,6 +1636,28 @@ class Parser {
|
|
|
1491
1636
|
}
|
|
1492
1637
|
return this.advance();
|
|
1493
1638
|
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Consumes a keyword that the grammar requires to be followed by whitespace.
|
|
1641
|
+
*
|
|
1642
|
+
* Most KIP keywords only need a word boundary, so `WHERE{...}` is legal.
|
|
1643
|
+
* A handful — the statement introducers and the clause keywords whose
|
|
1644
|
+
* operand may itself start with a brace or a quote — require real
|
|
1645
|
+
* whitespace, which is what keeps `UPSERT{` from reading as a statement.
|
|
1646
|
+
* The distinction is per-keyword-position, not per-keyword, so it lives at
|
|
1647
|
+
* the call site rather than in the lexer.
|
|
1648
|
+
*/
|
|
1649
|
+
expectKeywordWithSpace(type) {
|
|
1650
|
+
const tok = this.current();
|
|
1651
|
+
if (tok.type !== type) {
|
|
1652
|
+
this.error(`Expected '${type}' but got '${tok.value}'`, tok);
|
|
1653
|
+
return tok;
|
|
1654
|
+
}
|
|
1655
|
+
const after = this.source[tok.offset + tok.value.length] ?? '';
|
|
1656
|
+
if (after !== ' ' && after !== '\t' && after !== '\r' && after !== '\n') {
|
|
1657
|
+
this.error(`'${tok.value}' must be followed by whitespace`, tok, 'KIP_1001');
|
|
1658
|
+
}
|
|
1659
|
+
return this.advance();
|
|
1660
|
+
}
|
|
1494
1661
|
expectVariable() {
|
|
1495
1662
|
const tok = this.current();
|
|
1496
1663
|
if (tok.type !== TokenType.Variable) {
|
|
@@ -1507,7 +1674,7 @@ class Parser {
|
|
|
1507
1674
|
return '';
|
|
1508
1675
|
}
|
|
1509
1676
|
this.advance();
|
|
1510
|
-
return this.unescapeString(tok.value);
|
|
1677
|
+
return this.unescapeString(tok.value, tok);
|
|
1511
1678
|
}
|
|
1512
1679
|
expectStringValue() {
|
|
1513
1680
|
const tok = this.current();
|
|
@@ -1516,29 +1683,13 @@ class Parser {
|
|
|
1516
1683
|
return '';
|
|
1517
1684
|
}
|
|
1518
1685
|
this.advance();
|
|
1519
|
-
return this.unescapeString(tok.value);
|
|
1520
|
-
}
|
|
1521
|
-
expectKey() {
|
|
1522
|
-
const tok = this.current();
|
|
1523
|
-
// Keys can be identifiers, strings, or even some keywords used as keys
|
|
1524
|
-
if (tok.type === TokenType.String) {
|
|
1525
|
-
this.advance();
|
|
1526
|
-
return this.unescapeString(tok.value);
|
|
1527
|
-
}
|
|
1528
|
-
if (tok.type === TokenType.Identifier ||
|
|
1529
|
-
this.isNonAmbiguousKeyword(tok.type)) {
|
|
1530
|
-
this.advance();
|
|
1531
|
-
return tok.value;
|
|
1532
|
-
}
|
|
1533
|
-
this.error(`Expected object key but got '${tok.value}'`, tok);
|
|
1534
|
-
this.advance();
|
|
1535
|
-
return tok.value;
|
|
1686
|
+
return this.unescapeString(tok.value, tok);
|
|
1536
1687
|
}
|
|
1537
1688
|
expectKeyWithQuoting() {
|
|
1538
1689
|
const tok = this.current();
|
|
1539
1690
|
if (tok.type === TokenType.String) {
|
|
1540
1691
|
this.advance();
|
|
1541
|
-
return { key: this.unescapeString(tok.value), isQuoted: true };
|
|
1692
|
+
return { key: this.unescapeString(tok.value, tok), isQuoted: true };
|
|
1542
1693
|
}
|
|
1543
1694
|
if (tok.type === TokenType.Identifier ||
|
|
1544
1695
|
this.isNonAmbiguousKeyword(tok.type)) {
|
|
@@ -1549,6 +1700,12 @@ class Parser {
|
|
|
1549
1700
|
this.advance();
|
|
1550
1701
|
return { key: tok.value, isQuoted: false };
|
|
1551
1702
|
}
|
|
1703
|
+
/** Reports a clause written twice in a statement that allows it once. */
|
|
1704
|
+
rejectRepeat(seen, name, tok) {
|
|
1705
|
+
if (seen !== undefined) {
|
|
1706
|
+
this.error(`Duplicate ${name} clause`, tok);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1552
1709
|
skipComments() {
|
|
1553
1710
|
while (this.pos < this.tokens.length &&
|
|
1554
1711
|
this.current().type === TokenType.Comment) {
|
|
@@ -1565,6 +1722,62 @@ class Parser {
|
|
|
1565
1722
|
}
|
|
1566
1723
|
return comments;
|
|
1567
1724
|
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Consume the `:` separating an object key from its value. A colon written
|
|
1727
|
+
* with no space before an identifier value (e.g. `status:active`) is lexed as
|
|
1728
|
+
* a single parameter placeholder token (`:active`), so surface a targeted hint
|
|
1729
|
+
* instead of the generic "Expected ':'" message.
|
|
1730
|
+
*/
|
|
1731
|
+
/**
|
|
1732
|
+
* Consumes the second word of a two-word keyword (`SET ATTRIBUTES`,
|
|
1733
|
+
* `ORDER BY`, `EXPECT VERSION`, ...).
|
|
1734
|
+
*
|
|
1735
|
+
* The grammar joins these with whitespace only. A comment between the words
|
|
1736
|
+
* is not a smaller gap, it is a different token sequence, and reading
|
|
1737
|
+
* `SET//c\nMETADATA` as `SET METADATA` would accept text the reference
|
|
1738
|
+
* grammar rejects.
|
|
1739
|
+
*/
|
|
1740
|
+
expectSecondWord(type, first) {
|
|
1741
|
+
const tok = this.current();
|
|
1742
|
+
const gap = this.source.slice(first.offset + first.value.length, tok.offset);
|
|
1743
|
+
if (tok.type === type && !/^\s+$/.test(gap)) {
|
|
1744
|
+
this.error(`'${first.value} ${tok.value}' must be separated by whitespace only`, tok);
|
|
1745
|
+
}
|
|
1746
|
+
return this.expect(type);
|
|
1747
|
+
}
|
|
1748
|
+
expectObjectColon(_key) {
|
|
1749
|
+
if (this.check(TokenType.Colon)) {
|
|
1750
|
+
this.advance();
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
// `{"a":true}` lexes as a key followed by the parameter `:true`, because
|
|
1754
|
+
// `:name` is the placeholder syntax and the lexer cannot see that this
|
|
1755
|
+
// colon separates a key from its value. In key position the separator
|
|
1756
|
+
// reading is the only valid one, so split the token back apart and re-lex
|
|
1757
|
+
// the tail as the value.
|
|
1758
|
+
const tok = this.current();
|
|
1759
|
+
if (tok.type === TokenType.Parameter) {
|
|
1760
|
+
this.splitParameterAfterColon(tok);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
this.expect(TokenType.Colon);
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Rewrites a `:value` parameter token in separator position into the value
|
|
1767
|
+
* tokens it spells, so the parser sees `: value`.
|
|
1768
|
+
*/
|
|
1769
|
+
splitParameterAfterColon(tok) {
|
|
1770
|
+
const tail = tok.value.slice(1);
|
|
1771
|
+
const retoken = tokenize(tail)
|
|
1772
|
+
.filter((t) => !isTrivia(t.type) && t.type !== TokenType.EOF)
|
|
1773
|
+
.map((t) => ({
|
|
1774
|
+
...t,
|
|
1775
|
+
offset: tok.offset + 1 + t.offset,
|
|
1776
|
+
line: tok.line,
|
|
1777
|
+
column: tok.column + 1 + t.column
|
|
1778
|
+
}));
|
|
1779
|
+
this.tokens.splice(this.pos, 1, ...retoken);
|
|
1780
|
+
}
|
|
1568
1781
|
isFunctionToken(type) {
|
|
1569
1782
|
return (type === TokenType.Count ||
|
|
1570
1783
|
type === TokenType.Sum ||
|
|
@@ -1607,15 +1820,31 @@ class Parser {
|
|
|
1607
1820
|
type === TokenType.Mode ||
|
|
1608
1821
|
type === TokenType.Threshold);
|
|
1609
1822
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1823
|
+
/**
|
|
1824
|
+
* Reads the value of a string token.
|
|
1825
|
+
*
|
|
1826
|
+
* KIP strings are JSON strings, so `"a\xb"` and an unterminated literal are
|
|
1827
|
+
* both errors — but an editor still wants a tree, so the malformed value is
|
|
1828
|
+
* recovered leniently *and* reported. The lenient reading survives into the
|
|
1829
|
+
* tree: `lower` is handed a `Program` and never sees a diagnostic, so a
|
|
1830
|
+
* caller must reject on `severity === 'error'` before lowering, or `"a\xb"`
|
|
1831
|
+
* reaches the engine as `axb`.
|
|
1832
|
+
*/
|
|
1833
|
+
unescapeString(raw, tok) {
|
|
1834
|
+
if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
|
|
1612
1835
|
try {
|
|
1613
1836
|
return JSON.parse(raw);
|
|
1614
1837
|
}
|
|
1615
1838
|
catch {
|
|
1839
|
+
if (tok) {
|
|
1840
|
+
this.error(`Invalid string literal ${raw}: KIP strings are JSON strings`, tok);
|
|
1841
|
+
}
|
|
1616
1842
|
raw = raw.slice(1, -1);
|
|
1617
1843
|
}
|
|
1618
1844
|
}
|
|
1845
|
+
else if (tok) {
|
|
1846
|
+
this.error(`Unterminated string literal ${raw}`, tok);
|
|
1847
|
+
}
|
|
1619
1848
|
return raw.replace(/\\(.)/g, (_, ch) => {
|
|
1620
1849
|
switch (ch) {
|
|
1621
1850
|
case 'n':
|
|
@@ -1635,7 +1864,7 @@ class Parser {
|
|
|
1635
1864
|
}
|
|
1636
1865
|
});
|
|
1637
1866
|
}
|
|
1638
|
-
error(message, token) {
|
|
1867
|
+
error(message, token, code = 'KIP_1001') {
|
|
1639
1868
|
this.diagnostics.push({
|
|
1640
1869
|
range: {
|
|
1641
1870
|
start: { line: token.line, column: token.column },
|
|
@@ -1643,7 +1872,7 @@ class Parser {
|
|
|
1643
1872
|
},
|
|
1644
1873
|
severity: 'error',
|
|
1645
1874
|
message,
|
|
1646
|
-
code
|
|
1875
|
+
code
|
|
1647
1876
|
});
|
|
1648
1877
|
}
|
|
1649
1878
|
recoverToNextStatement() {
|