@ldclabs/kip-lang 0.2.1 → 0.3.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 CHANGED
@@ -49,14 +49,20 @@ class Parser {
49
49
  return this.parseFindStatement();
50
50
  case TokenType.Upsert:
51
51
  return this.parseUpsertStatement();
52
+ case TokenType.Update:
53
+ return this.parseUpdateStatement();
54
+ case TokenType.Merge:
55
+ return this.parseMergeStatement();
52
56
  case TokenType.Delete:
53
57
  return this.parseDeleteStatement();
54
58
  case TokenType.Describe:
55
59
  return this.parseDescribeStatement();
56
60
  case TokenType.Search:
57
61
  return this.parseSearchStatement();
62
+ case TokenType.Export:
63
+ return this.parseExportStatement();
58
64
  default:
59
- this.error(`Unexpected token '${tok.value}', expected a statement keyword (FIND, UPSERT, DELETE, DESCRIBE, SEARCH)`, tok);
65
+ this.error(`Unexpected token '${tok.value}', expected a statement keyword (FIND, UPSERT, UPDATE, MERGE, DELETE, DESCRIBE, SEARCH, EXPORT)`, tok);
60
66
  this.advance();
61
67
  return null;
62
68
  }
@@ -151,6 +157,10 @@ class Parser {
151
157
  const handle = this.expectVariable();
152
158
  this.expect(TokenType.LBrace);
153
159
  const matcher = this.parseConceptMatcher();
160
+ let expectVersion;
161
+ if (this.check(TokenType.Expect)) {
162
+ expectVersion = this.parseExpectVersion();
163
+ }
154
164
  let setAttributes;
155
165
  let setPropositions;
156
166
  let metadata;
@@ -197,6 +207,7 @@ class Parser {
197
207
  kind: 'ConceptBlock',
198
208
  handle,
199
209
  matcher,
210
+ expectVersion,
200
211
  setAttributes,
201
212
  setPropositions,
202
213
  metadata,
@@ -214,6 +225,10 @@ class Parser {
214
225
  }
215
226
  this.expect(TokenType.LBrace);
216
227
  const proposition = this.parsePropositionPatternBody(undefined);
228
+ let expectVersion;
229
+ if (this.check(TokenType.Expect)) {
230
+ expectVersion = this.parseExpectVersion();
231
+ }
217
232
  let setAttributes;
218
233
  let metadata;
219
234
  this.skipComments();
@@ -250,6 +265,7 @@ class Parser {
250
265
  subject: proposition.subject,
251
266
  predicate: proposition.predicate,
252
267
  object: proposition.object,
268
+ expectVersion,
253
269
  setAttributes,
254
270
  metadata,
255
271
  range: { start, end: this.currentPos() },
@@ -257,6 +273,74 @@ class Parser {
257
273
  };
258
274
  }
259
275
  // ────────────────────────────────────────────────────────────────────
276
+ // UPDATE
277
+ // ────────────────────────────────────────────────────────────────────
278
+ parseUpdateStatement() {
279
+ const start = this.currentPos();
280
+ const comments = this.collectLeadingComments();
281
+ this.expect(TokenType.Update);
282
+ const target = this.expectVariable();
283
+ let setAttributes;
284
+ let setMetadata;
285
+ this.skipComments();
286
+ while (this.check(TokenType.Set) && !this.isAtEnd()) {
287
+ const setStart = this.currentPos();
288
+ this.advance();
289
+ if (this.check(TokenType.Attributes)) {
290
+ this.advance();
291
+ setAttributes = this.parseSetAttributesBody(setStart);
292
+ }
293
+ else if (this.check(TokenType.Metadata)) {
294
+ this.advance();
295
+ setMetadata = this.parseSetMetadataBody(setStart);
296
+ }
297
+ else {
298
+ this.error(`Expected ATTRIBUTES or METADATA after SET in UPDATE statement`, this.current());
299
+ this.advance();
300
+ }
301
+ this.skipComments();
302
+ }
303
+ if (!setAttributes && !setMetadata) {
304
+ this.error(`Expected SET ATTRIBUTES or SET METADATA in UPDATE statement`, this.current());
305
+ }
306
+ const where = this.parseWhereClause();
307
+ let limit;
308
+ if (this.check(TokenType.Limit)) {
309
+ limit = this.parseLimitClause();
310
+ }
311
+ return {
312
+ kind: 'UpdateStatement',
313
+ target,
314
+ setAttributes,
315
+ setMetadata,
316
+ where,
317
+ limit,
318
+ range: { start, end: this.currentPos() },
319
+ leadingComments: comments.length > 0 ? comments : undefined
320
+ };
321
+ }
322
+ // ────────────────────────────────────────────────────────────────────
323
+ // MERGE
324
+ // ────────────────────────────────────────────────────────────────────
325
+ parseMergeStatement() {
326
+ const start = this.currentPos();
327
+ const comments = this.collectLeadingComments();
328
+ this.expect(TokenType.Merge);
329
+ this.expect(TokenType.Concept);
330
+ const source = this.expectVariable();
331
+ this.expect(TokenType.Into);
332
+ const target = this.expectVariable();
333
+ const where = this.parseWhereClause();
334
+ return {
335
+ kind: 'MergeStatement',
336
+ source,
337
+ target,
338
+ where,
339
+ range: { start, end: this.currentPos() },
340
+ leadingComments: comments.length > 0 ? comments : undefined
341
+ };
342
+ }
343
+ // ────────────────────────────────────────────────────────────────────
260
344
  // DELETE
261
345
  // ────────────────────────────────────────────────────────────────────
262
346
  parseDeleteStatement() {
@@ -338,6 +422,7 @@ class Parser {
338
422
  this.expect(TokenType.Describe);
339
423
  let describeType;
340
424
  let typeName;
425
+ let typeNameValue;
341
426
  let limit;
342
427
  let cursor;
343
428
  if (this.check(TokenType.Primer)) {
@@ -357,7 +442,11 @@ class Parser {
357
442
  else if (this.check(TokenType.Type)) {
358
443
  describeType = 'CONCEPT_TYPE';
359
444
  this.advance();
360
- typeName = this.expectStringValue();
445
+ typeNameValue = this.parseStringOrParameterValue('DESCRIBE CONCEPT TYPE');
446
+ typeName =
447
+ typeNameValue.kind === 'StringLiteral'
448
+ ? typeNameValue.parsed
449
+ : typeNameValue.name;
361
450
  }
362
451
  else {
363
452
  this.error(`Expected TYPE or TYPES after DESCRIBE CONCEPT`, this.current());
@@ -373,7 +462,11 @@ class Parser {
373
462
  else if (this.check(TokenType.Type)) {
374
463
  describeType = 'PROPOSITION_TYPE';
375
464
  this.advance();
376
- typeName = this.expectStringValue();
465
+ typeNameValue = this.parseStringOrParameterValue('DESCRIBE PROPOSITION TYPE');
466
+ typeName =
467
+ typeNameValue.kind === 'StringLiteral'
468
+ ? typeNameValue.parsed
469
+ : typeNameValue.name;
377
470
  }
378
471
  else {
379
472
  this.error(`Expected TYPE or TYPES after DESCRIBE PROPOSITION`, this.current());
@@ -394,6 +487,7 @@ class Parser {
394
487
  kind: 'DescribeStatement',
395
488
  describeType,
396
489
  typeName,
490
+ typeNameValue,
397
491
  limit,
398
492
  cursor,
399
493
  range: { start, end: this.currentPos() },
@@ -424,18 +518,35 @@ class Parser {
424
518
  const term = termValue.kind === 'StringLiteral' ? termValue.parsed : termValue.name;
425
519
  let withType;
426
520
  let withTypeValue;
427
- if (this.check(TokenType.With)) {
428
- this.advance();
429
- this.expect(TokenType.Type);
430
- withTypeValue = this.parseStringOrParameterValue('SEARCH WITH TYPE');
431
- withType =
432
- withTypeValue.kind === 'StringLiteral'
433
- ? withTypeValue.parsed
434
- : withTypeValue.name;
435
- }
521
+ let mode;
522
+ let modeValue;
523
+ let threshold;
436
524
  let limit;
437
- if (this.check(TokenType.Limit)) {
438
- limit = this.parseLimitClause();
525
+ while (!this.isAtEnd()) {
526
+ if (this.check(TokenType.With)) {
527
+ this.advance();
528
+ this.expect(TokenType.Type);
529
+ withTypeValue = this.parseStringOrParameterValue('SEARCH WITH TYPE');
530
+ withType =
531
+ withTypeValue.kind === 'StringLiteral'
532
+ ? withTypeValue.parsed
533
+ : withTypeValue.name;
534
+ }
535
+ else if (this.check(TokenType.Mode)) {
536
+ this.advance();
537
+ modeValue = this.parseStringOrParameterValue('SEARCH MODE');
538
+ mode =
539
+ modeValue.kind === 'StringLiteral' ? modeValue.parsed : modeValue.name;
540
+ }
541
+ else if (this.check(TokenType.Threshold)) {
542
+ threshold = this.parseThresholdClause();
543
+ }
544
+ else if (this.check(TokenType.Limit)) {
545
+ limit = this.parseLimitClause();
546
+ }
547
+ else {
548
+ break;
549
+ }
439
550
  }
440
551
  return {
441
552
  kind: 'SearchStatement',
@@ -444,6 +555,31 @@ class Parser {
444
555
  termValue,
445
556
  withType,
446
557
  withTypeValue,
558
+ mode,
559
+ modeValue,
560
+ threshold,
561
+ limit,
562
+ range: { start, end: this.currentPos() },
563
+ leadingComments: comments.length > 0 ? comments : undefined
564
+ };
565
+ }
566
+ // ────────────────────────────────────────────────────────────────────
567
+ // EXPORT
568
+ // ────────────────────────────────────────────────────────────────────
569
+ parseExportStatement() {
570
+ const start = this.currentPos();
571
+ const comments = this.collectLeadingComments();
572
+ this.expect(TokenType.Export);
573
+ const target = this.expectVariable();
574
+ const where = this.parseWhereClause();
575
+ let limit;
576
+ if (this.check(TokenType.Limit)) {
577
+ limit = this.parseLimitClause();
578
+ }
579
+ return {
580
+ kind: 'ExportStatement',
581
+ target,
582
+ where,
447
583
  limit,
448
584
  range: { start, end: this.currentPos() },
449
585
  leadingComments: comments.length > 0 ? comments : undefined
@@ -617,6 +753,31 @@ class Parser {
617
753
  }
618
754
  parsePredicateExpr() {
619
755
  const start = this.currentPos();
756
+ if (this.check(TokenType.Variable)) {
757
+ const pred = this.parsePredicateVariable();
758
+ if (this.check(TokenType.LBrace)) {
759
+ this.error(`Predicate variables cannot use hop ranges; use a quoted predicate literal for path traversal`, this.current());
760
+ this.parseHopRange();
761
+ }
762
+ if (this.check(TokenType.Pipe)) {
763
+ this.error(`Predicate variables cannot be used in predicate alternations`, this.current());
764
+ while (this.match(TokenType.Pipe)) {
765
+ if (this.check(TokenType.String)) {
766
+ this.parsePredicateLiteral();
767
+ }
768
+ else if (this.check(TokenType.Variable)) {
769
+ this.parsePredicateVariable();
770
+ }
771
+ else {
772
+ break;
773
+ }
774
+ }
775
+ }
776
+ return {
777
+ ...pred,
778
+ range: { start, end: this.currentPos() }
779
+ };
780
+ }
620
781
  const first = this.parsePredicateLiteral();
621
782
  // Check for alternation: "pred1" | "pred2"
622
783
  if (this.check(TokenType.Pipe)) {
@@ -632,6 +793,15 @@ class Parser {
632
793
  }
633
794
  return first;
634
795
  }
796
+ parsePredicateVariable() {
797
+ const start = this.currentPos();
798
+ const name = this.expectVariable();
799
+ return {
800
+ kind: 'PredicateVariable',
801
+ name,
802
+ range: { start, end: this.currentPos() }
803
+ };
804
+ }
635
805
  parsePredicateLiteral() {
636
806
  const start = this.currentPos();
637
807
  const value = this.expectStringValue();
@@ -736,6 +906,16 @@ class Parser {
736
906
  range: { start, end: this.currentPos() }
737
907
  };
738
908
  }
909
+ parseSetMetadataBody(start) {
910
+ this.expect(TokenType.LBrace);
911
+ const entries = this.parseObjectEntries();
912
+ this.expect(TokenType.RBrace);
913
+ return {
914
+ kind: 'SetMetadata',
915
+ entries,
916
+ range: { start, end: this.currentPos() }
917
+ };
918
+ }
739
919
  parseSetPropositionsBody(start) {
740
920
  this.expect(TokenType.LBrace);
741
921
  const items = [];
@@ -831,6 +1011,17 @@ class Parser {
831
1011
  range: { start, end: this.currentPos() }
832
1012
  };
833
1013
  }
1014
+ parseExpectVersion() {
1015
+ const start = this.currentPos();
1016
+ this.expect(TokenType.Expect);
1017
+ this.expect(TokenType.Version);
1018
+ const value = this.parseNumberOrParameterValue('EXPECT VERSION');
1019
+ return {
1020
+ kind: 'ExpectVersion',
1021
+ value,
1022
+ range: { start, end: this.currentPos() }
1023
+ };
1024
+ }
834
1025
  // ────────────────────────────────────────────────────────────────────
835
1026
  // ORDER BY, LIMIT, CURSOR
836
1027
  // ────────────────────────────────────────────────────────────────────
@@ -838,6 +1029,22 @@ class Parser {
838
1029
  const start = this.currentPos();
839
1030
  this.expect(TokenType.Order);
840
1031
  this.expect(TokenType.By);
1032
+ const keys = [];
1033
+ keys.push(this.parseOrderByKey());
1034
+ while (this.match(TokenType.Comma)) {
1035
+ keys.push(this.parseOrderByKey());
1036
+ }
1037
+ const first = keys[0];
1038
+ return {
1039
+ kind: 'OrderByClause',
1040
+ keys,
1041
+ expression: first.expression,
1042
+ direction: first.direction,
1043
+ range: { start, end: this.currentPos() }
1044
+ };
1045
+ }
1046
+ parseOrderByKey() {
1047
+ const start = this.currentPos();
841
1048
  const expression = this.parseExpression();
842
1049
  let direction = 'ASC';
843
1050
  if (this.check(TokenType.Asc)) {
@@ -849,45 +1056,59 @@ class Parser {
849
1056
  direction = 'DESC';
850
1057
  }
851
1058
  return {
852
- kind: 'OrderByClause',
1059
+ kind: 'OrderByKey',
853
1060
  expression,
854
1061
  direction,
855
1062
  range: { start, end: this.currentPos() }
856
1063
  };
857
1064
  }
858
- parseLimitClause() {
1065
+ parseThresholdClause() {
859
1066
  const start = this.currentPos();
860
- this.expect(TokenType.Limit);
1067
+ this.expect(TokenType.Threshold);
1068
+ const value = this.parseNumberOrParameterValue('THRESHOLD');
1069
+ return {
1070
+ kind: 'ThresholdClause',
1071
+ value,
1072
+ range: { start, end: this.currentPos() }
1073
+ };
1074
+ }
1075
+ parseNumberOrParameterValue(context) {
861
1076
  const tok = this.current();
1077
+ const start = this.currentPos();
862
1078
  let value;
863
1079
  if (tok.type === TokenType.Number) {
864
1080
  value = {
865
1081
  kind: 'NumberLiteral',
866
1082
  value: Number(tok.value),
867
1083
  raw: tok.value,
868
- range: { start: this.currentPos(), end: this.currentPos() }
1084
+ range: { start, end: start }
869
1085
  };
870
1086
  this.advance();
871
1087
  value.range.end = this.currentPos();
1088
+ return value;
872
1089
  }
873
- else if (tok.type === TokenType.Parameter) {
1090
+ if (tok.type === TokenType.Parameter) {
874
1091
  value = {
875
1092
  kind: 'ParameterRef',
876
1093
  name: tok.value,
877
- range: { start: this.currentPos(), end: this.currentPos() }
1094
+ range: { start, end: start }
878
1095
  };
879
1096
  this.advance();
880
1097
  value.range.end = this.currentPos();
1098
+ return value;
881
1099
  }
882
- else {
883
- this.error(`Expected number or parameter after LIMIT`, tok);
884
- value = {
885
- kind: 'NumberLiteral',
886
- value: 0,
887
- raw: '0',
888
- range: { start: this.currentPos(), end: this.currentPos() }
889
- };
890
- }
1100
+ this.error(`Expected number or parameter after ${context}`, tok);
1101
+ return {
1102
+ kind: 'NumberLiteral',
1103
+ value: 0,
1104
+ raw: '0',
1105
+ range: { start, end: start }
1106
+ };
1107
+ }
1108
+ parseLimitClause() {
1109
+ const start = this.currentPos();
1110
+ this.expect(TokenType.Limit);
1111
+ const value = this.parseNumberOrParameterValue('LIMIT');
891
1112
  return {
892
1113
  kind: 'LimitClause',
893
1114
  value,
@@ -1215,9 +1436,14 @@ class Parser {
1215
1436
  value,
1216
1437
  range: { start: entryStart, end: this.currentPos() }
1217
1438
  });
1218
- // Optional comma
1219
- this.match(TokenType.Comma);
1220
1439
  this.skipComments();
1440
+ if (this.check(TokenType.RBrace))
1441
+ break;
1442
+ if (this.match(TokenType.Comma)) {
1443
+ this.skipComments();
1444
+ continue;
1445
+ }
1446
+ this.error(`Expected ',' or '}' after object entry`, this.current());
1221
1447
  }
1222
1448
  return entries;
1223
1449
  }
@@ -1351,7 +1577,11 @@ class Parser {
1351
1577
  type === TokenType.Regex ||
1352
1578
  type === TokenType.In ||
1353
1579
  type === TokenType.IsNull ||
1354
- type === TokenType.IsNotNull);
1580
+ type === TokenType.IsNotNull ||
1581
+ type === TokenType.Add ||
1582
+ type === TokenType.Mul ||
1583
+ type === TokenType.Clamp ||
1584
+ type === TokenType.Coalesce);
1355
1585
  }
1356
1586
  /** Keywords that can also serve as property names in dot notation or object keys */
1357
1587
  isNonAmbiguousKeyword(type) {
@@ -1370,7 +1600,12 @@ class Parser {
1370
1600
  type === TokenType.By ||
1371
1601
  type === TokenType.Order ||
1372
1602
  type === TokenType.Set ||
1373
- type === TokenType.With);
1603
+ type === TokenType.With ||
1604
+ type === TokenType.Into ||
1605
+ type === TokenType.Expect ||
1606
+ type === TokenType.Version ||
1607
+ type === TokenType.Mode ||
1608
+ type === TokenType.Threshold);
1374
1609
  }
1375
1610
  unescapeString(raw) {
1376
1611
  if (raw.startsWith('"') && raw.endsWith('"')) {
@@ -1415,9 +1650,12 @@ class Parser {
1415
1650
  const stmtStarters = new Set([
1416
1651
  TokenType.Find,
1417
1652
  TokenType.Upsert,
1653
+ TokenType.Update,
1654
+ TokenType.Merge,
1418
1655
  TokenType.Delete,
1419
1656
  TokenType.Describe,
1420
1657
  TokenType.Search,
1658
+ TokenType.Export,
1421
1659
  TokenType.EOF
1422
1660
  ]);
1423
1661
  while (!this.isAtEnd() && !stmtStarters.has(this.current().type)) {