@ldclabs/kip-lang 0.1.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/dist/parser.js ADDED
@@ -0,0 +1,1359 @@
1
+ import { tokenize } from './lexer.js';
2
+ import { TokenType, isTrivia } from './token.js';
3
+ export function parse(source) {
4
+ const allTokens = tokenize(source);
5
+ const parser = new Parser(allTokens, source);
6
+ return parser.parse();
7
+ }
8
+ class Parser {
9
+ tokens;
10
+ pos = 0;
11
+ diagnostics = [];
12
+ source;
13
+ constructor(tokens, source) {
14
+ // Filter out trivia for parsing, but keep comments for attachment later
15
+ this.tokens = tokens.filter((t) => !isTrivia(t.type) || t.type === TokenType.Comment);
16
+ this.source = source;
17
+ }
18
+ parse() {
19
+ const statements = [];
20
+ const start = this.currentPos();
21
+ this.skipComments();
22
+ while (!this.isAtEnd()) {
23
+ this.skipComments();
24
+ if (this.isAtEnd())
25
+ break;
26
+ try {
27
+ const stmt = this.parseStatement();
28
+ if (stmt)
29
+ statements.push(stmt);
30
+ }
31
+ catch {
32
+ // Error recovery: skip to next statement-level keyword
33
+ this.recoverToNextStatement();
34
+ }
35
+ }
36
+ const end = this.currentPos();
37
+ return {
38
+ ast: { kind: 'Program', statements, range: { start, end } },
39
+ diagnostics: this.diagnostics
40
+ };
41
+ }
42
+ // ────────────────────────────────────────────────────────────────────
43
+ // Statement dispatch
44
+ // ────────────────────────────────────────────────────────────────────
45
+ parseStatement() {
46
+ const tok = this.current();
47
+ switch (tok.type) {
48
+ case TokenType.Find:
49
+ return this.parseFindStatement();
50
+ case TokenType.Upsert:
51
+ return this.parseUpsertStatement();
52
+ case TokenType.Delete:
53
+ return this.parseDeleteStatement();
54
+ case TokenType.Describe:
55
+ return this.parseDescribeStatement();
56
+ case TokenType.Search:
57
+ return this.parseSearchStatement();
58
+ default:
59
+ this.error(`Unexpected token '${tok.value}', expected a statement keyword (FIND, UPSERT, DELETE, DESCRIBE, SEARCH)`, tok);
60
+ this.advance();
61
+ return null;
62
+ }
63
+ }
64
+ // ────────────────────────────────────────────────────────────────────
65
+ // FIND
66
+ // ────────────────────────────────────────────────────────────────────
67
+ parseFindStatement() {
68
+ const start = this.currentPos();
69
+ const comments = this.collectLeadingComments();
70
+ this.expect(TokenType.Find);
71
+ this.expect(TokenType.LParen);
72
+ const projections = [];
73
+ if (!this.check(TokenType.RParen)) {
74
+ projections.push(this.parseExpression());
75
+ while (this.match(TokenType.Comma)) {
76
+ projections.push(this.parseExpression());
77
+ }
78
+ }
79
+ this.expect(TokenType.RParen);
80
+ let where;
81
+ let orderBy;
82
+ let limit;
83
+ let cursor;
84
+ if (this.check(TokenType.Where)) {
85
+ where = this.parseWhereClause();
86
+ }
87
+ if (this.check(TokenType.Order)) {
88
+ orderBy = this.parseOrderBy();
89
+ }
90
+ if (this.check(TokenType.Limit)) {
91
+ limit = this.parseLimitClause();
92
+ }
93
+ if (this.check(TokenType.Cursor)) {
94
+ cursor = this.parseCursorClause();
95
+ }
96
+ return {
97
+ kind: 'FindStatement',
98
+ projections,
99
+ where,
100
+ orderBy,
101
+ limit,
102
+ cursor,
103
+ range: { start, end: this.currentPos() },
104
+ leadingComments: comments.length > 0 ? comments : undefined
105
+ };
106
+ }
107
+ // ────────────────────────────────────────────────────────────────────
108
+ // UPSERT
109
+ // ────────────────────────────────────────────────────────────────────
110
+ parseUpsertStatement() {
111
+ const start = this.currentPos();
112
+ const comments = this.collectLeadingComments();
113
+ this.expect(TokenType.Upsert);
114
+ this.expect(TokenType.LBrace);
115
+ const blocks = [];
116
+ this.skipComments();
117
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
118
+ this.skipComments();
119
+ if (this.check(TokenType.Concept)) {
120
+ blocks.push(this.parseConceptBlock());
121
+ }
122
+ else if (this.check(TokenType.Proposition)) {
123
+ blocks.push(this.parsePropositionBlock());
124
+ }
125
+ else if (this.check(TokenType.RBrace)) {
126
+ break;
127
+ }
128
+ else {
129
+ this.error(`Expected CONCEPT or PROPOSITION inside UPSERT block`, this.current());
130
+ this.advance();
131
+ }
132
+ this.skipComments();
133
+ }
134
+ this.expect(TokenType.RBrace);
135
+ let metadata;
136
+ if (this.check(TokenType.With)) {
137
+ metadata = this.parseWithMetadata();
138
+ }
139
+ return {
140
+ kind: 'UpsertStatement',
141
+ blocks,
142
+ metadata,
143
+ range: { start, end: this.currentPos() },
144
+ leadingComments: comments.length > 0 ? comments : undefined
145
+ };
146
+ }
147
+ parseConceptBlock() {
148
+ const start = this.currentPos();
149
+ const comments = this.collectLeadingComments();
150
+ this.expect(TokenType.Concept);
151
+ const handle = this.expectVariable();
152
+ this.expect(TokenType.LBrace);
153
+ const matcher = this.parseConceptMatcher();
154
+ let setAttributes;
155
+ let setPropositions;
156
+ let metadata;
157
+ this.skipComments();
158
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
159
+ this.skipComments();
160
+ if (this.check(TokenType.Set)) {
161
+ const setStart = this.currentPos();
162
+ this.advance(); // skip SET
163
+ if (this.check(TokenType.Attributes)) {
164
+ this.advance();
165
+ setAttributes = this.parseSetAttributesBody(setStart);
166
+ }
167
+ else if (this.check(TokenType.Propositions)) {
168
+ this.advance();
169
+ setPropositions = this.parseSetPropositionsBody(setStart);
170
+ }
171
+ else {
172
+ this.error(`Expected ATTRIBUTES or PROPOSITIONS after SET`, this.current());
173
+ this.advance();
174
+ }
175
+ }
176
+ else if (this.check(TokenType.With)) {
177
+ metadata = this.parseWithMetadata();
178
+ }
179
+ else if (this.check(TokenType.RBrace)) {
180
+ break;
181
+ }
182
+ else {
183
+ this.skipComments();
184
+ if (this.check(TokenType.RBrace))
185
+ break;
186
+ this.error(`Unexpected token '${this.current().value}' in CONCEPT block`, this.current());
187
+ this.advance();
188
+ }
189
+ this.skipComments();
190
+ }
191
+ this.expect(TokenType.RBrace);
192
+ // Concept-level WITH METADATA (outside the CONCEPT braces)
193
+ if (!metadata && this.check(TokenType.With)) {
194
+ metadata = this.parseWithMetadata();
195
+ }
196
+ return {
197
+ kind: 'ConceptBlock',
198
+ handle,
199
+ matcher,
200
+ setAttributes,
201
+ setPropositions,
202
+ metadata,
203
+ range: { start, end: this.currentPos() },
204
+ leadingComments: comments.length > 0 ? comments : undefined
205
+ };
206
+ }
207
+ parsePropositionBlock() {
208
+ const start = this.currentPos();
209
+ const comments = this.collectLeadingComments();
210
+ this.expect(TokenType.Proposition);
211
+ let handle;
212
+ if (this.check(TokenType.Variable)) {
213
+ handle = this.expectVariable();
214
+ }
215
+ this.expect(TokenType.LBrace);
216
+ // Parse the proposition pattern (subject, "predicate", object)
217
+ this.expect(TokenType.LParen);
218
+ const subject = this.parsePropositionEndpoint();
219
+ this.expect(TokenType.Comma);
220
+ const predicate = this.parsePredicateExpr();
221
+ this.expect(TokenType.Comma);
222
+ const object = this.parsePropositionEndpoint();
223
+ this.expect(TokenType.RParen);
224
+ let setAttributes;
225
+ let metadata;
226
+ this.skipComments();
227
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
228
+ this.skipComments();
229
+ if (this.check(TokenType.Set)) {
230
+ const setStart = this.currentPos();
231
+ this.advance();
232
+ if (this.check(TokenType.Attributes)) {
233
+ this.advance();
234
+ setAttributes = this.parseSetAttributesBody(setStart);
235
+ }
236
+ else {
237
+ this.error(`Expected ATTRIBUTES after SET in PROPOSITION block`, this.current());
238
+ this.advance();
239
+ }
240
+ }
241
+ else if (this.check(TokenType.RBrace)) {
242
+ break;
243
+ }
244
+ else {
245
+ this.advance();
246
+ }
247
+ this.skipComments();
248
+ }
249
+ this.expect(TokenType.RBrace);
250
+ if (this.check(TokenType.With)) {
251
+ metadata = this.parseWithMetadata();
252
+ }
253
+ return {
254
+ kind: 'PropositionBlock',
255
+ handle,
256
+ subject,
257
+ predicate,
258
+ object,
259
+ setAttributes,
260
+ metadata,
261
+ range: { start, end: this.currentPos() },
262
+ leadingComments: comments.length > 0 ? comments : undefined
263
+ };
264
+ }
265
+ // ────────────────────────────────────────────────────────────────────
266
+ // DELETE
267
+ // ────────────────────────────────────────────────────────────────────
268
+ parseDeleteStatement() {
269
+ const start = this.currentPos();
270
+ const comments = this.collectLeadingComments();
271
+ this.expect(TokenType.Delete);
272
+ let deleteType;
273
+ let keys;
274
+ let target;
275
+ let detach = false;
276
+ if (this.check(TokenType.Attributes)) {
277
+ deleteType = 'ATTRIBUTES';
278
+ this.advance();
279
+ keys = this.parseDeleteKeySet();
280
+ this.expect(TokenType.From);
281
+ target = this.expectVariable();
282
+ }
283
+ else if (this.check(TokenType.Metadata)) {
284
+ deleteType = 'METADATA';
285
+ this.advance();
286
+ keys = this.parseDeleteKeySet();
287
+ this.expect(TokenType.From);
288
+ target = this.expectVariable();
289
+ }
290
+ else if (this.check(TokenType.Propositions)) {
291
+ deleteType = 'PROPOSITIONS';
292
+ this.advance();
293
+ target = this.expectVariable();
294
+ }
295
+ else if (this.check(TokenType.Concept)) {
296
+ deleteType = 'CONCEPT';
297
+ this.advance();
298
+ target = this.expectVariable();
299
+ if (this.check(TokenType.Detach)) {
300
+ detach = true;
301
+ this.advance();
302
+ }
303
+ }
304
+ else {
305
+ this.error(`Expected ATTRIBUTES, METADATA, PROPOSITIONS, or CONCEPT after DELETE`, this.current());
306
+ deleteType = 'ATTRIBUTES';
307
+ target = '?unknown';
308
+ }
309
+ const where = this.parseWhereClause();
310
+ return {
311
+ kind: 'DeleteStatement',
312
+ deleteType,
313
+ keys,
314
+ target,
315
+ detach: detach || undefined,
316
+ where,
317
+ range: { start, end: this.currentPos() },
318
+ leadingComments: comments.length > 0 ? comments : undefined
319
+ };
320
+ }
321
+ parseDeleteKeySet() {
322
+ this.expect(TokenType.LBrace);
323
+ const keys = [];
324
+ if (!this.check(TokenType.RBrace)) {
325
+ keys.push(this.expectString());
326
+ while (this.match(TokenType.Comma)) {
327
+ if (this.check(TokenType.RBrace))
328
+ break;
329
+ keys.push(this.expectString());
330
+ }
331
+ }
332
+ this.expect(TokenType.RBrace);
333
+ return keys;
334
+ }
335
+ // ────────────────────────────────────────────────────────────────────
336
+ // DESCRIBE
337
+ // ────────────────────────────────────────────────────────────────────
338
+ parseDescribeStatement() {
339
+ const start = this.currentPos();
340
+ const comments = this.collectLeadingComments();
341
+ this.expect(TokenType.Describe);
342
+ let describeType;
343
+ let typeName;
344
+ let limit;
345
+ let cursor;
346
+ if (this.check(TokenType.Primer)) {
347
+ describeType = 'PRIMER';
348
+ this.advance();
349
+ }
350
+ else if (this.check(TokenType.Domains)) {
351
+ describeType = 'DOMAINS';
352
+ this.advance();
353
+ }
354
+ else if (this.check(TokenType.Concept)) {
355
+ this.advance();
356
+ if (this.check(TokenType.Types)) {
357
+ describeType = 'CONCEPT_TYPES';
358
+ this.advance();
359
+ }
360
+ else if (this.check(TokenType.Type)) {
361
+ describeType = 'CONCEPT_TYPE';
362
+ this.advance();
363
+ typeName = this.expectStringValue();
364
+ }
365
+ else {
366
+ this.error(`Expected TYPE or TYPES after DESCRIBE CONCEPT`, this.current());
367
+ describeType = 'CONCEPT_TYPES';
368
+ }
369
+ }
370
+ else if (this.check(TokenType.Proposition)) {
371
+ this.advance();
372
+ if (this.check(TokenType.Types)) {
373
+ describeType = 'PROPOSITION_TYPES';
374
+ this.advance();
375
+ }
376
+ else if (this.check(TokenType.Type)) {
377
+ describeType = 'PROPOSITION_TYPE';
378
+ this.advance();
379
+ typeName = this.expectStringValue();
380
+ }
381
+ else {
382
+ this.error(`Expected TYPE or TYPES after DESCRIBE PROPOSITION`, this.current());
383
+ describeType = 'PROPOSITION_TYPES';
384
+ }
385
+ }
386
+ else {
387
+ this.error(`Expected PRIMER, DOMAINS, CONCEPT, or PROPOSITION after DESCRIBE`, this.current());
388
+ describeType = 'PRIMER';
389
+ }
390
+ if (this.check(TokenType.Limit)) {
391
+ limit = this.parseLimitClause();
392
+ }
393
+ if (this.check(TokenType.Cursor)) {
394
+ cursor = this.parseCursorClause();
395
+ }
396
+ return {
397
+ kind: 'DescribeStatement',
398
+ describeType,
399
+ typeName,
400
+ limit,
401
+ cursor,
402
+ range: { start, end: this.currentPos() },
403
+ leadingComments: comments.length > 0 ? comments : undefined
404
+ };
405
+ }
406
+ // ────────────────────────────────────────────────────────────────────
407
+ // SEARCH
408
+ // ────────────────────────────────────────────────────────────────────
409
+ parseSearchStatement() {
410
+ const start = this.currentPos();
411
+ const comments = this.collectLeadingComments();
412
+ this.expect(TokenType.Search);
413
+ let searchTarget;
414
+ if (this.check(TokenType.Concept)) {
415
+ searchTarget = 'CONCEPT';
416
+ this.advance();
417
+ }
418
+ else if (this.check(TokenType.Proposition)) {
419
+ searchTarget = 'PROPOSITION';
420
+ this.advance();
421
+ }
422
+ else {
423
+ this.error(`Expected CONCEPT or PROPOSITION after SEARCH`, this.current());
424
+ searchTarget = 'CONCEPT';
425
+ }
426
+ const term = this.expectStringValue();
427
+ let withType;
428
+ if (this.check(TokenType.With)) {
429
+ this.advance();
430
+ this.expect(TokenType.Type);
431
+ withType = this.expectStringValue();
432
+ }
433
+ let limit;
434
+ if (this.check(TokenType.Limit)) {
435
+ limit = this.parseLimitClause();
436
+ }
437
+ return {
438
+ kind: 'SearchStatement',
439
+ searchTarget,
440
+ term,
441
+ withType,
442
+ limit,
443
+ range: { start, end: this.currentPos() },
444
+ leadingComments: comments.length > 0 ? comments : undefined
445
+ };
446
+ }
447
+ // ────────────────────────────────────────────────────────────────────
448
+ // WHERE clause and patterns
449
+ // ────────────────────────────────────────────────────────────────────
450
+ parseWhereClause() {
451
+ const start = this.currentPos();
452
+ this.expect(TokenType.Where);
453
+ this.expect(TokenType.LBrace);
454
+ const patterns = this.parseWherePatterns();
455
+ this.expect(TokenType.RBrace);
456
+ return {
457
+ kind: 'WhereClause',
458
+ patterns,
459
+ range: { start, end: this.currentPos() }
460
+ };
461
+ }
462
+ parseWherePatterns() {
463
+ const patterns = [];
464
+ this.skipComments();
465
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
466
+ this.skipComments();
467
+ if (this.check(TokenType.RBrace))
468
+ break;
469
+ const pattern = this.parseWherePattern();
470
+ if (pattern)
471
+ patterns.push(pattern);
472
+ this.skipComments();
473
+ }
474
+ return patterns;
475
+ }
476
+ parseWherePattern() {
477
+ this.skipComments();
478
+ const tok = this.current();
479
+ if (tok.type === TokenType.Filter) {
480
+ return this.parseFilterClause();
481
+ }
482
+ if (tok.type === TokenType.Not) {
483
+ return this.parseNotClause();
484
+ }
485
+ if (tok.type === TokenType.Optional) {
486
+ return this.parseOptionalClause();
487
+ }
488
+ if (tok.type === TokenType.Union) {
489
+ return this.parseUnionClause();
490
+ }
491
+ // Variable: could be concept pattern or proposition pattern
492
+ if (tok.type === TokenType.Variable) {
493
+ return this.parseVariableLeadingPattern();
494
+ }
495
+ // Opening ( = proposition pattern without variable binding
496
+ if (tok.type === TokenType.LParen) {
497
+ return this.parsePropositionPatternBody(undefined);
498
+ }
499
+ this.error(`Unexpected token '${tok.value}' in WHERE clause`, tok);
500
+ this.advance();
501
+ return null;
502
+ }
503
+ parseVariableLeadingPattern() {
504
+ // ?var could be followed by:
505
+ // { ... } => concept pattern
506
+ // ( ... ) => proposition pattern
507
+ const start = this.currentPos();
508
+ const variable = this.expectVariable();
509
+ if (this.check(TokenType.LBrace)) {
510
+ return this.parseConceptPatternBody(variable, start);
511
+ }
512
+ if (this.check(TokenType.LParen)) {
513
+ return this.parsePropositionPatternBody(variable);
514
+ }
515
+ // Just a variable reference as a standalone concept pattern without matcher
516
+ // This occurs in WHERE like: ?drug {type: "Drug"}
517
+ this.error(`Expected '{' or '(' after variable '${variable}' in WHERE clause`, this.current());
518
+ return {
519
+ kind: 'ConceptPattern',
520
+ variable,
521
+ matcher: {
522
+ kind: 'ConceptMatcher',
523
+ entries: [],
524
+ range: { start, end: this.currentPos() }
525
+ },
526
+ range: { start, end: this.currentPos() }
527
+ };
528
+ }
529
+ parseConceptPatternBody(variable, start) {
530
+ const matcher = this.parseConceptMatcher();
531
+ return {
532
+ kind: 'ConceptPattern',
533
+ variable,
534
+ matcher,
535
+ range: { start, end: this.currentPos() }
536
+ };
537
+ }
538
+ parseConceptMatcher() {
539
+ const start = this.currentPos();
540
+ this.expect(TokenType.LBrace);
541
+ const entries = this.parseObjectEntries();
542
+ this.expect(TokenType.RBrace);
543
+ return {
544
+ kind: 'ConceptMatcher',
545
+ entries,
546
+ range: { start, end: this.currentPos() }
547
+ };
548
+ }
549
+ parsePropositionPatternBody(variable) {
550
+ const start = this.currentPos();
551
+ this.expect(TokenType.LParen);
552
+ const subject = this.parsePropositionEndpoint();
553
+ this.expect(TokenType.Comma);
554
+ const predicate = this.parsePredicateExpr();
555
+ this.expect(TokenType.Comma);
556
+ const object = this.parsePropositionEndpoint();
557
+ this.expect(TokenType.RParen);
558
+ return {
559
+ kind: 'PropositionPattern',
560
+ variable,
561
+ subject,
562
+ predicate,
563
+ object,
564
+ range: { start, end: this.currentPos() }
565
+ };
566
+ }
567
+ parsePropositionEndpoint() {
568
+ // Could be: ?var, {type: ..., name: ...}, or nested (subject, pred, object)
569
+ if (this.check(TokenType.Variable)) {
570
+ const start = this.currentPos();
571
+ const name = this.expectVariable();
572
+ return {
573
+ kind: 'VariableRef',
574
+ name,
575
+ range: { start, end: this.currentPos() }
576
+ };
577
+ }
578
+ if (this.check(TokenType.LBrace)) {
579
+ const start = this.currentPos();
580
+ const matcher = this.parseConceptMatcher();
581
+ return {
582
+ kind: 'ConceptPattern',
583
+ matcher,
584
+ range: { start, end: this.currentPos() }
585
+ };
586
+ }
587
+ if (this.check(TokenType.LParen)) {
588
+ return this.parsePropositionPatternBody(undefined);
589
+ }
590
+ this.error(`Expected variable, concept pattern, or proposition pattern`, this.current());
591
+ const start = this.currentPos();
592
+ return {
593
+ kind: 'VariableRef',
594
+ name: '?unknown',
595
+ range: { start, end: start }
596
+ };
597
+ }
598
+ parsePredicateExpr() {
599
+ const start = this.currentPos();
600
+ const first = this.parsePredicateLiteral();
601
+ // Check for alternation: "pred1" | "pred2"
602
+ if (this.check(TokenType.Pipe)) {
603
+ const predicates = [first];
604
+ while (this.match(TokenType.Pipe)) {
605
+ predicates.push(this.parsePredicateLiteral());
606
+ }
607
+ return {
608
+ kind: 'PredicateAlternation',
609
+ predicates,
610
+ range: { start, end: this.currentPos() }
611
+ };
612
+ }
613
+ return first;
614
+ }
615
+ parsePredicateLiteral() {
616
+ const start = this.currentPos();
617
+ const value = this.expectStringValue();
618
+ // Check for hop range: {m,n} {m,} {m}
619
+ let hopRange;
620
+ if (this.check(TokenType.LBrace)) {
621
+ hopRange = this.parseHopRange();
622
+ }
623
+ return {
624
+ kind: 'PredicateLiteral',
625
+ value,
626
+ hopRange,
627
+ range: { start, end: this.currentPos() }
628
+ };
629
+ }
630
+ parseHopRange() {
631
+ const start = this.currentPos();
632
+ this.expect(TokenType.LBrace);
633
+ const minTok = this.current();
634
+ if (minTok.type !== TokenType.Number) {
635
+ this.error(`Expected number in hop range`, minTok);
636
+ }
637
+ const min = Number(minTok.value);
638
+ this.advance();
639
+ let max;
640
+ if (this.match(TokenType.Comma)) {
641
+ if (this.check(TokenType.Number)) {
642
+ max = Number(this.current().value);
643
+ this.advance();
644
+ }
645
+ // else: {m,} means unbounded
646
+ }
647
+ else {
648
+ max = min; // {m} means exactly m
649
+ }
650
+ this.expect(TokenType.RBrace);
651
+ return {
652
+ kind: 'HopRange',
653
+ min,
654
+ max,
655
+ range: { start, end: this.currentPos() }
656
+ };
657
+ }
658
+ parseFilterClause() {
659
+ const start = this.currentPos();
660
+ this.expect(TokenType.Filter);
661
+ this.expect(TokenType.LParen);
662
+ const expression = this.parseExpression();
663
+ this.expect(TokenType.RParen);
664
+ return {
665
+ kind: 'FilterClause',
666
+ expression,
667
+ range: { start, end: this.currentPos() }
668
+ };
669
+ }
670
+ parseNotClause() {
671
+ const start = this.currentPos();
672
+ this.expect(TokenType.Not);
673
+ this.expect(TokenType.LBrace);
674
+ const patterns = this.parseWherePatterns();
675
+ this.expect(TokenType.RBrace);
676
+ return {
677
+ kind: 'NotClause',
678
+ patterns,
679
+ range: { start, end: this.currentPos() }
680
+ };
681
+ }
682
+ parseOptionalClause() {
683
+ const start = this.currentPos();
684
+ this.expect(TokenType.Optional);
685
+ this.expect(TokenType.LBrace);
686
+ const patterns = this.parseWherePatterns();
687
+ this.expect(TokenType.RBrace);
688
+ return {
689
+ kind: 'OptionalClause',
690
+ patterns,
691
+ range: { start, end: this.currentPos() }
692
+ };
693
+ }
694
+ parseUnionClause() {
695
+ const start = this.currentPos();
696
+ this.expect(TokenType.Union);
697
+ this.expect(TokenType.LBrace);
698
+ const patterns = this.parseWherePatterns();
699
+ this.expect(TokenType.RBrace);
700
+ return {
701
+ kind: 'UnionClause',
702
+ patterns,
703
+ range: { start, end: this.currentPos() }
704
+ };
705
+ }
706
+ // ────────────────────────────────────────────────────────────────────
707
+ // SET ATTRIBUTES / SET PROPOSITIONS / WITH METADATA
708
+ // ────────────────────────────────────────────────────────────────────
709
+ parseSetAttributesBody(start) {
710
+ this.expect(TokenType.LBrace);
711
+ const entries = this.parseObjectEntries();
712
+ this.expect(TokenType.RBrace);
713
+ return {
714
+ kind: 'SetAttributes',
715
+ entries,
716
+ range: { start, end: this.currentPos() }
717
+ };
718
+ }
719
+ parseSetPropositionsBody(start) {
720
+ this.expect(TokenType.LBrace);
721
+ const items = [];
722
+ this.skipComments();
723
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
724
+ this.skipComments();
725
+ if (this.check(TokenType.RBrace))
726
+ break;
727
+ items.push(this.parsePropositionItem());
728
+ this.skipComments();
729
+ }
730
+ this.expect(TokenType.RBrace);
731
+ return {
732
+ kind: 'SetPropositions',
733
+ items,
734
+ range: { start, end: this.currentPos() }
735
+ };
736
+ }
737
+ parsePropositionItem() {
738
+ const start = this.currentPos();
739
+ this.expect(TokenType.LParen);
740
+ const predicate = this.expectStringValue();
741
+ this.expect(TokenType.Comma);
742
+ const target = this.parsePropositionEndpoint();
743
+ this.expect(TokenType.RParen);
744
+ let metadata;
745
+ if (this.check(TokenType.With)) {
746
+ metadata = this.parseWithMetadata();
747
+ }
748
+ return {
749
+ kind: 'PropositionItem',
750
+ predicate,
751
+ target,
752
+ metadata,
753
+ range: { start, end: this.currentPos() }
754
+ };
755
+ }
756
+ parseWithMetadata() {
757
+ const start = this.currentPos();
758
+ this.expect(TokenType.With);
759
+ this.expect(TokenType.Metadata);
760
+ this.expect(TokenType.LBrace);
761
+ const entries = this.parseObjectEntries();
762
+ this.expect(TokenType.RBrace);
763
+ return {
764
+ kind: 'WithMetadata',
765
+ entries,
766
+ range: { start, end: this.currentPos() }
767
+ };
768
+ }
769
+ // ────────────────────────────────────────────────────────────────────
770
+ // ORDER BY, LIMIT, CURSOR
771
+ // ────────────────────────────────────────────────────────────────────
772
+ parseOrderBy() {
773
+ const start = this.currentPos();
774
+ this.expect(TokenType.Order);
775
+ this.expect(TokenType.By);
776
+ const expression = this.parseExpression();
777
+ let direction = 'ASC';
778
+ if (this.check(TokenType.Asc)) {
779
+ this.advance();
780
+ direction = 'ASC';
781
+ }
782
+ else if (this.check(TokenType.Desc)) {
783
+ this.advance();
784
+ direction = 'DESC';
785
+ }
786
+ return {
787
+ kind: 'OrderByClause',
788
+ expression,
789
+ direction,
790
+ range: { start, end: this.currentPos() }
791
+ };
792
+ }
793
+ parseLimitClause() {
794
+ const start = this.currentPos();
795
+ this.expect(TokenType.Limit);
796
+ const tok = this.current();
797
+ let value;
798
+ if (tok.type === TokenType.Number) {
799
+ value = {
800
+ kind: 'NumberLiteral',
801
+ value: Number(tok.value),
802
+ raw: tok.value,
803
+ range: { start: this.currentPos(), end: this.currentPos() }
804
+ };
805
+ this.advance();
806
+ value.range.end = this.currentPos();
807
+ }
808
+ else if (tok.type === TokenType.Parameter) {
809
+ value = {
810
+ kind: 'ParameterRef',
811
+ name: tok.value,
812
+ range: { start: this.currentPos(), end: this.currentPos() }
813
+ };
814
+ this.advance();
815
+ value.range.end = this.currentPos();
816
+ }
817
+ else {
818
+ this.error(`Expected number or parameter after LIMIT`, tok);
819
+ value = {
820
+ kind: 'NumberLiteral',
821
+ value: 0,
822
+ raw: '0',
823
+ range: { start: this.currentPos(), end: this.currentPos() }
824
+ };
825
+ }
826
+ return {
827
+ kind: 'LimitClause',
828
+ value,
829
+ range: { start, end: this.currentPos() }
830
+ };
831
+ }
832
+ parseCursorClause() {
833
+ const start = this.currentPos();
834
+ this.expect(TokenType.Cursor);
835
+ const tok = this.current();
836
+ let value;
837
+ if (tok.type === TokenType.String) {
838
+ value = {
839
+ kind: 'StringLiteral',
840
+ value: tok.value,
841
+ parsed: tok.value.slice(1, -1),
842
+ range: { start: this.currentPos(), end: this.currentPos() }
843
+ };
844
+ this.advance();
845
+ value.range.end = this.currentPos();
846
+ }
847
+ else if (tok.type === TokenType.Parameter) {
848
+ value = {
849
+ kind: 'ParameterRef',
850
+ name: tok.value,
851
+ range: { start: this.currentPos(), end: this.currentPos() }
852
+ };
853
+ this.advance();
854
+ value.range.end = this.currentPos();
855
+ }
856
+ else {
857
+ this.error(`Expected string or parameter after CURSOR`, tok);
858
+ value = {
859
+ kind: 'StringLiteral',
860
+ value: '""',
861
+ parsed: '',
862
+ range: { start: this.currentPos(), end: this.currentPos() }
863
+ };
864
+ }
865
+ return {
866
+ kind: 'CursorClause',
867
+ value,
868
+ range: { start, end: this.currentPos() }
869
+ };
870
+ }
871
+ // ────────────────────────────────────────────────────────────────────
872
+ // Expressions (for FILTER and FIND projections)
873
+ // ────────────────────────────────────────────────────────────────────
874
+ parseExpression() {
875
+ return this.parseOrExpression();
876
+ }
877
+ parseOrExpression() {
878
+ let left = this.parseAndExpression();
879
+ while (this.check(TokenType.Or)) {
880
+ const start = left.range.start;
881
+ this.advance();
882
+ const right = this.parseAndExpression();
883
+ left = {
884
+ kind: 'BinaryExpression',
885
+ operator: '||',
886
+ left,
887
+ right,
888
+ range: { start, end: right.range.end }
889
+ };
890
+ }
891
+ return left;
892
+ }
893
+ parseAndExpression() {
894
+ let left = this.parseComparisonExpression();
895
+ while (this.check(TokenType.And)) {
896
+ const start = left.range.start;
897
+ this.advance();
898
+ const right = this.parseComparisonExpression();
899
+ left = {
900
+ kind: 'BinaryExpression',
901
+ operator: '&&',
902
+ left,
903
+ right,
904
+ range: { start, end: right.range.end }
905
+ };
906
+ }
907
+ return left;
908
+ }
909
+ parseComparisonExpression() {
910
+ let left = this.parseUnaryExpression();
911
+ const compOps = [
912
+ TokenType.Eq,
913
+ TokenType.NotEq,
914
+ TokenType.Lt,
915
+ TokenType.Gt,
916
+ TokenType.LtEq,
917
+ TokenType.GtEq
918
+ ];
919
+ if (compOps.includes(this.current().type)) {
920
+ const start = left.range.start;
921
+ const op = this.current().value;
922
+ this.advance();
923
+ const right = this.parseUnaryExpression();
924
+ left = {
925
+ kind: 'BinaryExpression',
926
+ operator: op,
927
+ left,
928
+ right,
929
+ range: { start, end: right.range.end }
930
+ };
931
+ }
932
+ return left;
933
+ }
934
+ parseUnaryExpression() {
935
+ if (this.check(TokenType.Bang)) {
936
+ const start = this.currentPos();
937
+ this.advance();
938
+ const operand = this.parseUnaryExpression();
939
+ return {
940
+ kind: 'UnaryExpression',
941
+ operator: '!',
942
+ operand,
943
+ range: { start, end: operand.range.end }
944
+ };
945
+ }
946
+ return this.parsePrimaryExpression();
947
+ }
948
+ parsePrimaryExpression() {
949
+ const tok = this.current();
950
+ const start = this.currentPos();
951
+ // Function call: NAME(...)
952
+ if (this.isFunctionToken(tok.type)) {
953
+ return this.parseFunctionCall();
954
+ }
955
+ // Variable (may have dot access)
956
+ if (tok.type === TokenType.Variable) {
957
+ const name = tok.value;
958
+ this.advance();
959
+ let expr = {
960
+ kind: 'VariableRef',
961
+ name,
962
+ range: { start, end: this.currentPos() }
963
+ };
964
+ // Dot access chain
965
+ while (this.check(TokenType.Dot)) {
966
+ this.advance();
967
+ const propTok = this.current();
968
+ if (propTok.type === TokenType.Identifier ||
969
+ this.isNonAmbiguousKeyword(propTok.type)) {
970
+ const prop = propTok.value;
971
+ this.advance();
972
+ expr = {
973
+ kind: 'DotExpression',
974
+ object: expr,
975
+ property: prop,
976
+ range: { start, end: this.currentPos() }
977
+ };
978
+ }
979
+ else {
980
+ this.error(`Expected property name after '.'`, propTok);
981
+ break;
982
+ }
983
+ }
984
+ return expr;
985
+ }
986
+ // Parameter ref
987
+ if (tok.type === TokenType.Parameter) {
988
+ this.advance();
989
+ return {
990
+ kind: 'ParameterRef',
991
+ name: tok.value,
992
+ range: { start, end: this.currentPos() }
993
+ };
994
+ }
995
+ // String literal
996
+ if (tok.type === TokenType.String) {
997
+ this.advance();
998
+ return {
999
+ kind: 'StringLiteral',
1000
+ value: tok.value,
1001
+ parsed: this.unescapeString(tok.value),
1002
+ range: { start, end: this.currentPos() }
1003
+ };
1004
+ }
1005
+ // Number literal
1006
+ if (tok.type === TokenType.Number) {
1007
+ this.advance();
1008
+ return {
1009
+ kind: 'NumberLiteral',
1010
+ value: Number(tok.value),
1011
+ raw: tok.value,
1012
+ range: { start, end: this.currentPos() }
1013
+ };
1014
+ }
1015
+ // Boolean
1016
+ if (tok.type === TokenType.Boolean) {
1017
+ this.advance();
1018
+ return {
1019
+ kind: 'BooleanLiteral',
1020
+ value: tok.value === 'true',
1021
+ range: { start, end: this.currentPos() }
1022
+ };
1023
+ }
1024
+ // Null
1025
+ if (tok.type === TokenType.Null) {
1026
+ this.advance();
1027
+ return { kind: 'NullLiteral', range: { start, end: this.currentPos() } };
1028
+ }
1029
+ // Array
1030
+ if (tok.type === TokenType.LBracket) {
1031
+ return this.parseArrayLiteral();
1032
+ }
1033
+ // Object
1034
+ if (tok.type === TokenType.LBrace) {
1035
+ return this.parseObjectLiteral();
1036
+ }
1037
+ // Parenthesized expression
1038
+ if (tok.type === TokenType.LParen) {
1039
+ this.advance();
1040
+ const expr = this.parseExpression();
1041
+ this.expect(TokenType.RParen);
1042
+ return expr;
1043
+ }
1044
+ // System identifier as literal
1045
+ if (tok.type === TokenType.SystemIdent) {
1046
+ this.advance();
1047
+ return {
1048
+ kind: 'StringLiteral',
1049
+ value: `"${tok.value}"`,
1050
+ parsed: tok.value,
1051
+ range: { start, end: this.currentPos() }
1052
+ };
1053
+ }
1054
+ // Identifier (bare word — could be used as a key value)
1055
+ if (tok.type === TokenType.Identifier) {
1056
+ this.advance();
1057
+ return {
1058
+ kind: 'StringLiteral',
1059
+ value: `"${tok.value}"`,
1060
+ parsed: tok.value,
1061
+ range: { start, end: this.currentPos() }
1062
+ };
1063
+ }
1064
+ this.error(`Unexpected token '${tok.value}' in expression`, tok);
1065
+ this.advance();
1066
+ return { kind: 'NullLiteral', range: { start, end: this.currentPos() } };
1067
+ }
1068
+ parseFunctionCall() {
1069
+ const start = this.currentPos();
1070
+ const name = this.current().value;
1071
+ this.advance();
1072
+ this.expect(TokenType.LParen);
1073
+ const args = [];
1074
+ if (!this.check(TokenType.RParen)) {
1075
+ // Handle DISTINCT keyword inside COUNT
1076
+ if (this.current().type === TokenType.Distinct) {
1077
+ const dStart = this.currentPos();
1078
+ this.advance();
1079
+ const innerArg = this.parseExpression();
1080
+ args.push({
1081
+ kind: 'FunctionCallExpr',
1082
+ name: 'DISTINCT',
1083
+ args: [innerArg],
1084
+ range: { start: dStart, end: this.currentPos() }
1085
+ });
1086
+ }
1087
+ else {
1088
+ args.push(this.parseExpression());
1089
+ }
1090
+ while (this.match(TokenType.Comma)) {
1091
+ args.push(this.parseExpression());
1092
+ }
1093
+ }
1094
+ this.expect(TokenType.RParen);
1095
+ return {
1096
+ kind: 'FunctionCallExpr',
1097
+ name,
1098
+ args,
1099
+ range: { start, end: this.currentPos() }
1100
+ };
1101
+ }
1102
+ parseArrayLiteral() {
1103
+ const start = this.currentPos();
1104
+ this.expect(TokenType.LBracket);
1105
+ const elements = [];
1106
+ this.skipComments();
1107
+ if (!this.check(TokenType.RBracket)) {
1108
+ elements.push(this.parseExpression());
1109
+ while (this.match(TokenType.Comma)) {
1110
+ this.skipComments();
1111
+ if (this.check(TokenType.RBracket))
1112
+ break;
1113
+ elements.push(this.parseExpression());
1114
+ }
1115
+ }
1116
+ this.skipComments();
1117
+ this.expect(TokenType.RBracket);
1118
+ return {
1119
+ kind: 'ArrayLiteral',
1120
+ elements,
1121
+ range: { start, end: this.currentPos() }
1122
+ };
1123
+ }
1124
+ parseObjectLiteral() {
1125
+ const start = this.currentPos();
1126
+ this.expect(TokenType.LBrace);
1127
+ const entries = this.parseObjectEntries();
1128
+ this.expect(TokenType.RBrace);
1129
+ return {
1130
+ kind: 'ObjectLiteral',
1131
+ entries,
1132
+ range: { start, end: this.currentPos() }
1133
+ };
1134
+ }
1135
+ parseObjectEntries() {
1136
+ const entries = [];
1137
+ this.skipComments();
1138
+ while (!this.check(TokenType.RBrace) && !this.isAtEnd()) {
1139
+ this.skipComments();
1140
+ if (this.check(TokenType.RBrace))
1141
+ break;
1142
+ const entryStart = this.currentPos();
1143
+ const { key, isQuoted } = this.expectKeyWithQuoting();
1144
+ this.expect(TokenType.Colon);
1145
+ const value = this.parseExpression();
1146
+ entries.push({
1147
+ kind: 'ObjectEntry',
1148
+ key,
1149
+ isQuoted,
1150
+ value,
1151
+ range: { start: entryStart, end: this.currentPos() }
1152
+ });
1153
+ // Optional comma
1154
+ this.match(TokenType.Comma);
1155
+ this.skipComments();
1156
+ }
1157
+ return entries;
1158
+ }
1159
+ // ────────────────────────────────────────────────────────────────────
1160
+ // Helpers
1161
+ // ────────────────────────────────────────────────────────────────────
1162
+ current() {
1163
+ return (this.tokens[this.pos] ?? {
1164
+ type: TokenType.EOF,
1165
+ value: '',
1166
+ offset: this.source.length,
1167
+ line: 0,
1168
+ column: 0
1169
+ });
1170
+ }
1171
+ currentPos() {
1172
+ const tok = this.current();
1173
+ return { line: tok.line, column: tok.column };
1174
+ }
1175
+ isAtEnd() {
1176
+ return (this.pos >= this.tokens.length || this.current().type === TokenType.EOF);
1177
+ }
1178
+ check(type) {
1179
+ return this.current().type === type;
1180
+ }
1181
+ match(type) {
1182
+ if (this.check(type)) {
1183
+ this.advance();
1184
+ return true;
1185
+ }
1186
+ return false;
1187
+ }
1188
+ advance() {
1189
+ const tok = this.current();
1190
+ if (!this.isAtEnd())
1191
+ this.pos++;
1192
+ this.skipComments();
1193
+ return tok;
1194
+ }
1195
+ expect(type) {
1196
+ const tok = this.current();
1197
+ if (tok.type !== type) {
1198
+ this.error(`Expected '${type}' but got '${tok.value}'`, tok);
1199
+ return tok;
1200
+ }
1201
+ return this.advance();
1202
+ }
1203
+ expectVariable() {
1204
+ const tok = this.current();
1205
+ if (tok.type !== TokenType.Variable) {
1206
+ this.error(`Expected variable (e.g., ?name) but got '${tok.value}'`, tok);
1207
+ return '?unknown';
1208
+ }
1209
+ this.advance();
1210
+ return tok.value;
1211
+ }
1212
+ expectString() {
1213
+ const tok = this.current();
1214
+ if (tok.type !== TokenType.String) {
1215
+ this.error(`Expected string literal but got '${tok.value}'`, tok);
1216
+ return '';
1217
+ }
1218
+ this.advance();
1219
+ return this.unescapeString(tok.value);
1220
+ }
1221
+ expectStringValue() {
1222
+ const tok = this.current();
1223
+ if (tok.type !== TokenType.String) {
1224
+ this.error(`Expected quoted string but got '${tok.value}'`, tok);
1225
+ return '';
1226
+ }
1227
+ this.advance();
1228
+ return this.unescapeString(tok.value);
1229
+ }
1230
+ expectKey() {
1231
+ const tok = this.current();
1232
+ // Keys can be identifiers, strings, or even some keywords used as keys
1233
+ if (tok.type === TokenType.String) {
1234
+ this.advance();
1235
+ return this.unescapeString(tok.value);
1236
+ }
1237
+ if (tok.type === TokenType.Identifier ||
1238
+ this.isNonAmbiguousKeyword(tok.type)) {
1239
+ this.advance();
1240
+ return tok.value;
1241
+ }
1242
+ this.error(`Expected object key but got '${tok.value}'`, tok);
1243
+ this.advance();
1244
+ return tok.value;
1245
+ }
1246
+ expectKeyWithQuoting() {
1247
+ const tok = this.current();
1248
+ if (tok.type === TokenType.String) {
1249
+ this.advance();
1250
+ return { key: this.unescapeString(tok.value), isQuoted: true };
1251
+ }
1252
+ if (tok.type === TokenType.Identifier ||
1253
+ this.isNonAmbiguousKeyword(tok.type)) {
1254
+ this.advance();
1255
+ return { key: tok.value, isQuoted: false };
1256
+ }
1257
+ this.error(`Expected object key but got '${tok.value}'`, tok);
1258
+ this.advance();
1259
+ return { key: tok.value, isQuoted: false };
1260
+ }
1261
+ skipComments() {
1262
+ while (this.pos < this.tokens.length &&
1263
+ this.current().type === TokenType.Comment) {
1264
+ this.pos++;
1265
+ }
1266
+ }
1267
+ collectLeadingComments() {
1268
+ const comments = [];
1269
+ // Look backwards from current position to collect contiguous comment tokens
1270
+ let i = this.pos - 1;
1271
+ while (i >= 0 && this.tokens[i].type === TokenType.Comment) {
1272
+ comments.unshift(this.tokens[i].value);
1273
+ i--;
1274
+ }
1275
+ return comments;
1276
+ }
1277
+ isFunctionToken(type) {
1278
+ return (type === TokenType.Count ||
1279
+ type === TokenType.Sum ||
1280
+ type === TokenType.Avg ||
1281
+ type === TokenType.Min ||
1282
+ type === TokenType.Max ||
1283
+ type === TokenType.Contains ||
1284
+ type === TokenType.StartsWith ||
1285
+ type === TokenType.EndsWith ||
1286
+ type === TokenType.Regex ||
1287
+ type === TokenType.In ||
1288
+ type === TokenType.IsNull ||
1289
+ type === TokenType.IsNotNull);
1290
+ }
1291
+ /** Keywords that can also serve as property names in dot notation or object keys */
1292
+ isNonAmbiguousKeyword(type) {
1293
+ return (type === TokenType.Type ||
1294
+ type === TokenType.Types ||
1295
+ type === TokenType.Attributes ||
1296
+ type === TokenType.Metadata ||
1297
+ type === TokenType.Propositions ||
1298
+ type === TokenType.Identifier ||
1299
+ // Allow most keywords as property names since KIP uses snake_case for attrs
1300
+ type === TokenType.Asc ||
1301
+ type === TokenType.Desc ||
1302
+ type === TokenType.Primer ||
1303
+ type === TokenType.Domains ||
1304
+ type === TokenType.From ||
1305
+ type === TokenType.By ||
1306
+ type === TokenType.Order ||
1307
+ type === TokenType.Set ||
1308
+ type === TokenType.With);
1309
+ }
1310
+ unescapeString(raw) {
1311
+ // Strip quotes
1312
+ if (raw.startsWith('"') && raw.endsWith('"')) {
1313
+ raw = raw.slice(1, -1);
1314
+ }
1315
+ return raw.replace(/\\(.)/g, (_, ch) => {
1316
+ switch (ch) {
1317
+ case 'n':
1318
+ return '\n';
1319
+ case 't':
1320
+ return '\t';
1321
+ case 'r':
1322
+ return '\r';
1323
+ case '\\':
1324
+ return '\\';
1325
+ case '"':
1326
+ return '"';
1327
+ case '/':
1328
+ return '/';
1329
+ default:
1330
+ return ch;
1331
+ }
1332
+ });
1333
+ }
1334
+ error(message, token) {
1335
+ this.diagnostics.push({
1336
+ range: {
1337
+ start: { line: token.line, column: token.column },
1338
+ end: { line: token.line, column: token.column + token.value.length }
1339
+ },
1340
+ severity: 'error',
1341
+ message,
1342
+ code: 'KIP_PARSE'
1343
+ });
1344
+ }
1345
+ recoverToNextStatement() {
1346
+ const stmtStarters = new Set([
1347
+ TokenType.Find,
1348
+ TokenType.Upsert,
1349
+ TokenType.Delete,
1350
+ TokenType.Describe,
1351
+ TokenType.Search,
1352
+ TokenType.EOF
1353
+ ]);
1354
+ while (!this.isAtEnd() && !stmtStarters.has(this.current().type)) {
1355
+ this.pos++;
1356
+ }
1357
+ }
1358
+ }
1359
+ //# sourceMappingURL=parser.js.map